From 59bed7c28b8d6b9bb12eda1e5e77e96d43026d54 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 15 Jan 2025 19:45:56 +0000 Subject: [PATCH] Updated TTS Filters page to use resolvers, services and better UX. --- src/app/app.routes.ts | 4 + src/app/hermes-client.service.ts | 9 +- src/app/shared/models/filter.ts | 1 - .../shared/resolvers/tts-filter-resolver.ts | 14 +++ .../services/tts-filter.service.spec.ts | 16 +++ src/app/shared/services/tts-filter.service.ts | 55 +++++++++ .../filter-item-edit.component.html | 79 ++++++++----- .../filter-item-edit.component.scss | 4 +- .../filter-item-edit.component.ts | 104 +++++++++++++++--- .../filter-item/filter-item.component.ts | 27 ++--- .../filter-list/filter-list.component.scss | 2 + .../filter-list/filter-list.component.ts | 23 ++-- .../tts-filters/filters/filters.component.ts | 85 ++++++++------ 13 files changed, 313 insertions(+), 110 deletions(-) create mode 100644 src/app/shared/resolvers/tts-filter-resolver.ts create mode 100644 src/app/shared/services/tts-filter.service.spec.ts create mode 100644 src/app/shared/services/tts-filter.service.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 323c3dd..604000a 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -12,6 +12,7 @@ import { RedemptionsComponent } from './redemptions/redemptions/redemptions.comp import RedemptionResolver from './shared/resolvers/redemption-resolver'; import TwitchRedemptionResolver from './shared/resolvers/twitch-redemption-resolver'; import RedeemableActionResolver from './shared/resolvers/redeemable-action-resolver'; +import TtsFilterResolver from './shared/resolvers/tts-filter-resolver'; export const routes: Routes = [ { @@ -23,6 +24,9 @@ export const routes: Routes = [ path: 'filters', component: FiltersComponent, canActivate: [AuthUserGuard], + resolve: { + filters: TtsFilterResolver, + } }, { path: 'actions', diff --git a/src/app/hermes-client.service.ts b/src/app/hermes-client.service.ts index 246d3bd..57e085d 100644 --- a/src/app/hermes-client.service.ts +++ b/src/app/hermes-client.service.ts @@ -127,14 +127,15 @@ export class HermesClientService { }); } - public createTTSFilter(search: string, replace: string) { + public createTTSFilter(search: string, replace: string, flag: number) { if (!this.logged_in) return; this.send(3, { request_id: null, type: "create_tts_filter", - data: { search, replace }, + data: { search, replace, flag }, + nounce: this.session_id, }); } @@ -301,14 +302,14 @@ export class HermesClientService { }); } - public updateTTSFilter(id: string, search: string, replace: string) { + public updateTTSFilter(id: string, search: string, replace: string, flag: number) { if (!this.logged_in) return; this.send(3, { request_id: null, type: "update_tts_filter", - data: { id, search, replace }, + data: { id, search, replace, flag }, nounce: this.session_id, }); } diff --git a/src/app/shared/models/filter.ts b/src/app/shared/models/filter.ts index 1a74069..5db7c3d 100644 --- a/src/app/shared/models/filter.ts +++ b/src/app/shared/models/filter.ts @@ -12,5 +12,4 @@ export interface Filter { replace: string; user_id: string; flag: FilterFlag; - is_regex: boolean; } \ No newline at end of file diff --git a/src/app/shared/resolvers/tts-filter-resolver.ts b/src/app/shared/resolvers/tts-filter-resolver.ts new file mode 100644 index 0000000..98f5a1e --- /dev/null +++ b/src/app/shared/resolvers/tts-filter-resolver.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; +import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { Filter } from '../models/filter'; +import { TtsFilterService } from '../services/tts-filter.service'; + +@Injectable({ providedIn: 'root' }) +export default class TtsFilterResolver implements Resolve { + constructor(private service: TtsFilterService) { } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.service.fetch(); + } +} \ No newline at end of file diff --git a/src/app/shared/services/tts-filter.service.spec.ts b/src/app/shared/services/tts-filter.service.spec.ts new file mode 100644 index 0000000..0c58b36 --- /dev/null +++ b/src/app/shared/services/tts-filter.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { TtsFilterService } from './tts-filter.service'; + +describe('TtsFilterService', () => { + let service: TtsFilterService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(TtsFilterService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/shared/services/tts-filter.service.ts b/src/app/shared/services/tts-filter.service.ts new file mode 100644 index 0000000..6e91257 --- /dev/null +++ b/src/app/shared/services/tts-filter.service.ts @@ -0,0 +1,55 @@ +import { inject, Injectable } from '@angular/core'; +import { HermesClientService } from '../../hermes-client.service'; +import { map, Observable, of } from 'rxjs'; +import EventService from './EventService'; +import { Filter } from '../models/filter'; + +@Injectable({ + providedIn: 'root' +}) +export class TtsFilterService { + private readonly client = inject(HermesClientService); + private readonly events = inject(EventService); + private data: Filter[] = [] + private loaded = false; + create$: Observable | undefined; + update$: Observable | undefined; + delete$: Observable | undefined; + + constructor() { + this.create$ = this.client.filterByRequestType('create_tts_filter'); + this.update$ = this.client.filterByRequestType('update_tts_filter'); + this.delete$ = this.client.filterByRequestType('delete_tts_filter'); + + this.create$?.subscribe(d => this.data.push(d.data)); + this.update$?.subscribe(d => { + const filter = this.data.find(r => r.id == d.data.id); + if (filter) { + filter.search = d.data.action_name; + filter.replace = d.data.redemption_id; + filter.flag = d.data.order; + } + }); + this.delete$?.subscribe(d => this.data = this.data.filter(r => r.id != d.request.data.id)); + + this.events.listen('tts_logoff', () => { + this.data = []; + this.loaded = false; + }); + } + + + fetch() { + if (this.loaded) { + return of(this.data); + } + + const $ = this.client.first(d => d.d.request.type == 'get_tts_word_filters')!.pipe(map(d => d.d.data)); + $.subscribe(d => { + this.data = d; + this.loaded = true; + }); + this.client.fetchFilters(); + return $; + } +} diff --git a/src/app/tts-filters/filter-item-edit/filter-item-edit.component.html b/src/app/tts-filters/filter-item-edit/filter-item-edit.component.html index a3ac233..7122a2b 100644 --- a/src/app/tts-filters/filter-item-edit/filter-item-edit.component.html +++ b/src/app/tts-filters/filter-item-edit/filter-item-edit.component.html @@ -1,26 +1,55 @@ -

