diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 692c39e..f3182e7 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,19 +1,20 @@ -import { CommonModule, DatePipe, isPlatformBrowser } from '@angular/common'; +import { CommonModule, isPlatformBrowser } from '@angular/common'; import { Component, OnInit, Inject, PLATFORM_ID, NgZone, OnDestroy } from '@angular/core'; import { Router, RouterOutlet } from '@angular/router'; import { FormsModule } from '@angular/forms' import { HermesClientService } from './hermes-client.service'; import { AuthUserGuard } from './shared/auth/auth.user.guard' import { Subscription } from 'rxjs'; -import { PolicyComponent } from "./policies/policy/policy.component"; import { NavigationComponent } from "./navigation/navigation.component"; import EventService from './shared/services/EventService'; import { ApiAuthenticationService } from './shared/services/api/api-authentication.service'; +import { PoliciesModule } from './policies/policies.module'; +import { TtsFiltersModule } from './tts-filters/tts-filters.module'; @Component({ selector: 'app-root', standalone: true, - imports: [RouterOutlet, CommonModule, FormsModule, PolicyComponent, NavigationComponent], + imports: [RouterOutlet, CommonModule, FormsModule, PoliciesModule, TtsFiltersModule, NavigationComponent], providers: [AuthUserGuard], templateUrl: './app.component.html', styleUrl: './app.component.scss' @@ -22,7 +23,6 @@ export class AppComponent implements OnInit, OnDestroy { private isBrowser: boolean; private ngZone: NgZone; private subscriptions: Subscription[]; - pipe = new DatePipe('en-US') constructor(private auth: ApiAuthenticationService, private client: HermesClientService, private events: EventService, private router: Router, ngZone: NgZone, @Inject(PLATFORM_ID) private platformId: Object) { diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index b4e57c9..38d45a9 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -4,6 +4,7 @@ import { AuthUserGuard } from './shared/auth/auth.user.guard'; import { LoginComponent } from './login/login.component'; import { TtsLoginComponent } from './tts-login/tts-login.component'; import { TwitchAuthCallbackComponent } from './twitch-auth-callback/twitch-auth-callback.component'; +import { FiltersComponent } from './tts-filters/filters/filters.component'; export const routes: Routes = [ { @@ -11,6 +12,11 @@ export const routes: Routes = [ component: PolicyComponent, canActivate: [AuthUserGuard], }, + { + path: 'filters', + component: FiltersComponent, + canActivate: [AuthUserGuard], + }, { path: 'login', component: LoginComponent, diff --git a/src/app/hermes-client.service.ts b/src/app/hermes-client.service.ts index 6eb10fa..27ce587 100644 --- a/src/app/hermes-client.service.ts +++ b/src/app/hermes-client.service.ts @@ -2,176 +2,236 @@ import { Injectable } from '@angular/core'; import { DatePipe } from '@angular/common'; import { HermesSocketService } from './hermes-socket.service'; import EventService from './shared/services/EventService'; +import { Observable } from 'rxjs'; export interface Message { - d: object, - t: object, - o: object + d: object, + t: object, + o: object } @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class HermesClientService { - pipe = new DatePipe('en-US') - connected: boolean; - logged_in: boolean; - - private subscriptions: { [key: number]: ((data: any) => void)[] } + pipe = new DatePipe('en-US'); + session_id: string|undefined; + connected: boolean; + logged_in: boolean; - constructor(private socket: HermesSocketService, private events: EventService) { - this.subscriptions = {}; - this.connected = false; - this.logged_in = false; + private subscriptions: { [key: number]: ((data: any) => void)[] } - this.events.listen('tts_login', (payload) => { - this.login(payload); - }); - } + constructor(private socket: HermesSocketService, private events: EventService) { + this.subscriptions = {}; + this.connected = false; + this.logged_in = false; - public connect() { - if (this.connected) - return; + this.events.listen('tts_login', (payload) => { + this.login(payload); + }); + } - this.socket.connect(); - this.connected = true; - return this.listen(); - } + public connect() { + if (this.connected) + return; - public disconnect() { - if (!this.connected) - return; + this.socket.connect(); + this.connected = true; + return this.listen(); + } - this.connected = false; - this.logged_in = false; - this.socket.close(); - this.events.emit('tts_logoff', null); + public disconnect() { + if (!this.connected) + return; + + this.connected = false; + this.logged_in = false; + this.session_id = undefined; + this.socket.close(); + this.events.emit('tts_logoff', null); + } + + public first(predicate: (data: any) => boolean): Observable | null { + return this.socket.first(predicate); + } + + private send(op: number, data: any) { + if (op != 0) + console.log("TX:", data); + + this.socket.sendMessage({ + d: data, + op + }); + } + + public login(api_key: string) { + if (!this.connected) + this.connect(); + if (this.logged_in) + return; + + this.send(1, { + api_key, + web_login: true, + major_version: 0, + minor_version: 1 + }); + } + + public createPolicy(groupId: string, path: string, usage: number, timespan: number) { + if (!this.logged_in) + return; + + this.send(3, { + request_id: null, + type: "create_policy", + data: { + groupId, path, count: usage, span: timespan + }, + }); + } + + public createTTSFilter(search: string, replace: string) { + if (!this.logged_in) + return; + + this.send(3, { + request_id: null, + type: "create_tts_filter", + data: { search, replace }, + }); + } + + public deletePolicy(id: string) { + if (!this.logged_in) + return; + + this.send(3, { + request_id: null, + type: "delete_policy", + data: { id }, + }); + } + + public deleteTTSFilter(id: string) { + if (!this.logged_in) + return; + + this.send(3, { + request_id: null, + type: "delete_tts_filter", + data: { id }, + nounce: this.session_id, + }); + } + + public fetchFilters() { + if (!this.logged_in) + return; + + this.send(3, { + request_id: null, + type: "get_tts_word_filters", + data: null, + }); + } + + public fetchPermissionsAndGroups() { + if (!this.logged_in) + return; + + this.send(3, { + request_id: null, + type: "get_permissions", + data: null, + }); + } + + public fetchPolicies() { + if (!this.logged_in) + return; + + this.send(3, { + request_id: null, + type: "get_policies", + data: null, + }); + } + + public heartbeat() { + const date = new Date() + this.send(0, { + date_time: this.pipe.transform(date.getTime() + date.getUTCDate(), "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'") + }); + } + + public subscribe(code: number, action: (data: any) => void) { + if (!(code in this.subscriptions)) { + this.subscriptions[code] = [] } + this.subscriptions[code].push(action); + } - private send(op: number, data: any) { - if (op != 0) - console.log("TX:", data); + public updatePolicy(id: string, groupId: string, path: string, usage: number, timespan: number) { + if (!this.logged_in) + return; - this.socket.sendMessage({ - d: data, - op - }); - } + this.send(3, { + request_id: null, + type: "update_policy", + data: { + id, groupId, path, count: usage, span: timespan + }, + }); + } - public login(api_key: string) { - if (!this.connected) - this.connect(); - if (this.logged_in) - return; - - this.send(1, { - api_key, - web_login: true, - major_version: 0, - minor_version: 1 - }); - } + public updateTTSFilter(id: string, search: string, replace: string) { + if (!this.logged_in) + return; - public createPolicy(groupId: string, path: string, usage: number, timespan: number) { - if (!this.logged_in) - return; - - this.send(3, { - request_id: null, - type: "create_policy", - data: { - groupId, path, count: usage, span: timespan - }, - }); - } + this.send(3, { + request_id: null, + type: "update_tts_filter", + data: { id, search, replace }, + nounce: this.session_id, + }); + } - public deletePolicy(id: string) { - if (!this.logged_in) - return; - - this.send(3, { - request_id: null, - type: "delete_policy", - data: { - id - }, - }); - } - - public fetchPolicies() { - if (!this.logged_in) - return; - - this.send(3, { - request_id: null, - type: "get_policies", - data: null, - }); - } - - public fetchPermissionsAndGroups() { - if (!this.logged_in) - return; - - this.send(3, { - request_id: null, - type: "get_permissions", - data: null, - }); - } - - public heartbeat() { - const date = new Date() - this.send(0, { - date_time: this.pipe.transform(date.getTime() + date.getUTCDate(), "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'") - }); - } - - public subscribe(code: number, action: (data: any) => void) { - if (!(code in this.subscriptions)) { - this.subscriptions[code] = [] - } - this.subscriptions[code].push(action); - } - - public updatePolicy(id: string, groupId: string, path: string, usage: number, timespan: number) { - if (!this.logged_in) - return; - - this.send(3, { - request_id: null, - type: "update_policy", - data: { - id, groupId, path, count: usage, span: timespan - }, - }); - } - - private listen() { - return this.socket.subscribe({ - next: (message: any) => { - console.log("RX:", message); - switch (message.op) { - case 0: // Heartbeat - console.log("Heartbeat received. Potential connection problem?"); - break; - case 2: // Login Ack - console.log("Login successful."); - this.logged_in = true; - this.events.emit('tts_login_ack', null); - break; - case 4: // Request Ack - console.log("Request received."); - break; - } - if (message.op in this.subscriptions) { - console.log('found #' + message.op + ' subscription for ' + message.op); - for (let action of this.subscriptions[message.op]) - action(message.d); - } - }, - error: (err: any) => console.error('Websocket error', err), - complete: () => console.log('Websocket disconnected.') - }); - } + private listen() { + return this.socket.subscribe({ + next: (message: any) => { + console.log("RX:", message); + switch (message.op) { + case 0: // Heartbeat + console.log("Heartbeat received. Potential connection problem?"); + break; + case 2: // Login Ack + console.log("Login successful.", message.d.session_id); + 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) { + console.log('found #' + message.op + ' subscription for ' + message.op); + for (let action of this.subscriptions[message.op]) + action(message.d); + } + }, + error: (err: any) => { + console.error('Websocket error', err); + if (err.type == 'close') { + this.connected = false; + this.logged_in = false; + this.socket.close(); + this.events.emit('tts_logoff', null); + } + }, + complete: () => console.log('Websocket disconnected.') + }); + } } diff --git a/src/app/hermes-socket.service.ts b/src/app/hermes-socket.service.ts index 8a44de5..5b066a1 100644 --- a/src/app/hermes-socket.service.ts +++ b/src/app/hermes-socket.service.ts @@ -1,15 +1,14 @@ -import { Component, OnInit, Injectable } from '@angular/core'; +import { OnInit, Injectable } from '@angular/core'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; -import { catchError, tap, switchAll } from 'rxjs/operators'; -import { EMPTY, Observer, Subject } from 'rxjs'; +import { catchError, filter, first, timeout } from 'rxjs/operators'; import { environment } from '../environments/environment'; +import { Observable, throwError } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class HermesSocketService implements OnInit { - private WS_ENDPOINT = environment.WSS_ENDPOINT; private socket: WebSocketSubject | undefined = undefined constructor() { } @@ -23,6 +22,13 @@ export class HermesSocketService implements OnInit { } } + public first(predicate: (data: any) => boolean): Observable|null { + if (!this.socket || this.socket.closed) + return null; + + return this.socket.pipe(timeout(3000), catchError((e) => throwError(() => 'No response after 3 seconds.')), first(predicate)); + } + private getNewWebSocket() { return webSocket({ url: environment.WSS_ENDPOINT @@ -31,21 +37,21 @@ export class HermesSocketService implements OnInit { public sendMessage(msg: any) { if (!this.socket || this.socket.closed) - return + return; this.socket.next(msg); } public subscribe(subscriptions: any) { if (!this.socket || this.socket.closed) - return + return; return this.socket.subscribe(subscriptions); } public close() { if (!this.socket || this.socket.closed) - return + return; this.socket.complete(); } diff --git a/src/app/navigation/navigation.component.html b/src/app/navigation/navigation.component.html index 9f59bf9..9ae8f09 100644 --- a/src/app/navigation/navigation.component.html +++ b/src/app/navigation/navigation.component.html @@ -25,5 +25,13 @@ Policies +
  • + + Filters + +
  • \ No newline at end of file diff --git a/src/app/navigation/navigation.component.scss b/src/app/navigation/navigation.component.scss index fb51965..fa05adb 100644 --- a/src/app/navigation/navigation.component.scss +++ b/src/app/navigation/navigation.component.scss @@ -29,7 +29,7 @@ a { } a:hover { - background-color: #FCFCFC; + background-color: #FAFAFA; } a.active { diff --git a/src/app/policies/policies.module.ts b/src/app/policies/policies.module.ts index 9008c5f..a579740 100644 --- a/src/app/policies/policies.module.ts +++ b/src/app/policies/policies.module.ts @@ -1,12 +1,13 @@ import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - +import { PolicyComponent } from './policy/policy.component'; +import { PolicyTableComponent } from './policy-table/policy-table.component'; +import { PolicyAddFormComponent } from './policy-add-form/policy-add-form.component'; @NgModule({ declarations: [], imports: [ - CommonModule + PolicyComponent, PolicyTableComponent, PolicyAddFormComponent ] }) -export class PoliciesModule { } +export class PoliciesModule { } \ No newline at end of file diff --git a/src/app/policies/policy-add-form/policy-add-form.component.ts b/src/app/policies/policy-add-form/policy-add-form.component.ts index 9704fed..e1e3b17 100644 --- a/src/app/policies/policy-add-form/policy-add-form.component.ts +++ b/src/app/policies/policy-add-form/policy-add-form.component.ts @@ -1,10 +1,9 @@ import { AsyncPipe } from '@angular/common'; -import { Component, OnInit, Output, EventEmitter } from '@angular/core'; +import { Component } from '@angular/core'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms' import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; -import { Policy } from '../../shared/models/policy'; import EventService from '../../shared/services/EventService'; import { map, Observable, startWith } from 'rxjs'; import { HermesClientService } from '../../hermes-client.service'; @@ -15,7 +14,7 @@ const Policies = [ { path: "tts.chat.bits.read", description: "To read chat messages with bits via TTS" }, { path: "tts.chat.messages.read", description: "To read chat messages via TTS" }, { path: "tts.chat.redemptions.read", description: "To read channel point redemption messages via TTS" }, - //{ path: "tts.chat.subscriptions.read", description: "To read chat messages from subscriptions via TTS" }, + { path: "tts.chat.subscriptions.read", description: "To read chat messages from subscriptions via TTS" }, { path: "tts.commands", description: "To execute commands for TTS" }, { path: "tts.commands.nightbot", description: "To use !nightbot command" }, { path: "tts.commands.obs", description: "To use !obs command" }, diff --git a/src/app/policies/policy-table/policy-table.component.html b/src/app/policies/policy-table/policy-table.component.html index cea0c05..079c856 100644 --- a/src/app/policies/policy-table/policy-table.component.html +++ b/src/app/policies/policy-table/policy-table.component.html @@ -19,7 +19,7 @@ - Usage per span + Usage Rate @if (policy.editing) { diff --git a/src/app/policies/policy/policy.component.scss b/src/app/policies/policy/policy.component.scss index c500c81..61c6793 100644 --- a/src/app/policies/policy/policy.component.scss +++ b/src/app/policies/policy/policy.component.scss @@ -1,7 +1,3 @@ -div { - background-color: black; -} - h4 { text-align: center; } \ No newline at end of file diff --git a/src/app/policies/policy/policy.component.ts b/src/app/policies/policy/policy.component.ts index 894929a..65bd33f 100644 --- a/src/app/policies/policy/policy.component.ts +++ b/src/app/policies/policy/policy.component.ts @@ -1,10 +1,8 @@ -import { Component, Inject, NgZone, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; +import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { PolicyAddFormComponent } from "../policy-add-form/policy-add-form.component"; import { PolicyTableComponent } from "../policy-table/policy-table.component"; import { Policy, PolicyScope } from '../../shared/models/policy'; -import { DatePipe, isPlatformBrowser } from '@angular/common'; -import { OAuthService } from 'angular-oauth2-oidc'; -import { Subscription } from 'rxjs'; +import { isPlatformBrowser } from '@angular/common'; import { HermesClientService } from '../../hermes-client.service'; import { Router, RouterModule } from '@angular/router'; @@ -17,14 +15,10 @@ import { Router, RouterModule } from '@angular/router'; }) export class PolicyComponent implements OnInit, OnDestroy { private isBrowser: boolean; - private ngZone: NgZone; - private subscription: Subscription | undefined; items: Policy[]; - pipe = new DatePipe('en-US') - constructor(private client: HermesClientService, private oauthService: OAuthService, private router: Router, ngZone: NgZone, @Inject(PLATFORM_ID) private platformId: Object) { - this.ngZone = ngZone; + constructor(private client: HermesClientService, private router: Router, @Inject(PLATFORM_ID) private platformId: Object) { this.isBrowser = isPlatformBrowser(this.platformId) this.items = [] @@ -42,12 +36,8 @@ export class PolicyComponent implements OnInit, OnDestroy { this.router.navigate(["/tts-login"]); return; } - - this.subscription = this.client.connect(); } ngOnDestroy() { - if (this.subscription) - this.subscription.unsubscribe() } } \ No newline at end of file diff --git a/src/app/shared/models/filter.ts b/src/app/shared/models/filter.ts new file mode 100644 index 0000000..26aa50f --- /dev/null +++ b/src/app/shared/models/filter.ts @@ -0,0 +1,13 @@ +export enum FilterFlag { + None = 0, + IgnoreCase = 1, +} + +export interface Filter { + id: string; + search: string; + replace: string; + user_id: string; + flag: FilterFlag; + is_regex: boolean; +} \ No newline at end of file diff --git a/src/app/shared/services/api/api-authentication.service.ts b/src/app/shared/services/api/api-authentication.service.ts index 927da02..ca3b93b 100644 --- a/src/app/shared/services/api/api-authentication.service.ts +++ b/src/app/shared/services/api/api-authentication.service.ts @@ -25,7 +25,7 @@ export class ApiAuthenticationService { } getImpersonatedId() { - return this.user.impersonation?.id; + return this.user?.impersonation?.id; } getUsername() { @@ -45,7 +45,6 @@ export class ApiAuthenticationService { 'Authorization': 'Bearer ' + jwt } }).subscribe((data: any) => { - console.log('jwt validation', data); this.updateAuthenticated(data?.authenticated, data?.user); }); } 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 new file mode 100644 index 0000000..64f71c1 --- /dev/null +++ b/src/app/tts-filters/filter-item-edit/filter-item-edit.component.html @@ -0,0 +1,15 @@ +

    TTS Filter

    + + + Search + + + + Replace + + + + + + + \ 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 new file mode 100644 index 0000000..e69de29 diff --git a/src/app/tts-filters/filter-item-edit/filter-item-edit.component.spec.ts b/src/app/tts-filters/filter-item-edit/filter-item-edit.component.spec.ts new file mode 100644 index 0000000..003b6fd --- /dev/null +++ b/src/app/tts-filters/filter-item-edit/filter-item-edit.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FilterItemEditComponent } from './filter-item-edit.component'; + +describe('FilterItemEditComponent', () => { + let component: FilterItemEditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FilterItemEditComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FilterItemEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 0000000..9995ce0 --- /dev/null +++ b/src/app/tts-filters/filter-item-edit/filter-item-edit.component.ts @@ -0,0 +1,42 @@ +import { Component, inject, model } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA, MatDialogActions, MatDialogClose, MatDialogTitle, MatDialogContent } from '@angular/material/dialog'; +import { Filter } from '../../shared/models/filter'; +import { FormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatButtonModule } from '@angular/material/button'; + +@Component({ + selector: 'tts-filter-item-edit', + standalone: true, + imports: [ + FormsModule, + MatButtonModule, + MatDialogActions, + MatDialogClose, + MatDialogContent, + MatDialogTitle, + MatFormFieldModule, + MatInputModule, + ], + templateUrl: './filter-item-edit.component.html', + styleUrl: './filter-item-edit.component.scss' +}) +export class FilterItemEditComponent { + readonly dialogRef = inject(MatDialogRef); + readonly data = inject(MAT_DIALOG_DATA); + readonly search = model(this.data.search); + readonly replace = model(this.data.replace); + readonly flag = model(this.data.flag); + + onSaveClick(): Filter { + this.data.search = this.search(); + this.data.replace = this.replace(); + this.data.flag = this.flag(); + return this.data; + } + + onCancelClick(): void { + this.dialogRef.close(); + } +} diff --git a/src/app/tts-filters/filter-item/filter-item.component.html b/src/app/tts-filters/filter-item/filter-item.component.html new file mode 100644 index 0000000..cd0643b --- /dev/null +++ b/src/app/tts-filters/filter-item/filter-item.component.html @@ -0,0 +1,18 @@ +
      +
    • + {{item.search}} +
    • +
    • + {{item.replace}} +
    • +
    • + + + + + + +
    • +
    \ No newline at end of file diff --git a/src/app/tts-filters/filter-item/filter-item.component.scss b/src/app/tts-filters/filter-item/filter-item.component.scss new file mode 100644 index 0000000..90352b5 --- /dev/null +++ b/src/app/tts-filters/filter-item/filter-item.component.scss @@ -0,0 +1,33 @@ +input { + display: inline; + font-size: large; + row-gap: 2em; +} + +ul { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-evenly; + + li { + list-style-type: none; + white-space: pre; + text-align: justify; + text-wrap: wrap; + + > button { + background: #dddddd; + border-radius: 50%; + + :hover { + border-radius: 50%; + } + } + } + + li:nth-child(1), + li:nth-child(2) { + flex: 1; + } +} \ No newline at end of file diff --git a/src/app/tts-filters/filter-item/filter-item.component.spec.ts b/src/app/tts-filters/filter-item/filter-item.component.spec.ts new file mode 100644 index 0000000..3da816e --- /dev/null +++ b/src/app/tts-filters/filter-item/filter-item.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FilterItemComponent } from './filter-item.component'; + +describe('FilterItemComponent', () => { + let component: FilterItemComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FilterItemComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FilterItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/tts-filters/filter-item/filter-item.component.ts b/src/app/tts-filters/filter-item/filter-item.component.ts new file mode 100644 index 0000000..9d62987 --- /dev/null +++ b/src/app/tts-filters/filter-item/filter-item.component.ts @@ -0,0 +1,50 @@ +import { Component, EventEmitter, inject, Input, OnInit, Output, signal } from '@angular/core'; +import { Filter, FilterFlag } from '../../shared/models/filter'; +import { MatCardModule } from '@angular/material/card'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatIconModule } from '@angular/material/icon'; +import { MatDialog } from '@angular/material/dialog'; +import { FilterItemEditComponent } from '../filter-item-edit/filter-item-edit.component'; +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' +}) +export class FilterItemComponent implements OnInit { + @Input() item: Filter = { id: "", user_id: "", search: "", replace: "", flag: FilterFlag.None, is_regex: false }; + @Output() onDelete = new EventEmitter(); + readonly client = inject(HermesClientService); + readonly dialog = inject(MatDialog); + private loaded = false; + + + ngOnInit(): void { + this.loaded = true; + } + + openDialog(): void { + if (!this.loaded) + return; + + const dialogRef = this.dialog.open(FilterItemEditComponent, { + data: { id: this.item.id, search: this.item.search, replace: this.item.replace }, + }); + + dialogRef.afterClosed().subscribe(result => { + if (result !== undefined) { + console.log('update filter', result); + this.client.first(d => 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); + } + }); + } +} \ No newline at end of file diff --git a/src/app/tts-filters/filter-list/filter-list.component.html b/src/app/tts-filters/filter-list/filter-list.component.html new file mode 100644 index 0000000..6a2c648 --- /dev/null +++ b/src/app/tts-filters/filter-list/filter-list.component.html @@ -0,0 +1,18 @@ +
    +
      +
    • +
        +
      • Search
      • +
      • Replace
      • +
      • +
      +
    • + @for (filter of filters; track $index) { +
    • + +
    • + } +
    +
    \ No newline at end of file diff --git a/src/app/tts-filters/filter-list/filter-list.component.scss b/src/app/tts-filters/filter-list/filter-list.component.scss new file mode 100644 index 0000000..af28acf --- /dev/null +++ b/src/app/tts-filters/filter-list/filter-list.component.scss @@ -0,0 +1,40 @@ +ul { + margin: 0; + padding: 0; +} + +li { + display: block; + list-style-type: none; + padding: 0.75em 1em; + border-bottom: 1px solid #aaaaaa; +} + +li:first-child { + padding: 0 1em; + border-bottom: 0 solid #aaaaaa; +} + +li:last-child { + border-bottom: 0 solid #aaaaaa; +} + +ul.header { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-evenly; + + li { + border-bottom: 0 solid #aaaaaa; + font-weight: 500; + list-style-type: none; + white-space: pre; + text-align: justify; + } + + li:nth-child(1), + li:nth-child(2) { + flex: 1; + } +} \ No newline at end of file diff --git a/src/app/tts-filters/filter-list/filter-list.component.spec.ts b/src/app/tts-filters/filter-list/filter-list.component.spec.ts new file mode 100644 index 0000000..7d32a16 --- /dev/null +++ b/src/app/tts-filters/filter-list/filter-list.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FilterListComponent } from './filter-list.component'; + +describe('FilterListComponent', () => { + let component: FilterListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FilterListComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FilterListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/tts-filters/filter-list/filter-list.component.ts b/src/app/tts-filters/filter-list/filter-list.component.ts new file mode 100644 index 0000000..21ad3fc --- /dev/null +++ b/src/app/tts-filters/filter-list/filter-list.component.ts @@ -0,0 +1,22 @@ +import { Component, EventEmitter, inject, Input, Output } from '@angular/core'; +import { FilterItemComponent } from '../filter-item/filter-item.component'; +import { Filter, FilterFlag } 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' +}) +export class FilterListComponent { + @Input() filters: Filter[] = []; + client = inject(HermesClientService); + + deleteFilter(e: any): void { + console.log('deleting', e); + this.client.deleteTTSFilter(e.id); + this.filters = this.filters.filter(f => f.id != e.id); + } +} diff --git a/src/app/tts-filters/filters/filters.component.html b/src/app/tts-filters/filters/filters.component.html new file mode 100644 index 0000000..d80d91c --- /dev/null +++ b/src/app/tts-filters/filters/filters.component.html @@ -0,0 +1,14 @@ +
    +
    +

    Filters

    +
    + +
    +
    +
    + +
    +
    \ No newline at end of file diff --git a/src/app/tts-filters/filters/filters.component.scss b/src/app/tts-filters/filters/filters.component.scss new file mode 100644 index 0000000..00f008a --- /dev/null +++ b/src/app/tts-filters/filters/filters.component.scss @@ -0,0 +1,5 @@ +article { + display: flex; + justify-content: space-between; + width: 70%; +} \ No newline at end of file diff --git a/src/app/tts-filters/filters/filters.component.spec.ts b/src/app/tts-filters/filters/filters.component.spec.ts new file mode 100644 index 0000000..a3b0b36 --- /dev/null +++ b/src/app/tts-filters/filters/filters.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FiltersComponent } from './filters.component'; + +describe('FiltersComponent', () => { + let component: FiltersComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FiltersComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FiltersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/tts-filters/filters/filters.component.ts b/src/app/tts-filters/filters/filters.component.ts new file mode 100644 index 0000000..1a9b9c1 --- /dev/null +++ b/src/app/tts-filters/filters/filters.component.ts @@ -0,0 +1,91 @@ +import { Component, inject, Inject, Input, OnDestroy, OnInit, PLATFORM_ID, signal } 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 { isPlatformBrowser } from '@angular/common'; +import { Router } from '@angular/router'; +import { FilterItemEditComponent } from '../filter-item-edit/filter-item-edit.component'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; + +@Component({ + selector: 'filters', + standalone: true, + imports: [FilterListComponent, MatButtonModule, MatIconModule], + templateUrl: './filters.component.html', + styleUrl: './filters.component.scss' +}) +export class FiltersComponent implements OnInit, OnDestroy { + private isBrowser: boolean; + readonly dialog = inject(MatDialog); + items: Filter[]; + + + constructor(private client: HermesClientService, private router: Router, @Inject(PLATFORM_ID) private platformId: Object) { + this.isBrowser = isPlatformBrowser(this.platformId); + + 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; + + filter.search = d.data.search; + filter.replace = d.data.replace; + filter.flag = d.data.flag || 0; + } + }); + this.client.fetchFilters(); + } + + ngOnInit(): void { + if (!this.isBrowser) + return; + + if (!this.client.logged_in) { + this.router.navigate(["/tts-login"]); + return; + } + } + + ngOnDestroy() { + } + + openDialog(): void { + const dialogRef = this.dialog.open(FilterItemEditComponent, { + data: { search: '', replace: '' }, + }); + + 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); + } + }); + } +} \ No newline at end of file diff --git a/src/app/tts-filters/tts-filters.module.ts b/src/app/tts-filters/tts-filters.module.ts new file mode 100644 index 0000000..bce0a73 --- /dev/null +++ b/src/app/tts-filters/tts-filters.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { FiltersComponent } from './filters/filters.component'; +import { FilterListComponent } from './filter-list/filter-list.component'; + + +@NgModule({ + declarations: [], + imports: [ + FiltersComponent, FilterListComponent + ] +}) +export class TtsFiltersModule { }