Added pages to see, create, modify & delete redeemable actions. User card top right with disconnect & log out. Code clean up.
This commit is contained in:
parent
11dfde9a03
commit
d595c3500e
@ -0,0 +1,94 @@
|
||||
<body>
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title-group>
|
||||
<mat-card-title>{{isNew ? "New Action" : previousName}}</mat-card-title>
|
||||
<mat-card-subtitle>{{isNew ? 'Creating a new action' : 'Modifying an existing action'}}</mat-card-subtitle>
|
||||
</mat-card-title-group>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<form class="grid" [formGroup]="formGroup">
|
||||
<div class="item">
|
||||
<mat-form-field>
|
||||
<mat-label>Redeemable Action Name</mat-label>
|
||||
<input matInput type="text" formControlName="name">
|
||||
@if (isNew && formGroup.get('name')?.invalid && (formGroup.get('name')?.dirty ||
|
||||
formGroup.get('name')?.touched)) {
|
||||
@if (formGroup.get('name')?.hasError('required')) {
|
||||
<small class="error">The name is required.</small>
|
||||
}
|
||||
@if (formGroup.get('name')?.hasError('itemExistsInArray')) {
|
||||
<small class="error">The name is already in use.</small>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="item">
|
||||
<mat-form-field>
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-select matInput formControlName="type" (selectionChange)="action.type = $event.value">
|
||||
@for (type of actionTypes; track $index) {
|
||||
<mat-option value="{{type}}">{{type}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (isNew && formGroup.get('type')?.invalid && (formGroup.get('type')?.dirty ||
|
||||
formGroup.get('type')?.touched)) {
|
||||
@if (formGroup.get('type')?.hasError('required')) {
|
||||
<small class="error">The type is required.</small>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if (actionEntries.hasOwnProperty(action.type)) {
|
||||
<section class="grid">
|
||||
@for (field of actionEntries[action.type]; track $index) {
|
||||
<div class="item">
|
||||
@if (field.type == 'text') {
|
||||
<mat-form-field>
|
||||
<mat-label>{{field.label}}</mat-label>
|
||||
<input matInput type="text" placeholder="{{field.placeholder}}" [formControl]="field.control"
|
||||
[(ngModel)]="action.data[field.key]">
|
||||
@if (field.control.invalid && (field.control.dirty || field.control.touched)) {
|
||||
@if (field.control.hasError('required')) {
|
||||
<small class="error">This field is required.</small>
|
||||
}
|
||||
@if (field.control.hasError('minlength')) {
|
||||
<small class="error">The value needs to be longer.</small>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
}
|
||||
@else if (field.type == 'number') {
|
||||
<mat-form-field>
|
||||
<mat-label>{{field.label}}</mat-label>
|
||||
<input matInput type="number" [formControl]="field.control">
|
||||
@if (field.control.invalid && (field.control.dirty || field.control.touched)) {
|
||||
@if (field.control.hasError('required')) {
|
||||
<small class="error">This field is required.</small>
|
||||
}
|
||||
@if (field.control.hasError('min')) {
|
||||
<small class="error">The value must be higher.</small>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
</mat-card-content>
|
||||
|
||||
<mat-card-actions class="actions" align="end">
|
||||
@if (!isNew) {
|
||||
<button mat-raised-button class="delete" (click)="deleteAction(action)">Delete</button>
|
||||
}
|
||||
<button mat-raised-button (click)="dialogRef.close()">Cancel</button>
|
||||
<button mat-raised-button disabled="{{!formsDirty || !formsValidity}}" (click)="save()">Save</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</body>
|
@ -0,0 +1,30 @@
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-auto-flow: row dense;
|
||||
grid-gap: 0 1em;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: block;
|
||||
color: #ba1a1a;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.delete {
|
||||
background-color: #ea5151;
|
||||
color: #ba1a1a;
|
||||
}
|
||||
|
||||
.mdc-button ~ .mdc-button {
|
||||
margin-left: 1em;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ActionItemEditComponent } from './action-item-edit.component';
|
||||
|
||||
describe('ActionItemEditComponent', () => {
|
||||
let component: ActionItemEditComponent;
|
||||
let fixture: ComponentFixture<ActionItemEditComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ActionItemEditComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ActionItemEditComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
267
src/app/actions/action-item-edit/action-item-edit.component.ts
Normal file
267
src/app/actions/action-item-edit/action-item-edit.component.ts
Normal file
@ -0,0 +1,267 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { createItemExistsInArrayValidator } from '../../shared/validators/item-exists-in-array';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
|
||||
@Component({
|
||||
selector: 'action-item-edit',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule
|
||||
],
|
||||
templateUrl: './action-item-edit.component.html',
|
||||
styleUrl: './action-item-edit.component.scss'
|
||||
})
|
||||
export class ActionItemEditComponent implements OnInit {
|
||||
readonly client = inject(HermesClientService);
|
||||
readonly dialogRef = inject(MatDialogRef<ActionItemEditComponent>);
|
||||
readonly data = inject<{ action: RedeemableAction, actions:RedeemableAction[] }>(MAT_DIALOG_DATA);
|
||||
action = this.data.action;
|
||||
actions = this.data.actions;
|
||||
readonly actionEntries: ({ [key: string]: any[] }) = {
|
||||
'SLEEP': [
|
||||
{
|
||||
key: 'sleep',
|
||||
type: 'number',
|
||||
label: 'Sleep (ms)',
|
||||
control: new FormControl(this.action.data['sleep'], [Validators.required, Validators.min(500)]),
|
||||
}
|
||||
],
|
||||
'APPEND_TO_FILE': [
|
||||
{
|
||||
key: 'file_path',
|
||||
type: 'text',
|
||||
label: 'File Path',
|
||||
placeholder: '%userprofile%/Desktop/file.txt',
|
||||
control: new FormControl(this.action.data['file_path'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'file_content',
|
||||
type: 'text',
|
||||
label: 'Content',
|
||||
placeholder: '%chatter% from %broadcaster%\'s chat says hi.',
|
||||
control: new FormControl(this.action.data['file_content'], [Validators.required]),
|
||||
},
|
||||
],
|
||||
'WRITE_TO_FILE': [
|
||||
{
|
||||
key: 'file_path',
|
||||
type: 'text',
|
||||
label: 'File Path',
|
||||
placeholder: '%userprofile%/Desktop/file.txt',
|
||||
control: new FormControl(this.action.data['file_path'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'file_content',
|
||||
type: 'text',
|
||||
label: 'Content',
|
||||
placeholder: '%chatter% from %broadcaster%\'s chat says hi.',
|
||||
control: new FormControl(this.action.data['file_content'], [Validators.required]),
|
||||
},
|
||||
],
|
||||
'OBS_TRANSFORM': [
|
||||
{
|
||||
key: 'scene_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Name',
|
||||
placeholder: 'Main Scene',
|
||||
control: new FormControl(this.action.data['scene_name'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'scene_item_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Item Name',
|
||||
placeholder: 'Item',
|
||||
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'position_x',
|
||||
type: 'text',
|
||||
label: 'Position X',
|
||||
placeholder: 'x + 50',
|
||||
control: new FormControl(this.action.data['position_x'], []),
|
||||
},
|
||||
{
|
||||
key: 'position_y',
|
||||
type: 'text',
|
||||
label: 'Position Y',
|
||||
placeholder: 'x - 166',
|
||||
control: new FormControl(this.action.data['position_y'], []),
|
||||
},
|
||||
{
|
||||
key: 'rotation',
|
||||
type: 'text',
|
||||
label: 'Rotation (in degrees)',
|
||||
placeholder: 'mod(x + 45, 360)',
|
||||
control: new FormControl(this.action.data['rotation'], []),
|
||||
},
|
||||
],
|
||||
'AUDIO_FILE': [
|
||||
{
|
||||
key: 'file_path',
|
||||
type: 'text',
|
||||
label: 'Audio File Path',
|
||||
placeholder: '%userprofile%/Desktop/audio.mp3',
|
||||
control: new FormControl(this.action.data['file_path'], [Validators.required]),
|
||||
},
|
||||
],
|
||||
'RANDOM_TTS_VOICE': [],
|
||||
'SPECIFIC_TTS_VOICE': [
|
||||
{
|
||||
key: 'tts_voice',
|
||||
type: 'text',
|
||||
label: 'TTS Voice Name',
|
||||
placeholder: 'Brian',
|
||||
control: new FormControl(this.action.data['tts_voice'], [Validators.required, Validators.minLength(2)]),
|
||||
},
|
||||
],
|
||||
'TOGGLE_OBS_VISIBILITY': [
|
||||
{
|
||||
key: 'scene_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Name',
|
||||
placeholder: 'Main Scene',
|
||||
control: new FormControl(this.action.data['scene_name'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'scene_item_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Item Name',
|
||||
placeholder: 'Item',
|
||||
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||
},
|
||||
],
|
||||
'SPECIFIC_OBS_VISIBILITY': [
|
||||
{
|
||||
key: 'scene_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Name',
|
||||
placeholder: 'Main Scene',
|
||||
control: new FormControl(this.action.data['scene_name'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'scene_item_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Item Name',
|
||||
placeholder: 'Item',
|
||||
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'obs_visible',
|
||||
type: 'text-values',
|
||||
label: 'Visibility',
|
||||
values: ['visible', 'hidden'],
|
||||
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||
},
|
||||
],
|
||||
'SPECIFIC_OBS_INDEX': [
|
||||
{
|
||||
key: 'scene_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Name',
|
||||
placeholder: 'Main Scene',
|
||||
control: new FormControl(this.action.data['scene_name'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'scene_item_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Item Name',
|
||||
placeholder: 'Item',
|
||||
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'obs_index',
|
||||
type: 'number',
|
||||
label: 'Visibility',
|
||||
control: new FormControl(this.action.data['scene_item_name'], [Validators.required, Validators.min(0)]),
|
||||
},
|
||||
],
|
||||
'NIGHTBOT_PLAY': [],
|
||||
'VEADOTUBE_SET_STATE': [
|
||||
{
|
||||
key: 'state',
|
||||
type: 'text',
|
||||
label: 'Veadotube State name',
|
||||
placeholder: 'state #1',
|
||||
control: new FormControl(this.action.data['state'], [Validators.required]),
|
||||
},
|
||||
],
|
||||
};
|
||||
readonly actionTypes = Object.keys(this.actionEntries);
|
||||
|
||||
isNew: boolean = true;
|
||||
previousName: string = this.action.name;
|
||||
|
||||
readonly formGroup = new FormGroup({
|
||||
name: new FormControl(this.action.name, [Validators.required]),
|
||||
type: new FormControl(this.action.type, [Validators.required]),
|
||||
});
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isNew = this.action.name.length <= 0;
|
||||
this.previousName = this.action.name;
|
||||
if (!this.isNew)
|
||||
this.formGroup.get('name')!.disable()
|
||||
else {
|
||||
this.formGroup.get('name')?.addValidators(createItemExistsInArrayValidator(this.actions, a => a.name));
|
||||
}
|
||||
}
|
||||
|
||||
get exists(): boolean {
|
||||
return this.actions.some(a => a.name == this.action.name);
|
||||
}
|
||||
|
||||
get formsValidity(): boolean {
|
||||
return this.formGroup.valid && this.action.type in this.actionEntries
|
||||
&& this.actionEntries[this.action.type].every(f => f.control.valid);
|
||||
}
|
||||
|
||||
get formsDirty(): boolean {
|
||||
return this.formGroup.dirty || this.action.type in this.actionEntries
|
||||
&& this.actionEntries[this.action.type].some(f => f.control.dirty);
|
||||
}
|
||||
|
||||
deleteAction(action: RedeemableAction): void {
|
||||
if (this.isNew)
|
||||
return;
|
||||
|
||||
this.client.deleteRedeemableAction(action.name);
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fields = this.actionEntries[this.action.type];
|
||||
if (fields.some(f => f.control.invalid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.action.name = this.formGroup.get('name')!.value!;
|
||||
this.action.type = this.formGroup.get('type')!.value!;
|
||||
this.action.data = {}
|
||||
for (const entry of this.actionEntries[this.action.type]) {
|
||||
this.action.data[entry.key] = entry.control.value!.toString();
|
||||
}
|
||||
|
||||
if (!(this.action.type in this.actionEntries)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialogRef.close(this.action);
|
||||
}
|
||||
}
|
11
src/app/actions/action-list/action-list.component.html
Normal file
11
src/app/actions/action-list/action-list.component.html
Normal file
@ -0,0 +1,11 @@
|
||||
<main>
|
||||
@for (action of actions; track $index) {
|
||||
<button type="button" class="container" (click)="modify(action)">
|
||||
<span class="title">{{action.name}}</span>
|
||||
<span class="subtitle">{{action.type}}</span>
|
||||
</button>
|
||||
}
|
||||
<button type="button" class="container" (click)="create()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</main>
|
56
src/app/actions/action-list/action-list.component.scss
Normal file
56
src/app/actions/action-list/action-list.component.scss
Normal file
@ -0,0 +1,56 @@
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
|
||||
@media (min-width:1200px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width:1650px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width:2200px) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
grid-auto-flow: row dense;
|
||||
grid-gap: 1rem;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
background-color: #fafafa;
|
||||
width: 80%;
|
||||
justify-self: center;
|
||||
|
||||
& .container {
|
||||
border-color: grey;
|
||||
border-radius: 20px;
|
||||
border: 1px solid grey;
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
|
||||
& span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& .title {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
& .subtitle {
|
||||
font-size: smaller;
|
||||
color: lightgrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& article:first-child {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
23
src/app/actions/action-list/action-list.component.spec.ts
Normal file
23
src/app/actions/action-list/action-list.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ActionListComponent } from './action-list.component';
|
||||
|
||||
describe('ActionListComponent', () => {
|
||||
let component: ActionListComponent;
|
||||
let fixture: ComponentFixture<ActionListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ActionListComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ActionListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
66
src/app/actions/action-list/action-list.component.ts
Normal file
66
src/app/actions/action-list/action-list.component.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActionItemEditComponent } from '../action-item-edit/action-item-edit.component';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
|
||||
@Component({
|
||||
selector: 'action-list',
|
||||
standalone: true,
|
||||
imports: [MatButtonModule, MatFormFieldModule, MatIconModule, MatListModule],
|
||||
templateUrl: './action-list.component.html',
|
||||
styleUrl: './action-list.component.scss'
|
||||
})
|
||||
export class ActionListComponent {
|
||||
@Input() actions: RedeemableAction[] = []
|
||||
@Output() actionsChange = new EventEmitter<RedeemableAction>();
|
||||
readonly dialog = inject(MatDialog);
|
||||
readonly client = inject(HermesClientService);
|
||||
opened = false;
|
||||
|
||||
create(): void {
|
||||
this.openDialog({ user_id: '', name: '', type: '', data: {} });
|
||||
}
|
||||
|
||||
modify(action: RedeemableAction): void {
|
||||
this.openDialog(action);
|
||||
}
|
||||
|
||||
private openDialog(action: RedeemableAction): void {
|
||||
if (this.opened)
|
||||
return;
|
||||
|
||||
this.opened = true;
|
||||
|
||||
const dialogRef = this.dialog.open(ActionItemEditComponent, {
|
||||
data: { action: {user_id: action.user_id, name: action.name, type: action.type, data: action.data }, actions: this.actions },
|
||||
});
|
||||
const isNewAction = action.name.length <= 0;
|
||||
const requestType = isNewAction ? 'create_redeemable_action' : 'update_redeemable_action';
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: RedeemableAction) => {
|
||||
this.opened = false;
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == requestType && d.d.data.name == result.name)
|
||||
?.subscribe(_ => {
|
||||
if (isNewAction) {
|
||||
this.actionsChange.emit(result);
|
||||
} else {
|
||||
action.type = result.type;
|
||||
action.data = result.data;
|
||||
}
|
||||
});
|
||||
|
||||
if (isNewAction)
|
||||
this.client.createRedeemableAction(result.name, result.type, result.data);
|
||||
else
|
||||
this.client.updateRedeemableAction(result.name, result.type, result.data);
|
||||
});
|
||||
}
|
||||
}
|
17
src/app/actions/actions.module.ts
Normal file
17
src/app/actions/actions.module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActionsComponent } from './actions/actions.component';
|
||||
import { ActionListComponent } from './action-list/action-list.component';
|
||||
import { ActionItemComponent } from './action-item/action-item.component';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
ActionsComponent,
|
||||
ActionListComponent,
|
||||
ActionItemComponent,
|
||||
]
|
||||
})
|
||||
export class ActionsModule { }
|
31
src/app/actions/actions/actions.component.html
Normal file
31
src/app/actions/actions/actions.component.html
Normal file
@ -0,0 +1,31 @@
|
||||
<body>
|
||||
<h3>Redeemable Actions</h3>
|
||||
|
||||
<section>
|
||||
<article>
|
||||
<mat-form-field>
|
||||
<mat-label>Filter</mat-label>
|
||||
<mat-select (selectionChange)="onFilterChange($event.value)" value="0">
|
||||
<mat-select-trigger>
|
||||
<mat-icon matPrefix>filter_list</mat-icon> {{filter.name}}
|
||||
</mat-select-trigger>
|
||||
@for (item of filters; track $index) {
|
||||
<mat-option value="{{$index}}">{{item.name}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</article>
|
||||
<article>
|
||||
<mat-form-field>
|
||||
<mat-label>Search</mat-label>
|
||||
<input matInput
|
||||
type="text"
|
||||
placeholder="Name of action"
|
||||
[formControl]="searchControl"
|
||||
[(ngModel)]="search">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
</article>
|
||||
</section>
|
||||
<action-list [actions]="actions" (actionsChange)="items.push($event)" />
|
||||
</body>
|
23
src/app/actions/actions/actions.component.scss
Normal file
23
src/app/actions/actions/actions.component.scss
Normal file
@ -0,0 +1,23 @@
|
||||
body, h3 {
|
||||
background-color: #fafafa;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 70%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@media (max-width:1250px) {
|
||||
display: block;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
article {
|
||||
display: flex;
|
||||
justify-content:space-around;
|
||||
}
|
||||
}
|
23
src/app/actions/actions/actions.component.spec.ts
Normal file
23
src/app/actions/actions/actions.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ActionsComponent } from './actions.component';
|
||||
|
||||
describe('ActionsComponent', () => {
|
||||
let component: ActionsComponent;
|
||||
let fixture: ComponentFixture<ActionsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ActionsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ActionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
93
src/app/actions/actions/actions.component.ts
Normal file
93
src/app/actions/actions/actions.component.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { ActionListComponent } from "../action-list/action-list.component";
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
interface IActionFilter {
|
||||
name: string
|
||||
filter: (action: any) => boolean
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'actions',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ActionListComponent,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatSelectModule
|
||||
],
|
||||
templateUrl: './actions.component.html',
|
||||
styleUrl: './actions.component.scss'
|
||||
})
|
||||
export class ActionsComponent implements OnInit {
|
||||
filters: IActionFilter[] = [
|
||||
{ name: 'All', filter: _ => true },
|
||||
{ name: 'Local File', filter: data => data.type.includes('_FILE') },
|
||||
{ name: 'Nightbot', filter: data => data.type.includes('NIGHTBOT_') },
|
||||
{ name: 'OBS', filter: data => data.type.includes('OBS_') },
|
||||
{ name: 'Sleep', filter: data => data.type == "SLEEP" },
|
||||
{ name: 'TTS', filter: data => data.type.includes('TTS') },
|
||||
{ name: 'Veadotube', filter: data => data.type.includes('VEADOTUBE') },
|
||||
];
|
||||
|
||||
client = inject(HermesClientService);
|
||||
filter = this.filters[0];
|
||||
searchControl = new FormControl('');
|
||||
search = '';
|
||||
items: RedeemableAction[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.client.subscribeToRequests('get_redeemable_actions', d => {
|
||||
this.items = d.data;
|
||||
});
|
||||
this.client.subscribeToRequests('create_redeemable_action', d => {
|
||||
if (d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actions.push(d.data);
|
||||
});
|
||||
this.client.subscribeToRequests('update_redeemable_action', d => {
|
||||
if (d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = this.actions.find(a => a.name == d.data.name);
|
||||
if (action) {
|
||||
action.type = d.data.type;
|
||||
action.data = d.data.data;
|
||||
}
|
||||
});
|
||||
this.client.subscribeToRequests('delete_redeemable_action', d => {
|
||||
// if (d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id)) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
this.items = this.actions.filter(a => a.name != d.request.data.name);
|
||||
});
|
||||
|
||||
this.client.fetchRedeemableActions();
|
||||
}
|
||||
|
||||
get actions(): RedeemableAction[] {
|
||||
const searchLower = this.search.toLowerCase();
|
||||
return this.items.filter(this.filter.filter)
|
||||
.filter((action) => action.name.toLowerCase().includes(searchLower));
|
||||
}
|
||||
|
||||
set actions(value) {
|
||||
this.items = value;
|
||||
}
|
||||
|
||||
onFilterChange(event: any): void {
|
||||
this.filter = this.filters[event];
|
||||
}
|
||||
}
|
@ -13,7 +13,6 @@ export const appConfig: ApplicationConfig = {
|
||||
provideRouter(routes),
|
||||
provideHttpClient(
|
||||
withInterceptors([(req: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
||||
console.log(req.url);
|
||||
return next(req);
|
||||
}])
|
||||
),
|
||||
|
@ -6,8 +6,8 @@ import { TtsLoginComponent } from './auth/tts-login/tts-login.component';
|
||||
import { TwitchAuthCallbackComponent } from './twitch-auth-callback/twitch-auth-callback.component';
|
||||
import { FiltersComponent } from './tts-filters/filters/filters.component';
|
||||
import { AuthAdminGuard } from './shared/auth/auth.admin.guard';
|
||||
import { ActionComponent } from './actions/action/action.component';
|
||||
import { AuthVisitorGuard } from './shared/auth/auth.visitor.guard';
|
||||
import { ActionsComponent } from './actions/actions/actions.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@ -22,7 +22,7 @@ export const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'actions',
|
||||
component: ActionComponent,
|
||||
component: ActionsComponent,
|
||||
canActivate: [AuthAdminGuard],
|
||||
},
|
||||
{
|
||||
|
@ -2,15 +2,15 @@ import { NgModule } from '@angular/core';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { TtsLoginComponent } from './tts-login/tts-login.component';
|
||||
import { ImpersonationComponent } from './impersonation/impersonation.component';
|
||||
|
||||
|
||||
import { UserCardComponent } from './user-card/user-card.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
LoginComponent,
|
||||
TtsLoginComponent,
|
||||
ImpersonationComponent
|
||||
ImpersonationComponent,
|
||||
UserCardComponent,
|
||||
]
|
||||
})
|
||||
export class AuthModule { }
|
||||
|
@ -1,10 +1,5 @@
|
||||
@if (isAdmin()) {
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-header>
|
||||
<mat-card-title> Impersonation</mat-card-title>
|
||||
<mat-card-subtitle>Impersonate as another user</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-actions>
|
||||
<main>
|
||||
<mat-form-field>
|
||||
<mat-label>User to impersonate</mat-label>
|
||||
<mat-select (selectionChange)="onChange($event)" [(value)]="impersonated">
|
||||
@ -14,6 +9,5 @@
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</main>
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 1em;
|
||||
}
|
@ -51,7 +51,6 @@ export class ImpersonationComponent implements OnInit {
|
||||
}
|
||||
|
||||
public onChange(e: any) {
|
||||
console.log('impersonate befre', e.value);
|
||||
if (!e.value) {
|
||||
this.http.delete(environment.API_HOST + '/admin/impersonate', {
|
||||
headers: {
|
||||
|
@ -1,6 +1,12 @@
|
||||
<h4>TTS Login</h4>
|
||||
<div class="main-div">
|
||||
<main>
|
||||
<mat-card class="main-card">
|
||||
<mat-card-header class="header">
|
||||
<mat-card-title-group>
|
||||
<mat-card-title>TTS Login</mat-card-title>
|
||||
<mat-card-subtitle>Web Access to Tom-to-Speech</mat-card-subtitle>
|
||||
</mat-card-title-group>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="content">
|
||||
<mat-form-field>
|
||||
<mat-label>API Key</mat-label>
|
||||
<mat-select [(value)]="selected_api_key">
|
||||
@ -9,6 +15,9 @@
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions align="end">
|
||||
<button mat-raised-button (click)="login()">Log In</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
</main>
|
@ -1,14 +1,7 @@
|
||||
.main-div {
|
||||
main {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h4 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main-card {
|
||||
width: 20%;
|
||||
justify-content: center;
|
||||
}
|
@ -10,12 +10,12 @@ import { Router } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { MatCard } from '@angular/material/card';
|
||||
import { MatCard, MatCardModule } from '@angular/material/card';
|
||||
|
||||
@Component({
|
||||
selector: 'tts-login',
|
||||
standalone: true,
|
||||
imports: [MatButtonModule, MatCard, MatFormFieldModule, MatSelectModule, MatInputModule, FormsModule],
|
||||
imports: [MatButtonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatInputModule, FormsModule],
|
||||
templateUrl: './tts-login.component.html',
|
||||
styleUrl: './tts-login.component.scss'
|
||||
})
|
||||
@ -37,12 +37,11 @@ export class TtsLoginComponent implements OnInit, OnDestroy {
|
||||
}).subscribe((data: any) => this.api_keys = data);
|
||||
|
||||
this.subscription = this.events.listen('tts_login_ack', _ => {
|
||||
if (document.location.href.includes('/tts-login')) {
|
||||
this.router.navigate(['/policies'])
|
||||
}
|
||||
});
|
||||
this.events.listen('tts_logoff', _ => {
|
||||
this.selected_api_key = undefined;
|
||||
this.router.navigate(['/tts-login'])
|
||||
});
|
||||
this.events.listen('impersonation', _ => {
|
||||
this.selected_api_key = undefined;
|
||||
@ -61,7 +60,6 @@ export class TtsLoginComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
login() {
|
||||
console.log('api key for login', this.selected_api_key)
|
||||
if (!this.selected_api_key)
|
||||
return;
|
||||
|
||||
|
20
src/app/auth/user-card/user-card.component.html
Normal file
20
src/app/auth/user-card/user-card.component.html
Normal file
@ -0,0 +1,20 @@
|
||||
@if (auth.isAuthenticated()) {
|
||||
<main>
|
||||
<mat-card appearance="outlined" class="card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{username}}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<impersonation />
|
||||
</mat-card-content>
|
||||
<mat-card-actions class="actions">
|
||||
<div>
|
||||
@if (isTTSLoggedIn) {
|
||||
<button mat-raised-button (click)="client.disconnect()"><span class="disconnect">Disconnect</span></button>
|
||||
}
|
||||
<button mat-raised-button (click)="auth.logout()"><span class="logoff">Log Off</span></button>
|
||||
</div>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</main>
|
||||
}
|
23
src/app/auth/user-card/user-card.component.scss
Normal file
23
src/app/auth/user-card/user-card.component.scss
Normal file
@ -0,0 +1,23 @@
|
||||
main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 0 0 0.5em;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.disconnect, .logoff {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.mdc-button ~ .mdc-button {
|
||||
margin-left: 1em;
|
||||
}
|
23
src/app/auth/user-card/user-card.component.spec.ts
Normal file
23
src/app/auth/user-card/user-card.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserCardComponent } from './user-card.component';
|
||||
|
||||
describe('UserCardComponent', () => {
|
||||
let component: UserCardComponent;
|
||||
let fixture: ComponentFixture<UserCardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserCardComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UserCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
26
src/app/auth/user-card/user-card.component.ts
Normal file
26
src/app/auth/user-card/user-card.component.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { ImpersonationComponent } from '../impersonation/impersonation.component';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { ApiAuthenticationService } from '../../shared/services/api/api-authentication.service';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
|
||||
@Component({
|
||||
selector: 'user-card',
|
||||
standalone: true,
|
||||
imports: [ImpersonationComponent, MatButtonModule, MatCardModule],
|
||||
templateUrl: './user-card.component.html',
|
||||
styleUrl: './user-card.component.scss'
|
||||
})
|
||||
export class UserCardComponent {
|
||||
auth = inject(ApiAuthenticationService);
|
||||
client = inject(HermesClientService);
|
||||
|
||||
get isTTSLoggedIn() {
|
||||
return this.client.logged_in;
|
||||
}
|
||||
|
||||
get username() {
|
||||
return this.auth.getUsername();
|
||||
}
|
||||
}
|
@ -19,10 +19,7 @@ export class HermesClientService {
|
||||
connected: boolean;
|
||||
logged_in: boolean;
|
||||
|
||||
private subscriptions: { [key: number]: ((data: any) => void)[] }
|
||||
|
||||
constructor(private socket: HermesSocketService, private events: EventService) {
|
||||
this.subscriptions = {};
|
||||
this.connected = false;
|
||||
this.logged_in = false;
|
||||
|
||||
@ -92,6 +89,18 @@ export class HermesClientService {
|
||||
});
|
||||
}
|
||||
|
||||
public createRedeemableAction(name: string, type: string, d: { [key: string]: any }) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
||||
this.send(3, {
|
||||
request_id: null,
|
||||
type: "create_redeemable_action",
|
||||
data: { name, type, data: d },
|
||||
nounce: this.session_id,
|
||||
});
|
||||
}
|
||||
|
||||
public createTTSFilter(search: string, replace: string) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
@ -114,6 +123,18 @@ export class HermesClientService {
|
||||
});
|
||||
}
|
||||
|
||||
public deleteRedeemableAction(name: string) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
||||
this.send(3, {
|
||||
request_id: null,
|
||||
type: "delete_redeemable_action",
|
||||
data: { name },
|
||||
nounce: this.session_id,
|
||||
});
|
||||
}
|
||||
|
||||
public deleteTTSFilter(id: string) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
@ -159,6 +180,17 @@ export class HermesClientService {
|
||||
});
|
||||
}
|
||||
|
||||
public fetchRedeemableActions() {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
||||
this.send(3, {
|
||||
request_id: null,
|
||||
type: "get_redeemable_actions",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
public heartbeat() {
|
||||
const date = new Date()
|
||||
this.send(0, {
|
||||
@ -166,11 +198,21 @@ export class HermesClientService {
|
||||
});
|
||||
}
|
||||
|
||||
public subscribe(code: number, action: (data: any) => void) {
|
||||
if (!(code in this.subscriptions)) {
|
||||
this.subscriptions[code] = []
|
||||
public subscribe(code: number, next: (data: any) => void) {
|
||||
return this.socket.subscribe({
|
||||
next: (message: any) => {
|
||||
if (message.op == code)
|
||||
next(message.d);
|
||||
}
|
||||
this.subscriptions[code].push(action);
|
||||
});
|
||||
}
|
||||
|
||||
public subscribeToRequests(requestType: string, action: (data: any) => void) {
|
||||
return this.subscribe(4, (data) => {
|
||||
const type = data.request.type;
|
||||
if (type == requestType)
|
||||
action(data);
|
||||
});
|
||||
}
|
||||
|
||||
public updatePolicy(id: string, groupId: string, path: string, usage: number, timespan: number) {
|
||||
@ -186,6 +228,18 @@ export class HermesClientService {
|
||||
});
|
||||
}
|
||||
|
||||
public updateRedeemableAction(name: string, type: string, d: { [key: string]: any }) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
||||
this.send(3, {
|
||||
request_id: null,
|
||||
type: "update_redeemable_action",
|
||||
data: { name, type, data: d },
|
||||
nounce: this.session_id,
|
||||
});
|
||||
}
|
||||
|
||||
public updateTTSFilter(id: string, search: string, replace: string) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
@ -207,18 +261,11 @@ export class HermesClientService {
|
||||
console.log("Heartbeat received. Potential connection problem?");
|
||||
break;
|
||||
case 2: // Login Ack
|
||||
console.log("Login successful.", message.d.session_id);
|
||||
console.log("Login successful.");
|
||||
this.logged_in = true;
|
||||
this.session_id = message.d.session_id;
|
||||
this.events.emit('tts_login_ack', null);
|
||||
break;
|
||||
case 4: // Request Ack
|
||||
console.log("Request ack received.");
|
||||
break;
|
||||
}
|
||||
if (message.op in this.subscriptions) {
|
||||
for (let action of this.subscriptions[message.op])
|
||||
action(message.d);
|
||||
}
|
||||
},
|
||||
error: (err: any) => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { OnInit, Injectable } from '@angular/core';
|
||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
||||
import { catchError, filter, first, timeout } from 'rxjs/operators';
|
||||
import { catchError, first, timeout } from 'rxjs/operators';
|
||||
import { environment } from '../environments/environment';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<nav>
|
||||
<impersonation />
|
||||
<user-card class="card" />
|
||||
<ul>
|
||||
<li>
|
||||
<a routerLink="/login" routerLinkActive="active" *ngIf="!isLoggedIn()">
|
||||
|
@ -4,7 +4,6 @@ $primary_font_color: #111111;
|
||||
$secondary_background_color: #DDDDDD;
|
||||
$secondary_font_color: #333333;
|
||||
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -5,12 +5,12 @@ import { HermesClientService } from '../hermes-client.service';
|
||||
import { ApiAuthenticationService } from '../shared/services/api/api-authentication.service';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { ImpersonationComponent } from "../auth/impersonation/impersonation.component";
|
||||
import { UserCardComponent } from "../auth/user-card/user-card.component";
|
||||
|
||||
@Component({
|
||||
selector: 'navigation',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterModule, MatCardModule, AuthModule, ImpersonationComponent],
|
||||
imports: [CommonModule, RouterModule, MatCardModule, AuthModule, UserCardComponent],
|
||||
templateUrl: './navigation.component.html',
|
||||
styleUrl: './navigation.component.scss'
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div>
|
||||
<form
|
||||
standalone>
|
||||
<mat-form-field class="example-full-width">
|
||||
<mat-form-field>
|
||||
<input
|
||||
name="path"
|
||||
type="text"
|
||||
|
@ -38,16 +38,12 @@ export class PolicyTableComponent implements OnInit, OnDestroy {
|
||||
this.table.renderRows();
|
||||
});
|
||||
this.hermes.subscribe(4, (response: any) => {
|
||||
console.log('request received: ', response);
|
||||
if (response.request.type == "get_policies") {
|
||||
for (let policy of response.data) {
|
||||
this.policies.push(new Policy(policy.id, policy.group_id, policy.path, policy.usage, policy.span, "", false, false));
|
||||
}
|
||||
if (isDevMode())
|
||||
console.log('policies', this.policies);
|
||||
this.table.renderRows();
|
||||
} else if (response.request.type == "create_policy") {
|
||||
console.log("create policy", response);
|
||||
const policy = this.policies.find(p => this.groups[response.data.group_id].name == p.temp_group_name && p.path == response.data.path);
|
||||
if (policy == null) {
|
||||
this.policies.push(new Policy(response.data.id, response.data.group_id, response.data.path, response.data.usage, response.data.span));
|
||||
@ -59,7 +55,6 @@ export class PolicyTableComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.table.renderRows();
|
||||
} else if (response.request.type == "update_policy") {
|
||||
console.log("update policy", response);
|
||||
const policy = this.policies.find(p => p.id == response.data.id);
|
||||
if (policy == null) {
|
||||
this.policies.push(new Policy(response.data.id, response.data.group_id, response.data.path, response.data.usage, response.data.span));
|
||||
@ -71,10 +66,8 @@ export class PolicyTableComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.table.renderRows();
|
||||
} else if (response.request.type == "delete_policy") {
|
||||
console.log('delete policy', response.request.data.id);
|
||||
const policy = this.policies.find(p => p.id == response.request.data.id);
|
||||
if (!policy) {
|
||||
console.log('Could not find the policy by id. Already deleted.');
|
||||
return;
|
||||
}
|
||||
const index = this.policies.indexOf(policy);
|
||||
@ -84,7 +77,6 @@ export class PolicyTableComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
} else if (response.request.type == "get_permissions") {
|
||||
this.groups = Object.assign({}, ...response.data.groups.map((g: any) => ({ [g.id]: g })));
|
||||
console.log('groups', response.data)
|
||||
}
|
||||
});
|
||||
this.hermes.fetchPolicies();
|
||||
|
@ -1,6 +1,9 @@
|
||||
export enum FilterFlag {
|
||||
None = 0,
|
||||
IgnoreCase = 1,
|
||||
ExplicitCapture = 4,
|
||||
CultureInvariant = 512,
|
||||
NonBacktracking = 1024,
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
|
6
src/app/shared/models/redeemable_action.ts
Normal file
6
src/app/shared/models/redeemable_action.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export default interface RedeemableAction {
|
||||
user_id: string
|
||||
name: string
|
||||
type: string
|
||||
data: any
|
||||
}
|
@ -32,6 +32,11 @@ export class ApiAuthenticationService {
|
||||
return this.user?.name;
|
||||
}
|
||||
|
||||
logout() {
|
||||
localStorage.removeItem('jwt');
|
||||
this.updateAuthenticated(false, null);
|
||||
}
|
||||
|
||||
update() {
|
||||
const jwt = localStorage.getItem('jwt');
|
||||
if (!jwt) {
|
||||
|
14
src/app/shared/validators/item-exists-in-array.ts
Normal file
14
src/app/shared/validators/item-exists-in-array.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
|
||||
|
||||
|
||||
export function createItemExistsInArrayValidator(items: any[], getter: (value: any) => any): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const value = control.value;
|
||||
|
||||
if (!value)
|
||||
return null;
|
||||
|
||||
const matches = items.some(i => getter(i) == value);
|
||||
return matches ? { itemExistsInArray: true } : null;
|
||||
}
|
||||
}
|
@ -33,7 +33,11 @@ export class FilterItemEditComponent {
|
||||
});
|
||||
|
||||
|
||||
onSaveClick(): Filter {
|
||||
onSaveClick(): Filter|undefined {
|
||||
if (this.forms.invalid) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.data.search = this.forms.value.search ?? '';
|
||||
this.data.replace = this.forms.value.replace ?? '';
|
||||
this.data.flag = this.forms.value.flag ?? 0;
|
||||
|
@ -37,7 +37,6 @@ export class FilterItemComponent implements OnInit {
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: Filter) => {
|
||||
if (result !== undefined) {
|
||||
console.log('update filter', result);
|
||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'update_tts_filter' && d.d.data.id == this.item.id)
|
||||
?.subscribe(_ => {
|
||||
this.item.search = result.search;
|
||||
|
@ -28,24 +28,19 @@ export class FiltersComponent implements OnInit, OnDestroy {
|
||||
this.items = []
|
||||
this.client.subscribe(4, d => {
|
||||
const type = d.request.type;
|
||||
console.log('filters', type, d.data);
|
||||
|
||||
if (type == 'get_tts_word_filters') {
|
||||
this.items = d.data;
|
||||
return;
|
||||
}
|
||||
if (d.request.nounce == client.session_id) {
|
||||
console.log('from us. ignore.');
|
||||
return;
|
||||
}
|
||||
if (type == 'create_tts_filter') {
|
||||
console.log('create filter', d.data);
|
||||
this.items = [d.data, ...this.items];
|
||||
} else if (type == 'delete_tts_filter') {
|
||||
console.log('delete filter', d.data);
|
||||
this.items = this.items.filter(i => i.id != d.data.id);
|
||||
} else if (type == 'update_tts_filter') {
|
||||
console.log('update filter', d.data);
|
||||
const filter = this.items.find(f => f.id == d.data.id);
|
||||
if (filter == null)
|
||||
return;
|
||||
@ -78,10 +73,8 @@ export class FiltersComponent implements OnInit, OnDestroy {
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: any) => {
|
||||
if (result !== undefined) {
|
||||
console.log('filters create result', result);
|
||||
this.client.first(d => d.op == 4 && d.d.request.type == 'create_tts_filter' && d.d.data.search == result.search && d.d.data.replace == result.replace)
|
||||
?.subscribe(d => {
|
||||
console.log('adding filter', d.d.data);
|
||||
this.items = [d.d.data, ...this.items];
|
||||
});
|
||||
this.client.createTTSFilter(result.search, result.replace);
|
||||
|
@ -28,13 +28,11 @@ export class TwitchAuthCallbackComponent implements OnInit {
|
||||
const scope = this.route.snapshot.queryParamMap.get('scope');
|
||||
const state = this.route.snapshot.queryParamMap.get('state');
|
||||
|
||||
console.log('twitch callback', code, scope, state);
|
||||
if (!code || !scope || !state)
|
||||
return;
|
||||
|
||||
this.http.post(environment.API_HOST + '/auth/twitch/callback', { code, scope, state })
|
||||
.subscribe((data: any) => {
|
||||
console.log('twitch callback response', code, scope, state, data);
|
||||
if (!data?.authenticated) {
|
||||
this.router.navigate(['/login?error=callback_error']);
|
||||
return;
|
||||
|
Loading…
x
Reference in New Issue
Block a user