Added pages to see, create, modify & delete redeemable actions. User card top right with disconnect & log out. Code clean up.
This commit is contained in:
@ -0,0 +1,94 @@
|
||||
<body>
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title-group>
|
||||
<mat-card-title>{{isNew ? "New Action" : previousName}}</mat-card-title>
|
||||
<mat-card-subtitle>{{isNew ? 'Creating a new action' : 'Modifying an existing action'}}</mat-card-subtitle>
|
||||
</mat-card-title-group>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<form class="grid" [formGroup]="formGroup">
|
||||
<div class="item">
|
||||
<mat-form-field>
|
||||
<mat-label>Redeemable Action Name</mat-label>
|
||||
<input matInput type="text" formControlName="name">
|
||||
@if (isNew && formGroup.get('name')?.invalid && (formGroup.get('name')?.dirty ||
|
||||
formGroup.get('name')?.touched)) {
|
||||
@if (formGroup.get('name')?.hasError('required')) {
|
||||
<small class="error">The name is required.</small>
|
||||
}
|
||||
@if (formGroup.get('name')?.hasError('itemExistsInArray')) {
|
||||
<small class="error">The name is already in use.</small>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="item">
|
||||
<mat-form-field>
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-select matInput formControlName="type" (selectionChange)="action.type = $event.value">
|
||||
@for (type of actionTypes; track $index) {
|
||||
<mat-option value="{{type}}">{{type}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (isNew && formGroup.get('type')?.invalid && (formGroup.get('type')?.dirty ||
|
||||
formGroup.get('type')?.touched)) {
|
||||
@if (formGroup.get('type')?.hasError('required')) {
|
||||
<small class="error">The type is required.</small>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if (actionEntries.hasOwnProperty(action.type)) {
|
||||
<section class="grid">
|
||||
@for (field of actionEntries[action.type]; track $index) {
|
||||
<div class="item">
|
||||
@if (field.type == 'text') {
|
||||
<mat-form-field>
|
||||
<mat-label>{{field.label}}</mat-label>
|
||||
<input matInput type="text" placeholder="{{field.placeholder}}" [formControl]="field.control"
|
||||
[(ngModel)]="action.data[field.key]">
|
||||
@if (field.control.invalid && (field.control.dirty || field.control.touched)) {
|
||||
@if (field.control.hasError('required')) {
|
||||
<small class="error">This field is required.</small>
|
||||
}
|
||||
@if (field.control.hasError('minlength')) {
|
||||
<small class="error">The value needs to be longer.</small>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
}
|
||||
@else if (field.type == 'number') {
|
||||
<mat-form-field>
|
||||
<mat-label>{{field.label}}</mat-label>
|
||||
<input matInput type="number" [formControl]="field.control">
|
||||
@if (field.control.invalid && (field.control.dirty || field.control.touched)) {
|
||||
@if (field.control.hasError('required')) {
|
||||
<small class="error">This field is required.</small>
|
||||
}
|
||||
@if (field.control.hasError('min')) {
|
||||
<small class="error">The value must be higher.</small>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
</mat-card-content>
|
||||
|
||||
<mat-card-actions class="actions" align="end">
|
||||
@if (!isNew) {
|
||||
<button mat-raised-button class="delete" (click)="deleteAction(action)">Delete</button>
|
||||
}
|
||||
<button mat-raised-button (click)="dialogRef.close()">Cancel</button>
|
||||
<button mat-raised-button disabled="{{!formsDirty || !formsValidity}}" (click)="save()">Save</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</body>
|
@ -0,0 +1,30 @@
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-auto-flow: row dense;
|
||||
grid-gap: 0 1em;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: block;
|
||||
color: #ba1a1a;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.delete {
|
||||
background-color: #ea5151;
|
||||
color: #ba1a1a;
|
||||
}
|
||||
|
||||
.mdc-button ~ .mdc-button {
|
||||
margin-left: 1em;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ActionItemEditComponent } from './action-item-edit.component';
|
||||
|
||||
describe('ActionItemEditComponent', () => {
|
||||
let component: ActionItemEditComponent;
|
||||
let fixture: ComponentFixture<ActionItemEditComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ActionItemEditComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ActionItemEditComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
267
src/app/actions/action-item-edit/action-item-edit.component.ts
Normal file
267
src/app/actions/action-item-edit/action-item-edit.component.ts
Normal file
@ -0,0 +1,267 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { createItemExistsInArrayValidator } from '../../shared/validators/item-exists-in-array';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
|
||||
@Component({
|
||||
selector: 'action-item-edit',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule
|
||||
],
|
||||
templateUrl: './action-item-edit.component.html',
|
||||
styleUrl: './action-item-edit.component.scss'
|
||||
})
|
||||
export class ActionItemEditComponent implements OnInit {
|
||||
readonly client = inject(HermesClientService);
|
||||
readonly dialogRef = inject(MatDialogRef<ActionItemEditComponent>);
|
||||
readonly data = inject<{ action: RedeemableAction, actions:RedeemableAction[] }>(MAT_DIALOG_DATA);
|
||||
action = this.data.action;
|
||||
actions = this.data.actions;
|
||||
readonly actionEntries: ({ [key: string]: any[] }) = {
|
||||
'SLEEP': [
|
||||
{
|
||||
key: 'sleep',
|
||||
type: 'number',
|
||||
label: 'Sleep (ms)',
|
||||
control: new FormControl(this.action.data['sleep'], [Validators.required, Validators.min(500)]),
|
||||
}
|
||||
],
|
||||
'APPEND_TO_FILE': [
|
||||
{
|
||||
key: 'file_path',
|
||||
type: 'text',
|
||||
label: 'File Path',
|
||||
placeholder: '%userprofile%/Desktop/file.txt',
|
||||
control: new FormControl(this.action.data['file_path'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'file_content',
|
||||
type: 'text',
|
||||
label: 'Content',
|
||||
placeholder: '%chatter% from %broadcaster%\'s chat says hi.',
|
||||
control: new FormControl(this.action.data['file_content'], [Validators.required]),
|
||||
},
|
||||
],
|
||||
'WRITE_TO_FILE': [
|
||||
{
|
||||
key: 'file_path',
|
||||
type: 'text',
|
||||
label: 'File Path',
|
||||
placeholder: '%userprofile%/Desktop/file.txt',
|
||||
control: new FormControl(this.action.data['file_path'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'file_content',
|
||||
type: 'text',
|
||||
label: 'Content',
|
||||
placeholder: '%chatter% from %broadcaster%\'s chat says hi.',
|
||||
control: new FormControl(this.action.data['file_content'], [Validators.required]),
|
||||
},
|
||||
],
|
||||
'OBS_TRANSFORM': [
|
||||
{
|
||||
key: 'scene_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Name',
|
||||
placeholder: 'Main Scene',
|
||||
control: new FormControl(this.action.data['scene_name'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'scene_item_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Item Name',
|
||||
placeholder: 'Item',
|
||||
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'position_x',
|
||||
type: 'text',
|
||||
label: 'Position X',
|
||||
placeholder: 'x + 50',
|
||||
control: new FormControl(this.action.data['position_x'], []),
|
||||
},
|
||||
{
|
||||
key: 'position_y',
|
||||
type: 'text',
|
||||
label: 'Position Y',
|
||||
placeholder: 'x - 166',
|
||||
control: new FormControl(this.action.data['position_y'], []),
|
||||
},
|
||||
{
|
||||
key: 'rotation',
|
||||
type: 'text',
|
||||
label: 'Rotation (in degrees)',
|
||||
placeholder: 'mod(x + 45, 360)',
|
||||
control: new FormControl(this.action.data['rotation'], []),
|
||||
},
|
||||
],
|
||||
'AUDIO_FILE': [
|
||||
{
|
||||
key: 'file_path',
|
||||
type: 'text',
|
||||
label: 'Audio File Path',
|
||||
placeholder: '%userprofile%/Desktop/audio.mp3',
|
||||
control: new FormControl(this.action.data['file_path'], [Validators.required]),
|
||||
},
|
||||
],
|
||||
'RANDOM_TTS_VOICE': [],
|
||||
'SPECIFIC_TTS_VOICE': [
|
||||
{
|
||||
key: 'tts_voice',
|
||||
type: 'text',
|
||||
label: 'TTS Voice Name',
|
||||
placeholder: 'Brian',
|
||||
control: new FormControl(this.action.data['tts_voice'], [Validators.required, Validators.minLength(2)]),
|
||||
},
|
||||
],
|
||||
'TOGGLE_OBS_VISIBILITY': [
|
||||
{
|
||||
key: 'scene_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Name',
|
||||
placeholder: 'Main Scene',
|
||||
control: new FormControl(this.action.data['scene_name'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'scene_item_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Item Name',
|
||||
placeholder: 'Item',
|
||||
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||
},
|
||||
],
|
||||
'SPECIFIC_OBS_VISIBILITY': [
|
||||
{
|
||||
key: 'scene_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Name',
|
||||
placeholder: 'Main Scene',
|
||||
control: new FormControl(this.action.data['scene_name'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'scene_item_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Item Name',
|
||||
placeholder: 'Item',
|
||||
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'obs_visible',
|
||||
type: 'text-values',
|
||||
label: 'Visibility',
|
||||
values: ['visible', 'hidden'],
|
||||
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||
},
|
||||
],
|
||||
'SPECIFIC_OBS_INDEX': [
|
||||
{
|
||||
key: 'scene_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Name',
|
||||
placeholder: 'Main Scene',
|
||||
control: new FormControl(this.action.data['scene_name'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'scene_item_name',
|
||||
type: 'text',
|
||||
label: 'OBS Scene Item Name',
|
||||
placeholder: 'Item',
|
||||
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||
},
|
||||
{
|
||||
key: 'obs_index',
|
||||
type: 'number',
|
||||
label: 'Visibility',
|
||||
control: new FormControl(this.action.data['scene_item_name'], [Validators.required, Validators.min(0)]),
|
||||
},
|
||||
],
|
||||
'NIGHTBOT_PLAY': [],
|
||||
'VEADOTUBE_SET_STATE': [
|
||||
{
|
||||
key: 'state',
|
||||
type: 'text',
|
||||
label: 'Veadotube State name',
|
||||
placeholder: 'state #1',
|
||||
control: new FormControl(this.action.data['state'], [Validators.required]),
|
||||
},
|
||||
],
|
||||
};
|
||||
readonly actionTypes = Object.keys(this.actionEntries);
|
||||
|
||||
isNew: boolean = true;
|
||||
previousName: string = this.action.name;
|
||||
|
||||
readonly formGroup = new FormGroup({
|
||||
name: new FormControl(this.action.name, [Validators.required]),
|
||||
type: new FormControl(this.action.type, [Validators.required]),
|
||||
});
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isNew = this.action.name.length <= 0;
|
||||
this.previousName = this.action.name;
|
||||
if (!this.isNew)
|
||||
this.formGroup.get('name')!.disable()
|
||||
else {
|
||||
this.formGroup.get('name')?.addValidators(createItemExistsInArrayValidator(this.actions, a => a.name));
|
||||
}
|
||||
}
|
||||
|
||||
get exists(): boolean {
|
||||
return this.actions.some(a => a.name == this.action.name);
|
||||
}
|
||||
|
||||
get formsValidity(): boolean {
|
||||
return this.formGroup.valid && this.action.type in this.actionEntries
|
||||
&& this.actionEntries[this.action.type].every(f => f.control.valid);
|
||||
}
|
||||
|
||||
get formsDirty(): boolean {
|
||||
return this.formGroup.dirty || this.action.type in this.actionEntries
|
||||
&& this.actionEntries[this.action.type].some(f => f.control.dirty);
|
||||
}
|
||||
|
||||
deleteAction(action: RedeemableAction): void {
|
||||
if (this.isNew)
|
||||
return;
|
||||
|
||||
this.client.deleteRedeemableAction(action.name);
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fields = this.actionEntries[this.action.type];
|
||||
if (fields.some(f => f.control.invalid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.action.name = this.formGroup.get('name')!.value!;
|
||||
this.action.type = this.formGroup.get('type')!.value!;
|
||||
this.action.data = {}
|
||||
for (const entry of this.actionEntries[this.action.type]) {
|
||||
this.action.data[entry.key] = entry.control.value!.toString();
|
||||
}
|
||||
|
||||
if (!(this.action.type in this.actionEntries)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialogRef.close(this.action);
|
||||
}
|
||||
}
|
11
src/app/actions/action-list/action-list.component.html
Normal file
11
src/app/actions/action-list/action-list.component.html
Normal file
@ -0,0 +1,11 @@
|
||||
<main>
|
||||
@for (action of actions; track $index) {
|
||||
<button type="button" class="container" (click)="modify(action)">
|
||||
<span class="title">{{action.name}}</span>
|
||||
<span class="subtitle">{{action.type}}</span>
|
||||
</button>
|
||||
}
|
||||
<button type="button" class="container" (click)="create()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</main>
|
56
src/app/actions/action-list/action-list.component.scss
Normal file
56
src/app/actions/action-list/action-list.component.scss
Normal file
@ -0,0 +1,56 @@
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
|
||||
@media (min-width:1200px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width:1650px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width:2200px) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
grid-auto-flow: row dense;
|
||||
grid-gap: 1rem;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
background-color: #fafafa;
|
||||
width: 80%;
|
||||
justify-self: center;
|
||||
|
||||
& .container {
|
||||
border-color: grey;
|
||||
border-radius: 20px;
|
||||
border: 1px solid grey;
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
|
||||
& span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& .title {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
& .subtitle {
|
||||
font-size: smaller;
|
||||
color: lightgrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& article:first-child {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
23
src/app/actions/action-list/action-list.component.spec.ts
Normal file
23
src/app/actions/action-list/action-list.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ActionListComponent } from './action-list.component';
|
||||
|
||||
describe('ActionListComponent', () => {
|
||||
let component: ActionListComponent;
|
||||
let fixture: ComponentFixture<ActionListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ActionListComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ActionListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
66
src/app/actions/action-list/action-list.component.ts
Normal file
66
src/app/actions/action-list/action-list.component.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActionItemEditComponent } from '../action-item-edit/action-item-edit.component';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
|
||||
@Component({
|
||||
selector: 'action-list',
|
||||
standalone: true,
|
||||
imports: [MatButtonModule, MatFormFieldModule, MatIconModule, MatListModule],
|
||||
templateUrl: './action-list.component.html',
|
||||
styleUrl: './action-list.component.scss'
|
||||
})
|
||||
export class ActionListComponent {
|
||||
@Input() actions: RedeemableAction[] = []
|
||||
@Output() actionsChange = new EventEmitter<RedeemableAction>();
|
||||
readonly dialog = inject(MatDialog);
|
||||
readonly client = inject(HermesClientService);
|
||||
opened = false;
|
||||
|
||||
create(): void {
|
||||
this.openDialog({ user_id: '', name: '', type: '', data: {} });
|
||||
}
|
||||
|
||||
modify(action: RedeemableAction): void {
|
||||
this.openDialog(action);
|
||||
}
|
||||
|
||||
private openDialog(action: RedeemableAction): void {
|
||||
if (this.opened)
|
||||
return;
|
||||
|
||||
this.opened = true;
|
||||
|
||||
const dialogRef = this.dialog.open(ActionItemEditComponent, {
|
||||
data: { action: {user_id: action.user_id, name: action.name, type: action.type, data: action.data }, actions: this.actions },
|
||||
});
|
||||
const isNewAction = action.name.length <= 0;
|
||||
const requestType = isNewAction ? 'create_redeemable_action' : 'update_redeemable_action';
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: RedeemableAction) => {
|
||||
this.opened = false;
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == requestType && d.d.data.name == result.name)
|
||||
?.subscribe(_ => {
|
||||
if (isNewAction) {
|
||||
this.actionsChange.emit(result);
|
||||
} else {
|
||||
action.type = result.type;
|
||||
action.data = result.data;
|
||||
}
|
||||
});
|
||||
|
||||
if (isNewAction)
|
||||
this.client.createRedeemableAction(result.name, result.type, result.data);
|
||||
else
|
||||
this.client.updateRedeemableAction(result.name, result.type, result.data);
|
||||
});
|
||||
}
|
||||
}
|
17
src/app/actions/actions.module.ts
Normal file
17
src/app/actions/actions.module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActionsComponent } from './actions/actions.component';
|
||||
import { ActionListComponent } from './action-list/action-list.component';
|
||||
import { ActionItemComponent } from './action-item/action-item.component';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
ActionsComponent,
|
||||
ActionListComponent,
|
||||
ActionItemComponent,
|
||||
]
|
||||
})
|
||||
export class ActionsModule { }
|
31
src/app/actions/actions/actions.component.html
Normal file
31
src/app/actions/actions/actions.component.html
Normal file
@ -0,0 +1,31 @@
|
||||
<body>
|
||||
<h3>Redeemable Actions</h3>
|
||||
|
||||
<section>
|
||||
<article>
|
||||
<mat-form-field>
|
||||
<mat-label>Filter</mat-label>
|
||||
<mat-select (selectionChange)="onFilterChange($event.value)" value="0">
|
||||
<mat-select-trigger>
|
||||
<mat-icon matPrefix>filter_list</mat-icon> {{filter.name}}
|
||||
</mat-select-trigger>
|
||||
@for (item of filters; track $index) {
|
||||
<mat-option value="{{$index}}">{{item.name}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</article>
|
||||
<article>
|
||||
<mat-form-field>
|
||||
<mat-label>Search</mat-label>
|
||||
<input matInput
|
||||
type="text"
|
||||
placeholder="Name of action"
|
||||
[formControl]="searchControl"
|
||||
[(ngModel)]="search">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
</article>
|
||||
</section>
|
||||
<action-list [actions]="actions" (actionsChange)="items.push($event)" />
|
||||
</body>
|
23
src/app/actions/actions/actions.component.scss
Normal file
23
src/app/actions/actions/actions.component.scss
Normal file
@ -0,0 +1,23 @@
|
||||
body, h3 {
|
||||
background-color: #fafafa;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 70%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@media (max-width:1250px) {
|
||||
display: block;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
article {
|
||||
display: flex;
|
||||
justify-content:space-around;
|
||||
}
|
||||
}
|
23
src/app/actions/actions/actions.component.spec.ts
Normal file
23
src/app/actions/actions/actions.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ActionsComponent } from './actions.component';
|
||||
|
||||
describe('ActionsComponent', () => {
|
||||
let component: ActionsComponent;
|
||||
let fixture: ComponentFixture<ActionsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ActionsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ActionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
93
src/app/actions/actions/actions.component.ts
Normal file
93
src/app/actions/actions/actions.component.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { ActionListComponent } from "../action-list/action-list.component";
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
interface IActionFilter {
|
||||
name: string
|
||||
filter: (action: any) => boolean
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'actions',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ActionListComponent,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatSelectModule
|
||||
],
|
||||
templateUrl: './actions.component.html',
|
||||
styleUrl: './actions.component.scss'
|
||||
})
|
||||
export class ActionsComponent implements OnInit {
|
||||
filters: IActionFilter[] = [
|
||||
{ name: 'All', filter: _ => true },
|
||||
{ name: 'Local File', filter: data => data.type.includes('_FILE') },
|
||||
{ name: 'Nightbot', filter: data => data.type.includes('NIGHTBOT_') },
|
||||
{ name: 'OBS', filter: data => data.type.includes('OBS_') },
|
||||
{ name: 'Sleep', filter: data => data.type == "SLEEP" },
|
||||
{ name: 'TTS', filter: data => data.type.includes('TTS') },
|
||||
{ name: 'Veadotube', filter: data => data.type.includes('VEADOTUBE') },
|
||||
];
|
||||
|
||||
client = inject(HermesClientService);
|
||||
filter = this.filters[0];
|
||||
searchControl = new FormControl('');
|
||||
search = '';
|
||||
items: RedeemableAction[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.client.subscribeToRequests('get_redeemable_actions', d => {
|
||||
this.items = d.data;
|
||||
});
|
||||
this.client.subscribeToRequests('create_redeemable_action', d => {
|
||||
if (d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actions.push(d.data);
|
||||
});
|
||||
this.client.subscribeToRequests('update_redeemable_action', d => {
|
||||
if (d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = this.actions.find(a => a.name == d.data.name);
|
||||
if (action) {
|
||||
action.type = d.data.type;
|
||||
action.data = d.data.data;
|
||||
}
|
||||
});
|
||||
this.client.subscribeToRequests('delete_redeemable_action', d => {
|
||||
// if (d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id)) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
this.items = this.actions.filter(a => a.name != d.request.data.name);
|
||||
});
|
||||
|
||||
this.client.fetchRedeemableActions();
|
||||
}
|
||||
|
||||
get actions(): RedeemableAction[] {
|
||||
const searchLower = this.search.toLowerCase();
|
||||
return this.items.filter(this.filter.filter)
|
||||
.filter((action) => action.name.toLowerCase().includes(searchLower));
|
||||
}
|
||||
|
||||
set actions(value) {
|
||||
this.items = value;
|
||||
}
|
||||
|
||||
onFilterChange(event: any): void {
|
||||
this.filter = this.filters[event];
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user