Auto-formatted every file to keep everything consistent.

This commit is contained in:
Tom
2025-03-18 14:03:07 +00:00
parent 74b282ccfd
commit 9201f9b6c5
91 changed files with 14891 additions and 14767 deletions

View File

@ -1,124 +1,124 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"hermes-web-angular": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/hermes-web-angular",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
],
"scripts": [],
"server": "src/main.server.ts",
"prerender": true,
"ssr": {
"entry": "server.ts"
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "1024kB",
"maximumError": "1MB"
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"hermes-web-angular": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/hermes-web-angular",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
],
"scripts": [],
"server": "src/main.server.ts",
"prerender": true,
"ssr": {
"entry": "server.ts"
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "1024kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
}
],
"optimization": true,
"sourceMap": false,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"index": {
"input": "src/index.prod.html",
"output": "index.html"
},
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},
"defaultConfiguration": "production"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "hermes-web-angular:build:production"
},
"development": {
"buildTarget": "hermes-web-angular:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
],
"scripts": []
}
}
],
"optimization": true,
"sourceMap": false,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"index": {
"input": "src/index.prod.html",
"output": "index.html"
},
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "hermes-web-angular:build:production"
},
"development": {
"buildTarget": "hermes-web-angular:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
],
"scripts": []
}
}
}
}
}
}

28210
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +1,50 @@
{
"name": "hermes-web-angular",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve -c production --host 0.0.0.0 --watch false",
"build": "ng build",
"watch": "ng serve -c development --host 0.0.0.0",
"test": "ng test",
"serve:ssr:hermes-web-angular": "node dist/hermes-web-angular/server/server.mjs"
},
"private": true,
"dependencies": {
"@angular/animations": "^19.0.5",
"@angular/cdk": "^19.0.4",
"@angular/common": "^19.0.5",
"@angular/compiler": "^19.0.5",
"@angular/core": "^19.0.5",
"@angular/forms": "^19.0.5",
"@angular/material": "^19.0.4",
"@angular/platform-browser": "^19.0.5",
"@angular/platform-browser-dynamic": "^19.0.5",
"@angular/platform-server": "^19.0.5",
"@angular/router": "^19.0.5",
"@angular/ssr": "^19.0.6",
"angular-oauth2-oidc": "^17.0.2",
"express": "^4.18.2",
"ngx-socket-io": "^4.7.0",
"rxjs": "~7.8.0",
"rxjs-websockets": "^9.0.0",
"tslib": "^2.3.0",
"uuidv4": "^6.2.13",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.0.6",
"@angular/cli": "^19.0.6",
"@angular/compiler-cli": "^19.0.5",
"@types/express": "^4.17.17",
"@types/jasmine": "~5.1.0",
"@types/node": "^18.18.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.6.3"
}
}
"name": "hermes-web-angular",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve -c production --host 0.0.0.0 --watch false",
"build": "ng build",
"watch": "ng serve -c development --host 0.0.0.0",
"test": "ng test",
"serve:ssr:hermes-web-angular": "node dist/hermes-web-angular/server/server.mjs"
},
"private": true,
"dependencies": {
"@angular/animations": "^19.0.5",
"@angular/cdk": "^19.0.4",
"@angular/common": "^19.0.5",
"@angular/compiler": "^19.0.5",
"@angular/core": "^19.0.5",
"@angular/forms": "^19.0.5",
"@angular/material": "^19.0.4",
"@angular/platform-browser": "^19.0.5",
"@angular/platform-browser-dynamic": "^19.0.5",
"@angular/platform-server": "^19.0.5",
"@angular/router": "^19.0.5",
"@angular/ssr": "^19.0.6",
"angular-oauth2-oidc": "^17.0.2",
"express": "^4.18.2",
"ngx-socket-io": "^4.7.0",
"rxjs": "~7.8.0",
"rxjs-websockets": "^9.0.0",
"tslib": "^2.3.0",
"uuidv4": "^6.2.13",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.0.6",
"@angular/cli": "^19.0.6",
"@angular/compiler-cli": "^19.0.5",
"@types/express": "^4.17.17",
"@types/jasmine": "~5.1.0",
"@types/node": "^18.18.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.6.3"
}
}

View File

@ -1,15 +1,16 @@
<mat-form-field>
<mat-label>Redeemable Action</mat-label>
<input
matInput
type="text"
placeholder="Pick a Redeemable Action"
aria-label="redeemable action"
[formControl]="formControl"
[matAutocomplete]="auto"
(blur)="blur()"
(input)="input()">
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" (optionSelected)="select($event.option.value)">
<input matInput
type="text"
placeholder="Pick a Redeemable Action"
aria-label="redeemable action"
[formControl]="formControl"
[matAutocomplete]="auto"
(blur)="blur()"
(input)="input()">
<mat-autocomplete #auto="matAutocomplete"
[displayWith]="displayFn"
(optionSelected)="select($event.option.value)">
@for (action of filteredActions; track action.name) {
<mat-option [value]="action">{{action.name}}</mat-option>
}

View File