TTS Filter

- -
-
- - Search - - @if (forms.get('search')?.invalid && (forms.get('search')?.dirty || forms.get('search')?.touched)) { -
Search is required.
+ + +

TTS Filter

+ + +
+ + Search + + @if (forms.get('search')?.invalid && (forms.get('search')?.dirty || forms.get('search')?.touched)) { + Search is required. + } + +
+
+ + Replace + + +
+
+ + Regex Options + + + {{optionsSelected[0] || ''}} + @if ((flagControl.value?.length || 0) > 1) { + + (+{{flagControl.value!.length - 1}}) + + } + + @for (option of regexOptions; track option) { + {{option.name}} + } + + +
+ @if (errorMessage) { + {{errorMessage}} } -
-
-
- - Replace - - -
-
-
- - - - \ No newline at end of file + + + + + + + + \ No newline at end of file diff --git a/src/app/tts-filters/filter-item-edit/filter-item-edit.component.scss b/src/app/tts-filters/filter-item-edit/filter-item-edit.component.scss index 3a1808f..16c425e 100644 --- a/src/app/tts-filters/filter-item-edit/filter-item-edit.component.scss +++ b/src/app/tts-filters/filter-item-edit/filter-item-edit.component.scss @@ -1,3 +1,3 @@ -.validation-error { - color: red($color: #000000); +.error { + color: #ba1a1a; } \ No newline at end of file diff --git a/src/app/tts-filters/filter-item-edit/filter-item-edit.component.ts b/src/app/tts-filters/filter-item-edit/filter-item-edit.component.ts index 796c2a8..69bab96 100644 --- a/src/app/tts-filters/filter-item-edit/filter-item-edit.component.ts +++ b/src/app/tts-filters/filter-item-edit/filter-item-edit.component.ts @@ -1,10 +1,13 @@ -import { Component, inject, model } from '@angular/core'; -import { MatDialogRef, MAT_DIALOG_DATA, MatDialogActions, MatDialogClose, MatDialogTitle, MatDialogContent } from '@angular/material/dialog'; +import { Component, inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA, MatDialogActions, MatDialogTitle, MatDialogContent } from '@angular/material/dialog'; import { Filter } from '../../shared/models/filter'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatSelectChange, MatSelectModule } from '@angular/material/select'; +import { HermesClientService } from '../../hermes-client.service'; @Component({ selector: 'tts-filter-item-edit', @@ -12,39 +15,110 @@ import { MatButtonModule } from '@angular/material/button'; imports: [ FormsModule, MatButtonModule, + MatCardModule, MatDialogActions, - MatDialogClose, MatDialogContent, MatDialogTitle, MatFormFieldModule, MatInputModule, + MatSelectModule, ReactiveFormsModule, ], templateUrl: './filter-item-edit.component.html', styleUrl: './filter-item-edit.component.scss' }) export class FilterItemEditComponent { - readonly dialogRef = inject(MatDialogRef); + private readonly client = inject(HermesClientService); + + private readonly dialogRef = inject(MatDialogRef); readonly data = inject(MAT_DIALOG_DATA); + + readonly regexOptions = [ + { + name: 'Ignore Case', + flag: 1 + }, + { + name: 'Explicit Capture', + flag: 4 + }, + { + name: 'Ignore Pattern Whitespace', + flag: 16 + }, + { + name: 'No Backtracking', + flag: 1024 + }, + ]; + + readonly searchControl = new FormControl(this.data.search, [Validators.required]); + readonly replaceControl = new FormControl(this.data.replace); + readonly flagControl = new FormControl(this.optionsSelected); readonly forms = new FormGroup({ - search: new FormControl(this.data.search, [Validators.required]), - replace: new FormControl(this.data.replace), - flag: new FormControl(this.data.flag), + search: this.searchControl, + replace: this.replaceControl, + flag: this.flagControl, }); + waitForResponse: boolean = false; + errorMessage: string | undefined; - onSaveClick(): Filter|undefined { - if (this.forms.invalid) { - return undefined; + + get optionsSelected() { + const flag = this.data.flag; + return this.regexOptions.filter(o => (flag & o.flag) > 0).map(o => o.name); + } + + onSaveClick(): void { + if (this.forms.invalid) + return; + + this.waitForResponse = true; + this.errorMessage = undefined; + + const search = this.searchControl.value!; + const replace = this.replaceControl.value!; + const flag = this.data.flag; + + if (!this.data.id) { + this.client.first(d => d.op == 4 && d.d.request.type == 'create_tts_filter' && d.d.data.search == search && d.d.data.replace == replace && d.d.data.flag == flag) + ?.subscribe({ + next: d => { + if (d.error) + this.errorMessage = d.error; + else + this.dialogRef.close(d.d.data); + }, + error: () => this.waitForResponse = false, + complete: () => this.waitForResponse = false, + }); + this.client.createTTSFilter(search, replace, flag); + } else { + this.client.first(d => d.op == 4 && d.d.request.type == 'update_tts_filter' && d.d.data.id == this.data.id && d.d.data.search == search && d.d.data.replace == replace && d.d.data.flag == flag) + ?.subscribe({ + next: d => { + if (d.error) + this.errorMessage = d.error; + else + this.dialogRef.close(d.d.data); + }, + error: () => this.waitForResponse = false, + complete: () => this.waitForResponse = false, + }); + this.client.updateTTSFilter(this.data.id, search, replace, flag); } - - this.data.search = this.forms.value.search ?? ''; - this.data.replace = this.forms.value.replace ?? ''; - this.data.flag = this.forms.value.flag ?? 0; - return this.data; } onCancelClick(): void { this.dialogRef.close(); } + + onSelectionChange(event: MatSelectChange) { + this.data.flag = event.value.reduce((acc: number, option: any) => acc | option.flag, 0); + } + + compare(o: any, s: any) { + return o.flag == s || o.name == s; + } } diff --git a/src/app/tts-filters/filter-item/filter-item.component.ts b/src/app/tts-filters/filter-item/filter-item.component.ts index f2acdaa..b6fa1f4 100644 --- a/src/app/tts-filters/filter-item/filter-item.component.ts +++ b/src/app/tts-filters/filter-item/filter-item.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, inject, Input, OnInit, Output, signal } from '@angular/core'; +import { Component, EventEmitter, inject, Input, OnInit, Output } from '@angular/core'; import { Filter, FilterFlag } from '../../shared/models/filter'; import { MatCardModule } from '@angular/material/card'; import { MatMenuModule } from '@angular/material/menu'; @@ -9,14 +9,14 @@ import { MatButtonModule } from '@angular/material/button'; import { HermesClientService } from '../../hermes-client.service'; @Component({ - selector: 'tts-filter-item', - standalone: true, - imports: [MatButtonModule, MatCardModule, MatMenuModule, MatIconModule], - templateUrl: './filter-item.component.html', - styleUrl: './filter-item.component.scss' + selector: 'tts-filter-item', + standalone: true, + imports: [MatButtonModule, MatCardModule, MatMenuModule, MatIconModule], + templateUrl: './filter-item.component.html', + styleUrl: './filter-item.component.scss' }) export class FilterItemComponent implements OnInit { - @Input() item: Filter = { id: "", user_id: "", search: "", replace: "", flag: FilterFlag.None, is_regex: false }; + @Input() item: Filter = { id: "", user_id: "", search: "", replace: "", flag: FilterFlag.None }; @Output() onDelete = new EventEmitter(); readonly client = inject(HermesClientService); readonly dialog = inject(MatDialog); @@ -32,17 +32,14 @@ export class FilterItemComponent implements OnInit { return; const dialogRef = this.dialog.open(FilterItemEditComponent, { - data: { id: this.item.id, search: this.item.search, replace: this.item.replace }, + data: { id: this.item.id, search: this.item.search, replace: this.item.replace, flag: this.item.flag }, }); dialogRef.afterClosed().subscribe((result: Filter) => { - if (result !== undefined) { - 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; - this.item.replace = result.replace; - }); - this.client.updateTTSFilter(this.item.id, result.search, result.replace); + if (result) { + this.item.search = result.search; + this.item.replace = result.replace; + this.item.flag = result.flag; } }); } diff --git a/src/app/tts-filters/filter-list/filter-list.component.scss b/src/app/tts-filters/filter-list/filter-list.component.scss index 2c973f9..454635e 100644 --- a/src/app/tts-filters/filter-list/filter-list.component.scss +++ b/src/app/tts-filters/filter-list/filter-list.component.scss @@ -5,6 +5,8 @@ div { ul.data { margin: 0; padding: 0; + overflow: auto; + height: calc(100vh - 60px); > li { display: block; diff --git a/src/app/tts-filters/filter-list/filter-list.component.ts b/src/app/tts-filters/filter-list/filter-list.component.ts index 08a383e..ae8ed7f 100644 --- a/src/app/tts-filters/filter-list/filter-list.component.ts +++ b/src/app/tts-filters/filter-list/filter-list.component.ts @@ -4,18 +4,17 @@ import { Filter } from '../../shared/models/filter'; import { HermesClientService } from '../../hermes-client.service'; @Component({ - selector: 'tts-filter-list', - standalone: true, - imports: [FilterItemComponent], - templateUrl: './filter-list.component.html', - styleUrl: './filter-list.component.scss' + selector: 'tts-filter-list', + standalone: true, + imports: [FilterItemComponent], + templateUrl: './filter-list.component.html', + styleUrl: './filter-list.component.scss' }) export class FilterListComponent { - @Input() filters: Filter[] = []; - client = inject(HermesClientService); + @Input() filters: Filter[] = []; + client = inject(HermesClientService); - deleteFilter(e: any): void { - this.client.deleteTTSFilter(e.id); - this.filters = this.filters.filter(f => f.id != e.id); - } -} + deleteFilter(e: any): void { + this.client.deleteTTSFilter(e.id); + } +} \ No newline at end of file diff --git a/src/app/tts-filters/filters/filters.component.ts b/src/app/tts-filters/filters/filters.component.ts index ede3922..544e50e 100644 --- a/src/app/tts-filters/filters/filters.component.ts +++ b/src/app/tts-filters/filters/filters.component.ts @@ -1,22 +1,27 @@ -import { Component, inject, Inject, Input, OnDestroy, OnInit, PLATFORM_ID, signal } from '@angular/core'; +import { Component, inject, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { FilterListComponent } from "../filter-list/filter-list.component"; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { HermesClientService } from '../../hermes-client.service'; -import { Filter } from '../../shared/models/filter'; +import { Filter, FilterFlag } from '../../shared/models/filter'; import { isPlatformBrowser } from '@angular/common'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { FilterItemEditComponent } from '../filter-item-edit/filter-item-edit.component'; import { MatDialog } from '@angular/material/dialog'; +import { TtsFilterService } from '../../shared/services/tts-filter.service'; +import { Subscription } from 'rxjs'; @Component({ - selector: 'filters', - standalone: true, - imports: [FilterListComponent, MatButtonModule, MatIconModule], - templateUrl: './filters.component.html', - styleUrl: './filters.component.scss' + selector: 'filters', + standalone: true, + imports: [FilterListComponent, MatButtonModule, MatIconModule], + templateUrl: './filters.component.html', + styleUrl: './filters.component.scss' }) export class FiltersComponent implements OnInit, OnDestroy { + private readonly filterService = inject(TtsFilterService); + private readonly route = inject(ActivatedRoute); + private readonly subscriptions: Subscription[] = []; private isBrowser: boolean; readonly dialog = inject(MatDialog); items: Filter[]; @@ -24,33 +29,45 @@ export class FiltersComponent implements OnInit, OnDestroy { constructor(private client: HermesClientService, private router: Router, @Inject(PLATFORM_ID) private platformId: Object) { this.isBrowser = isPlatformBrowser(this.platformId); + this.items = []; - this.items = [] - this.client.subscribe(4, d => { - const type = d.request.type; - - if (type == 'get_tts_word_filters') { - this.items = d.data; + this.route.data.subscribe(data => { + if (!data['filters']) return; - } - if (d.request.nounce == client.session_id) { - return; - } - if (type == 'create_tts_filter') { - this.items = [d.data, ...this.items]; - } else if (type == 'delete_tts_filter') { - this.items = this.items.filter(i => i.id != d.data.id); - } else if (type == 'update_tts_filter') { - const filter = this.items.find(f => f.id == d.data.id); - if (filter == null) - return; + this.items = [...data['filters']]; + }); + let subscription = this.filterService.create$?.subscribe(d => { + if (d.error || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id)) + return; + + this.items.push(d.data); + }); + if (subscription) + this.subscriptions.push(subscription); + + subscription = this.filterService.update$?.subscribe(d => { + if (d.error || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id)) + return; + + const filter = this.items.find(f => f.id == d.data.id); + if (filter) { filter.search = d.data.search; filter.replace = d.data.replace; filter.flag = d.data.flag || 0; } }); - this.client.fetchFilters(); + if (subscription) + this.subscriptions.push(subscription); + + subscription = this.filterService.delete$?.subscribe(d => { + if (d.error) + return; + + this.items = this.items.filter(a => a.id != d.request.data.id); + }); + if (subscription) + this.subscriptions.push(subscription); } ngOnInit(): void { @@ -64,21 +81,17 @@ export class FiltersComponent implements OnInit, OnDestroy { } ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); } openDialog(): void { const dialogRef = this.dialog.open(FilterItemEditComponent, { - data: { search: '', replace: '' }, + data: { id: '', user_id: '', search: '', replace: '', flag: FilterFlag.None }, }); - dialogRef.afterClosed().subscribe((result: any) => { - if (result !== undefined) { - 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 => { - this.items = [d.d.data, ...this.items]; - }); - this.client.createTTSFilter(result.search, result.replace); - } + dialogRef.afterClosed().subscribe((result: Filter) => { + if (result) + this.items.push(result); }); } } \ No newline at end of file