Added TTS chat filters.
This commit is contained in:
parent
0afa2138b4
commit
275069697f
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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<any> | 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.')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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<any> | undefined = undefined
|
||||
|
||||
constructor() { }
|
||||
@ -23,6 +22,13 @@ export class HermesSocketService implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public first(predicate: (data: any) => boolean): Observable<any>|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();
|
||||
}
|
||||
|
@ -25,5 +25,13 @@
|
||||
Policies
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
routerLink="/filters"
|
||||
routerLinkActive="active"
|
||||
*ngIf="isLoggedIn() && isTTSLoggedIn()">
|
||||
Filters
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
@ -29,7 +29,7 @@ a {
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: #FCFCFC;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
a.active {
|
||||
|
@ -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 { }
|
@ -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" },
|
||||
|
@ -19,7 +19,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="usage">
|
||||
<th mat-header-cell *matHeaderCellDef>Usage per span</th>
|
||||
<th mat-header-cell *matHeaderCellDef>Usage Rate</th>
|
||||
<td mat-cell *matCellDef="let policy">
|
||||
@if (policy.editing) {
|
||||
<input type="number" [(ngModel)]="policy.usage" (keypress)="($event.charCode >= 48 && $event.charCode < 58)" />
|
||||
|
@ -1,7 +1,3 @@
|
||||
div {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
h4 {
|
||||
text-align: center;
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
13
src/app/shared/models/filter.ts
Normal file
13
src/app/shared/models/filter.ts
Normal file
@ -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;
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
<h2 mat-dialog-title>TTS Filter</h2>
|
||||
<mat-dialog-content>
|
||||
<mat-form-field>
|
||||
<mat-label>Search</mat-label>
|
||||
<input matInput [(ngModel)]="search" />
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Replace</mat-label>
|
||||
<input matInput [(ngModel)]="replace" />
|
||||
</mat-form-field>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onCancelClick()">Cancel</button>
|
||||
<button mat-button [mat-dialog-close]="onSaveClick()" cdkFocusInitial>Save</button>
|
||||
</mat-dialog-actions>
|
@ -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<FilterItemEditComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FilterItemEditComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FilterItemEditComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -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<FilterItemEditComponent>);
|
||||
readonly data = inject<Filter>(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();
|
||||
}
|
||||
}
|
18
src/app/tts-filters/filter-item/filter-item.component.html
Normal file
18
src/app/tts-filters/filter-item/filter-item.component.html
Normal file
@ -0,0 +1,18 @@
|
||||
<ul>
|
||||
<li>
|
||||
{{item.search}}
|
||||
</li>
|
||||
<li>
|
||||
{{item.replace}}
|
||||
</li>
|
||||
<li>
|
||||
<mat-menu #filterMenu>
|
||||
<button mat-menu-item (click)="openDialog()">Edit</button>
|
||||
<button mat-menu-item (click)="onDelete.emit(item)">Delete</button>
|
||||
</mat-menu>
|
||||
|
||||
<button mat-icon-button [matMenuTriggerFor]="filterMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
33
src/app/tts-filters/filter-item/filter-item.component.scss
Normal file
33
src/app/tts-filters/filter-item/filter-item.component.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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<FilterItemComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FilterItemComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FilterItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
50
src/app/tts-filters/filter-item/filter-item.component.ts
Normal file
50
src/app/tts-filters/filter-item/filter-item.component.ts
Normal file
@ -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<Filter>();
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
18
src/app/tts-filters/filter-list/filter-list.component.html
Normal file
18
src/app/tts-filters/filter-list/filter-list.component.html
Normal file
@ -0,0 +1,18 @@
|
||||
<div>
|
||||
<ul>
|
||||
<li>
|
||||
<ul class="header">
|
||||
<li>Search</li>
|
||||
<li>Replace</li>
|
||||
<li></li>
|
||||
</ul>
|
||||
</li>
|
||||
@for (filter of filters; track $index) {
|
||||
<li>
|
||||
<tts-filter-item
|
||||
[item]="filter"
|
||||
(onDelete)="deleteFilter($event)" />
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
40
src/app/tts-filters/filter-list/filter-list.component.scss
Normal file
40
src/app/tts-filters/filter-list/filter-list.component.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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<FilterListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FilterListComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FilterListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
22
src/app/tts-filters/filter-list/filter-list.component.ts
Normal file
22
src/app/tts-filters/filter-list/filter-list.component.ts
Normal file
@ -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);
|
||||
}
|
||||
}
|
14
src/app/tts-filters/filters/filters.component.html
Normal file
14
src/app/tts-filters/filters/filters.component.html
Normal file
@ -0,0 +1,14 @@
|
||||
<main>
|
||||
<article>
|
||||
<h3>Filters</h3>
|
||||
<div>
|
||||
<button mat-fab aria-label="Add a filter" (click)="openDialog()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
<div>
|
||||
<tts-filter-list
|
||||
[filters]="items" />
|
||||
</div>
|
||||
</main>
|
5
src/app/tts-filters/filters/filters.component.scss
Normal file
5
src/app/tts-filters/filters/filters.component.scss
Normal file
@ -0,0 +1,5 @@
|
||||
article {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 70%;
|
||||
}
|
23
src/app/tts-filters/filters/filters.component.spec.ts
Normal file
23
src/app/tts-filters/filters/filters.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FiltersComponent } from './filters.component';
|
||||
|
||||
describe('FiltersComponent', () => {
|
||||
let component: FiltersComponent;
|
||||
let fixture: ComponentFixture<FiltersComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FiltersComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FiltersComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
91
src/app/tts-filters/filters/filters.component.ts
Normal file
91
src/app/tts-filters/filters/filters.component.ts
Normal file
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
12
src/app/tts-filters/tts-filters.module.ts
Normal file
12
src/app/tts-filters/tts-filters.module.ts
Normal file
@ -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 { }
|
Loading…
x
Reference in New Issue
Block a user