@ -10,7 +10,7 @@ describe('ActionDropdownComponent', () => {
await TestBed.configureTestingModule({
imports: [ActionDropdownComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(ActionDropdownComponent);
component = fixture.componentInstance;

View File

@ -75,7 +75,7 @@ export class ActionDropdownComponent implements OnInit {
} else if (insenstiveActions.length == 1) {
newValue = insenstiveActions[0];
}
if (newValue && newValue.name != this.formControl.value) {
this.formControl.setValue(newValue);
this.actionChange.emit(newValue.name);

View File

@ -8,11 +8,14 @@
</mat-card-header>
<mat-card-content>
<form class="grid" [formGroup]="formGroup">
<form class="grid"
[formGroup]="formGroup">
<div class="item">
<mat-form-field>
<mat-label>Redeemable Action Name</mat-label>
<input matInput type="text" formControlName="name">
<input matInput
type="text"
formControlName="name">
@if (isNew && formGroup.get('name')?.invalid && (formGroup.get('name')?.dirty ||
formGroup.get('name')?.touched)) {
@if (formGroup.get('name')?.hasError('required')) {
@ -27,7 +30,9 @@
<div class="item">
<mat-form-field>
<mat-label>Type</mat-label>
<mat-select matInput formControlName="type" (selectionChange)="action.type = $event.value">
<mat-select matInput
formControlName="type"
(selectionChange)="action.type = $event.value">
@for (type of actionTypes; track $index) {
<mat-option value="{{type}}">{{type}}</mat-option>
}
@ -50,8 +55,11 @@
@if (field.type == 'text') {
<mat-form-field>
<mat-label>{{field.label}}</mat-label>
<input matInput type="text" placeholder="{{field.placeholder}}" [formControl]="field.control"
[(ngModel)]="action.data[field.key]">
<input matInput
type="text"
placeholder="{{field.placeholder}}"
[formControl]="field.control"
[(ngModel)]="action.data[field.key]">
@if (field.control.invalid && (field.control.dirty || field.control.touched)) {
@if (field.control.hasError('required')) {
<small class="error">This field is required.</small>
@ -65,7 +73,9 @@
@else if (field.type == 'number') {
<mat-form-field>
<mat-label>{{field.label}}</mat-label>
<input matInput type="number" [formControl]="field.control">
<input matInput
type="number"
[formControl]="field.control">
@if (field.control.invalid && (field.control.dirty || field.control.touched)) {
@if (field.control.hasError('required')) {
<small class="error">This field is required.</small>
@ -77,19 +87,19 @@
</mat-form-field>
}
@else if (field.type == 'text-values') {
<mat-form-field>
<mat-label>{{field.label}}</mat-label>
<mat-select [formControl]="field.control">
@for (value of field.values; track $index) {
<mat-option [value]="value">{{value}}</mat-option>
}
</mat-select>
@if (field.control.invalid && (field.control.dirty || field.control.touched)) {
@if (field.control.hasError('required')) {
<small class="error">This field is required.</small>
<mat-form-field>
<mat-label>{{field.label}}</mat-label>
<mat-select [formControl]="field.control">
@for (value of field.values; track $index) {
<mat-option [value]="value">{{value}}</mat-option>
}
}
</mat-form-field>
</mat-select>
@if (field.control.invalid && (field.control.dirty || field.control.touched)) {
@if (field.control.hasError('required')) {
<small class="error">This field is required.</small>
}
}
</mat-form-field>
}
</div>
@ -98,17 +108,18 @@
}
</mat-card-content>
<mat-card-actions class="actions" align="end">
<mat-card-actions class="actions"
align="end">
@if (!isNew) {
<button mat-raised-button class="delete" (click)="deleteAction(action)">Delete</button>
<button mat-raised-button
class="delete"
(click)="deleteAction(action)">Delete</button>
}
<button
mat-raised-button
(click)="dialogRef.close()">Cancel</button>
<button
mat-raised-button
disabled="{{!formsDirty || !formsValidity || waitForResponse}}"
(click)="save()">Save</button>
<button mat-raised-button
(click)="dialogRef.close()">Cancel</button>
<button mat-raised-button
disabled="{{!formsDirty || !formsValidity || waitForResponse}}"
(click)="save()">Save</button>
</mat-card-actions>
</mat-card>
</body>

View File

@ -25,6 +25,6 @@
color: #ba1a1a;
}
.mdc-button ~ .mdc-button {
.mdc-button~.mdc-button {
margin-left: 1em;
}

View File

@ -10,7 +10,7 @@ describe('ActionItemEditComponent', () => {
await TestBed.configureTestingModule({
imports: [ActionItemEditComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(ActionItemEditComponent);
component = fixture.componentInstance;

View File

@ -1,11 +1,15 @@
<main>
@for (action of actions; track $index) {
<button type="button" class="container" (click)="modify(action)">
<button type="button"
class="container"
(click)="modify(action)">
<span class="title">{{action.name}}</span>
<span class="subtitle">{{action.type}}</span>
</button>
}
<button type="button" class="container" (click)="create()">
<button type="button"
class="container"
(click)="create()">
<mat-icon>add</mat-icon>
</button>
</main>

View File

@ -10,7 +10,7 @@ describe('ActionListComponent', () => {
await TestBed.configureTestingModule({
imports: [ActionListComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(ActionListComponent);
component = fixture.componentInstance;

View File

@ -5,7 +5,8 @@
<article>
<mat-form-field>
<mat-label>Filter by type</mat-label>
<mat-select (selectionChange)="onFilterChange($event.value)" value="0">
<mat-select (selectionChange)="onFilterChange($event.value)"
value="0">
<mat-select-trigger>
<mat-icon matPrefix>filter_list</mat-icon>&nbsp;{{filter.name}}
</mat-select-trigger>
@ -19,13 +20,15 @@
<mat-form-field>
<mat-label>Search</mat-label>
<input matInput
type="text"
placeholder="Name of action"
[formControl]="searchControl"
[(ngModel)]="search">
type="text"
placeholder="Name of action"
[formControl]="searchControl"
[(ngModel)]="search">
<mat-icon matPrefix>search</mat-icon>
</mat-form-field>
</article>
</section>
<action-list class="center" [actions]="actions" (actionsChange)="items.push($event)" />
<action-list class="center"
[actions]="actions"
(actionsChange)="items.push($event)" />
</body>

View File

@ -1,4 +1,5 @@
body, h3 {
body,
h3 {
padding: 0;
margin: 0;
}
@ -21,14 +22,14 @@ section {
margin-left: auto;
margin-right: auto;
@media (max-width:1250px) {
@media (max-width:1250px) {
display: block;
justify-content: center;
}
article {
display: flex;
justify-content:space-around;
justify-content: space-around;
}
}

View File

@ -10,7 +10,7 @@ describe('ActionsComponent', () => {
await TestBed.configureTestingModule({
imports: [ActionsComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(ActionsComponent);
component = fixture.componentInstance;

View File

@ -1,4 +1,4 @@
<main class="main">
<navigation class="navigation" />
<router-outlet class="content" />
<navigation class="navigation" />
<router-outlet class="content" />
</main>

View File

@ -1,4 +1,4 @@
.main {
display: grid;
grid-template-columns: 20em 0px 1fr;
display: grid;
grid-template-columns: 20em 0px 1fr;
}

View File

@ -12,9 +12,9 @@ export const appConfig: ApplicationConfig = {
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(
withInterceptors([(req: HttpRequest<unknown>, next: HttpHandlerFn) => {
return next(req);
}])
withInterceptors([(req: HttpRequest<unknown>, next: HttpHandlerFn) => {
return next(req);
}])
),
provideOAuthClient(),
provideClientHydration(), provideAnimationsAsync()

View File

@ -2,7 +2,8 @@
<main>
<mat-form-field>
<mat-label>User to impersonate</mat-label>
<mat-select (selectionChange)="onChange($event)" [(value)]="impersonated">
<mat-select (selectionChange)="onChange($event)"
[(value)]="impersonated">
<mat-option>{{getUsername()}}</mat-option>
@for (user of users; track user.id) {
<mat-option [value]="user.id">{{ user.name }}</mat-option>

View File

@ -10,7 +10,7 @@ describe('ImpersonationComponent', () => {
await TestBed.configureTestingModule({
imports: [ImpersonationComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(ImpersonationComponent);
component = fixture.componentInstance;

View File

@ -1,5 +1,6 @@
<div class="login">
<mat-card class="outer" appearance="outlined">
<mat-card class="outer"
appearance="outlined">
<mat-card-header>
<h1 class="title">Login</h1>
</mat-card-header>
@ -8,7 +9,9 @@
<p>Log in with your favorite livestream service</p>
<a>
<mat-card appearance="outlined" class="twitch" (click)="login()">
<mat-card appearance="outlined"
class="twitch"
(click)="login()">
<mat-card-content>
Twitch
</mat-card-content>

View File

@ -10,7 +10,7 @@ describe('LoginComponent', () => {
await TestBed.configureTestingModule({
imports: [LoginComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;

View File

@ -5,29 +5,29 @@ import { Subscription } from 'rxjs';
import { environment } from '../../../environments/environment';
@Component({
selector: 'login',
standalone: true,
imports: [MatCardModule, RouterModule],
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
selector: 'login',
standalone: true,
imports: [MatCardModule, RouterModule],
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
})
export class LoginComponent implements OnInit, OnDestroy {
subscription: Subscription | null;
subscription: Subscription | null;
constructor(private router: Router) {
this.subscription = null;
}
constructor(private router: Router) {
this.subscription = null;
}
ngOnInit(): void {
}
ngOnInit(): void {
ngOnDestroy(): void {
if (this.subscription)
this.subscription.unsubscribe()
}
}
login() {
document.location.replace(environment.API_HOST + '/auth');
}
ngOnDestroy(): void {
if (this.subscription)
this.subscription.unsubscribe()
}
login() {
document.location.replace(environment.API_HOST + '/auth');
}
}

View File

@ -17,7 +17,8 @@
</mat-form-field>
</mat-card-content>
<mat-card-actions align="end">
<button mat-raised-button (click)="login()">Log In</button>
<button mat-raised-button
(click)="login()">Log In</button>
</mat-card-actions>
</mat-card>
</main>

View File

@ -10,7 +10,7 @@ describe('TtsLoginComponent', () => {
await TestBed.configureTestingModule({
imports: [TtsLoginComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(TtsLoginComponent);
component = fixture.componentInstance;

View File

@ -1,6 +1,7 @@
@if (auth.isAuthenticated()) {
<main>
<mat-card appearance="outlined" class="card">
<mat-card appearance="outlined"
class="card">
<mat-card-header>
<mat-card-title>{{username}}</mat-card-title>
</mat-card-header>
@ -10,9 +11,11 @@
<mat-card-actions class="actions">
<div>
@if (isTTSLoggedIn) {
<button mat-raised-button (click)="client.disconnect()"><span class="disconnect">Disconnect</span></button>
<button mat-raised-button
(click)="client.disconnect()"><span class="disconnect">Disconnect</span></button>
}
<button mat-raised-button (click)="auth.logout()"><span class="logoff">Log Off</span></button>
<button mat-raised-button
(click)="auth.logout()"><span class="logoff">Log Off</span></button>
</div>
</mat-card-actions>
</mat-card>

View File

@ -14,10 +14,11 @@ main {
justify-content: center;
}
.disconnect, .logoff {
.disconnect,
.logoff {
color: red;
}
.mdc-button ~ .mdc-button {
.mdc-button~.mdc-button {
margin-left: 1em;
}

View File

@ -10,7 +10,7 @@ describe('UserCardComponent', () => {
await TestBed.configureTestingModule({
imports: [UserCardComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
component = fixture.componentInstance;

View File

@ -1,17 +1,18 @@
<mat-form-field>
<mat-label>Group</mat-label>
<input
matInput
type="text"
placeholder="Pick a group"
aria-label="group"
[formControl]="formControl"
[matAutocomplete]="auto"
[disabled]="!!groupDisabled"
[readonly]="!!groupDisabled"
(blur)="blur()"
(input)="input()">
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" (optionSelected)="select($event.option.value)">
<input matInput
type="text"
placeholder="Pick a group"
aria-label="group"
[formControl]="formControl"
[matAutocomplete]="auto"
[disabled]="!!groupDisabled"
[readonly]="!!groupDisabled"
(blur)="blur()"
(input)="input()">
<mat-autocomplete #auto="matAutocomplete"
[displayWith]="displayFn"
(optionSelected)="select($event.option.value)">
@for (group of filteredGroups; track group.id) {
<mat-option [value]="group">{{group.name}}</mat-option>
}

View File

@ -10,7 +10,7 @@ describe('GroupDropdownComponent', () => {
await TestBed.configureTestingModule({
imports: [GroupDropdownComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(GroupDropdownComponent);
component = fixture.componentInstance;

View File

@ -35,7 +35,7 @@ export class GroupDropdownComponent implements OnInit {
this.route.data.subscribe(data => {
if (!data['groups'])
return;
this.groups = data['groups'];
});
@ -83,13 +83,13 @@ export class GroupDropdownComponent implements OnInit {
} else if (insenstiveGroups.length == 1) {
newValue = insenstiveGroups[0];
}
if (newValue) {
this.formControl.setValue(newValue);
//this.groupChange.emit(newValue.name);
} else if (!newValue)
this.formControl.setValue(undefined);
//this.groupChange.emit(undefined);
//this.groupChange.emit(undefined);
}
}

View File

@ -7,7 +7,10 @@
<mat-card-content>
<mat-form-field>
<mat-label>Group Name</mat-label>
<input matInput type="text" [formControl]="nameForm" [disabled]="isSpecial" />
<input matInput
type="text"
[formControl]="nameForm"
[disabled]="isSpecial" />
@if (nameForm.invalid && (nameForm.dirty || nameForm.touched)) {
@if (nameForm.hasError('required')) {
<small class="error">This field is required.</small>
@ -16,7 +19,9 @@
</mat-form-field>
<mat-form-field>
<mat-label>TTS Priority</mat-label>
<input matInput type="number" [formControl]="priorityForm" />
<input matInput
type="number"
[formControl]="priorityForm" />
@if (priorityForm.invalid && (priorityForm.dirty || priorityForm.touched)) {
@if (priorityForm.hasError('required')) {
<small class="error">This field is required.</small>
@ -34,16 +39,14 @@
</mat-form-field>
</mat-card-content>
<mat-card-actions>
<button
mat-button
[disabled]="waitForResponse || formGroup.invalid"
(click)="add()">
<button mat-button
[disabled]="waitForResponse || formGroup.invalid"
(click)="add()">
<mat-icon>add</mat-icon>Add
</button>
<button
mat-button
[disabled]="waitForResponse"
(click)="cancel()">
<button mat-button
[disabled]="waitForResponse"
(click)="cancel()">
<mat-icon>cancel</mat-icon>Cancel
</button>
</mat-card-actions>

View File

@ -10,7 +10,7 @@ describe('GroupItemEditComponent', () => {
await TestBed.configureTestingModule({
imports: [GroupItemEditComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(GroupItemEditComponent);
component = fixture.componentInstance;

View File

@ -14,7 +14,8 @@
<small class="muted block">polic{{item().chatters.length == 1 ? 'y' : 'ies'}}</small>
</section>
<section>
<button mat-button (click)="router.navigate([link])">
<button mat-button
(click)="router.navigate([link])">
<mat-icon>pageview</mat-icon>View
</button>
</section>

View File

@ -10,7 +10,7 @@ describe('GroupItemComponent', () => {
await TestBed.configureTestingModule({
imports: [GroupItemComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(GroupItemComponent);
component = fixture.componentInstance;

View File

@ -25,7 +25,7 @@ export class GroupItemComponent implements OnInit {
item = input.required<{ group: Group, chatters: GroupChatter[], policies: Policy[] }>();
link: string = '';
special: boolean = true;
ngOnInit() {

View File

@ -1,7 +1,7 @@
<ul>
@for (group of groups; track $index) {
@for (group of groups; track $index) {
<li>
<group-item [item]="group" />
</li>
}
}
</ul>

View File

@ -10,7 +10,7 @@ describe('GroupListComponent', () => {
await TestBed.configureTestingModule({
imports: [GroupListComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(GroupListComponent);
component = fixture.componentInstance;

View File

@ -8,7 +8,10 @@
{{policies.length}} polic{{policies.length == 1 ? 'y' : 'ies'}}
</mat-panel-description>
</mat-expansion-panel-header>
<policy-add-button class="add" [groups]="groups" [policies]="policies" [group]="group?.id" />
<policy-add-button class="add"
[groups]="groups"
[policies]="policies"
[group]="group?.id" />
@if (policies.length > 0) {
<policy-table [policies]="policies" />
}
@ -28,7 +31,9 @@
<p>Deleting this group will delete everything that is part of it, including policies and permissions.</p>
</article>
<article class="right">
<button mat-raised-button class="delete" (click)="delete()">
<button mat-raised-button
class="delete"
(click)="delete()">
<mat-icon>delete</mat-icon>Delete this group.
</button>
</article>

View File

@ -1,4 +1,4 @@
.mat-expansion-panel ~ .mat-expansion-panel {
.mat-expansion-panel~.mat-expansion-panel {
margin-top: 4em;
}

View File

@ -10,7 +10,7 @@ describe('GroupPageComponent', () => {
await TestBed.configureTestingModule({
imports: [GroupPageComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(GroupPageComponent);
component = fixture.componentInstance;

View File

@ -1,15 +1,21 @@
<button mat-button [mat-menu-trigger-for]="menu">
<button mat-button
[mat-menu-trigger-for]="menu">
<mat-icon>add</mat-icon>
Add a group
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="openDialog('')">Custom Group</button>
<button mat-menu-item (click)="openDialog('everyone')">Everyone Group</button>
<button mat-menu-item (click)="openDialog('subscribers')">Subscriber Group</button>
<button mat-menu-item (click)="openDialog('moderators')">Moderator Group</button>
<button mat-menu-item (click)="openDialog('vip')">VIP Group</button>
<button mat-menu-item (click)="openDialog('broadcaster')">Broadcaster Group</button>
<button mat-menu-item
(click)="openDialog('')">Custom Group</button>
<button mat-menu-item
(click)="openDialog('everyone')">Everyone Group</button>
<button mat-menu-item
(click)="openDialog('subscribers')">Subscriber Group</button>
<button mat-menu-item
(click)="openDialog('moderators')">Moderator Group</button>
<button mat-menu-item
(click)="openDialog('vip')">VIP Group</button>
<button mat-menu-item
(click)="openDialog('broadcaster')">Broadcaster Group</button>
</mat-menu>
<group-list
class="groups"
[groups]="items" />
<group-list class="groups"
[groups]="items" />

View File

@ -10,7 +10,7 @@ describe('GroupsComponent', () => {
await TestBed.configureTestingModule({
imports: [GroupsComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(GroupsComponent);
component = fixture.componentInstance;

View File

@ -44,14 +44,14 @@ export class HermesClientService {
this.events.emit('tts_logoff', null);
}
public filter(predicate: (data: any) => boolean): Observable<any>|undefined {
public filter(predicate: (data: any) => boolean): Observable<any> | undefined {
return this.socket.get$()?.pipe(
filter(predicate),
map(d => d.d)
);
}
public filterByRequestType(requestName: string): Observable<any>|undefined {
public filterByRequestType(requestName: string): Observable<any> | undefined {
return this.socket.get$()?.pipe(
filter(d => d.op == 4 && d.d.request.type === requestName),
map(d => d.d)

View File

@ -3,45 +3,52 @@
<ul>
@if (!isLoggedIn()) {
<li>
<a routerLink="/login" routerLinkActive="active">
<a routerLink="/login"
routerLinkActive="active">
Login
</a>
</li>
}
@if (isLoggedIn() && !isTTSLoggedIn()) {
<li>
<a routerLink="/tts-login" routerLinkActive="active">
<a routerLink="/tts-login"
routerLinkActive="active">
TTS Login
</a>
</li>
}
@if (isLoggedIn() && isTTSLoggedIn()) {
<li>
<a routerLink="/policies" routerLinkActive="active">
<a routerLink="/policies"
routerLinkActive="active">
Policies
</a>
</li>
<li>
<a routerLink="/filters" routerLinkActive="active">
<a routerLink="/filters"
routerLinkActive="active">
Filters
</a>
</li>
<li>
<a routerLink="/actions" routerLinkActive="active">
<a routerLink="/actions"
routerLinkActive="active">
Actions
</a>
</li>
<li>
<a routerLink="/redemptions" routerLinkActive="active">
<a routerLink="/redemptions"
routerLinkActive="active">
Redemptions
</a>
</li>
@if (isAdmin()) {
<li>
<a routerLink="/groups" routerLinkActive="active">
Groups
</a>
</li>
<li>
<a routerLink="/groups"
routerLinkActive="active">
Groups
</a>
</li>
}
}
</ul>

View File

@ -5,33 +5,33 @@ $secondary_background_color: #DDDDDD;
$secondary_font_color: #333333;
ul {
padding: 0;
padding: 0;
}
li {
list-style: none;
width: 100%;
display: flex;
flex-grow: 1;
justify-content: center;
flex-direction: column;
list-style: none;
width: 100%;
display: flex;
flex-grow: 1;
justify-content: center;
flex-direction: column;
}
a {
background-color: transparent;
padding: 1em;
border: 0;
margin: 0;
font-size: large;
text-decoration: none;
color: $primary_font_color;
border-radius: 10px;
background-color: transparent;
padding: 1em;
border: 0;
margin: 0;
font-size: large;
text-decoration: none;
color: $primary_font_color;
border-radius: 10px;
}
a:hover {
background-color: #FAFAFA;
background-color: #FAFAFA;
}
a.active {
background-color: #F5F5F5;
background-color: #F5F5F5;
}

View File

@ -10,7 +10,7 @@ describe('NavigationComponent', () => {
await TestBed.configureTestingModule({
imports: [NavigationComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(NavigationComponent);
component = fixture.componentInstance;

View File

@ -3,6 +3,7 @@ import { PolicyComponent } from './policy/policy.component';
import { PolicyTableComponent } from './policy-table/policy-table.component';
import { PolicyItemEditComponent } from './policy-item-edit/policy-item-edit.component';
import { PolicyAddButtonComponent } from './policy-add-button/policy-add-button.component';
import { PolicyAddFormComponent } from './policy-add-form/policy-add-form.component';
@NgModule({
@ -12,6 +13,7 @@ import { PolicyAddButtonComponent } from './policy-add-button/policy-add-button.
PolicyTableComponent,
PolicyAddButtonComponent,
PolicyItemEditComponent,
PolicyAddFormComponent,
]
})
export class PoliciesModule { }

View File

@ -1,4 +1,5 @@
<button mat-button (click)="openDialog()">
<button mat-button
(click)="openDialog()">
<mat-icon>add</mat-icon>
Add a policy
</button>

View File

@ -10,7 +10,7 @@ describe('PolicyAddButtonComponent', () => {
await TestBed.configureTestingModule({
imports: [PolicyAddButtonComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(PolicyAddButtonComponent);
component = fixture.componentInstance;

View File

@ -19,9 +19,9 @@ export class PolicyAddButtonComponent {
private readonly dialog = inject(MatDialog);
@Input({ required: true }) policies: Policy[] = [];
@Input({ required: true }) groups: Group[] = [];
@Input() group: string|undefined = undefined;
@Input() group: string | undefined = undefined;
@Output() policy = new EventEmitter<Policy>();
openDialog(): void {
const dialogRef = this.dialog.open(PolicyItemEditComponent, {

View File

@ -1,25 +1,17 @@
<div>
<form
standalone>
<mat-form-field>
<input
name="path"
type="text"
placeholder="Pick one"
[(ngModel)]="newPolicyName"
matInput
[formControl]="myControl"
[matAutocomplete]="auto" />
<mat-autocomplete #auto="matAutocomplete">
@for (option of filteredPolicies | async; track option) {
<mat-option [value]="option">{{option}}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
<button
mat-flat-button
(click)="addNewPolicy()">
Add
</button>
</form>
</div>
<form standalone>
<mat-form-field>
<mat-label>Path</mat-label>
<input name="path"
type="text"
placeholder="Pick one"
[(ngModel)]="policy"
matInput
[formControl]="policyControl"
[matAutocomplete]="auto" />
<mat-autocomplete #auto="matAutocomplete">
@for (option of filteredPolicies | async; track option) {
<mat-option [value]="option">{{option}}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
</form>

View File

@ -10,7 +10,7 @@ describe('PolicyAddFormComponent', () => {
await TestBed.configureTestingModule({
imports: [PolicyAddFormComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(PolicyAddFormComponent);
component = fixture.componentInstance;

View File

@ -4,71 +4,68 @@ 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 EventService from '../../shared/services/EventService';
import { map, Observable, startWith } from 'rxjs';
import { HermesClientService } from '../../hermes-client.service';
const Policies = [
{ path: "tts", description: "Anything to do with TTS" },
{ path: "tts.chat", description: "Anything to do with chat" },
{ 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.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" },
{ path: "tts.commands.refresh", description: "To use !refresh command" },
{ path: "tts.commands.skip", description: "To use !skip command" },
{ path: "tts.commands.skipall", description: "To use !skipall command" },
{ path: "tts.commands.tts", description: "To use !tts command" },
{ path: "tts.commands.tts.join", description: "To use !tts join command" },
{ path: "tts.commands.tts.leave", description: "To use !tts leave command" },
{ path: "tts.commands.version", description: "To use !version command" },
{ path: "tts.commands.voice", description: "To use !voice command" },
{ path: "tts.commands.voice.admin", description: "To use !voice command on others" },
{ path: "tts", description: "Anything to do with TTS" },
{ path: "tts.chat", description: "Anything to do with chat" },
{ 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.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" },
{ path: "tts.commands.refresh", description: "To use !refresh command" },
{ path: "tts.commands.skip", description: "To use !skip command" },
{ path: "tts.commands.skipall", description: "To use !skipall command" },
{ path: "tts.commands.tts", description: "To use !tts command" },
{ path: "tts.commands.tts.join", description: "To use !tts join command" },
{ path: "tts.commands.tts.leave", description: "To use !tts leave command" },
{ path: "tts.commands.version", description: "To use !version command" },
{ path: "tts.commands.voice", description: "To use !voice command" },
{ path: "tts.commands.voice.admin", description: "To use !voice command on others" },
]
@Component({
selector: 'policy-add-form',
imports: [
AsyncPipe,
FormsModule,
MatAutocompleteModule,
MatButtonModule,
MatInputModule,
ReactiveFormsModule,
],
templateUrl: './policy-add-form.component.html',
styleUrl: './policy-add-form.component.scss'
selector: 'policy-add-form',
imports: [
AsyncPipe,
FormsModule,
MatAutocompleteModule,
MatButtonModule,
MatInputModule,
ReactiveFormsModule,
],
templateUrl: './policy-add-form.component.html',
styleUrl: './policy-add-form.component.scss'
})
export class PolicyAddFormComponent {
myControl = new FormControl('');
newPolicyName: string = '';
filteredPolicies: Observable<string[]>;
policyControl = new FormControl('');
policy: string = '';
filteredPolicies: Observable<string[]>;
constructor(private events: EventService, private hermes: HermesClientService) {
this.filteredPolicies = this.myControl.valueChanges.pipe(
startWith(''),
map(value => this._filter(value || '')),
);
constructor() {
this.filteredPolicies = this.policyControl.valueChanges.pipe(
startWith(''),
map(value => this._filter(value || '')),
);
}
ngOnInit() {
this.filteredPolicies = this.policyControl.valueChanges.pipe(
startWith(''),
map(value => this._filter(value || '')),
);
}
private _filter(value: string): string[] {
const filterValue = value.toLowerCase();
const names = Policies.map(p => p.path);
if (names.includes(filterValue)) {
return names;
}
ngOnInit() {
this.filteredPolicies = this.myControl.valueChanges.pipe(
startWith(''),
map(value => this._filter(value || '')),
);
}
private _filter(value: string): string[] {
const filterValue = value.toLowerCase();
return Policies.map(p => p.path).filter(option => option.toLowerCase().includes(filterValue));
}
addNewPolicy() {
this.events.emit('addPolicy', this.newPolicyName);
this.newPolicyName = "";
}
return names.filter(option => option.toLowerCase().includes(filterValue));
}
}

View File

@ -3,16 +3,17 @@
<mat-card-title>{{isNew ? 'Add' : 'Edit'}} Policy</mat-card-title>
</mat-card-header>
<mat-card-content>
<group-dropdown
ngDefaultControl
[formControl]="groupControl"
[groups]="data.groups"
[group]="data.group_id"
[groupDisabled]="data.groupDisabled"
[errorMessages]="groupErrorMessages" />
<group-dropdown ngDefaultControl
[formControl]="groupControl"
[groups]="data.groups"
[group]="data.group_id"
[groupDisabled]="data.groupDisabled"
[errorMessages]="groupErrorMessages" />
<mat-form-field>
<mat-label>Path</mat-label>
<input matInput placeholder="Path" [formControl]="pathControl" />
<input matInput
placeholder="Path"
[formControl]="pathControl" />
@if (pathControl.invalid && (pathControl.dirty || pathControl.touched)) {
@if (pathControl.hasError('required')) {
<small class="error">This field is required.</small>
@ -21,7 +22,9 @@
</mat-form-field>
<mat-form-field>
<mat-label>Usage</mat-label>
<input matInput type="number" [formControl]="usageControl" />
<input matInput
type="number"
[formControl]="usageControl" />
@if (usageControl.invalid && (usageControl.dirty || usageControl.touched)) {
@if (usageControl.hasError('required')) {
<small class="error">This field is required.</small>
@ -39,7 +42,9 @@
</mat-form-field>
<mat-form-field>
<mat-label>Span</mat-label>
<input matInput type="number" [formControl]="spanControl" />
<input matInput
type="number"
[formControl]="spanControl" />
@if (spanControl.invalid && (spanControl.dirty || spanControl.touched)) {
@if (spanControl.hasError('required')) {
<small class="error">This field is required.</small>
@ -58,15 +63,18 @@
</mat-card-content>
<mat-card-actions>
@if (isNew) {
<button mat-button (click)="save()">
<button mat-button
(click)="save()">
<mat-icon>add</mat-icon>Add
</button>
} @else {
<button mat-button (click)="save()">
<button mat-button
(click)="save()">
<mat-icon>save</mat-icon>Save
</button>
}
<button mat-button (click)="dialogRef.close()">
<button mat-button
(click)="dialogRef.close()">
<mat-icon>cancel</mat-icon>Cancel
</button>
</mat-card-actions>

View File

@ -10,7 +10,7 @@ describe('PolicyItemEditComponent', () => {
await TestBed.configureTestingModule({
imports: [PolicyItemEditComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(PolicyItemEditComponent);
component = fixture.componentInstance;

View File

@ -10,6 +10,7 @@ import { HermesClientService } from '../../hermes-client.service';
import { GroupDropdownComponent } from '../../groups/group-dropdown/group-dropdown.component';
import { Group } from '../../shared/models/group';
import { Policy } from '../../shared/models/policy';
import { PolicyAddFormComponent } from '../policy-add-form/policy-add-form.component';
@Component({
selector: 'policy-item-edit',
@ -21,6 +22,7 @@ import { Policy } from '../../shared/models/policy';
MatIconModule,
MatInputModule,
ReactiveFormsModule,
PolicyAddFormComponent
],
templateUrl: './policy-item-edit.component.html',
styleUrl: './policy-item-edit.component.scss'

View File

@ -1,40 +1,59 @@
<table mat-table [dataSource]="policies" class="mat-elevation-z8">
<table mat-table
[dataSource]="policies"
class="mat-elevation-z8">
<ng-container matColumnDef="path">
<th mat-header-cell *matHeaderCellDef>Path</th>
<td mat-cell *matCellDef="let policy">
<th mat-header-cell
*matHeaderCellDef>Path</th>
<td mat-cell
*matCellDef="let policy">
{{policy.path}}
</td>
</ng-container>
<ng-container matColumnDef="group">
<th mat-header-cell *matHeaderCellDef>Group</th>
<td mat-cell *matCellDef="let policy">
<th mat-header-cell
*matHeaderCellDef>Group</th>
<td mat-cell
*matCellDef="let policy">
{{getGroupById(policy.group_id)?.name || '\<unknown group\>'}}
</td>
</ng-container>
<ng-container matColumnDef="usage">
<th mat-header-cell *matHeaderCellDef>Usage Rate</th>
<td mat-cell class="center" *matCellDef="let policy">
<th mat-header-cell
*matHeaderCellDef>Usage Rate</th>
<td mat-cell
class="center"
*matCellDef="let policy">
{{policy.usage}}
</td>
</ng-container>
<ng-container matColumnDef="span">
<th mat-header-cell *matHeaderCellDef>Span (ms)</th>
<td mat-cell class="center" *matCellDef="let policy">
<th mat-header-cell
*matHeaderCellDef>Span (ms)</th>
<td mat-cell
class="center"
*matCellDef="let policy">
{{policy.span}}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let policy">
<button mat-button (click)="edit(policy)"><mat-icon>edit</mat-icon>Edit</button>
<button mat-button class="delete" (click)="delete(policy)"><mat-icon>delete</mat-icon>Delete</button>
<th mat-header-cell
*matHeaderCellDef> Actions </th>
<td mat-cell
*matCellDef="let policy">
<button mat-button
(click)="edit(policy)"><mat-icon>edit</mat-icon>Edit</button>
<button mat-button
class="delete"
(click)="delete(policy)"><mat-icon>delete</mat-icon>Delete</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<tr mat-header-row
*matHeaderRowDef="displayedColumns"></tr>
<tr mat-row
*matRowDef="let row; columns: displayedColumns;"></tr>
</table>

View File

@ -7,6 +7,6 @@ table {
color: red;
}
button ~ button {
button~button {
margin-left: 1em;
}

View File

@ -10,7 +10,7 @@ describe('PolicyTableComponent', () => {
await TestBed.configureTestingModule({
imports: [PolicyTableComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(PolicyTableComponent);
component = fixture.componentInstance;

View File

@ -1,6 +1,8 @@
<h4>Policies</h4>
<div class="add">
<policy-add-button [policies]="policies" [groups]="groups" (policy)="addPolicy($event)" />
<policy-add-button [policies]="policies"
[groups]="groups"
(policy)="addPolicy($event)" />
</div>
<div>

View File

@ -1,5 +1,5 @@
h4 {
text-align: center;
text-align: center;
}
.add {

View File

@ -10,7 +10,7 @@ describe('PolicyComponent', () => {
await TestBed.configureTestingModule({
imports: [PolicyComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(PolicyComponent);
component = fixture.componentInstance;

View File

@ -5,23 +5,24 @@
</mat-card-title>
</mat-card-header>
<mat-card-content>
<twitch-redemption-dropdown
ngDefaultControl
[formControl]="redemptionFormControl"
[errorMessages]="redemptionErrorMessages"
[twitchRedemptions]="twitchRedemptions"
[(twitchRedemptionId)]="redemption.redemption_id" />
<twitch-redemption-dropdown ngDefaultControl
[formControl]="redemptionFormControl"
[errorMessages]="redemptionErrorMessages"
[twitchRedemptions]="twitchRedemptions"
[(twitchRedemptionId)]="redemption.redemption_id" />
<action-dropdown ngDefaultControl
[formControl]="actionFormControl"
[errorMessages]="actionErrorMessages"
[actions]="redeemableActions"
[(action)]="redemption.action_name" />
<action-dropdown
ngDefaultControl
[formControl]="actionFormControl"
[errorMessages]="actionErrorMessages"
[actions]="redeemableActions"
[(action)]="redemption.action_name" />
<mat-form-field>
<mat-label>Order</mat-label>
<input matInput type="number" [formControl]="orderFormControl" [value]="redemption.order" />
<input matInput
type="number"
[formControl]="orderFormControl"
[value]="redemption.order" />
@if (orderFormControl.invalid && (orderFormControl.dirty || orderFormControl.touched)) {
@for (error of orderErrorMessageKeys; track $index) {
@if (orderFormControl.hasError(error)) {
@ -31,19 +32,17 @@
}
</mat-form-field>
<div class="buttons">
<button
mat-icon-button
class="save"
[disabled]="waitForResponse || formGroups.invalid"
(click)="save()">
<button mat-icon-button
class="save"
[disabled]="waitForResponse || formGroups.invalid"
(click)="save()">
<mat-icon>save</mat-icon>
</button>
@if (redemption.id) {
<button
mat-icon-button
class="delete"
[disabled]="waitForResponse"
(click)="delete()">
<button mat-icon-button
class="delete"
[disabled]="waitForResponse"
(click)="delete()">
<mat-icon>delete</mat-icon>
</button>
}

View File

@ -42,7 +42,7 @@ button,
margin: 0;
}
button ~ button {
button~button {
margin-left: 2em;
}

View File

@ -10,7 +10,7 @@ describe('RedemptionItemEditComponent', () => {
await TestBed.configureTestingModule({
imports: [RedemptionItemEditComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(RedemptionItemEditComponent);
component = fixture.componentInstance;

View File

@ -1,8 +1,11 @@
<div class="content">
<button mat-button class="add" (click)="add()"><mat-icon>add</mat-icon> Add Redemption</button>
<button mat-button
class="add"
(click)="add()"><mat-icon>add</mat-icon> Add Redemption</button>
<mat-expansion-panel class="filters-expander" (opened)="panelOpenState.set(true)"
(closed)="panelOpenState.set(false)">
<mat-expansion-panel class="filters-expander"
(opened)="panelOpenState.set(true)"
(closed)="panelOpenState.set(false)">
<mat-expansion-panel-header>
<mat-panel-title>Filters</mat-panel-title>
<mat-panel-description>
@ -11,40 +14,55 @@
</mat-expansion-panel-header>
<div class="filters">
<twitch-redemption-dropdown [(twitchRedemptionId)]="filter_redemption" [search]="true" />
<action-dropdown [(action)]="filter_action_name" [search]="true" />
<twitch-redemption-dropdown [(twitchRedemptionId)]="filter_redemption"
[search]="true" />
<action-dropdown [(action)]="filter_action_name"
[search]="true" />
</div>
</mat-expansion-panel>
<div class="table-container">
<table mat-table [dataSource]="redemptions" class="mat-elevation-z8">
<table mat-table
[dataSource]="redemptions"
class="mat-elevation-z8">
<ng-container matColumnDef="twitch-redemption">
<th mat-header-cell *matHeaderCellDef>Twitch Redemption Name</th>
<td mat-cell *matCellDef="let redemption">{{getTwitchRedemptionNameById(redemption.redemption_id) || 'Unknown
<th mat-header-cell
*matHeaderCellDef>Twitch Redemption Name</th>
<td mat-cell
*matCellDef="let redemption">{{getTwitchRedemptionNameById(redemption.redemption_id) || 'Unknown
Twitch Redemption'}}</td>
</ng-container>
<ng-container matColumnDef="action-name">
<th mat-header-cell *matHeaderCellDef>Action Name</th>
<td mat-cell *matCellDef="let redemption">{{redemption.action_name}}</td>
<th mat-header-cell
*matHeaderCellDef>Action Name</th>
<td mat-cell
*matCellDef="let redemption">{{redemption.action_name}}</td>
</ng-container>
<ng-container matColumnDef="order">
<th mat-header-cell *matHeaderCellDef>Order</th>
<td mat-cell *matCellDef="let redemption">{{redemption.order}}</td>
<th mat-header-cell
*matHeaderCellDef>Order</th>
<td mat-cell
*matCellDef="let redemption">{{redemption.order}}</td>
</ng-container>
<ng-container matColumnDef="misc">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let redemption">
<button mat-icon-button (click)="openDialog(redemption)">
<th mat-header-cell
*matHeaderCellDef></th>
<td mat-cell
*matCellDef="let redemption">
<button mat-icon-button
(click)="openDialog(redemption)">
<mat-icon>edit</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<tr mat-header-row
*matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row
*matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</div>

View File

@ -10,7 +10,7 @@ describe('RedemptionListComponent', () => {
await TestBed.configureTestingModule({
imports: [RedemptionListComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(RedemptionListComponent);
component = fixture.componentInstance;

View File

@ -10,7 +10,7 @@ describe('RedemptionsComponent', () => {
await TestBed.configureTestingModule({
imports: [RedemptionsComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(RedemptionsComponent);
component = fixture.componentInstance;

View File

@ -18,9 +18,9 @@ export class RedemptionsComponent implements OnInit {
http = inject(HttpClient);
route = inject(ActivatedRoute);
redemptionService = inject(RedemptionService);
redemptions: Observable<Redemption[]>|undefined;
redemptions: Observable<Redemption[]> | undefined;
ngOnInit(): void {
}
}

View File

@ -1,8 +1,16 @@
<mat-form-field>
<mat-label>Twitch Redemption</mat-label>
<input type="text" matInput placeholder="Pick a Twitch redemption" aria-label="twitch redemption"
[formControl]="formControl" [matAutocomplete]="auto" (blur)="blur()" (input)="input()">
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" (optionSelected)="select($event.option.value)">
<input type="text"
matInput
placeholder="Pick a Twitch redemption"
aria-label="twitch redemption"
[formControl]="formControl"
[matAutocomplete]="auto"
(blur)="blur()"
(input)="input()">
<mat-autocomplete #auto="matAutocomplete"
[displayWith]="displayFn"
(optionSelected)="select($event.option.value)">
@for (redemption of filteredRedemptions; track redemption.id) {
<mat-option [value]="redemption">{{redemption.title}}</mat-option>
}

View File

@ -10,7 +10,7 @@ describe('TwitchRedemptionDropdownComponent', () => {
await TestBed.configureTestingModule({
imports: [TwitchRedemptionDropdownComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(TwitchRedemptionDropdownComponent);
component = fixture.componentInstance;

View File

@ -3,11 +3,11 @@ import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from
import { ApiAuthenticationService } from '../services/api/api-authentication.service';
@Injectable({
providedIn: 'root'
providedIn: 'root'
})
export class AuthAdminGuard implements CanActivate {
constructor(private auth: ApiAuthenticationService, private router: Router) {}
constructor(private auth: ApiAuthenticationService, private router: Router) { }
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
return this.auth.isAuthenticated() && this.auth.isAdmin();

View File

@ -2,20 +2,20 @@ import { Injectable } from "@angular/core";
import { Subject } from "rxjs"
@Injectable({
providedIn: 'root'
providedIn: 'root'
})
export default class EventService {
private subject = new Subject();
private subject = new Subject();
emit(eventName: string, payload: any) {
this.subject.next({ eventName, payload })
}
emit(eventName: string, payload: any) {
this.subject.next({ eventName, payload })
}
listen(eventName: string, callback: (event: any) => void) {
return this.subject.asObservable().subscribe((next: any) => {
if (eventName == next.eventName) {
callback(next.payload)
}
})
}
listen(eventName: string, callback: (event: any) => void) {
return this.subject.asObservable().subscribe((next: any) => {
if (eventName == next.eventName) {
callback(next.payload)
}
})
}
}

View File

@ -4,7 +4,7 @@ import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
export function createItemExistsInArrayValidator(items: any[], getter: (value: any) => any): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (!value)
return null;

View File

@ -4,11 +4,11 @@ import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
export function createTypeValidator(type: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (!value)
return null;
const matches = value.constructor.name === type
return matches ? null: { invalidType: 'Invalid choice.' };
return matches ? null : { invalidType: 'Invalid choice.' };
}
}

View File

@ -6,7 +6,10 @@
<div>
<mat-form-field>
<mat-label>Search</mat-label>
<input matInput cdkFocusInitial type="text" formControlName="search" />
<input matInput
cdkFocusInitial
type="text"
formControlName="search" />
@if (forms.get('search')?.invalid && (forms.get('search')?.dirty || forms.get('search')?.touched)) {
<small class="error">Search is required.</small>
}
@ -15,14 +18,17 @@
<div>
<mat-form-field>
<mat-label>Replace</mat-label>
<input matInput formControlName="replace" />
<input matInput
formControlName="replace" />
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-label>Regex Options</mat-label>
<mat-select multiple [formControl]="flagControl" [compareWith]="compare"
(selectionChange)="onSelectionChange($event)">
<mat-select multiple
[formControl]="flagControl"
[compareWith]="compare"
(selectionChange)="onSelectionChange($event)">
<mat-select-trigger>
{{optionsSelected[0] || ''}}
@if ((flagControl.value?.length || 0) > 1) {
@ -43,13 +49,11 @@
</form>
</mat-dialog-content>
<mat-dialog-actions>
<button
mat-button
(click)="onCancelClick()">Cancel</button>
<button
mat-button
(click)="onSaveClick()"
[disabled]="!forms.dirty || forms.invalid || waitForResponse">Save</button>
<button mat-button
(click)="onCancelClick()">Cancel</button>
<button mat-button
(click)="onSaveClick()"
[disabled]="!forms.dirty || forms.invalid || waitForResponse">Save</button>
</mat-dialog-actions>
</mat-card-content>
</mat-card>

View File

@ -10,7 +10,7 @@ describe('FilterItemEditComponent', () => {
await TestBed.configureTestingModule({
imports: [FilterItemEditComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(FilterItemEditComponent);
component = fixture.componentInstance;

View File

@ -11,11 +11,15 @@
</li>
<li>
<mat-menu #filterMenu>
<button mat-menu-item (click)="openDialog()">Edit</button>
<button mat-menu-item (click)="onDelete.emit(item)">Delete</button>
<button mat-menu-item
(click)="openDialog()">Edit</button>
<button mat-menu-item
(click)="onDelete.emit(item)">Delete</button>
</mat-menu>
<button mat-icon-button class="small-button" [matMenuTriggerFor]="filterMenu">
<button mat-icon-button
class="small-button"
[matMenuTriggerFor]="filterMenu">
<mat-icon>more_vert</mat-icon>
</button>
</li>

View File

@ -1,40 +1,40 @@
input {
display: inline;
font-size: large;
row-gap: 2em;
display: inline;
font-size: large;
row-gap: 2em;
}
ul {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
li {
list-style-type: none;
white-space: pre;
text-align: start;
text-wrap: wrap;
li {
list-style-type: none;
white-space: pre;
text-align: start;
text-wrap: wrap;
span {
overflow: hidden;
text-overflow: hidden;
}
> button {
background: #dddddd;
border-radius: 50%;
:hover {
border-radius: 50%;
}
}
span {
overflow: hidden;
text-overflow: hidden;
}
li:nth-child(1),
li:nth-child(2) {
flex: 1;
>button {
background: #dddddd;
border-radius: 50%;
:hover {
border-radius: 50%;
}
}
}
li:nth-child(1),
li:nth-child(2) {
flex: 1;
}
}
.small-button {

View File

@ -11,7 +11,7 @@ describe('FilterItemComponent', () => {
await TestBed.configureTestingModule({
imports: [FilterItemComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(FilterItemComponent);
component = fixture.componentInstance;

View File

@ -1,17 +1,16 @@
<div>
<ul class="data">
<li>
<ul class="header">
<li>Search</li>
<li>Replace</li>
<li></li>
</ul>
<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)" />
<tts-filter-item [item]="filter"
(onDelete)="deleteFilter($event)" />
</li>
}
</ul>

View File

@ -7,23 +7,23 @@ ul.data {
padding: 0;
overflow: auto;
> li {
>li {
display: block;
list-style-type: none;
padding: 0.75em 1em;
border-bottom: 1px solid #aaaaaa;
}
> li:first-child {
>li:first-child {
padding: 0 1em;
border-bottom: 0 solid #aaaaaa;
}
> li:last-child {
>li:last-child {
border-bottom: 0 solid #aaaaaa;
}
> li:nth-child(2n) {
>li:nth-child(2n) {
background-color: #f5f5f5;
}
}

View File

@ -10,7 +10,7 @@ describe('FilterListComponent', () => {
await TestBed.configureTestingModule({
imports: [FilterListComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(FilterListComponent);
component = fixture.componentInstance;

View File

@ -2,13 +2,15 @@
<article>
<h3>Filters</h3>
<div>
<button mat-button class="add" aria-label="Add a filter" (click)="openDialog()">
<button mat-button
class="add"
aria-label="Add a filter"
(click)="openDialog()">
<mat-icon>add</mat-icon>Add TTS Filter
</button>
</div>
</article>
<div class="grow">
<tts-filter-list
[filters]="items" />
<tts-filter-list [filters]="items" />
</div>
</main>

View File

@ -10,7 +10,7 @@ describe('FiltersComponent', () => {
await TestBed.configureTestingModule({
imports: [FiltersComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(FiltersComponent);
component = fixture.componentInstance;

View File

@ -10,7 +10,7 @@ describe('TwitchAuthCallbackComponent', () => {
await TestBed.configureTestingModule({
imports: [TwitchAuthCallbackComponent]
})
.compileComponents();
.compileComponents();
fixture = TestBed.createComponent(TwitchAuthCallbackComponent);
component = fixture.componentInstance;

View File

@ -1,15 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tom-to-Speech</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>
<head>
<meta charset="utf-8">
<title>Tom-to-Speech</title>
<base href="/">
<meta name="viewport"
content="width=device-width, initial-scale=1">
<link rel="icon"
type="image/x-icon"
href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

View File

@ -1,7 +1,14 @@
/* You can add global styles to this file, and also import other style files */
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
.mat-mdc-dialog-surface {
background-color: transparent !important;
@ -14,17 +21,17 @@ body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
/* Track */
::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px grey;
box-shadow: inset 0 0 5px grey;
border-radius: 10px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: darkgrey;
background: darkgrey;
border-radius: 10px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: rgb(122, 122, 122);
background: rgb(122, 122, 122);
}

View File

@ -1,19 +1,19 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": [
"node"
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": [
"node"
]
},
"files": [
"src/main.ts",
"src/main.server.ts",
"server.ts"
],
"include": [
"src/**/*.d.ts"
]
},
"files": [
"src/main.ts",
"src/main.server.ts",
"server.ts"
],
"include": [
"src/**/*.d.ts"
]
}
}

View File

@ -1,33 +1,33 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@ -1,15 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
}