mirror of
https://github.com/chuot/rdio-scanner.git
synced 2026-08-15 01:13:02 -06:00
Version 5.1.0
This commit is contained in:
parent
4fbed0acb0
commit
07e7a563bb
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -1,3 +1,21 @@
|
|||
# Version 5.1
|
||||
|
||||
This one is a big one... **Be sure to backup your config.json and your database.sqlite before updating.**
|
||||
|
||||
- With the exception of some parameters like the SSL certificates, all configurations have been moved to an administrative dashboard for easier configuration. No more config.json editing!
|
||||
- Access codes can now be set with a limit of simultaneous connections. It is also possible to configure an expiration date for each access codes.
|
||||
- Auto populate option can now be set per system in addition to globally.
|
||||
- Duplicate call detection is now optional and can be disabled from the options section of the administrative dashboard.
|
||||
- On a per system basis, it is now possible to blacklist certain talkgroup IDs against ingestion.
|
||||
- Groups and tags are now defined in their own section, then linked to talkgroup definitions.
|
||||
- Server logs are now stored in the database and accessed through the administrative dashboard, in addition to the standard output.
|
||||
- Talkgroups CSV files can now be loaded in from the administrative dashboard.
|
||||
- Server configuration can be exported/imported to/from a JSON file.
|
||||
- The downstream id_as property is gone due to its complexity of implementation with the new systems/talkgroups selection dialog for access codes, downstreams and apikeys.
|
||||
- The keyboard shortcuts are a thing of the past. They caused conflicts with other features.
|
||||
- Minor changes to the webapp look, less rounded.
|
||||
- Talkgroup buttons label now wraps on 2 lines.
|
||||
|
||||
# Version 5.0
|
||||
|
||||
- Add rdioScanner.options.autoPopulate which by default is true. The configuration file will now be automatically populated from new received calls with unknown system/talkgroup.
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ FROM node:lts-alpine
|
|||
LABEL maintainer="Chrystian Huot <chrystian.huot@saubeo.solutions>"
|
||||
WORKDIR /app
|
||||
ENV APP_DATA=/app/data \
|
||||
APP_PORT=3000
|
||||
RUN apk --no-cache --no-progress add ffmpeg sqlite tzdata && \
|
||||
APP_PORT=3000 \
|
||||
DOCKER=1
|
||||
RUN apk --no-cache --no-progress add ffmpeg openssl sqlite tzdata && \
|
||||
mkdir -p "${APP_DATA}"
|
||||
COPY LICENSE .
|
||||
COPY --from=client-build /app/client/dist/rdio-scanner/. client/.
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -10,12 +10,12 @@ The client web app is responsive and also works well on mobile devices.
|
|||
|
||||
[Rdio Scanner](https://github.com/chuot/rdio-scanner) has all the backend and frontend components to deal with your audio files.
|
||||
|
||||
The server side of [Rdio Scanner](https://github.com/chuot/rdio-scanner) can monitor directories for audio files to be ingested, or has an API to upload audio files from a remote (or local) location. Those files are associated to defined systems/talkgroups from the configuration file, then stored locally in its database. The web application clients are notified of these new audio files based on their own systems/talkgroups selection. It is also possible for web application clients to browse for archived audio files and replay them in an offline manner, or download them individually.
|
||||
The server side of [Rdio Scanner](https://github.com/chuot/rdio-scanner) can monitor directories for audio files to be ingested, or has an API to upload audio files from a remote (or local) location. Those files are associated to defined systems/talkgroups in the server's configuration, then stored locally in its database. The web application clients are notified of these new audio files based on their own systems/talkgroups selection. It is also possible for clients to browse for archived audio files and replay them in an offline manner, or download them individually.
|
||||
|
||||
## Main features
|
||||
|
||||
- It's a progressive web app that also works great on mobile devices.
|
||||
- Simplistic interface, but strong usability.
|
||||
- Administrative dashboard for easy server configuration.
|
||||
- Configurable systems and talkgroups.
|
||||
- Talkgroups from any systems can be grouped together for better selection of live streams.
|
||||
- Access control via password with possible restriction to systems/talkgroups.
|
||||
|
|
@ -45,10 +45,16 @@ The server side of [Rdio Scanner](https://github.com/chuot/rdio-scanner) can mon
|
|||
- [docs/examples](./docs/examples) - Some miscellaneous examples
|
||||
- [CONTRIBUTING.md](./CONTRIBUTING.md) - Contribution guidelines
|
||||
|
||||
> Raspberry Pi users can use this [script](https://github.com/chuot/rdio-scanner-pi-setup) to simplify the installation procedure.
|
||||
|
||||
## Questions / Comments
|
||||
|
||||
You can ask your questions or post your comments on the [Gitter Rdio Scanner Lobby](https://gitter.im/rdio-scanner/Lobby) where the community continues to grow.
|
||||
|
||||
## Commercial support
|
||||
|
||||
Commercial support is available at https://saubeo.solutions/.
|
||||
|
||||
## Show your appreciation / Support the author
|
||||
|
||||
If you like [Rdio Scanner](https://github.com/chuot/rdio-scanner) please consider **starring the repository** to show you appreciation to the author for his hard work. It cost nothing but is really appreciated.
|
||||
|
|
|
|||
36
client/.eslintrc.json
Normal file
36
client/.eslintrc.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"env": {
|
||||
"commonjs": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"max-len": [
|
||||
"error",
|
||||
{
|
||||
"code": 140
|
||||
}
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"prefix": "rdio-scanner",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "rdio-scanner:build",
|
||||
"proxyConfig": "proxy.conf.js"
|
||||
"proxyConfig": "src/proxy.conf.js"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3387
client/package-lock.json
generated
3387
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,30 +1,32 @@
|
|||
{
|
||||
"author": "Chrystian Huot <chrystian.huot@saubeo.solutions>",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^11.2.8",
|
||||
"@angular/cdk": "^11.2.7",
|
||||
"@angular/common": "~11.2.8",
|
||||
"@angular/compiler": "~11.2.8",
|
||||
"@angular/core": "~11.2.8",
|
||||
"@angular/forms": "~11.2.8",
|
||||
"@angular/material": "^11.2.7",
|
||||
"@angular/platform-browser": "~11.2.8",
|
||||
"@angular/platform-browser-dynamic": "~11.2.8",
|
||||
"@angular/service-worker": "~11.2.8",
|
||||
"@angular/animations": "^11.2.14",
|
||||
"@angular/cdk": "^11.2.12",
|
||||
"@angular/common": "~11.2.14",
|
||||
"@angular/compiler": "~11.2.14",
|
||||
"@angular/core": "~11.2.14",
|
||||
"@angular/forms": "~11.2.14",
|
||||
"@angular/material": "^11.2.12",
|
||||
"@angular/platform-browser": "~11.2.14",
|
||||
"@angular/platform-browser-dynamic": "~11.2.14",
|
||||
"@angular/router": "^11.2.14",
|
||||
"@angular/service-worker": "^11.2.14",
|
||||
"rxjs": "~6.6.7",
|
||||
"tslib": "^2.1.0",
|
||||
"tslib": "^2.2.0",
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.1102.7",
|
||||
"@angular/cli": "^11.2.7",
|
||||
"@angular/compiler-cli": "~11.2.8",
|
||||
"@types/node": "~14.14.37",
|
||||
"codelyzer": "^6.0.1",
|
||||
"@angular-devkit/build-angular": "~0.1102.13",
|
||||
"@angular/cli": "^11.2.10",
|
||||
"@angular/compiler-cli": "~11.2.11",
|
||||
"@types/node": "~14.14.41",
|
||||
"ts-node": "~9.1.1",
|
||||
"tslint": "~6.1.3",
|
||||
"typescript": "~4.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
},
|
||||
"license": "LICENSE",
|
||||
"name": "rdio-scanner-client",
|
||||
"private": true,
|
||||
|
|
@ -32,5 +34,5 @@
|
|||
"build": "ng build --base-href ./ --prod",
|
||||
"start": "ng serve"
|
||||
},
|
||||
"version": "5.0.5"
|
||||
"version": "5.1.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<app-rdio-scanner></app-rdio-scanner>
|
||||
<router-outlet></router-outlet>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -23,18 +23,25 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
|||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { environment } from '../environments/environment';
|
||||
import { AppComponent } from './app.component';
|
||||
import { AppRdioScannerModule } from './components/rdio-scanner/rdio-scanner.module';
|
||||
import { RdioScannerModule } from './components/rdio-scanner/rdio-scanner.module';
|
||||
import { AppSharedModule } from './shared/shared.module';
|
||||
import { routes } from './app.routes';
|
||||
|
||||
@NgModule({
|
||||
bootstrap: [AppComponent],
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
AppRdioScannerModule,
|
||||
AppSharedModule.forRoot(),
|
||||
RdioScannerModule,
|
||||
AppSharedModule.forRoot({
|
||||
routerExtraOptions: { enableTracing: false },
|
||||
routerRoutes: routes,
|
||||
}),
|
||||
BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||
ServiceWorkerModule.register('ngsw-worker.js', {
|
||||
enabled: environment.production,
|
||||
registrationStrategy: 'registerWhenStable:30000',
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class AppModule { }
|
||||
|
|
|
|||
19
client/src/app/app.routes.ts
Normal file
19
client/src/app/app.routes.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Saubeo Solutions All Rights Reserved
|
||||
*
|
||||
* This source code is proprietary and confidential
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
*/
|
||||
|
||||
import { Routes } from '@angular/router';
|
||||
import { routes as rdioScannerRoutes } from './pages/rdio-scanner';
|
||||
|
||||
export const routes: Routes = [
|
||||
...rdioScannerRoutes,
|
||||
{
|
||||
path: '**',
|
||||
pathMatch: 'full',
|
||||
redirectTo: '',
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<ng-container *ngIf="!authenticated">
|
||||
<rdio-scanner-admin-login></rdio-scanner-admin-login>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="authenticated">
|
||||
<rdio-scanner-admin-todos></rdio-scanner-admin-todos>
|
||||
<mat-accordion displayMode="flat">
|
||||
<mat-expansion-panel #configPanel (afterCollapse)="configComponent?.closeAll()">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>settings</mat-icon>
|
||||
Config
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-config #configComponent></rdio-scanner-admin-config>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel (afterExpand)="logsComponent.reload()">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>article</mat-icon>
|
||||
Logs
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-logs #logsComponent></rdio-scanner-admin-logs>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel (afterCollapse)="toolsComponent?.closeAll()">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>build</mat-icon>
|
||||
Tools
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-tools #toolsComponent (config)="configComponent.reset($event, { dirty: true }); configPanel.open()"></rdio-scanner-admin-tools>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel hideToggle (click)="logout()">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>logout</mat-icon>
|
||||
Logout
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
rdio-scanner-admin {
|
||||
.mat-expansion-panel-header {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mat-expansion-panel-header-title {
|
||||
align-items: center;
|
||||
|
||||
.mat-icon:not(.mat-warn):first-of-type {
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 719px) {
|
||||
rdio-scanner-admin {
|
||||
.mat-expansion-panel-header {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.mat-expansion-panel-body {
|
||||
padding: 0 0.5rem 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
rdio-scanner-admin {
|
||||
.mat-expansion-panel-header {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.mat-expansion-panel-body {
|
||||
padding: 0 1rem 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component, OnDestroy, ViewEncapsulation } from '@angular/core';
|
||||
import { AdminEvent, RdioScannerAdminService, Group, Tag } from './admin.service';
|
||||
|
||||
@Component({
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
selector: 'rdio-scanner-admin',
|
||||
styleUrls: ['./admin.component.scss'],
|
||||
templateUrl: './admin.component.html',
|
||||
})
|
||||
export class RdioScannerAdminComponent implements OnDestroy {
|
||||
authenticated = this.adminService.authenticated;
|
||||
|
||||
groups: Group[] = [];
|
||||
|
||||
tags: Tag[] = [];
|
||||
|
||||
private eventSubscription = this.adminService.event.subscribe(async (event: AdminEvent) => {
|
||||
if ('authenticated' in event) {
|
||||
this.authenticated = event.authenticated || false;
|
||||
}
|
||||
});
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService) { }
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.eventSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await this.adminService.logout();
|
||||
}
|
||||
}
|
||||
75
client/src/app/components/rdio-scanner/admin/admin.module.ts
Normal file
75
client/src/app/components/rdio-scanner/admin/admin.module.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { AppSharedModule } from '../../../shared/shared.module';
|
||||
import { RdioScannerAdminComponent } from './admin.component';
|
||||
import { RdioScannerAdminService } from './admin.service';
|
||||
import { RdioScannerAdminConfigComponent } from './config/config.component';
|
||||
import { RdioScannerAdminAccessComponent } from './config/access/access.component';
|
||||
import { RdioScannerAdminApiKeysComponent } from './config/api-keys/api-keys.component';
|
||||
import { RdioScannerAdminDirWatchComponent } from './config/dir-watch/dir-watch.component';
|
||||
import { RdioScannerAdminDownstreamsComponent } from './config/downstreams/downstreams.component';
|
||||
import { RdioScannerAdminGroupsComponent } from './config/groups/groups.component';
|
||||
import { RdioScannerAdminOptionsComponent } from './config/options/options.component';
|
||||
import { RdioScannerAdminSystemsSelectComponent } from './config/systems/select/select.component';
|
||||
import { RdioScannerAdminSystemComponent } from './config/systems/system/system.component';
|
||||
import { RdioScannerAdminSystemsComponent } from './config/systems/systems.component';
|
||||
import { RdioScannerAdminTalkgroupComponent } from './config/systems/talkgroup/talkgroup.component';
|
||||
import { RdioScannerAdminUnitComponent } from './config/systems/unit/unit.component';
|
||||
import { RdioScannerAdminTagsComponent } from './config/tags/tags.component';
|
||||
import { RdioScannerAdminLoginComponent } from './login/login.component';
|
||||
import { RdioScannerAdminLogsComponent } from './logs/logs.component';
|
||||
import { RdioScannerAdminTodosComponent } from './todos/todos.component';
|
||||
import { RdioScannerAdminToolsComponent } from './tools/tools.component';
|
||||
import { RdioScannerAdminImportExportConfigComponent } from './tools/import-export-config/import-export-config.component';
|
||||
import { RdioScannerAdminImportCsvComponent } from './tools/import-csv/import-csv.component';
|
||||
import { RdioScannerAdminPasswordComponent } from './tools/password/password.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
RdioScannerAdminComponent,
|
||||
RdioScannerAdminConfigComponent,
|
||||
RdioScannerAdminAccessComponent,
|
||||
RdioScannerAdminApiKeysComponent,
|
||||
RdioScannerAdminDirWatchComponent,
|
||||
RdioScannerAdminDownstreamsComponent,
|
||||
RdioScannerAdminGroupsComponent,
|
||||
RdioScannerAdminImportExportConfigComponent,
|
||||
RdioScannerAdminImportCsvComponent,
|
||||
RdioScannerAdminLoginComponent,
|
||||
RdioScannerAdminLogsComponent,
|
||||
RdioScannerAdminOptionsComponent,
|
||||
RdioScannerAdminPasswordComponent,
|
||||
RdioScannerAdminSystemComponent,
|
||||
RdioScannerAdminSystemsComponent,
|
||||
RdioScannerAdminSystemsSelectComponent,
|
||||
RdioScannerAdminTagsComponent,
|
||||
RdioScannerAdminTalkgroupComponent,
|
||||
RdioScannerAdminTodosComponent,
|
||||
RdioScannerAdminToolsComponent,
|
||||
RdioScannerAdminUnitComponent,
|
||||
],
|
||||
entryComponents: [RdioScannerAdminSystemsSelectComponent],
|
||||
exports: [RdioScannerAdminComponent],
|
||||
imports: [AppSharedModule, HttpClientModule],
|
||||
providers: [RdioScannerAdminService],
|
||||
})
|
||||
export class RdioScannerAdminModule { }
|
||||
726
client/src/app/components/rdio-scanner/admin/admin.service.ts
Normal file
726
client/src/app/components/rdio-scanner/admin/admin.service.ts
Normal file
|
|
@ -0,0 +1,726 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { EventEmitter, Injectable, OnDestroy } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { timer } from 'rxjs';
|
||||
|
||||
export interface Access {
|
||||
_id?: string;
|
||||
code?: string;
|
||||
expiration?: Date;
|
||||
ident?: string;
|
||||
limit?: number;
|
||||
order?: number;
|
||||
systems?: {
|
||||
id: number;
|
||||
talkgroups: {
|
||||
id: number;
|
||||
}[] | number[] | '*';
|
||||
}[] | number[] | '*';
|
||||
}
|
||||
|
||||
export interface ApiKey {
|
||||
_id?: string;
|
||||
disabled?: boolean;
|
||||
ident?: string;
|
||||
key?: string;
|
||||
order?: number;
|
||||
systems?: {
|
||||
id: number;
|
||||
talkgroups: number[] | '*';
|
||||
}[] | number[] | '*';
|
||||
}
|
||||
|
||||
export interface DirWatch {
|
||||
_id?: string;
|
||||
delay?: number;
|
||||
deleteAfter?: boolean;
|
||||
directory?: string;
|
||||
disabled?: boolean;
|
||||
extension?: string;
|
||||
frequency?: number;
|
||||
mask?: string;
|
||||
order?: number;
|
||||
systemId?: number;
|
||||
talkgroupId?: number;
|
||||
type?: string;
|
||||
usePolling?: boolean;
|
||||
}
|
||||
|
||||
export interface Downstream {
|
||||
_id?: string;
|
||||
apiKey?: string;
|
||||
disabled?: boolean;
|
||||
order?: number;
|
||||
systems?: {
|
||||
id?: number;
|
||||
id_as?: number;
|
||||
talkgroups?: {
|
||||
id: number;
|
||||
id_as?: number;
|
||||
}[] | number[] | '*';
|
||||
}[] | number[] | '*';
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
autoPopulate?: boolean;
|
||||
dimmerDelay?: number;
|
||||
disableAudioConversion?: boolean;
|
||||
disableDuplicateDetection?: boolean;
|
||||
keypadBeeps?: string;
|
||||
pruneDays?: number;
|
||||
sortTalkgroups?: boolean;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
_id?: number;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface Log {
|
||||
_id: number;
|
||||
dateTime: Date;
|
||||
level: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface LogsQuery {
|
||||
count: number;
|
||||
dateStart: Date;
|
||||
dateStop: Date;
|
||||
options: LogsQueryOptions;
|
||||
logs: Log[];
|
||||
}
|
||||
|
||||
export interface LogsQueryOptions {
|
||||
date?: Date;
|
||||
level?: 'error' | 'info' | 'warn';
|
||||
limit: number;
|
||||
offset: number;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
_id?: number;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface Talkgroup {
|
||||
frequency?: number | null;
|
||||
groupId?: number;
|
||||
id?: number;
|
||||
label?: string;
|
||||
led?: string | null;
|
||||
name?: string;
|
||||
patches?: number[];
|
||||
tagId?: number;
|
||||
}
|
||||
|
||||
export interface Unit {
|
||||
id?: number | null;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface System {
|
||||
_id?: number;
|
||||
autoPopulate?: boolean;
|
||||
blacklists?: string;
|
||||
id?: number;
|
||||
label?: string;
|
||||
led?: string | null;
|
||||
order?: number | null;
|
||||
talkgroups?: Talkgroup[];
|
||||
units?: Unit[];
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
access?: Access[];
|
||||
apiKeys?: ApiKey[];
|
||||
dirWatch?: DirWatch[];
|
||||
docker?: boolean;
|
||||
downstreams?: Downstream[];
|
||||
groups?: Group[];
|
||||
options?: Options;
|
||||
systems?: System[];
|
||||
tags?: Tag[];
|
||||
}
|
||||
|
||||
export interface AdminEvent {
|
||||
authenticated?: boolean;
|
||||
config?: Config;
|
||||
passwordNeedChange?: boolean;
|
||||
}
|
||||
|
||||
enum url {
|
||||
config = 'config',
|
||||
login = 'login',
|
||||
logout = 'logout',
|
||||
logs = 'logs',
|
||||
password = 'password',
|
||||
}
|
||||
|
||||
const SESSION_STORAGE_KEY = 'rdio-scanner-admin-token';
|
||||
|
||||
@Injectable()
|
||||
export class RdioScannerAdminService implements OnDestroy {
|
||||
event = new EventEmitter<AdminEvent>();
|
||||
|
||||
get authenticated() {
|
||||
return !!this.token;
|
||||
}
|
||||
|
||||
get passwordNeedChange() {
|
||||
return this._passwordNeedChange;
|
||||
}
|
||||
|
||||
private configWebSocket: WebSocket | undefined;
|
||||
|
||||
private _passwordNeedChange = false;
|
||||
|
||||
private get token(): string {
|
||||
return window?.sessionStorage?.getItem(SESSION_STORAGE_KEY) || '';
|
||||
}
|
||||
|
||||
private set token(token: string) {
|
||||
window?.sessionStorage?.setItem(SESSION_STORAGE_KEY, token);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private matSnackBar: MatSnackBar,
|
||||
private ngFormBuilder: FormBuilder,
|
||||
private ngHttpClient: HttpClient,
|
||||
) {
|
||||
this.configWebSocketOpen();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.event.complete();
|
||||
|
||||
this.configWebSocketClose();
|
||||
}
|
||||
|
||||
async changePassword(currentPassword: string, newPassword: string): Promise<void> {
|
||||
try {
|
||||
const res = await this.ngHttpClient.post<{ passwordNeedChange: boolean }>(
|
||||
this.getUrl(url.password),
|
||||
{ currentPassword, newPassword },
|
||||
{ headers: this.getHeaders(), responseType: 'json' },
|
||||
).toPromise();
|
||||
|
||||
this._passwordNeedChange = res.passwordNeedChange;
|
||||
|
||||
this.event.next({ passwordNeedChange: this.passwordNeedChange });
|
||||
|
||||
} catch (error) {
|
||||
this.errorHandler(error);
|
||||
|
||||
throw error;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async getConfig(): Promise<Config> {
|
||||
try {
|
||||
const res = await this.ngHttpClient.get<{
|
||||
config: Config;
|
||||
passwordNeedChange: boolean;
|
||||
}>(
|
||||
this.getUrl(url.config),
|
||||
{ headers: this.getHeaders(), responseType: 'json' },
|
||||
).toPromise();
|
||||
|
||||
if (res.passwordNeedChange !== this._passwordNeedChange) {
|
||||
this._passwordNeedChange = res.passwordNeedChange;
|
||||
|
||||
this.event.emit({ passwordNeedChange: this.passwordNeedChange });
|
||||
}
|
||||
|
||||
return res.config;
|
||||
|
||||
} catch (error) {
|
||||
this.errorHandler(error);
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
getLeds(): string[] {
|
||||
return ['blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow'];
|
||||
}
|
||||
|
||||
async getLogs(options: LogsQueryOptions): Promise<LogsQuery | null> {
|
||||
try {
|
||||
const res = await this.ngHttpClient.post<LogsQuery>(
|
||||
this.getUrl(url.logs),
|
||||
options,
|
||||
{ headers: this.getHeaders(), responseType: 'json' },
|
||||
).toPromise();
|
||||
|
||||
return res;
|
||||
|
||||
} catch (error) {
|
||||
this.errorHandler(error);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async login(password: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await this.ngHttpClient.post<{
|
||||
passwordNeedChange: boolean,
|
||||
token: string
|
||||
}>(
|
||||
this.getUrl(url.login),
|
||||
{ password },
|
||||
{ headers: this.getHeaders(), responseType: 'json' },
|
||||
).toPromise();
|
||||
|
||||
this.token = res.token;
|
||||
|
||||
this._passwordNeedChange = res.passwordNeedChange;
|
||||
|
||||
this.event.emit({
|
||||
authenticated: this.authenticated,
|
||||
passwordNeedChange: res.passwordNeedChange,
|
||||
});
|
||||
|
||||
this.configWebSocketOpen();
|
||||
|
||||
return !!this.token;
|
||||
|
||||
} catch (error) {
|
||||
this.errorHandler(error);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async logout(): Promise<boolean> {
|
||||
try {
|
||||
await this.ngHttpClient.post(
|
||||
this.getUrl(url.logout),
|
||||
null,
|
||||
{ headers: this.getHeaders(), responseType: 'text' },
|
||||
).toPromise();
|
||||
|
||||
this.configWebSocketClose();
|
||||
|
||||
this.token = '';
|
||||
|
||||
this.event.emit({ authenticated: this.authenticated });
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.errorHandler(error);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async saveConfig(config: Config): Promise<Config> {
|
||||
try {
|
||||
const res = await this.ngHttpClient.put<{ config: Config }>(
|
||||
this.getUrl(url.config),
|
||||
config,
|
||||
{ headers: this.getHeaders(), responseType: 'json' },
|
||||
).toPromise();
|
||||
|
||||
return res.config;
|
||||
|
||||
} catch (error) {
|
||||
this.errorHandler(error);
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
newAccessForm(access?: Access): FormGroup {
|
||||
return this.ngFormBuilder.group({
|
||||
_id: [access?._id],
|
||||
code: [access?.code, [Validators.required, this.validateAccessCode()]],
|
||||
expiration: [access?.expiration],
|
||||
ident: [access?.ident, Validators.required],
|
||||
limit: [access?.limit],
|
||||
order: [access?.order],
|
||||
systems: [access?.systems, Validators.required],
|
||||
});
|
||||
}
|
||||
|
||||
newApiKeyForm(apiKey?: ApiKey): FormGroup {
|
||||
return this.ngFormBuilder.group({
|
||||
_id: [apiKey?._id],
|
||||
disabled: [apiKey?.disabled],
|
||||
ident: [apiKey?.ident, Validators.required],
|
||||
key: [apiKey?.key, [Validators.required, this.validateApiKey()]],
|
||||
order: [apiKey?.order],
|
||||
systems: [apiKey?.systems, Validators.required],
|
||||
});
|
||||
}
|
||||
|
||||
newConfigForm(config?: Config): FormGroup {
|
||||
return this.ngFormBuilder.group({
|
||||
access: this.ngFormBuilder.array(config?.access?.map((access) => this.newAccessForm(access)) || []),
|
||||
apiKeys: this.ngFormBuilder.array(config?.apiKeys?.map((apiKey) => this.newApiKeyForm(apiKey)) || []),
|
||||
dirWatch: this.ngFormBuilder.array(config?.dirWatch?.map((dirWatch) => this.newDirWatchForm(dirWatch)) || []),
|
||||
downstreams: this.ngFormBuilder.array(config?.downstreams?.map((downstream) => this.newDownstreamForm(downstream)) || []),
|
||||
groups: this.ngFormBuilder.array(config?.groups?.map((group) => this.newGroupForm(group)) || []),
|
||||
options: this.newOptionsForm(config?.options),
|
||||
systems: this.ngFormBuilder.array(config?.systems?.map((system) => this.newSystemForm(system)) || []),
|
||||
tags: this.ngFormBuilder.array(config?.tags?.map((tag) => this.newTagForm(tag)) || []),
|
||||
});
|
||||
}
|
||||
|
||||
newDirWatchForm(dirWatch?: DirWatch): FormGroup {
|
||||
return this.ngFormBuilder.group({
|
||||
_id: [dirWatch?._id],
|
||||
delay: [dirWatch?.delay, Validators.min(0)],
|
||||
deleteAfter: [dirWatch?.deleteAfter],
|
||||
directory: [dirWatch?.directory, [Validators.required, this.validateDirectory()]],
|
||||
disabled: [dirWatch?.disabled],
|
||||
extension: [dirWatch?.extension, this.validateExtension()],
|
||||
frequency: [dirWatch?.frequency, Validators.min(0)],
|
||||
mask: [dirWatch?.mask, this.validateMask()],
|
||||
order: [dirWatch?.order],
|
||||
systemId: [dirWatch?.systemId, this.validateDirwatchSystemId()],
|
||||
talkgroupId: [dirWatch?.talkgroupId, this.validateDirwatchTalkgroupId()],
|
||||
type: [dirWatch?.type],
|
||||
usePolling: [dirWatch?.usePolling],
|
||||
});
|
||||
}
|
||||
|
||||
newDownstreamForm(downstream?: Downstream): FormGroup {
|
||||
return this.ngFormBuilder.group({
|
||||
_id: [downstream?._id],
|
||||
apiKey: [downstream?.apiKey, [Validators.required, this.validateApiKey()]],
|
||||
disabled: [downstream?.disabled],
|
||||
order: [downstream?.order],
|
||||
systems: [downstream?.systems, Validators.required],
|
||||
url: [downstream?.url, [Validators.required, this.validateUrl(), this.validateDownstreamUrl()]],
|
||||
});
|
||||
}
|
||||
|
||||
newGroupForm(group?: Group): FormGroup {
|
||||
return this.ngFormBuilder.group({
|
||||
_id: [group?._id],
|
||||
label: [group?.label, Validators.required],
|
||||
});
|
||||
}
|
||||
|
||||
newTagForm(tag?: Tag): FormGroup {
|
||||
return this.ngFormBuilder.group({
|
||||
_id: [tag?._id],
|
||||
label: [tag?.label, Validators.required],
|
||||
});
|
||||
}
|
||||
|
||||
newSystemForm(system?: System): FormGroup {
|
||||
return this.ngFormBuilder.group({
|
||||
_id: [system?._id],
|
||||
autoPopulate: [system?.autoPopulate],
|
||||
blacklists: [system?.blacklists, this.validateBlacklists()],
|
||||
id: [system?.id, [Validators.required, Validators.min(0), this.validateId()]],
|
||||
label: [system?.label, Validators.required],
|
||||
led: [system?.led],
|
||||
order: [system?.order],
|
||||
talkgroups: this.ngFormBuilder.array(system?.talkgroups?.map((talkgroup) => this.newTalkgroupForm(talkgroup)) || []),
|
||||
units: this.ngFormBuilder.array(system?.units?.map((unit) => this.newUnitForm(unit)) || []),
|
||||
});
|
||||
}
|
||||
|
||||
newTalkgroupForm(talkgroup?: Talkgroup): FormGroup {
|
||||
return this.ngFormBuilder.group({
|
||||
frequency: [talkgroup?.frequency, Validators.min(0)],
|
||||
groupId: [talkgroup?.groupId, [Validators.required, this.validateGroup()]],
|
||||
id: [talkgroup?.id, [Validators.required, Validators.min(0), this.validateId()]],
|
||||
label: [talkgroup?.label, Validators.required],
|
||||
led: [talkgroup?.led],
|
||||
name: [talkgroup?.name, Validators.required],
|
||||
patches: [talkgroup?.patches, this.validatePatches()],
|
||||
tagId: [talkgroup?.tagId, [Validators.required, this.validateTag()]],
|
||||
});
|
||||
}
|
||||
|
||||
newUnitForm(unit?: Unit): FormGroup {
|
||||
return this.ngFormBuilder.group({
|
||||
id: [unit?.id, [Validators.required, Validators.min(0), this.validateId()]],
|
||||
label: [unit?.label, Validators.required],
|
||||
});
|
||||
}
|
||||
|
||||
newOptionsForm(options?: Options): FormGroup {
|
||||
return this.ngFormBuilder.group({
|
||||
autoPopulate: [options?.autoPopulate],
|
||||
dimmerDelay: [options?.dimmerDelay, [Validators.required, Validators.min(0)]],
|
||||
disableAudioConversion: [options?.disableAudioConversion],
|
||||
disableDuplicateDetection: [options?.disableDuplicateDetection],
|
||||
keypadBeeps: [options?.keypadBeeps, Validators.required],
|
||||
pruneDays: [options?.pruneDays, [Validators.required, Validators.min(0)]],
|
||||
sortTalkgroups: [options?.sortTalkgroups],
|
||||
});
|
||||
}
|
||||
|
||||
private configWebSocketClose(): void {
|
||||
if (this.configWebSocket instanceof WebSocket) {
|
||||
this.configWebSocket.onclose = null;
|
||||
this.configWebSocket.onmessage = null;
|
||||
this.configWebSocket.onopen = null;
|
||||
|
||||
this.configWebSocket.close();
|
||||
|
||||
this.configWebSocket = undefined;
|
||||
};
|
||||
}
|
||||
|
||||
private configWebSocketReconnect(): void {
|
||||
this.configWebSocketClose();
|
||||
|
||||
this.configWebSocketOpen();
|
||||
}
|
||||
|
||||
private configWebSocketOpen(): void {
|
||||
if (!this.token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const webSocketUrl = new URL(this.getUrl(url.config), window.location.href).href.replace(/^http/, 'ws');
|
||||
|
||||
this.configWebSocket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.configWebSocket.onclose = (ev: CloseEvent) => {
|
||||
if (ev.code === 1000) {
|
||||
this.token = '';
|
||||
|
||||
this.event.emit({ authenticated: this.authenticated });
|
||||
} else {
|
||||
timer(2000).subscribe(() => this.configWebSocketReconnect());
|
||||
}
|
||||
};
|
||||
|
||||
this.configWebSocket.onopen = () => {
|
||||
this.configWebSocket?.send(this.token);
|
||||
|
||||
if (this.configWebSocket instanceof WebSocket) {
|
||||
this.configWebSocket.onmessage = (ev: MessageEvent<string>) => {
|
||||
this.event.emit({ config: JSON.parse(ev.data) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private errorHandler(error: HttpErrorResponse): void {
|
||||
if (error.status === 401) {
|
||||
this.token = '';
|
||||
|
||||
this.event.emit({ authenticated: this.authenticated });
|
||||
|
||||
this.configWebSocketClose();
|
||||
|
||||
} else {
|
||||
this.matSnackBar.open(error.message, '', { duration: 5000 });
|
||||
}
|
||||
}
|
||||
|
||||
private getHeaders(): HttpHeaders {
|
||||
return new HttpHeaders({
|
||||
Authorization: this.token || '',
|
||||
});
|
||||
}
|
||||
|
||||
private getUrl(path: string): string {
|
||||
return `${window.location.href}/../api/admin${path.charAt(0) === '/' ? path : `/${path}`}`;
|
||||
}
|
||||
|
||||
private validateAccessCode(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
if (typeof control.value !== 'string' || !control.value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const access: Access[] = control.parent?.parent?.getRawValue() || [];
|
||||
|
||||
const count = access.reduce((c, a) => c += a.code === control.value ? 1 : 0, 0);
|
||||
|
||||
return count > 1 ? { duplicate: true } : null;
|
||||
};
|
||||
}
|
||||
|
||||
private validateApiKey(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
if (typeof control.value !== 'string' || !control.value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const apiKeys: ApiKey[] = control.parent?.parent?.getRawValue() || [];
|
||||
|
||||
const count = apiKeys.reduce((c, a) => c += a.key === control.value ? 1 : 0, 0);
|
||||
|
||||
return count > 1 ? { duplicate: true } : null;
|
||||
};
|
||||
}
|
||||
|
||||
private validateBlacklists(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return typeof control.value === 'string' && control.value.length ? /^[0-9]+(,[0-9]+)*$/.test(control.value) ? null : { invalid: true } : null;
|
||||
};
|
||||
}
|
||||
|
||||
private validateDirectory(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
if (typeof control.value !== 'string' || !control.value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dirWatch: DirWatch[] = control.parent?.parent?.getRawValue() || [];
|
||||
|
||||
const count = dirWatch.reduce((c, a) => c += a.directory === control.value ? 1 : 0, 0);
|
||||
|
||||
return count > 1 ? { duplicate: true } : null;
|
||||
};
|
||||
}
|
||||
|
||||
private validateDownstreamUrl(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
if (typeof control.value !== 'string' || !control.value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const downstream: Downstream[] = control.parent?.parent?.getRawValue() || [];
|
||||
|
||||
const count = downstream.reduce((c, a) => c += a.url === control.value ? 1 : 0, 0);
|
||||
|
||||
return count > 1 ? { duplicate: true } : null;
|
||||
};
|
||||
}
|
||||
|
||||
private validateDirwatchSystemId(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const dirwatch = control.parent?.getRawValue() || {};
|
||||
|
||||
const mask = dirwatch.mask || '';
|
||||
|
||||
const type = dirwatch.type;
|
||||
|
||||
return ['sdr-trunk'].includes(type) || control.value !== null || /#SYS/.test(mask) ? null : { required: true };
|
||||
};
|
||||
}
|
||||
|
||||
private validateDirwatchTalkgroupId(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const dirwatch = control.parent?.getRawValue() || {};
|
||||
|
||||
const mask = dirwatch.mask || '';
|
||||
|
||||
const type = dirwatch.type;
|
||||
|
||||
return ['trunk-recorder', 'sdr-trunk'].includes(type) || control.value !== null || /#TG/.test(mask) ? null : { required: true };
|
||||
};
|
||||
}
|
||||
|
||||
private validateExtension(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
if (typeof control.value !== 'string' || !control.value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return /^[0-9a-zA-Z]+$/.test(control.value) ? null : { invalid: true };
|
||||
};
|
||||
}
|
||||
|
||||
private validateGroup(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
if (typeof control.value !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const groupIds = control.root.get('groups')?.value.map((group: Group) => group._id);
|
||||
|
||||
return groupIds ? groupIds.includes(control.value) ? null : { required: true } : null;
|
||||
};
|
||||
}
|
||||
|
||||
private validateId(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
if (control.value === null || typeof control.value !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const systems: System[] = control.parent?.parent?.getRawValue() || [];
|
||||
|
||||
const count = systems.reduce((c, s) => c += s.id === control.value ? 1 : 0, 0);
|
||||
|
||||
return count > 1 ? { duplicate: true } : null;
|
||||
};
|
||||
}
|
||||
|
||||
private validateMask(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
if (typeof control.value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const masks = ['#DATE', '#HZ', '#KHZ', '#MHZ', '#SYS', '#TIME', '#TG', '#UNIT', '#ZTIME'];
|
||||
|
||||
const metas = control.value.match(/(#[A-Z]+)/g) || [];
|
||||
|
||||
const count = metas.reduce((c, m) => {
|
||||
if (masks.includes(m)) {
|
||||
c++;
|
||||
}
|
||||
|
||||
return c;
|
||||
}, 0);
|
||||
|
||||
return count ? null : { invalid: true };
|
||||
};
|
||||
}
|
||||
|
||||
private validatePatches(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return typeof control.value === 'string' && control.value.length ? /^[0-9]+(,[0-9]+)*$/.test(control.value) ? null : { invalid: true } : null;
|
||||
};
|
||||
}
|
||||
|
||||
private validateTag(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
if (typeof control.value !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tagIds = control.root.get('tags')?.value.map((tag: Tag) => tag._id);
|
||||
|
||||
return tagIds ? tagIds.includes(control.value) ? null : { required: true } : null;
|
||||
};
|
||||
}
|
||||
|
||||
private validateUrl(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
if (typeof control.value !== 'string' || !control.value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return /^https?:\/\/.+$/.test(control.value) ? null : { invalid: true }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<div class="row top">
|
||||
<p class="mat-body">You can decide to leave your instance unlocked and accessible to everyone, or set passwords with
|
||||
specific systems/talkgroups. When used, the user will be prompted once for their password. It is then stored
|
||||
internally on the client side and reused thereafter.</p>
|
||||
<button type="button" mat-button color="accent" (click)="add()">New access</button>
|
||||
</div>
|
||||
<p *ngIf="!accesses.length" class="mat-small text-center">No access is defined, your instance is open and accessible to
|
||||
all</p>
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListData]="accesses" (cdkDropListDropped)="drop($event)">
|
||||
<mat-expansion-panel *ngFor="let access of accesses; index as i" cdkDrag>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
{{ access.value.ident || 'NewAccess' }}
|
||||
<mat-icon *ngIf="access.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-container [formGroup]="access">
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Code</span><br>
|
||||
<span class="mat-caption">The access code to unlock the client.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input [type]="hideCode ? 'password' : 'text'" matInput formControlName="code" placeholder="Code">
|
||||
<button type="button" mat-icon-button matSuffix (click)="hideCode = !hideCode">
|
||||
<mat-icon>{{ hideCode ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="access.get('code')?.hasError('duplicate')">
|
||||
Code is already defined
|
||||
</mat-error>
|
||||
<mat-error *ngIf="access.get('code')?.hasError('required')">
|
||||
Code is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Ident</span><br>
|
||||
<span class="mat-caption">Dummy identifier which serves only to identify this access code is for
|
||||
whom.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="text" matInput formControlName="ident" placeholder="Ident">
|
||||
<mat-error *ngIf="access.get('ident')?.hasError('required')">
|
||||
Ident is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Expiration</span><br>
|
||||
<span class="mat-caption">Expiration date for this access code.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="text" matInput formControlName="expiration" placeholder="Expiration date"
|
||||
[matDatepicker]="expirationDate" (click)="expirationDate.open()">
|
||||
<mat-datepicker-toggle matSuffix [for]="expirationDate"></mat-datepicker-toggle>
|
||||
<mat-datepicker #expirationDate></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Limit</span><br>
|
||||
<span class="mat-caption">Simultaneous connection limit. Access will be refused if the connection
|
||||
with this access code exceeds the defined limit.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="number" matInput formControlName="limit" placeholder="Limit">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Access</span><br>
|
||||
<span class="mat-caption">
|
||||
The access code allows access to <u>
|
||||
<ng-container *ngIf="access.value.systems === '*'">all</ng-container>
|
||||
<ng-container *ngIf="access.value.systems !== '*'">some</ng-container>
|
||||
</u> systems and talkgroups.
|
||||
</span>
|
||||
</p>
|
||||
<div>
|
||||
<button type="button" mat-button [disabled]="access.disabled" (click)="select(access)">
|
||||
Choose systems
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row bottom">
|
||||
<button type="button" mat-button color="warn" (click)="remove(i)">
|
||||
Delete access
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Component, Input, QueryList, ViewChildren } from '@angular/core';
|
||||
import { FormArray, FormGroup } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||
import { RdioScannerAdminService } from '../../admin.service';
|
||||
import { RdioScannerAdminSystemsSelectComponent } from '../systems/select/select.component';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-access',
|
||||
templateUrl: './access.component.html',
|
||||
})
|
||||
export class RdioScannerAdminAccessComponent {
|
||||
@Input() form: FormArray | undefined;
|
||||
|
||||
hideCode = true;
|
||||
|
||||
get accesses(): FormGroup[] {
|
||||
return this.form?.controls
|
||||
.sort((a, b) => a.value.order - b.value.order) as FormGroup[];
|
||||
}
|
||||
|
||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService, private matDialog: MatDialog) { }
|
||||
|
||||
add(): void {
|
||||
const access = this.adminService.newAccessForm({ systems: '*' });
|
||||
|
||||
access.markAllAsTouched();
|
||||
|
||||
this.form?.insert(0, access);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
|
||||
closeAll(): void {
|
||||
this.panels?.forEach((panel) => panel.close());
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<FormGroup[]>): void {
|
||||
if (event.previousIndex !== event.currentIndex) {
|
||||
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
|
||||
|
||||
event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false }));
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
remove(index: number): void {
|
||||
this.form?.removeAt(index);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
|
||||
select(access: FormGroup): void {
|
||||
const matDialogRef = this.matDialog.open(RdioScannerAdminSystemsSelectComponent, { data: access });
|
||||
|
||||
matDialogRef.afterClosed().subscribe((data) => {
|
||||
if (data) {
|
||||
access.get('systems')?.setValue(data);
|
||||
|
||||
access.markAsDirty();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<div class="row top">
|
||||
<p class="mat-body">API keys are used for recorder upload scripts and downstream instances. Each of them must
|
||||
authenticate with an API key to exchange audio files.</p>
|
||||
<button type="button" mat-button color="accent" (click)="add()">New API key</button>
|
||||
</div>
|
||||
<p *ngIf="!apiKeys.length" class="mat-small text-center">No defined API keys</p>
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListData]="apiKeys" (cdkDropListDropped)="drop($event)">
|
||||
<mat-expansion-panel *ngFor="let apiKey of apiKeys; index as i" cdkDrag>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
{{ apiKey.value.ident || 'NewApiKey' }}
|
||||
<mat-icon *ngIf="apiKey.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-container [formGroup]="apiKey">
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Disabled</span><br>
|
||||
<span class="mat-caption">Disable the API key.</span>
|
||||
</p>
|
||||
<div>
|
||||
<mat-slide-toggle color="primary" formControlName="disabled"></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Key</span><br>
|
||||
<span class="mat-caption">Api key.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input #key type="text" matInput formControlName="key" placeholder="Key">
|
||||
<button type="button" mat-icon-button matSuffix (click)="copy(key)">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="apiKey.get('key')?.hasError('duplicate')">
|
||||
Key is already defined
|
||||
</mat-error>
|
||||
<mat-error *ngIf="apiKey.get('key')?.hasError('required')">
|
||||
Key is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Ident</span><br>
|
||||
<span class="mat-caption">Dummy identifier which serves only to identify this API key is for
|
||||
whom.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="text" matInput formControlName="ident" placeholder="Ident">
|
||||
<mat-error *ngIf="apiKey.get('ident')?.hasError('required')">
|
||||
Ident is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Access</span><br>
|
||||
<span class="mat-caption">
|
||||
This API key allows access to <u>
|
||||
<ng-container *ngIf="apiKey.value.systems === '*'">all</ng-container>
|
||||
<ng-container *ngIf="apiKey.value.systems !== '*'">some</ng-container>
|
||||
</u> systems and talkgroups.
|
||||
</span>
|
||||
</p>
|
||||
<div>
|
||||
<button type="button" mat-button [disabled]="apiKey.disabled" (click)="select(apiKey)">
|
||||
Choose systems
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row bottom">
|
||||
<button type="button" mat-button color="warn" (click)="remove(i)">
|
||||
Delete API key
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Component, Input, QueryList, ViewChildren } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { FormArray, FormGroup } from '@angular/forms';
|
||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||
import { RdioScannerAdminService } from '../../admin.service';
|
||||
import { RdioScannerAdminSystemsSelectComponent } from '../systems/select/select.component';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-api-keys',
|
||||
templateUrl: './api-keys.component.html',
|
||||
})
|
||||
export class RdioScannerAdminApiKeysComponent {
|
||||
@Input() form: FormArray | undefined;
|
||||
|
||||
get apiKeys(): FormGroup[] {
|
||||
return this.form?.controls
|
||||
.sort((a, b) => a.value.order - b.value.order) as FormGroup[];
|
||||
}
|
||||
|
||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService, private matDialog: MatDialog) { }
|
||||
|
||||
add(): void {
|
||||
const apiKey = this.adminService.newApiKeyForm({
|
||||
key: this.uuid(),
|
||||
systems: '*',
|
||||
});
|
||||
|
||||
apiKey.markAllAsTouched();
|
||||
|
||||
this.form?.insert(0, apiKey);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
|
||||
closeAll(): void {
|
||||
this.panels?.forEach((panel) => panel.close());
|
||||
}
|
||||
|
||||
copy(inputElement: HTMLInputElement): void {
|
||||
inputElement.select();
|
||||
|
||||
document.execCommand('copy');
|
||||
|
||||
inputElement.setSelectionRange(0, 0);
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<FormGroup[]>): void {
|
||||
if (event.previousIndex !== event.currentIndex) {
|
||||
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
|
||||
|
||||
event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false }));
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
remove(index: number): void {
|
||||
this.form?.removeAt(index);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
|
||||
select(access: FormGroup): void {
|
||||
const matDialogRef = this.matDialog.open(RdioScannerAdminSystemsSelectComponent, { data: access });
|
||||
|
||||
matDialogRef.afterClosed().subscribe((data) => {
|
||||
if (data) {
|
||||
access.get('systems')?.setValue(data);
|
||||
|
||||
access.markAsDirty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private uuid() {
|
||||
let dt = new Date().getTime();
|
||||
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = (dt + Math.random() * 16) % 16 | 0;
|
||||
|
||||
dt = Math.floor(dt / 16);
|
||||
|
||||
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
<form *ngIf="form" autocomplete="off" [formGroup]="form" (ngSubmit)="save()">
|
||||
<mat-accordion displayMode="flat">
|
||||
<mat-expansion-panel (afterCollapse)="accessComponent.closeAll()">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>manage_accounts</mat-icon>
|
||||
Access
|
||||
<mat-icon *ngIf="form?.get('access')?.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-access #accessComponent [form]="access"></rdio-scanner-admin-access>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel (afterCollapse)="apiKeyComponent.closeAll()">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>vpn_key</mat-icon>
|
||||
ApiKey
|
||||
<mat-icon *ngIf="form?.get('apiKey')?.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-api-keys #apiKeyComponent [form]="apiKeys"></rdio-scanner-admin-api-keys>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel (afterCollapse)="dirWatchComponent.closeAll()">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>folder</mat-icon>
|
||||
Dirwatch
|
||||
<mat-icon *ngIf="form?.get('dirWatch')?.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-dir-watch #dirWatchComponent [docker]="docker" [form]="dirWatch"></rdio-scanner-admin-dir-watch>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel (afterCollapse)="downstreamsComponent.closeAll()">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>share</mat-icon>
|
||||
Downstreams
|
||||
<mat-icon *ngIf="form?.get('downstreams')?.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-downstreams #downstreamsComponent [form]="downstreams"></rdio-scanner-admin-downstreams>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>workspaces</mat-icon>
|
||||
Groups
|
||||
<mat-icon *ngIf="form?.get('groups')?.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-groups [form]="groups"></rdio-scanner-admin-groups>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>tune</mat-icon>
|
||||
Options
|
||||
<mat-icon *ngIf="form?.get('options')?.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-options [form]="options"></rdio-scanner-admin-options>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel (afterCollapse)="systemsComponent.closeAll()">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>podcasts</mat-icon>
|
||||
Systems
|
||||
<mat-icon *ngIf="form?.get('systems')?.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-systems #systemsComponent [form]="systems"></rdio-scanner-admin-systems>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>sell</mat-icon>
|
||||
Tags
|
||||
<mat-icon *ngIf="form?.get('tags')?.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-tags [form]="tags"></rdio-scanner-admin-tags>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
<div class="row bottom">
|
||||
<button type="button" mat-raised-button [disabled]="form?.disabled || form?.pristine"
|
||||
(click)="reset()">Reset</button>
|
||||
<button type="submit" mat-raised-button color="primary"
|
||||
[disabled]="form?.disabled || form?.pristine || !form?.valid">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
rdio-scanner-admin-config {
|
||||
.row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
&.bottom {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
&.top {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
> p {
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.mat-button:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.cdk-drag-handle {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.mat-expansion-panel-header-title {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mat-icon[color="warn"] {
|
||||
font-size: 1rem;
|
||||
height: 1rem;
|
||||
opacity: 0.9;
|
||||
padding-left: 0.5rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 719px) {
|
||||
rdio-scanner-admin-config .mat-form-field {
|
||||
flex: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.row {
|
||||
> p ~ div,
|
||||
.mat-form-field {
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
> p ~ div > {
|
||||
.mat-button,
|
||||
.mat-slide-toggle {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, QueryList, ViewChildren, ViewEncapsulation } from '@angular/core';
|
||||
import { FormArray, FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||
import { AdminEvent, RdioScannerAdminService, Config } from '../admin.service';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
selector: 'rdio-scanner-admin-config',
|
||||
styleUrls: ['./config.component.scss'],
|
||||
templateUrl: './config.component.html',
|
||||
})
|
||||
export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
||||
form: FormGroup | undefined;
|
||||
|
||||
get access(): FormArray {
|
||||
return this.form?.get('access') as FormArray;
|
||||
}
|
||||
|
||||
get apiKeys(): FormArray {
|
||||
return this.form?.get('apiKeys') as FormArray;
|
||||
}
|
||||
|
||||
get dirWatch(): FormArray {
|
||||
return this.form?.get('dirWatch') as FormArray;
|
||||
}
|
||||
|
||||
get docker(): boolean {
|
||||
return this.config?.docker || false;
|
||||
}
|
||||
|
||||
get downstreams(): FormArray {
|
||||
return this.form?.get('downstreams') as FormArray;
|
||||
}
|
||||
|
||||
get groups(): FormArray {
|
||||
return this.form?.get('groups') as FormArray;
|
||||
}
|
||||
|
||||
get options(): FormGroup {
|
||||
return this.form?.get('options') as FormGroup;
|
||||
}
|
||||
|
||||
get systems(): FormArray {
|
||||
return this.form?.get('systems') as FormArray;
|
||||
}
|
||||
|
||||
get tags(): FormArray {
|
||||
return this.form?.get('tags') as FormArray;
|
||||
}
|
||||
|
||||
private config: Config | undefined;
|
||||
|
||||
private eventSubscription = this.adminService.event.subscribe(async (event: AdminEvent) => {
|
||||
if ('authenticated' in event && event.authenticated === true) {
|
||||
this.config = await this.adminService.getConfig();
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
if ('config' in event) {
|
||||
this.config = event.config;
|
||||
|
||||
if (this.form?.pristine) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||
|
||||
constructor(
|
||||
private adminService: RdioScannerAdminService,
|
||||
private ngChangeDetectorRef: ChangeDetectorRef,
|
||||
) { }
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.eventSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.config = await this.adminService.getConfig();
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
closeAll(): void {
|
||||
this.panels?.forEach((panel) => panel.close());
|
||||
}
|
||||
|
||||
reset(config = this.config, options?: { dirty?: boolean }): void {
|
||||
this.form = this.adminService.newConfigForm(config);
|
||||
|
||||
this.form.statusChanges.subscribe(() => this.ngChangeDetectorRef.markForCheck());
|
||||
|
||||
if (options?.dirty === true) {
|
||||
this.form.markAsDirty();
|
||||
}
|
||||
|
||||
this.groups.valueChanges.subscribe(() => {
|
||||
this.systems.controls.forEach((system) => {
|
||||
const talkgroups = system.get('talkgroups') as FormArray;
|
||||
|
||||
talkgroups.controls.forEach((talkgroup) => {
|
||||
const groupId = talkgroup.get('groupId') as FormControl;
|
||||
|
||||
groupId.updateValueAndValidity();
|
||||
|
||||
if (groupId.errors) {
|
||||
groupId.markAsTouched();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.tags.valueChanges.subscribe(() => {
|
||||
this.systems.controls.forEach((system) => {
|
||||
const talkgroups = system.get('talkgroups') as FormArray;
|
||||
|
||||
talkgroups.controls.forEach((talkgroup) => {
|
||||
const tagId = talkgroup.get('tagId') as FormControl;
|
||||
|
||||
tagId.updateValueAndValidity();
|
||||
|
||||
if (tagId.errors) {
|
||||
tagId.markAsTouched();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.ngChangeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
this.form?.markAsPristine();
|
||||
|
||||
await this.adminService.saveConfig(this.form?.getRawValue());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
<div class="row top">
|
||||
<p class="mat-body">Define a dirwatch to monitor new audio files from any directory.</p>
|
||||
<button type="button" mat-button color="accent" (click)="add()">New dir watch</button>
|
||||
</div>
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListData]="dirWatches" (cdkDropListDropped)="drop($event)">
|
||||
<mat-expansion-panel *ngFor="let dirWatch of dirWatches; index as i" cdkDrag>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
{{ dirWatch.value.directory || 'NewDirWatch' }}
|
||||
<mat-icon *ngIf="dirWatch.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-container [formGroup]="dirWatch">
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Disabled</span><br>
|
||||
<span class="mat-caption">Disable the dirwatch.</span>
|
||||
</p>
|
||||
<div>
|
||||
<mat-slide-toggle color="primary" formControlName="disabled"></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Delete After</span><br>
|
||||
<span class="mat-caption">Delete the audio file after being ingested. If activated, all pre-existing
|
||||
audio files will be ingested and deleted as soon as the server starts. If not activated,
|
||||
pre-existing audio files are neither ingested nor deleted.</span>
|
||||
</p>
|
||||
<div>
|
||||
<mat-slide-toggle color="primary" formControlName="deleteAfter"></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Directory</span><br>
|
||||
<span class="mat-caption">
|
||||
<ng-container *ngIf="docker">
|
||||
Absolute or relatives inside the <u>Docker container</u>, path of the directory to be
|
||||
monitored.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!docker">
|
||||
Absolute or relatives to the server, path of the directory to be monitored.
|
||||
</ng-container>
|
||||
</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="text" matInput formControlName="directory" placeholder="Directory">
|
||||
<mat-error *ngIf="dirWatch.get('directory')?.hasError('required')">
|
||||
Directory is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="dirWatch.get('directory')?.hasError('duplicate')">
|
||||
Directory is already defined
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Extension</span><br>
|
||||
<span class="mat-caption">The audio call extension to monitor without the period. Ex.: "mp3",
|
||||
"wav".</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="text" matInput formControlName="extension" placeholder="Extension">
|
||||
<mat-error *ngIf="dirWatch.get('extension')?.hasError('invalid')">
|
||||
Invalid extension
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Type</span><br>
|
||||
<span class="mat-caption">When SDR Trunk, metadata are obtained from the MP3 tags. Note that
|
||||
the label of the SDR Trunk system must match the label of the Rdio Scanner system label. When
|
||||
trunk-recorder, metadata are obtained from the JSON file.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<mat-select formControlName="type" placeholder="Type">
|
||||
<mat-option [value]="null">Default</mat-option>
|
||||
<mat-option value="trunk-recorder">Trunk Recorder</mat-option>
|
||||
<mat-option value="sdr-trunk">SDR Trunk</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">System</span><br>
|
||||
<span class="mat-caption">System to where the audio files should go.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<mat-select #sysId formControlName="systemId" placeholder="System">
|
||||
<mat-option [value]="null">Metatag #SYS</mat-option>
|
||||
<mat-option *ngFor="let system of systems" [value]="system.value.id">
|
||||
{{ system.value.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="dirWatch.get('systemId')?.hasError('required')">
|
||||
System is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Talkgroup</span><br>
|
||||
<span class="mat-caption">Talkgroup to where the audio files should go.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<mat-select formControlName="talkgroupId" placeholder="Talkgroup">
|
||||
<mat-option [value]="null">Metatag #TG</mat-option>
|
||||
<mat-option *ngFor="let talkgroup of talkgroups[dirWatch.value.systemId] || []"
|
||||
[value]="talkgroup.value.id">
|
||||
{{ talkgroup.value.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="dirWatch.get('talkgroupId')?.hasError('required')">
|
||||
Talkgroup is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Mask</span><br>
|
||||
<span class="mat-caption">Some metadata can be extracted from the file name of the audio file using
|
||||
specific META tags. Possible META tags:
|
||||
<ul>
|
||||
<li><b>#DATE</b> - extract the date like 20201231 (YYYYMMMDD) or 2020-12-31 (YYYY-MM-DD).
|
||||
</li>
|
||||
<li><b>#HZ</b> - extract the frequency in hertz like 119100000.</li>
|
||||
<li><b>#KHZ</b> - extract the frequency in kilohertz like 119100.</li>
|
||||
<li><b>#MHZ</b> - extract the frequency in megahertz like 119.100.</li>
|
||||
<li><b>#SYS</b> - extract the system id like 11.</li>
|
||||
<li><b>#TIME</b> - extract the local time like 0853439 or 08:34:39.</li>
|
||||
<li><b>#TG</b> - extract the talkgroup id like 54241.</li>
|
||||
<li><b>#UNIT</b> - extract the unit id like 4424001.</li>
|
||||
<li><b>#ZTIME</b> - extract the zulu time like 0453439 or 04:34:39.</li>
|
||||
</ul>
|
||||
Example: cymx_#TG_#DATE_#TIME_#HZ
|
||||
</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="text" matInput formControlName="mask" placeholder="Mask">
|
||||
<mat-error *ngIf="dirWatch.get('mask')?.hasError('invalid')">
|
||||
Invalid mask
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Frequency</span><br>
|
||||
<span class="mat-caption">Fake frequency in hertz displayed on the main screen.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="number" matInput formControlName="frequency" placeholder="Frequency">
|
||||
<mat-error *ngIf="dirWatch.get('frequency')?.errors">
|
||||
Invalid frequency
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Delay</span><br>
|
||||
<span class="mat-caption">Depending on the recorder, audio files can be ingested too soon after the
|
||||
recorder has created the file. You can set a timeout value in milliseconds for the audio file to
|
||||
settle before ingesting it.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="number" matInput formControlName="delay" placeholder="Delay">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Use Polling</span><br>
|
||||
<span class="mat-caption">Use polling when monitoring the directory. While this takes a bit more CPU
|
||||
resources, it is necessary if the monitored directory is from a remote host or from outside of a
|
||||
Docker container.</span>
|
||||
</p>
|
||||
<div>
|
||||
<mat-slide-toggle color="primary" formControlName="usePolling"></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row bottom">
|
||||
<button type="button" mat-button color="warn" (click)="remove(i)">
|
||||
Delete dirwatch
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Component, Input, OnChanges, QueryList, ViewChildren } from '@angular/core';
|
||||
import { FormArray, FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||
import { RdioScannerAdminService } from '../../admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-dir-watch',
|
||||
templateUrl: './dir-watch.component.html',
|
||||
})
|
||||
export class RdioScannerAdminDirWatchComponent implements OnChanges {
|
||||
@Input() docker: boolean | undefined;
|
||||
|
||||
@Input() form: FormArray | undefined;
|
||||
|
||||
get dirWatches(): FormGroup[] {
|
||||
return this.form?.controls
|
||||
.sort((a, b) => a.value.order - b.value.order) as FormGroup[];
|
||||
}
|
||||
|
||||
get systems(): FormGroup[] {
|
||||
const systems = this.form?.root.get('systems') as FormArray;
|
||||
|
||||
return systems.controls as FormGroup[];
|
||||
}
|
||||
|
||||
get talkgroups(): FormGroup[][] {
|
||||
return this.systems.reduce((talkgroups, system) => {
|
||||
const faTalkgroups = system.get('talkgroups') as FormArray;
|
||||
|
||||
talkgroups[system.value.id] = faTalkgroups.controls as FormGroup[];
|
||||
|
||||
return talkgroups;
|
||||
}, [] as FormGroup[][]);
|
||||
}
|
||||
|
||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService) { }
|
||||
|
||||
ngOnChanges(): void {
|
||||
if (this.form) {
|
||||
this.dirWatches.flatMap((control) => this.registerOnChanges(control));
|
||||
}
|
||||
}
|
||||
|
||||
add(): void {
|
||||
const dirWatch = this.adminService.newDirWatchForm({ usePolling: this.docker });
|
||||
|
||||
dirWatch.markAllAsTouched();
|
||||
|
||||
this.registerOnChanges(dirWatch);
|
||||
|
||||
this.form?.insert(0, dirWatch);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
|
||||
closeAll(): void {
|
||||
this.panels?.forEach((panel) => panel.close());
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<FormGroup[]>): void {
|
||||
if (event.previousIndex !== event.currentIndex) {
|
||||
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
|
||||
|
||||
event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false }));
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
remove(index: number): void {
|
||||
this.form?.removeAt(index);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
|
||||
private registerOnChanges(control: FormGroup): void {
|
||||
const mask = control.get('mask') as FormControl;
|
||||
const type = control.get('type') as FormControl;
|
||||
|
||||
mask.valueChanges.subscribe(() => this.validateIds(control));
|
||||
type.valueChanges.subscribe(() => this.validateIds(control));
|
||||
}
|
||||
|
||||
private validateIds(control: FormGroup): void {
|
||||
const systemId = control.get('systemId');
|
||||
const talkgroupId = control.get('talkgroupId');
|
||||
|
||||
systemId?.updateValueAndValidity();
|
||||
systemId?.markAsTouched();
|
||||
|
||||
talkgroupId?.updateValueAndValidity();
|
||||
talkgroupId?.markAsTouched();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<div class="row top">
|
||||
<p class="mat-body">Ingested audio calls can be sent downstream to other instances.</p>
|
||||
<button type="button" mat-button color="accent" (click)="add()">New downstream</button>
|
||||
</div>
|
||||
<p *ngIf="!downstreams.length" class="mat-small text-center">No defined downstreams</p>
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListData]="downstreams" (cdkDropListDropped)="drop($event)">
|
||||
<mat-expansion-panel *ngFor="let downstream of downstreams; index as i" cdkDrag>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
{{ downstream.value.url || 'NewDownstream' }}
|
||||
<mat-icon *ngIf="downstream.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-container [formGroup]="downstream">
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Disabled</span><br>
|
||||
<span class="mat-caption">Disable the downstream.</span>
|
||||
</p>
|
||||
<div>
|
||||
<mat-slide-toggle color="primary" formControlName="disabled"></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">API Key</span><br>
|
||||
<span class="mat-caption">Api key of the remote instance.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input #key type="text" matInput formControlName="apiKey" placeholder="API key">
|
||||
<mat-error *ngIf="downstream.get('apiKey')?.hasError('duplicate')">
|
||||
API key is already defined
|
||||
</mat-error>
|
||||
<mat-error *ngIf="downstream.get('apiKey')?.hasError('required')">
|
||||
API key is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">URL</span><br>
|
||||
<span class="mat-caption">URL of the remote instance.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="text" matInput formControlName="url" placeholder="URL">
|
||||
<mat-error *ngIf="downstream.get('url')?.hasError('required')">
|
||||
URL is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="downstream.get('url')?.hasError('invalid')">
|
||||
URL is invalid
|
||||
</mat-error>
|
||||
<mat-error *ngIf="downstream.get('url')?.hasError('duplicate')">
|
||||
URL is already defined
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Access</span><br>
|
||||
<span class="mat-caption">
|
||||
This downstream allows access to <u>
|
||||
<ng-container *ngIf="downstream.value.systems === '*'">all</ng-container>
|
||||
<ng-container *ngIf="downstream.value.systems !== '*'">some</ng-container>
|
||||
</u> systems and talkgroups.
|
||||
</span>
|
||||
</p>
|
||||
<div>
|
||||
<button type="button" mat-button [disabled]="downstream.disabled" (click)="select(downstream)">
|
||||
Choose systems
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row bottom">
|
||||
<button type="button" mat-button color="warn" (click)="remove(i)">
|
||||
Delete downstream
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Component, Input, QueryList, ViewChildren } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { FormArray, FormGroup } from '@angular/forms';
|
||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||
import { RdioScannerAdminService } from '../../admin.service';
|
||||
import { RdioScannerAdminSystemsSelectComponent } from '../systems/select/select.component';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-downstreams',
|
||||
templateUrl: './downstreams.component.html',
|
||||
})
|
||||
export class RdioScannerAdminDownstreamsComponent {
|
||||
@Input() form: FormArray | undefined;
|
||||
|
||||
get downstreams(): FormGroup[] {
|
||||
return this.form?.controls
|
||||
.sort((a, b) => a.value.order - b.value.order) as FormGroup[];
|
||||
}
|
||||
|
||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService, private matDialog: MatDialog) { }
|
||||
|
||||
add(): void {
|
||||
const downstream = this.adminService.newDownstreamForm({ systems: '*' });
|
||||
|
||||
downstream.markAllAsTouched();
|
||||
|
||||
this.form?.insert(0, downstream);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
|
||||
closeAll(): void {
|
||||
this.panels?.forEach((panel) => panel.close());
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<FormGroup[]>): void {
|
||||
if (event.previousIndex !== event.currentIndex) {
|
||||
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
|
||||
|
||||
event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false }));
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
remove(index: number): void {
|
||||
this.form?.removeAt(index);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
|
||||
select(access: FormGroup): void {
|
||||
const matDialogRef = this.matDialog.open(RdioScannerAdminSystemsSelectComponent, { data: access });
|
||||
|
||||
matDialogRef.afterClosed().subscribe((data) => {
|
||||
if (data) {
|
||||
access.get('systems')?.setValue(data);
|
||||
|
||||
access.markAsDirty();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<div class="row top">
|
||||
<p class="mat-body">All system talkgroups must be associated with a group, which is then used to toggle between
|
||||
active talkgroups.</p>
|
||||
<button type="button" mat-button color="accent" (click)="add()">New group</button>
|
||||
</div>
|
||||
<div class="groups">
|
||||
<div *ngFor="let group of groups; index as i" [formGroup]="group">
|
||||
<button type="button" mat-icon-button color="warn" (click)="remove(i)">
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="text" matInput formControlName="label" placeholder="Group">
|
||||
<mat-error *ngIf="groups[i].get('label')?.hasError('required')">
|
||||
Group is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
.groups {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
> div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 719px) {
|
||||
.groups > div {
|
||||
flex: 100%;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { FormArray, FormGroup } from '@angular/forms';
|
||||
import { RdioScannerAdminService } from '../../admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-groups',
|
||||
styleUrls: ['./groups.component.scss'],
|
||||
templateUrl: './groups.component.html',
|
||||
})
|
||||
export class RdioScannerAdminGroupsComponent {
|
||||
@Input() form: FormArray | undefined;
|
||||
|
||||
get groups(): FormGroup[] {
|
||||
return this.form?.controls
|
||||
.sort((a, b) => (a.value.label || '').localeCompare(b.value.label || '')) as FormGroup[];
|
||||
}
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService) { }
|
||||
|
||||
add(): void {
|
||||
const id = this.groups.reduce((pv, cv) => cv.value._id >= pv ? cv.value._id + 1 : pv, 0);
|
||||
|
||||
const group = this.adminService.newGroupForm({ _id: id });
|
||||
|
||||
group.markAllAsTouched();
|
||||
|
||||
this.form?.insert(0, group);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
|
||||
remove(index: number): void {
|
||||
this.form?.removeAt(index);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<ng-container *ngIf="form" [formGroup]="form">
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Auto Populate</span><br>
|
||||
<span class="mat-caption">Globaly allows the automatic creation of unconfigured systems and
|
||||
talkgroups.</span>
|
||||
</p>
|
||||
<div>
|
||||
<mat-slide-toggle color="primary" formControlName="autoPopulate"></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Dimmer Delay</span><br>
|
||||
<span class="mat-caption">Delay in milliseconds before turning off the screen backlight when
|
||||
inactive.</span>
|
||||
</p>
|
||||
<mat-form-field>
|
||||
<input matInput formControlName="dimmerDelay" type="number">
|
||||
<mat-error *ngIf="form?.get('dimmerDelay')?.hasError('required')">
|
||||
Dimmer delay is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('dimmerDelay')?.hasError('min')">
|
||||
Dimmer delay is invalid
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Disable Audio Conversion</span><br>
|
||||
<span class="mat-caption">Disable the audio format conversion to m4a/aac. This is usefull if another Rdio
|
||||
Scanner instance is feeding this instance where audio has already been converted.</span>
|
||||
</p>
|
||||
<div>
|
||||
<mat-slide-toggle color="primary" formControlName="disableAudioConversion"></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Disable Duplicate Call Detection</span><br>
|
||||
<span class="mat-caption">Disable duplicate audio file detection.</span>
|
||||
</p>
|
||||
<div>
|
||||
<mat-slide-toggle color="primary" formControlName="disableDuplicateDetection"></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Keypad Beep Style</span><br>
|
||||
<span class="mat-caption">Provide audio feedback when pressing buttons.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<mat-select formControlName="keypadBeeps" placeholder="Style">
|
||||
<mat-option value="uniden">Uniden</mat-option>
|
||||
<mat-option value="whistler">Whistler</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Prune Days</span><br>
|
||||
<span class="mat-caption">Prune the database for data older than the specified number of days. Set to 0 to
|
||||
disable.</span>
|
||||
</p>
|
||||
<mat-form-field>
|
||||
<input matInput formControlName="pruneDays" type="number">
|
||||
<mat-error *ngIf="form?.get('pruneDays')?.hasError('required')">
|
||||
Prune days is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('pruneDays')?.hasError('min')">
|
||||
Prune days is invalid
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Sort Talkgroups</span><br>
|
||||
<span class="mat-caption">Automatically sort talkgroups by their id.</span>
|
||||
</p>
|
||||
<div>
|
||||
<mat-slide-toggle color="primary" formControlName="sortTalkgroups"></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-options',
|
||||
templateUrl: './options.component.html',
|
||||
})
|
||||
export class RdioScannerAdminOptionsComponent {
|
||||
@Input() form: FormGroup | undefined;
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<div>
|
||||
<div mat-dialog-title>Systems and talkgroups selection</div>
|
||||
<div [formGroup]="select" mat-dialog-content>
|
||||
<mat-checkbox color="primary" [disableRipple]="true" formControlName="all"
|
||||
[indeterminate]="indeterminate.everything">
|
||||
Everything
|
||||
</mat-checkbox>
|
||||
<h3 class="mat-caption">Groups</h3>
|
||||
<div class="flex-row wrap" formArrayName="groups">
|
||||
<div *ngFor="let group of configGroups; index as groupIndex" [formGroupName]="groupIndex">
|
||||
<mat-checkbox color="primary" [disableRipple]="true" formControlName="checked"
|
||||
[indeterminate]="indeterminate.groups[groupIndex]">
|
||||
{{ group.value.label }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="mat-caption">Tags</h3>
|
||||
<div class="flex-row wrap" formArrayName="tags">
|
||||
<div *ngFor="let tag of configTags; index as tagIndex" [formGroupName]="tagIndex">
|
||||
<mat-checkbox color="primary" [disableRipple]="true" formControlName="checked"
|
||||
[indeterminate]="indeterminate.tags[tagIndex]">
|
||||
{{ tag.value.label }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="mat-caption">Systems</h3>
|
||||
<div formArrayName="systems">
|
||||
<div *ngFor="let system of configSystems; index as systemIndex" [formGroupName]="systemIndex">
|
||||
<div class="flex-row margin">
|
||||
<mat-checkbox color="primary" [disableRipple]="true" formControlName="all"
|
||||
[indeterminate]="indeterminate.systems[systemIndex]">
|
||||
{{ system.value.label }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div class="flex-row wrap indent margin" formArrayName="talkgroups">
|
||||
<div *ngFor="let talkgroup of configTalkgroups[systemIndex]; index as talkgroupIndex"
|
||||
[formGroupName]="talkgroupIndex">
|
||||
<mat-checkbox color="primary" [disableRipple]="true" formControlName="checked">
|
||||
{{ talkgroup.value.label }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-raised-button (click)="cancel()">Cancel</button>
|
||||
<button mat-raised-button color="primary" (click)="accept()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
rdio-scanner-admin-systems-selection {
|
||||
> div {
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&.wrap {
|
||||
flex-wrap: wrap;
|
||||
|
||||
> * {
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
padding: 0 0.125rem;
|
||||
|
||||
&:last-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.indent {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.margin {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mat-checkbox,
|
||||
.mat-checkbox-layout {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mat-checkbox-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mat-dialog-actions {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media (max-width: 719px) {
|
||||
.flex-row.wrap > * {
|
||||
flex: 0 0 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.flex-row.wrap > * {
|
||||
flex: 0 0 20%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,349 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { ChangeDetectorRef, Component, Inject, ViewEncapsulation } from '@angular/core';
|
||||
import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Access } from '../../../admin.service';
|
||||
|
||||
interface System {
|
||||
all: boolean;
|
||||
id: number;
|
||||
talkgroups: Talkgroup[];
|
||||
}
|
||||
|
||||
interface Talkgroup {
|
||||
checked: boolean;
|
||||
id: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
selector: 'rdio-scanner-admin-systems-selection',
|
||||
styleUrls: ['./select.component.scss'],
|
||||
templateUrl: './select.component.html',
|
||||
})
|
||||
export class RdioScannerAdminSystemsSelectComponent {
|
||||
indeterminate = {
|
||||
everything: false,
|
||||
groups: [] as boolean[],
|
||||
systems: [] as boolean[],
|
||||
tags: [] as boolean[],
|
||||
};
|
||||
|
||||
select = this.ngFormBuilder.group({
|
||||
all: this.ngFormBuilder.control(false),
|
||||
groups: this.ngFormBuilder.array([]),
|
||||
tags: this.ngFormBuilder.array([]),
|
||||
systems: this.ngFormBuilder.array([]),
|
||||
});
|
||||
|
||||
configTalkgroups = this.configSystems.map((fgSystem) => {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
return faTalkgroups.controls as FormGroup[];
|
||||
});
|
||||
|
||||
get configGroups(): FormGroup[] {
|
||||
const faGroups = this.access.root.get('groups') as FormArray;
|
||||
|
||||
return faGroups.controls as FormGroup[];
|
||||
}
|
||||
|
||||
get configSystems(): FormGroup[] {
|
||||
const faSystems = this.access.root.get('systems') as FormArray;
|
||||
|
||||
return faSystems.controls as FormGroup[];
|
||||
}
|
||||
|
||||
get configTags(): FormGroup[] {
|
||||
const faTags = this.access.root.get('tags') as FormArray;
|
||||
|
||||
return faTags.controls as FormGroup[];
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public access: FormGroup,
|
||||
private matDialogRef: MatDialogRef<RdioScannerAdminSystemsSelectComponent>,
|
||||
private ngChangeDetectorRef: ChangeDetectorRef,
|
||||
private ngFormBuilder: FormBuilder,
|
||||
) {
|
||||
const fcAll = this.select.get('all') as FormControl;
|
||||
const faGroups = this.select.get('groups') as FormArray;
|
||||
const faSystems = this.select.get('systems') as FormArray;
|
||||
const faTags = this.select.get('tags') as FormArray;
|
||||
|
||||
this.configGroups.forEach((configGroup) => {
|
||||
const fgGroup = this.ngFormBuilder.group({
|
||||
_id: [configGroup.value._id],
|
||||
checked: [false],
|
||||
});
|
||||
|
||||
faGroups.push(fgGroup);
|
||||
|
||||
fgGroup.valueChanges.subscribe((vGroup) => {
|
||||
faSystems.controls.forEach((fgSystem) => {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
faTalkgroups.controls.forEach((fgTalkgroup) => {
|
||||
if (fgTalkgroup.value.groupId === vGroup._id && fgTalkgroup.value.checked !== vGroup.checked) {
|
||||
fgTalkgroup.get('checked')?.setValue(vGroup.checked);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.configSystems.forEach((configSystem, index) => {
|
||||
const fcSystemAll = this.ngFormBuilder.control(false);
|
||||
|
||||
const faSystemTalkgroups = this.ngFormBuilder.array([]);
|
||||
|
||||
const fgSystem = this.ngFormBuilder.group({
|
||||
all: fcSystemAll,
|
||||
id: configSystem.value.id,
|
||||
talkgroups: faSystemTalkgroups
|
||||
});
|
||||
|
||||
this.configTalkgroups[index].forEach((configTalkgroup) => {
|
||||
const fgSystemTalkgroup = this.ngFormBuilder.group({
|
||||
checked: [false],
|
||||
groupId: configTalkgroup.value.groupId,
|
||||
id: configTalkgroup.value.id,
|
||||
tagId: configTalkgroup.value.tagId
|
||||
});
|
||||
|
||||
faSystemTalkgroups.push(fgSystemTalkgroup);
|
||||
|
||||
fgSystemTalkgroup.valueChanges.subscribe(() => {
|
||||
const vAll = faSystemTalkgroups.controls.every((systemTalkgroup) => systemTalkgroup.value.checked);
|
||||
|
||||
fcSystemAll.setValue(vAll, { emitEvent: false });
|
||||
});
|
||||
});
|
||||
|
||||
faSystems.push(fgSystem);
|
||||
|
||||
fgSystem.valueChanges.subscribe(() => {
|
||||
this.rebuildGroupIndeterminates();
|
||||
this.rebuildTagIndeterminates();
|
||||
});
|
||||
|
||||
fcSystemAll.valueChanges.subscribe((vAll) => {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
faTalkgroups.controls.forEach((fgTalkgroup) => fgTalkgroup.get('checked')?.setValue(vAll));
|
||||
|
||||
this.rebuildGroupIndeterminates();
|
||||
this.rebuildTagIndeterminates();
|
||||
});
|
||||
|
||||
faSystemTalkgroups.valueChanges.subscribe((vSystemTalkgroups) => {
|
||||
let off = 0;
|
||||
let on = 0;
|
||||
|
||||
vSystemTalkgroups.forEach((vSystemTalkgroup: Talkgroup) => {
|
||||
if (vSystemTalkgroup.checked) {
|
||||
on++;
|
||||
|
||||
} else {
|
||||
off++;
|
||||
}
|
||||
});
|
||||
|
||||
this.indeterminate.systems[index] = !!off && !!on;
|
||||
|
||||
faSystems.at(index).get('all')?.setValue(!off && on, { emitEvent: false });
|
||||
});
|
||||
});
|
||||
|
||||
this.configTags.forEach((configTag) => {
|
||||
const fgTag = this.ngFormBuilder.group({
|
||||
_id: [configTag.value._id],
|
||||
checked: [false],
|
||||
});
|
||||
|
||||
faTags.push(fgTag);
|
||||
|
||||
fgTag.valueChanges.subscribe((vTag) => {
|
||||
faSystems.controls.forEach((fgSystem) => {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
faTalkgroups.controls.forEach((fgTalkgroup) => {
|
||||
if (fgTalkgroup.value.tagId === vTag._id && fgTalkgroup.value.checked !== vTag.checked) {
|
||||
fgTalkgroup.get('checked')?.setValue(vTag.checked);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
fcAll.valueChanges.subscribe((vAll) => {
|
||||
faSystems.controls.flatMap((fgSystem) => {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
return faTalkgroups.controls;
|
||||
}).concat(faGroups.controls, faTags.controls).forEach((control) => {
|
||||
control.get('checked')?.setValue(vAll);
|
||||
});
|
||||
});
|
||||
|
||||
faSystems.valueChanges.subscribe((vSystems) => {
|
||||
let off = 0;
|
||||
let on = 0;
|
||||
|
||||
vSystems.forEach((vSystem: System) => {
|
||||
if (vSystem.all) {
|
||||
on++;
|
||||
|
||||
} else {
|
||||
off++;
|
||||
}
|
||||
});
|
||||
|
||||
this.indeterminate.everything = !!off && !!on;
|
||||
|
||||
fcAll.setValue(!off && on, { emitEvent: false });
|
||||
});
|
||||
|
||||
const vAccess: Access = this.access.value;
|
||||
|
||||
if (vAccess.systems === '*') {
|
||||
this.select.get('all')?.setValue(true);
|
||||
|
||||
} else if (Array.isArray(vAccess.systems)) {
|
||||
vAccess.systems.forEach((vSystem: { id: number; talkgroups: { id: number }[] | number[] | '*' } | number) => {
|
||||
if (typeof vSystem === 'number') {
|
||||
faSystems.controls.find((fgSystem) => fgSystem.value.id === vSystem)?.get('all')?.setValue(true);
|
||||
|
||||
} else if (vSystem !== null && typeof vSystem === 'object') {
|
||||
const fgSystem = faSystems.controls.find((fg) => fg.value.id === vSystem.id);
|
||||
|
||||
if (fgSystem) {
|
||||
if (vSystem.talkgroups === '*') {
|
||||
fgSystem.get('all')?.setValue(true);
|
||||
|
||||
} else if (Array.isArray(vSystem.talkgroups)) {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
vSystem.talkgroups.forEach((talkgroup: { id: number } | number) => {
|
||||
const talkgroupId = typeof talkgroup === 'number' ? talkgroup : talkgroup.id;
|
||||
|
||||
const fgTalkgroup = faTalkgroups.controls.find((fg) => fg.value.id === talkgroupId);
|
||||
|
||||
fgTalkgroup?.get('checked')?.setValue(true);
|
||||
});
|
||||
|
||||
fgSystem?.updateValueAndValidity();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
accept(): void {
|
||||
const select = this.select.value;
|
||||
|
||||
const access: Access['systems'] = select.all ? '*' : select.systems.filter((system: System) => {
|
||||
return system.all || system.talkgroups.some((talkgroup: Talkgroup) => talkgroup.checked);
|
||||
}).map((system: System) => {
|
||||
if (system.all) {
|
||||
return {
|
||||
id: system.id,
|
||||
talkgroups: '*',
|
||||
};
|
||||
|
||||
} else {
|
||||
return {
|
||||
id: system.id,
|
||||
talkgroups: system.talkgroups
|
||||
.filter((talkgroup: Talkgroup) => talkgroup.checked)
|
||||
.map((talkgroup: Talkgroup) => talkgroup.id),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
this.matDialogRef.close(access);
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.matDialogRef.close(null);
|
||||
}
|
||||
|
||||
private rebuildGroupIndeterminates(): void {
|
||||
const faGroups = this.select.get('groups') as FormArray;
|
||||
|
||||
const faSystems = this.select.get('systems') as FormArray;
|
||||
|
||||
faGroups.controls.forEach((fgGroup, index) => {
|
||||
let off = 0;
|
||||
let on = 0;
|
||||
|
||||
faSystems.controls.forEach((fgSystem) => {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
faTalkgroups.controls.forEach((fgTalkgroup) => {
|
||||
if (fgTalkgroup.value.groupId === fgGroup.value._id) {
|
||||
if (fgTalkgroup.value.checked) {
|
||||
on++;
|
||||
|
||||
} else {
|
||||
off++;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.indeterminate.groups[index] = !!off && !!on;
|
||||
|
||||
fgGroup.get('checked')?.setValue(!off && on, { emitEvent: false });
|
||||
});
|
||||
}
|
||||
|
||||
private rebuildTagIndeterminates(): void {
|
||||
const faTags = this.select.get('tags') as FormArray;
|
||||
|
||||
const faSystems = this.select.get('systems') as FormArray;
|
||||
|
||||
faTags.controls.forEach((fgTag, index) => {
|
||||
let off = 0;
|
||||
let on = 0;
|
||||
|
||||
faSystems.controls.forEach((fgSystem) => {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
faTalkgroups.controls.forEach((fgTalkgroup) => {
|
||||
if (fgTalkgroup.value.tagId === fgTag.value._id) {
|
||||
if (fgTalkgroup.value.checked) {
|
||||
on++;
|
||||
|
||||
} else {
|
||||
off++;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.indeterminate.tags[index] = !!off && !!on;
|
||||
|
||||
fgTag.get('checked')?.setValue(!off && on, { emitEvent: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
<ng-container [formGroup]="form">
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Id</span><br>
|
||||
<span class="mat-caption">System identifier in decimal format.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="number" matInput formControlName="id" placeholder="Id">
|
||||
<mat-error *ngIf="form.get('id')?.hasError('duplicate')">
|
||||
Id is already defined
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form.get('id')?.hasError('min')">
|
||||
Id is invalid
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form.get('id')?.hasError('required')">
|
||||
Id is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Label</span><br>
|
||||
<span class="mat-caption">System label displayed on the main screen and on the search panel.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="text" matInput formControlName="label" placeholder="Label">
|
||||
<mat-error *ngIf="form.get('label')?.hasError('required')">
|
||||
Label is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Led Color</span><br>
|
||||
<span class="mat-caption">Indicator color when playing an audio file from this system. If not
|
||||
specified, the indicator will light green.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<mat-select formControlName="led" placeholder="Color">
|
||||
<mat-option [value]="null">Default</mat-option>
|
||||
<mat-option *ngFor="let led of leds" [value]="led">
|
||||
{{ led | titlecase }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Auto Populate</span><br>
|
||||
<span class="mat-caption">Allows the automatic creation of unconfigured talkgroups.</span>
|
||||
</p>
|
||||
<mat-slide-toggle color="primary" formControlName="autoPopulate"></mat-slide-toggle>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Blacklists</span><br>
|
||||
<span class="mat-caption">A comma separated list of talkgroup Ids to blacklist from this system. This only
|
||||
has an effect if the auto populate option is enabled.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<textarea type="text" matInput [matTextareaAutosize]="true" formControlName="blacklists"
|
||||
placeholder="Blacklists"></textarea>
|
||||
<mat-error *ngIf="form?.get('blacklists')?.hasError('invalid')">
|
||||
Comma separated list of talkgroup Ids
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<mat-accordion displayMode="flat">
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Talkgroups
|
||||
<mat-icon *ngIf="form.get('talkgroups')?.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<div class="row top">
|
||||
<p class="mat-caption">System talkgroups, you can drag and drop talkgroups to rearrange them. They
|
||||
will appear with the same order on the talkgroup selection panel unless the sort talkgroups
|
||||
option is enabled.</p>
|
||||
<button type="button" mat-button color="accent" (click)="addTalkgroup()">New talkgroup</button>
|
||||
</div>
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListData]="talkgroups"
|
||||
(cdkDropListDropped)="drop($event)">
|
||||
<mat-expansion-panel *ngFor="let talkgroup of talkgroups; index as i" [formGroup]="talkgroup"
|
||||
cdkDrag>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
{{ talkgroup.value.label?.trim() || 'NewTalkgroup' }}
|
||||
<mat-icon *ngIf="talkgroup.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<rdio-scanner-admin-talkgroup [form]="talkgroup" (blacklist)="blacklistTalkgroup(i)"
|
||||
(remove)="removeTalkgroup(i)">
|
||||
</rdio-scanner-admin-talkgroup>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
<mat-accordion displayMode="flat">
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Units
|
||||
<mat-icon *ngIf="form.get('units')?.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<div class="row top">
|
||||
<p class="mat-caption">Create units to display the label instead of showing the unit id on the main
|
||||
screen.<br>You can drag and drop them to rearrange</p>
|
||||
<button type="button" mat-button color="accent" (click)="addUnit()">New unit</button>
|
||||
</div>
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListData]="units"
|
||||
(cdkDropListDropped)="drop($event)">
|
||||
<mat-expansion-panel *ngFor="let unit of units; index as i" [formGroup]="unit" cdkDrag>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
{{ unit.value.label?.trim() || 'NewUnit' }}
|
||||
<mat-icon *ngIf="unit.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<rdio-scanner-admin-unit [form]="unit" (remove)="removeUnit(i)">
|
||||
</rdio-scanner-admin-unit>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
<div class="row bottom">
|
||||
<button type="button" mat-button color="warn" (click)="remove.emit()">
|
||||
Delete system
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Component, EventEmitter, Input, Output, QueryList, ViewChildren } from '@angular/core';
|
||||
import { FormArray, FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||
import { RdioScannerAdminService, Group, Tag } from '../../../admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-system',
|
||||
templateUrl: './system.component.html',
|
||||
})
|
||||
export class RdioScannerAdminSystemComponent {
|
||||
@Input() form = new FormGroup({});
|
||||
|
||||
@Input() groups: Group[] = [];
|
||||
|
||||
@Input() tags: Tag[] = [];
|
||||
|
||||
@Output() add = new EventEmitter<void>();
|
||||
|
||||
@Output() remove = new EventEmitter<void>();
|
||||
|
||||
leds = this.adminService.getLeds();
|
||||
|
||||
get talkgroups(): FormGroup[] {
|
||||
const talkgroups = this.form.get('talkgroups') as FormArray;
|
||||
|
||||
return talkgroups.controls as FormGroup[];
|
||||
}
|
||||
|
||||
get units(): FormGroup[] {
|
||||
const units = this.form.get('units') as FormArray;
|
||||
|
||||
return units.controls as FormGroup[];
|
||||
}
|
||||
|
||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService) { }
|
||||
|
||||
addTalkgroup(): void {
|
||||
const talkgroups = this.form.get('talkgroups') as FormArray;
|
||||
|
||||
talkgroups.insert(0, this.adminService.newTalkgroupForm());
|
||||
|
||||
this.form.markAsDirty();
|
||||
}
|
||||
|
||||
addUnit(): void {
|
||||
const units = this.form.get('units') as FormArray;
|
||||
|
||||
units.insert(0, this.adminService.newUnitForm());
|
||||
|
||||
this.form.markAsDirty();
|
||||
}
|
||||
|
||||
blacklistTalkgroup(index: number): void {
|
||||
const talkgroup = this.talkgroups[index];
|
||||
|
||||
const id = talkgroup.value.id;
|
||||
|
||||
if (typeof id !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
const blacklists = this.form?.get('blacklists') as FormControl;
|
||||
|
||||
blacklists.setValue(blacklists.value?.trim() ? `${blacklists.value},${id}` : `${id}`);
|
||||
|
||||
this.removeTalkgroup(index);
|
||||
}
|
||||
|
||||
closeAll(): void {
|
||||
this.panels?.forEach((panel) => panel.close());
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<FormGroup[]>): void {
|
||||
if (event.previousIndex !== event.currentIndex) {
|
||||
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
|
||||
|
||||
this.form.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
removeTalkgroup(index: number): void {
|
||||
const talkgroups = this.form.get('talkgroups') as FormArray;
|
||||
|
||||
talkgroups.removeAt(index);
|
||||
|
||||
talkgroups.markAsDirty();
|
||||
}
|
||||
|
||||
removeUnit(index: number): void {
|
||||
const units = this.form.get('units') as FormArray;
|
||||
|
||||
units.removeAt(index);
|
||||
|
||||
units.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<div class="row top">
|
||||
<p class="mat-body">This is where you define all your systems and talkgroups are defined. Audio files to unknown
|
||||
systems/talkgroups won't be ingested unless the auto populate option is activated. You can also drag and drop
|
||||
systems to rearrange their order.</p>
|
||||
<button type="button" mat-button color="accent" (click)="add()">New system</button>
|
||||
</div>
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListData]="systems" (cdkDropListDropped)="drop($event)">
|
||||
<mat-expansion-panel *ngFor="let system of systems; index as i" [formGroup]="system"
|
||||
(afterCollapse)="systemComponent.closeAll()" cdkDrag>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
{{ system.value.label?.trim() || 'NewSystem' }}
|
||||
<mat-icon *ngIf="system.invalid" color="warn">error</mat-icon>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-system #systemComponent [form]="system" (add)="add()" (remove)="remove(i)">
|
||||
</rdio-scanner-admin-system>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Component, Input, QueryList, ViewChildren } from '@angular/core';
|
||||
import { FormArray, FormGroup } from '@angular/forms';
|
||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||
import { RdioScannerAdminService, System } from '../../admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-systems',
|
||||
templateUrl: './systems.component.html',
|
||||
})
|
||||
export class RdioScannerAdminSystemsComponent {
|
||||
@Input() form: FormArray | undefined;
|
||||
|
||||
get systems(): FormGroup[] {
|
||||
return this.form?.controls
|
||||
.sort((a, b) => (a.value.order || 0) - (b.value.order || 0)) as FormGroup[];
|
||||
}
|
||||
|
||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService) { }
|
||||
|
||||
add(): void {
|
||||
const system = this.adminService.newSystemForm();
|
||||
|
||||
system.markAllAsTouched();
|
||||
|
||||
this.form?.insert(0, system);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
|
||||
closeAll(): void {
|
||||
this.panels?.forEach((panel) => panel.close());
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<FormGroup[]>): void {
|
||||
if (event.previousIndex !== event.currentIndex) {
|
||||
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
|
||||
|
||||
event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false }));
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
remove(index: number): void {
|
||||
this.form?.removeAt(index);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
<ng-container *ngIf="form" [formGroup]="form">
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Id</span><br>
|
||||
<span class="mat-caption">Talkgroup identifier in decimal format.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="number" matInput formControlName="id" placeholder="Id">
|
||||
<mat-error *ngIf="form?.get('id')?.hasError('duplicate')">
|
||||
Id is already defined
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('id')?.hasError('min')">
|
||||
Id is invalid
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('id')?.hasError('required')">
|
||||
Id is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Label</span><br>
|
||||
<span class="mat-caption">Talkgroup label displayed on the main screen and on buttons.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="text" matInput formControlName="label" placeholder="Label">
|
||||
<mat-error *ngIf="form?.get('label')?.hasError('required')">
|
||||
Label is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Name</span><br>
|
||||
<span class="mat-caption">Talkgroup name displayed on the main screen.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="text" matInput formControlName="name" placeholder="Name">
|
||||
<mat-error *ngIf="form?.get('name')?.hasError('required')">
|
||||
Name is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Group</span><br>
|
||||
<span class="mat-caption">Group to which this talkgoups belongs.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<mat-select formControlName="groupId" placeholder="Group">
|
||||
<mat-option *ngFor="let group of groups" [value]="group._id">
|
||||
{{ group.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="form?.get('groupId')?.hasError('required')">
|
||||
Group is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Tag</span><br>
|
||||
<span class="mat-caption">Tag to which this talkgoups belongs.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<mat-select formControlName="tagId" placeholder="Tag">
|
||||
<mat-option *ngFor="let tag of tags" [value]="tag._id">
|
||||
{{ tag.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="form?.get('tagId')?.hasError('required')">
|
||||
Tag is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Led Color</span><br>
|
||||
<span class="mat-caption">Indicator color when playing an audio file from this talkgroup. If not specified,
|
||||
the color configured for the system is used.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<mat-select formControlName="led" placeholder="Color">
|
||||
<mat-option [value]="null">Default</mat-option>
|
||||
<mat-option *ngFor="let led of leds" [value]="led">
|
||||
{{ led | titlecase }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Frequency</span><br>
|
||||
<span class="mat-caption">Fake frequency in hertz displayed on the main screen.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="number" matInput formControlName="frequency" placeholder="Frequency">
|
||||
<mat-error *ngIf="form?.get('frequency')?.errors">
|
||||
Invalid frequency
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Patches</span><br>
|
||||
<span class="mat-caption">A comma separated list of talkgroup id to patch in this one.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<textarea type="text" matInput [matTextareaAutosize]="true" formControlName="patches" placeholder="Patches"></textarea>
|
||||
<mat-error *ngIf="form?.get('patches')?.hasError('invalid')">
|
||||
Comma separated list of talkgroup Ids
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row bottom">
|
||||
<button *ngIf="form.get('id')?.value" type="button" mat-button (click)="blacklist.emit()">
|
||||
Blacklist talkgroup
|
||||
</button>
|
||||
<button type="button" mat-button color="warn" (click)="remove.emit()">
|
||||
Delete talkgroup
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormArray, FormControl, FormGroup } from '@angular/forms';
|
||||
import { RdioScannerAdminService, Group, Tag } from '../../../admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-talkgroup',
|
||||
templateUrl: './talkgroup.component.html',
|
||||
})
|
||||
export class RdioScannerAdminTalkgroupComponent {
|
||||
@Input() form: FormGroup | undefined;
|
||||
|
||||
@Output() blacklist = new EventEmitter<void>();
|
||||
|
||||
@Output() remove = new EventEmitter<void>();
|
||||
|
||||
leds = this.adminService.getLeds();
|
||||
|
||||
get groups(): Group[] {
|
||||
return this.form?.root.get('groups')?.value as Group[];
|
||||
}
|
||||
|
||||
get patches(): FormControl[] {
|
||||
const patches = this.form?.get('patches') as FormArray;
|
||||
|
||||
return patches.controls as FormControl[];
|
||||
}
|
||||
|
||||
get tags(): Tag[] {
|
||||
return this.form?.root.get('tags')?.value as Tag[];
|
||||
}
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService) { }
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<ng-container *ngIf="form" [formGroup]="form">
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Id</span><br>
|
||||
<span class="mat-caption">Unit identifier in decimal format.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="number" matInput formControlName="id" placeholder="Id">
|
||||
<mat-error *ngIf="form?.get('id')?.hasError('duplicate')">
|
||||
Id is already defined
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('id')?.hasError('min')">
|
||||
Id is invalid
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('id')?.hasError('required')">
|
||||
Id is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Label</span><br>
|
||||
<span class="mat-caption">Unit label.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="text" matInput formControlName="label" placeholder="Label">
|
||||
<mat-error *ngIf="form?.get('label')?.hasError('required')">
|
||||
Label is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row bottom">
|
||||
<button type="button" mat-button color="warn" (click)="remove.emit()">
|
||||
Delete unit
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-unit',
|
||||
templateUrl: './unit.component.html',
|
||||
})
|
||||
export class RdioScannerAdminUnitComponent {
|
||||
@Input() form: FormGroup | undefined;
|
||||
|
||||
@Output() remove = new EventEmitter<void>();
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<div class="row top">
|
||||
<p class="mat-body">All system talkgroups must be associated with a tag, which is then used to search for calls
|
||||
based on their tag.</p>
|
||||
<button type="button" mat-button color="accent" (click)="add()">New tag</button>
|
||||
</div>
|
||||
<div class="tags">
|
||||
<div *ngFor="let tag of tags; index as i" [formGroup]="tag">
|
||||
<button type="button" mat-icon-button color="warn" (click)="remove(i)">
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="text" matInput formControlName="label" placeholder="Tag">
|
||||
<mat-error *ngIf="tags[i].get('label')?.hasError('required')">
|
||||
Tag is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
.tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
> div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 719px) {
|
||||
.tags > div {
|
||||
flex: 100%;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { FormArray, FormGroup } from '@angular/forms';
|
||||
import { RdioScannerAdminService } from '../../admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-tags',
|
||||
styleUrls: ['./tags.component.scss'],
|
||||
templateUrl: './tags.component.html',
|
||||
})
|
||||
export class RdioScannerAdminTagsComponent {
|
||||
@Input() form: FormArray | undefined;
|
||||
|
||||
get tags(): FormGroup[] {
|
||||
return this.form?.controls
|
||||
.sort((a, b) => (a.value.label || '').localeCompare(b.value.label || '')) as FormGroup[];
|
||||
}
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService) { }
|
||||
|
||||
add(): void {
|
||||
const id = this.tags.reduce((pv, cv) => cv.value._id >= pv ? cv.value._id + 1 : pv, 0);
|
||||
|
||||
const tag = this.adminService.newTagForm({ _id: id });
|
||||
|
||||
tag.markAllAsTouched();
|
||||
|
||||
this.form?.insert(0, tag);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
|
||||
remove(index: number): void {
|
||||
this.form?.removeAt(index);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
}
|
||||
}
|
||||
20
client/src/app/components/rdio-scanner/admin/index.ts
Normal file
20
client/src/app/components/rdio-scanner/admin/index.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
export * from './admin.module';
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<form [formGroup]="form" (ngSubmit)="login()">
|
||||
<p class="mat-body-1">Please enter the admin password to gain access to the administrative dashboard</p>
|
||||
<mat-form-field hideRequiredMarker>
|
||||
<mat-label>Password</mat-label>
|
||||
<input matInput formControlName="password" type="password" required>
|
||||
<mat-error>{{ message }}</mat-error>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="form.disabled || form.invalid">Login</button>
|
||||
</form>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
form > button {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mat-raised-button,
|
||||
.mat-form-field {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { FormBuilder, Validators } from '@angular/forms';
|
||||
import { RdioScannerAdminService } from '../admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-login',
|
||||
styleUrls: ['./login.component.scss'],
|
||||
templateUrl: './login.component.html',
|
||||
})
|
||||
export class RdioScannerAdminLoginComponent {
|
||||
@Output() loggedIn = new EventEmitter<void>();
|
||||
|
||||
form = this.formBuilder.group({
|
||||
password: [null, Validators.required],
|
||||
});
|
||||
|
||||
message = '';
|
||||
|
||||
constructor(
|
||||
private adminService: RdioScannerAdminService,
|
||||
private formBuilder: FormBuilder,
|
||||
) { }
|
||||
|
||||
async login(password = this.form.get('password')?.value): Promise<void> {
|
||||
if (!password) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.disable();
|
||||
|
||||
const loggedIn = await this.adminService.login(password);
|
||||
|
||||
if (loggedIn) {
|
||||
this.loggedIn.emit();
|
||||
|
||||
} else {
|
||||
this.form.enable();
|
||||
this.form.reset();
|
||||
|
||||
this.message = 'Invalid password';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<mat-table [dataSource]="logs">
|
||||
<ng-container matColumnDef="level">
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<span>Level</span>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let log">
|
||||
<mat-icon *ngIf="log?.level === 'error'" class="error">error</mat-icon>
|
||||
<mat-icon *ngIf="log?.level === 'info'" class="info">notifications</mat-icon>
|
||||
<mat-icon *ngIf="log?.level === 'warn'" class="warn">warning</mat-icon>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="date">
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<span>Date</span>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let log">
|
||||
<span>{{ log?.dateTime | date:'MM/dd' }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="time">
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<span>Time</span>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let log">
|
||||
<span>{{ log?.dateTime | date:'HH:mm' }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="message">
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<span>Message</span>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let log">
|
||||
<span>{{ log?.message }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<mat-header-row *matHeaderRowDef="['level', 'date', 'time', 'message']">
|
||||
</mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: ['level', 'date', 'time', 'message']">
|
||||
</mat-row>
|
||||
</mat-table>
|
||||
<mat-progress-bar color="primary" [mode]="logsQueryPending ? 'query' : 'determinate'">
|
||||
</mat-progress-bar>
|
||||
<mat-paginator [disabled]="logsQueryPending" [length]="logsQuery?.count" [hidePageSize]="true"
|
||||
[pageSize]="logs.value.length" [showFirstLastButtons]="true" (page)="refresh()">
|
||||
</mat-paginator>
|
||||
<form autocomplete="off" [formGroup]="form">
|
||||
<mat-form-field>
|
||||
<mat-label>
|
||||
Sort order
|
||||
</mat-label>
|
||||
<mat-select formControlName="sort" (selectionChange)="formHandler()">
|
||||
<mat-option [value]="1">
|
||||
Ascending
|
||||
</mat-option>
|
||||
<mat-option [value]="-1">
|
||||
Descending
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>
|
||||
Date
|
||||
</mat-label>
|
||||
<input formControlName="date" [matDatepicker]="date" matInput [max]="logsQuery?.dateStop"
|
||||
[min]="logsQuery?.dateStart" placeholder="Choose a date" readonly (click)="date.open()"
|
||||
(dateChange)="formHandler()">
|
||||
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
|
||||
<mat-datepicker #date></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>
|
||||
Level
|
||||
</mat-label>
|
||||
<mat-select formControlName="level" (selectionChange)="formHandler()">
|
||||
<mat-option value="info">Informative</mat-option>
|
||||
<mat-option value="warn">Warning</mat-option>
|
||||
<mat-option value="error">Error</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div class="reset">
|
||||
<button type="button" mat-raised-button [disabled]="logsQueryPending" (click)="reload()">
|
||||
Reload
|
||||
</button>
|
||||
<button type="button" mat-raised-button [disabled]="logsQueryPending || form.pristine" (click)="reset()">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.warn {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.mat-cell,
|
||||
.mat-header-cell {
|
||||
&:nth-child(1),
|
||||
&:nth-child(2),
|
||||
&:nth-child(3) {
|
||||
flex: 0 0 60px;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.mat-form-field,
|
||||
.reset {
|
||||
box-sizing: border-box;
|
||||
padding: 0 0.5rem 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.mat-form-field {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.reset {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 719px) {
|
||||
form > .mat-form-field {
|
||||
flex: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
form > .mat-form-field {
|
||||
flex: 33%;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { Log, LogsQuery, LogsQueryOptions, RdioScannerAdminService } from '../admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-logs',
|
||||
styleUrls: ['./logs.component.scss'],
|
||||
templateUrl: './logs.component.html',
|
||||
})
|
||||
export class RdioScannerAdminLogsComponent {
|
||||
form = this.ngFormBuilder.group({
|
||||
date: [null],
|
||||
level: [null],
|
||||
sort: [-1],
|
||||
});
|
||||
|
||||
logs = new BehaviorSubject(new Array<Log | null>(10));
|
||||
|
||||
logsQuery: LogsQuery | null = null;
|
||||
|
||||
logsQueryPending = false;
|
||||
|
||||
private limit = 200;
|
||||
|
||||
private offset = 0;
|
||||
|
||||
@ViewChild(MatPaginator) private paginator: MatPaginator | undefined;
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService, private ngFormBuilder: FormBuilder) { }
|
||||
|
||||
formHandler(): void {
|
||||
this.paginator?.firstPage();
|
||||
|
||||
this.reload();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.form.reset({
|
||||
date: null,
|
||||
level: null,
|
||||
sort: -1,
|
||||
});
|
||||
|
||||
this.formHandler();
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
if (!this.paginator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const from = this.paginator.pageIndex * this.paginator.pageSize;
|
||||
|
||||
const to = this.paginator.pageIndex * this.paginator.pageSize + this.paginator.pageSize - 1;
|
||||
|
||||
if (!this.logsQueryPending && (from >= this.offset + this.limit || from < this.offset)) {
|
||||
this.reload();
|
||||
|
||||
} else if (this.logsQuery) {
|
||||
const logs: Array<Log | null> = this.logsQuery.logs.slice(from % this.limit, to % this.limit + 1);
|
||||
|
||||
while (logs.length < this.logs.value.length) {
|
||||
logs.push(null);
|
||||
}
|
||||
|
||||
this.logs.next(logs);
|
||||
}
|
||||
}
|
||||
|
||||
async reload(): Promise<void> {
|
||||
const pageIndex = this.paginator?.pageIndex || 0;
|
||||
|
||||
const pageSize = this.paginator?.pageSize || 0;
|
||||
|
||||
this.offset = Math.floor((pageIndex * pageSize) / this.limit) * this.limit;
|
||||
|
||||
const options: LogsQueryOptions = {
|
||||
limit: this.limit,
|
||||
offset: this.offset,
|
||||
sort: this.form.value.sort,
|
||||
};
|
||||
|
||||
if (typeof this.form.value.level === 'string') {
|
||||
options.level = this.form.value.level;
|
||||
}
|
||||
|
||||
if (this.form.value.date instanceof Date) {
|
||||
options.date = this.form.value.date;
|
||||
}
|
||||
|
||||
this.logsQueryPending = true;
|
||||
|
||||
this.form.disable();
|
||||
|
||||
this.logsQuery = await this.adminService.getLogs(options);
|
||||
|
||||
this.form.enable();
|
||||
|
||||
this.logsQueryPending = false;
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<mat-card *ngIf="todos.length">
|
||||
<mat-card-title>Todos</mat-card-title>
|
||||
<mat-list>
|
||||
<mat-list-item *ngFor="let todo of todos">
|
||||
<mat-icon *ngIf="todo.level === 'info'" class="info" mat-list-icon>notifications</mat-icon>
|
||||
<mat-icon *ngIf="todo.level === 'warn'" class="warn" mat-list-icon>warning</mat-icon>
|
||||
<div mat-line>
|
||||
<span class="todo">{{ todo.message }}</span>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</mat-card>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
.info {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.warn {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.todo {
|
||||
text-overflow: unset;
|
||||
white-space: normal;
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { AdminEvent, Config, RdioScannerAdminService } from '../admin.service';
|
||||
|
||||
interface Todo {
|
||||
level: 'info' | 'warn';
|
||||
message: String;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-todos',
|
||||
styleUrls: ['./todos.component.scss'],
|
||||
templateUrl: './todos.component.html',
|
||||
})
|
||||
export class RdioScannerAdminTodosComponent implements OnDestroy, OnInit {
|
||||
todos: Todo[] = [];
|
||||
|
||||
private config: Config | undefined;
|
||||
|
||||
private eventSubscription = this.adminService.event.subscribe(async (event: AdminEvent) => {
|
||||
if ('config' in event) {
|
||||
this.config = event.config;
|
||||
}
|
||||
|
||||
if ('passwordNeedChange' in event) {
|
||||
this.passwordNeedChange = event.passwordNeedChange || false;
|
||||
}
|
||||
|
||||
this.rebuildTodos();
|
||||
});
|
||||
|
||||
private passwordNeedChange = this.adminService.passwordNeedChange;
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService) { }
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.eventSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.adminService.getConfig().then((config) => {
|
||||
this.config = config;
|
||||
|
||||
this.rebuildTodos();
|
||||
});
|
||||
}
|
||||
|
||||
private rebuildTodos(): void {
|
||||
const todos: Todo[] = [];
|
||||
|
||||
if (this.passwordNeedChange) {
|
||||
todos.push({
|
||||
level: 'warn',
|
||||
message: 'You are using the default admin password, please change it from the tools / admin password menu.'
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.config?.systems?.length) {
|
||||
todos.push({
|
||||
level: 'info',
|
||||
message: 'No systems defined. You can define one from the systems menu, or import one from a CSV file from the tools menu, or turn on the global auto populate option from the options menu.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.config?.apiKeys?.length && !this.config?.dirWatch?.length) {
|
||||
todos.push({
|
||||
level: 'info',
|
||||
message: 'No apikeys and no dirwatch defined. Please set at least one to allow ingesting audio files.',
|
||||
});
|
||||
}
|
||||
|
||||
this.todos = todos;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
<section>
|
||||
<p class="mat-body">step 1: read the CSV file</p>
|
||||
<p class="text-center">
|
||||
<ng-container *ngIf="csv.length === 0">
|
||||
<button mat-raised-button (click)="input.click()">Read the CSV file</button>
|
||||
<input #input type="file" accept=".csv" style="display: none" (change)="read($event)">
|
||||
</ng-container>
|
||||
<ng-container *ngIf="csv.length !== 0">
|
||||
<button mat-raised-button (click)="csv=[]">Reset</button>
|
||||
</ng-container>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<p class="mat-body">step 2: select CSV style</p>
|
||||
<p class="text-center">
|
||||
<mat-radio-group [(ngModel)]="mode">
|
||||
<mat-radio-button [value]="0">Trunk Recorder</mat-radio-button>
|
||||
<mat-radio-button [value]="1">Radio Reference</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<p class="mat-body">step 3: review talkgroups to import</p>
|
||||
<div class="scroll">
|
||||
<mat-table #table [dataSource]="csv">
|
||||
<ng-container matColumnDef="id">
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<span>Id</span>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let tg">
|
||||
<span>{{ tg[fields[mode][0]] }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="label">
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<span>Label</span>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let tg">
|
||||
<span>{{ tg[fields[mode][1]] }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="description">
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<span>Description</span>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let tg">
|
||||
<span>{{ tg[fields[mode][2]] }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="tag">
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<span>Tag</span>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let tg">
|
||||
<span>{{ tg[fields[mode][3]] }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="group">
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<span>Group</span>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let tg">
|
||||
<span>{{ tg[fields[mode][4]] }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="action">
|
||||
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
||||
<mat-cell *matCellDef="let tg; index as i">
|
||||
<button mat-icon-button (click)="csv.splice(i, 1); table.renderRows()">
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<mat-header-row *matHeaderRowDef="tableColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: tableColumns"></mat-row>
|
||||
</mat-table>
|
||||
</div>
|
||||
<p *ngIf="!csv.length" class="mat-body text-center">No CSV data yet</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<p class="mat-body">step 4: import to configuration</p>
|
||||
<p class="text-center">
|
||||
<button mat-raised-button [disabled]="!csv.length" (click)="import()">
|
||||
Import to configuration
|
||||
</button>
|
||||
</p>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
.mat-radio-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-table {
|
||||
min-width: 720px;
|
||||
}
|
||||
|
||||
.mat-cell > span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mat-column-action {
|
||||
max-width: 40px;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { Config, Group, RdioScannerAdminService } from '../../admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-import-csv',
|
||||
styleUrls: ['./import-csv.component.scss'],
|
||||
templateUrl: './import-csv.component.html',
|
||||
})
|
||||
export class RdioScannerAdminImportCsvComponent {
|
||||
@Output() config = new EventEmitter<Config>();
|
||||
|
||||
csv: string[][] = [];
|
||||
|
||||
fields = [
|
||||
// [id, label, description, tag, group]
|
||||
[0, 3, 4, 5, 6], // trunk-recorder
|
||||
[0, 2, 4, 5, 6], // radioreference.com
|
||||
];
|
||||
|
||||
mode = 0;
|
||||
|
||||
tableColumns = ['id', 'label', 'description', 'tag', 'group', 'action'];
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService) { }
|
||||
|
||||
async import(): Promise<void> {
|
||||
const config = await this.adminService.getConfig();
|
||||
|
||||
this.csv.forEach((tg) => {
|
||||
const group = tg[this.fields[this.mode][4]];
|
||||
|
||||
if (!config.groups?.find((g) => g.label === group)) {
|
||||
const id = config.groups?.reduce((pv, cv) => typeof cv._id === 'number' && cv._id >= pv ? cv._id + 1 : pv, 0);
|
||||
|
||||
config.groups?.push({ _id: id, label: group });
|
||||
}
|
||||
|
||||
const tag = tg[this.fields[this.mode][3]];
|
||||
|
||||
if (!config.tags?.find((t) => t.label === tag)) {
|
||||
const id = config.tags?.reduce((pv, cv) => typeof cv._id === 'number' && cv._id >= pv ? cv._id + 1 : pv, 0);
|
||||
|
||||
config.tags?.push({ _id: id, label: tag });
|
||||
}
|
||||
});
|
||||
|
||||
const talkgroups = this.csv.map((csv) => {
|
||||
const groupId = config.groups?.find((g) => g.label === csv[this.fields[this.mode][4]])?._id;
|
||||
const tagId = config.tags?.find((t) => t.label === csv[this.fields[this.mode][3]])?._id;
|
||||
|
||||
return {
|
||||
id: +csv[this.fields[this.mode][0]],
|
||||
label: csv[this.fields[this.mode][1]],
|
||||
name: csv[this.fields[this.mode][2]],
|
||||
tagId,
|
||||
groupId,
|
||||
};
|
||||
});
|
||||
|
||||
config.systems?.unshift({ talkgroups });
|
||||
|
||||
this.csv = [];
|
||||
|
||||
this.config.emit(config);
|
||||
}
|
||||
|
||||
async read(event: Event): Promise<void> {
|
||||
const target = (event.target as HTMLInputElement & EventTarget);
|
||||
|
||||
const file = target.files?.item(0);
|
||||
|
||||
if (!(file instanceof File)) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = () => {
|
||||
target.value = '';
|
||||
|
||||
if (typeof reader.result !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.csv = reader.result
|
||||
.split(/\n|\r\n/)
|
||||
.map((tg) => tg.replace(/^"|"$/g, '').split(/"*,"*/))
|
||||
.filter((tg) => tg && /^[0-9]+$/.test(tg[0]))
|
||||
.filter((tg, idx, arr) => arr.findIndex((a) => a[0] === tg[0]) === idx);
|
||||
};
|
||||
|
||||
reader.readAsBinaryString(file);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<div>
|
||||
<p>
|
||||
<span class="mat-body">Import</span><br>
|
||||
<span class="mat-caption">Import a JSON file into the configuration panel where you can then review it before submitting it.</span>
|
||||
</p>
|
||||
<button mat-raised-button (click)="input.click()">Import</button>
|
||||
<input #input type="file" style="display: none" (change)="import($event)">
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<span class="mat-body">Export</span><br>
|
||||
<span class="mat-caption">Export the configuration to a JSON file.</span>
|
||||
</p>
|
||||
<button mat-raised-button (click)="export()">Export</button>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
:host {
|
||||
> div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Component, EventEmitter, Inject, Output } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Config, RdioScannerAdminService } from '../../admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-import-export-config',
|
||||
styleUrls: ['./import-export-config.component.scss'],
|
||||
templateUrl: './import-export-config.component.html',
|
||||
})
|
||||
export class RdioScannerAdminImportExportConfigComponent {
|
||||
@Output() config = new EventEmitter<Config>();
|
||||
|
||||
constructor(
|
||||
private adminService: RdioScannerAdminService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
private matSnackBar: MatSnackBar,
|
||||
) { }
|
||||
|
||||
async export(): Promise<void> {
|
||||
const config = await this.adminService.getConfig();
|
||||
|
||||
if ('docker' in config) {
|
||||
delete config.docker;
|
||||
}
|
||||
|
||||
const file = JSON.stringify(config);
|
||||
const fileName = 'rdio-scanner.json';
|
||||
const fileType = 'application/json';
|
||||
const fileUri = `data:${fileType};base64,${btoa(file)}`;
|
||||
|
||||
const el = this.document.createElement('a');
|
||||
|
||||
el.style.display = 'none';
|
||||
|
||||
el.setAttribute('href', fileUri);
|
||||
el.setAttribute('download', fileName);
|
||||
|
||||
this.document.body.appendChild(el);
|
||||
|
||||
el.click();
|
||||
|
||||
this.document.body.removeChild(el);
|
||||
}
|
||||
|
||||
async import(event: Event): Promise<void> {
|
||||
const target = (event.target as HTMLInputElement & EventTarget);
|
||||
|
||||
const file = target.files?.item(0);
|
||||
|
||||
if (!(file instanceof File)) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = () => {
|
||||
target.value = '';
|
||||
|
||||
try {
|
||||
const config = JSON.parse(reader.result as string);
|
||||
|
||||
this.config.emit(config);
|
||||
|
||||
} catch (error) {
|
||||
this.matSnackBar.open(error.message, '', { duration: 5000 });
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsBinaryString(file);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<p class="mat-body">
|
||||
Change the administrator password which gives access to the server administration dashboard.
|
||||
</p>
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field>
|
||||
<mat-label>Current password</mat-label>
|
||||
<input type="password" matInput formControlName="currentPassword" required>
|
||||
<mat-error *ngIf="form.get('currentPassword')?.hasError('required')">
|
||||
Current password is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>New password</mat-label>
|
||||
<input type="password" matInput formControlName="newPassword" required>
|
||||
<mat-error *ngIf="form.get('newPassword')?.hasError('required')">
|
||||
New password is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form.get('newPassword')?.hasError('minlength')">
|
||||
Password is too short
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Verify new password</mat-label>
|
||||
<input type="password" matInput formControlName="verifyNewPassword" required>
|
||||
<mat-error *ngIf="form.get('verifyNewPassword')?.hasError('required')">
|
||||
New password verification is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form.get('verifyNewPassword')?.hasError('invalid')">
|
||||
Password don't match
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<div class="row bottom">
|
||||
<button type="button" mat-raised-button [disabled]="form.disabled || form.untouched"
|
||||
(click)="reset();">Reset</button>
|
||||
<button type="button" mat-raised-button color="primary"
|
||||
[disabled]="form.disabled || form.pristine || !form.valid" (click)="save()">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
:host > form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mat-form-field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, ValidatorFn, ValidationErrors, Validators } from '@angular/forms';
|
||||
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
|
||||
import { RdioScannerAdminService } from '../../admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-password',
|
||||
styleUrls: ['./password.component.scss'],
|
||||
templateUrl: './password.component.html',
|
||||
})
|
||||
export class RdioScannerAdminPasswordComponent {
|
||||
form = this.ngFormBuilder.group({
|
||||
currentPassword: [null, Validators.required],
|
||||
newPassword: [null, [Validators.required, Validators.minLength(8)]],
|
||||
verifyNewPassword: [null, [Validators.required, this.validatePassword()]],
|
||||
});
|
||||
|
||||
constructor(
|
||||
private adminService: RdioScannerAdminService,
|
||||
private matSnackBar: MatSnackBar,
|
||||
private ngFormBuilder: FormBuilder,
|
||||
) { }
|
||||
|
||||
private validatePassword(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return typeof control.value === 'string' && control.value.length
|
||||
? control.value === control.parent?.value.newPassword
|
||||
? null : { invalid: true } : null;
|
||||
}
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.form.reset();
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
const config: MatSnackBarConfig = { duration: 5000 };
|
||||
|
||||
this.form.disable();
|
||||
|
||||
try {
|
||||
await this.adminService.changePassword(this.form.value.currentPassword, this.form.value.newPassword);
|
||||
|
||||
this.matSnackBar.open('Password changed successfully', '', config);
|
||||
|
||||
this.form.reset();
|
||||
} catch (_) {
|
||||
this.matSnackBar.open('Unable to change password', '', config);
|
||||
}
|
||||
|
||||
this.form.enable();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<mat-accordion displayMode="flat">
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>description</mat-icon>
|
||||
Talkgroups CSV
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-import-csv (config)="config.emit($event)"></rdio-scanner-admin-import-csv>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>password</mat-icon>
|
||||
Admin password
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-password></rdio-scanner-admin-password>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>sync_alt</mat-icon>
|
||||
Import/export config
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-import-export-config (config)="config.emit($event)"></rdio-scanner-admin-import-export-config>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Output, QueryList, ViewChildren } from '@angular/core';
|
||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||
import { Config } from '../admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-tools',
|
||||
templateUrl: './tools.component.html',
|
||||
})
|
||||
export class RdioScannerAdminToolsComponent {
|
||||
@Output() config = new EventEmitter<Config>();
|
||||
|
||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||
|
||||
closeAll(): void {
|
||||
this.panels?.forEach((panel) => panel.close());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,25 @@
|
|||
.rdio-button,
|
||||
.rdio-button-mini,
|
||||
.rdio-button-none {
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.rdio-button,
|
||||
.rdio-button-mini {
|
||||
background: rgb(45, 45, 45);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 1px 2px rgb(255, 255, 255) inset,
|
||||
-1px -1px 1px rgb(0, 0, 0) inset, 0 0 2px 2px rgb(0, 0, 0);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-bottom-color: rgba(0, 0, 0, 0.87);
|
||||
border-left-color: rgba(255, 255, 255, 0.7);
|
||||
border-right-color: rgba(0, 0, 0, 0.87);
|
||||
border-top-color: rgba(255, 255, 255, 0.7);
|
||||
box-sizing: border-box;
|
||||
color: rgb(255, 255, 255);
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
line-height: 18px;
|
||||
margin: 2px;
|
||||
overflow: hidden;
|
||||
padding: 2px 8px;
|
||||
position: relative;
|
||||
text-overflow: ellipsis;
|
||||
text-overflow: clip;
|
||||
text-shadow: 0 0 4px rgb(0, 0, 0);
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
white-space: normal;
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
|
|
@ -30,7 +27,7 @@
|
|||
}
|
||||
|
||||
&:active {
|
||||
top: 4px;
|
||||
top: 2px;
|
||||
transform: scale(0.98);
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
|
@ -39,45 +36,36 @@
|
|||
&.on::after,
|
||||
&.partial::after {
|
||||
content: "";
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
height: 6px;
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&.off::after {
|
||||
background: rgb(230, 50, 50);
|
||||
box-shadow: 1px 1px 1px rgb(255, 255, 255) inset,
|
||||
0 0 3px 2px rgb(230, 50, 50);
|
||||
background: #e63232;
|
||||
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset,
|
||||
0 0 3px 1px rgb(230, 50, 50);
|
||||
}
|
||||
|
||||
&.on::after {
|
||||
background: rgb(100, 230, 50);
|
||||
box-shadow: 1px 1px 1px rgb(255, 255, 255) inset,
|
||||
0 0 3px 2px rgb(100, 230, 50);
|
||||
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset,
|
||||
0 0 3px 1px rgb(100, 230, 50);
|
||||
}
|
||||
|
||||
&.partial::after {
|
||||
background: rgb(230, 165, 50);
|
||||
box-shadow: 1px 1px 1px rgb(255, 255, 255) inset,
|
||||
0 0 3px 2px rgb(230, 165, 50);
|
||||
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset,
|
||||
0 0 3px 1px rgb(230, 165, 50);
|
||||
}
|
||||
}
|
||||
|
||||
.rdio-button {
|
||||
color: rgb(250, 250, 250);
|
||||
font-size: 12px;
|
||||
min-width: 64px;
|
||||
min-width: 80px;
|
||||
text-transform: uppercase;
|
||||
width: calc(25% - 8px);
|
||||
}
|
||||
|
||||
.rdio-button-mini {
|
||||
font-size: 10px;
|
||||
min-width: 30px;
|
||||
text-transform: uppercase;
|
||||
width: calc(12.5% - 8px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
|
|||
|
|
@ -61,6 +61,12 @@
|
|||
<mat-form-field>
|
||||
<input #password matInput type="password" autocomplete="off" formControlName="password"
|
||||
placeholder="Unlock code" (blur)="authFocus()">
|
||||
<mat-error *ngIf="authForm.get('password')?.hasError('expired')">
|
||||
This unlock code has expired
|
||||
</mat-error>
|
||||
<mat-error *ngIf="authForm.get('password')?.hasError('tooMany')">
|
||||
Too many connections
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -106,9 +112,11 @@
|
|||
[ngClass]="{ off: livefeedOffline && !playbackMode, on: livefeedOnline, partial: playbackMode }">
|
||||
LIVE<br>FEED
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="rdio-button" (click)="holdSystem()" [ngClass]="{ off: !holdSys, on: holdSys }">
|
||||
HOLD<br>SYS
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="rdio-button" (click)="holdTalkgroup()" [ngClass]="{ off: !holdTg, on: holdTg }">
|
||||
HOLD<br>TG
|
||||
</button>
|
||||
|
|
@ -117,9 +125,11 @@
|
|||
<button class="rdio-button" (click)="replay()">
|
||||
REPLAY<br>LAST
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="rdio-button" (click)="skip()">
|
||||
SKIP<br>NEXT
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="rdio-button" (click)="avoid()">
|
||||
AVOID
|
||||
</button>
|
||||
|
|
@ -128,9 +138,11 @@
|
|||
<button class="rdio-button" (click)="showSearchPanel()">
|
||||
SEARCH<br>CALL
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="rdio-button" (click)="pause()" [ngClass]="{ off: !livefeedPaused, on: livefeedPaused }">
|
||||
PAUSE
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="rdio-button" (click)="showSelectPanel()">
|
||||
SELECT<br>TG
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -9,21 +9,26 @@
|
|||
}
|
||||
|
||||
.rdio-control {
|
||||
.rdio-button {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.rdio-button {
|
||||
flex: 1;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
display: block;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rdio-display {
|
||||
background: rgb(209, 238, 238);
|
||||
border-radius: 4px;
|
||||
box-shadow: 2px 2px 4px rgb(0, 0, 0) inset, 1px 1px 2px 1px rgb(255, 255, 255);
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
cursor: default;
|
||||
|
|
@ -55,7 +60,7 @@
|
|||
|
||||
* {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
|
@ -148,7 +153,6 @@
|
|||
.rdio-status {
|
||||
.led {
|
||||
background: rgb(80, 80, 80);
|
||||
border-radius: 2px;
|
||||
display: block;
|
||||
height: 12px;
|
||||
margin-left: auto;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { ChangeDetectorRef, Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
import { Subscription, timer } from 'rxjs';
|
||||
|
|
@ -31,19 +31,19 @@ import {
|
|||
RdioScannerLivefeedMap,
|
||||
RdioScannerLivefeedMode,
|
||||
} from '../rdio-scanner';
|
||||
import { AppRdioScannerService } from '../rdio-scanner.service';
|
||||
import { RdioScannerService } from '../rdio-scanner.service';
|
||||
|
||||
const LOCAL_STORAGE_KEY = AppRdioScannerService.LOCAL_STORAGE_KEY + '-pin';
|
||||
const LOCAL_STORAGE_KEY = RdioScannerService.LOCAL_STORAGE_KEY + '-pin';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rdio-scanner-main',
|
||||
selector: 'rdio-scanner-main',
|
||||
styleUrls: [
|
||||
'../common.scss',
|
||||
'./main.component.scss',
|
||||
],
|
||||
templateUrl: './main.component.html',
|
||||
})
|
||||
export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
||||
export class RdioScannerMainComponent implements OnDestroy, OnInit {
|
||||
auth = false;
|
||||
authForm = this.ngFormBuilder.group({ password: [] });
|
||||
|
||||
|
|
@ -98,10 +98,10 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
|||
|
||||
private dimmerTimer: Subscription | undefined;
|
||||
|
||||
private eventSubscription = this.appRdioScannerService.event.subscribe((event: RdioScannerEvent) => this.eventHandler(event));
|
||||
private eventSubscription = this.rdioScannerService.event.subscribe((event: RdioScannerEvent) => this.eventHandler(event));
|
||||
|
||||
constructor(
|
||||
private appRdioScannerService: AppRdioScannerService,
|
||||
private rdioScannerService: RdioScannerService,
|
||||
private ngChangeDetectorRef: ChangeDetectorRef,
|
||||
private ngFormBuilder: FormBuilder,
|
||||
) { }
|
||||
|
|
@ -109,7 +109,7 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
|||
authenticate(password = this.authForm.value.password): void {
|
||||
this.authForm.disable();
|
||||
|
||||
this.appRdioScannerService.authenticate(password);
|
||||
this.rdioScannerService.authenticate(password);
|
||||
}
|
||||
|
||||
authFocus(): void {
|
||||
|
|
@ -118,12 +118,7 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.a', ['$event'])
|
||||
avoid(options?: RdioScannerAvoidOptions, event?: KeyboardEvent): void {
|
||||
if (event && !this.config?.keyboardShortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
avoid(options?: RdioScannerAvoidOptions): void {
|
||||
if (this.auth) {
|
||||
this.authFocus();
|
||||
|
||||
|
|
@ -131,82 +126,67 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
|||
const call = this.call || this.callPrevious;
|
||||
|
||||
if (options || call) {
|
||||
this.appRdioScannerService.avoid(options);
|
||||
this.rdioScannerService.avoid(options);
|
||||
|
||||
if (call && !this.map[call.system][call.talkgroup]) {
|
||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||
|
||||
} else {
|
||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Deactivate);
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Deactivate);
|
||||
}
|
||||
|
||||
} else {
|
||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
||||
}
|
||||
|
||||
this.updateDimmer();
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.s', ['$event'])
|
||||
holdSystem(event?: KeyboardEvent): void {
|
||||
if (event && !this.config?.keyboardShortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
holdSystem(): void {
|
||||
if (this.auth) {
|
||||
this.authFocus();
|
||||
|
||||
} else {
|
||||
if (this.call || this.callPrevious) {
|
||||
this.appRdioScannerService.beep(this.holdSys ? RdioScannerBeepStyle.Deactivate : RdioScannerBeepStyle.Activate);
|
||||
this.rdioScannerService.beep(this.holdSys ? RdioScannerBeepStyle.Deactivate : RdioScannerBeepStyle.Activate);
|
||||
|
||||
this.appRdioScannerService.holdSystem();
|
||||
this.rdioScannerService.holdSystem();
|
||||
|
||||
} else {
|
||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
||||
}
|
||||
|
||||
this.updateDimmer();
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.t', ['$event'])
|
||||
holdTalkgroup(event?: KeyboardEvent): void {
|
||||
if (event && !this.config?.keyboardShortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
holdTalkgroup(): void {
|
||||
if (this.auth) {
|
||||
this.authFocus();
|
||||
|
||||
} else {
|
||||
if (this.call || this.callPrevious) {
|
||||
this.appRdioScannerService.beep(this.holdTg ? RdioScannerBeepStyle.Deactivate : RdioScannerBeepStyle.Activate);
|
||||
this.rdioScannerService.beep(this.holdTg ? RdioScannerBeepStyle.Deactivate : RdioScannerBeepStyle.Activate);
|
||||
|
||||
this.appRdioScannerService.holdTalkgroup();
|
||||
this.rdioScannerService.holdTalkgroup();
|
||||
|
||||
} else {
|
||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
||||
}
|
||||
|
||||
this.updateDimmer();
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.l', ['$event'])
|
||||
livefeed(event?: KeyboardEvent): void {
|
||||
if (event && !this.config?.keyboardShortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
livefeed(): void {
|
||||
if (this.auth) {
|
||||
this.authFocus();
|
||||
|
||||
} else {
|
||||
this.appRdioScannerService.beep(this.livefeedOffline ? RdioScannerBeepStyle.Activate : RdioScannerBeepStyle.Deactivate);
|
||||
this.rdioScannerService.beep(this.livefeedOffline ? RdioScannerBeepStyle.Activate : RdioScannerBeepStyle.Deactivate);
|
||||
|
||||
this.appRdioScannerService.livefeed();
|
||||
this.rdioScannerService.livefeed();
|
||||
|
||||
this.updateDimmer();
|
||||
}
|
||||
|
|
@ -222,49 +202,36 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
|||
this.syncClock();
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.space', ['$event'])
|
||||
@HostListener('document:keydown.p', ['$event'])
|
||||
pause(event?: KeyboardEvent): void {
|
||||
if (event && !this.config?.keyboardShortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
event?.preventDefault();
|
||||
|
||||
pause(): void {
|
||||
if (this.auth) {
|
||||
this.authFocus();
|
||||
|
||||
} else {
|
||||
if (this.livefeedPaused) {
|
||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Deactivate).then(() => this.appRdioScannerService.pause());
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Deactivate).then(() => this.rdioScannerService.pause());
|
||||
|
||||
} else {
|
||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||
|
||||
this.appRdioScannerService.pause();
|
||||
this.rdioScannerService.pause();
|
||||
}
|
||||
|
||||
this.updateDimmer();
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.r', ['$event'])
|
||||
replay(event?: KeyboardEvent): void {
|
||||
if (event && !this.config?.keyboardShortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
replay(): void {
|
||||
if (this.auth) {
|
||||
this.authFocus();
|
||||
|
||||
} else {
|
||||
if (!this.livefeedPaused && (this.call || this.callPrevious)) {
|
||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||
|
||||
this.appRdioScannerService.replay();
|
||||
this.rdioScannerService.replay();
|
||||
|
||||
} else {
|
||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
||||
}
|
||||
|
||||
this.updateDimmer();
|
||||
|
|
@ -280,7 +247,7 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
|||
this.authFocus();
|
||||
|
||||
} else {
|
||||
this.appRdioScannerService.beep();
|
||||
this.rdioScannerService.beep();
|
||||
|
||||
this.openSearchPanel.emit();
|
||||
}
|
||||
|
|
@ -295,32 +262,27 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
|||
this.authFocus();
|
||||
|
||||
} else {
|
||||
this.appRdioScannerService.beep();
|
||||
this.rdioScannerService.beep();
|
||||
|
||||
this.openSelectPanel.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.n', ['$event'])
|
||||
skip(options?: { delay?: boolean }, event?: KeyboardEvent): void {
|
||||
if (event && !this.config?.keyboardShortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
skip(options?: { delay?: boolean }): void {
|
||||
if (this.auth) {
|
||||
this.authFocus();
|
||||
|
||||
} else {
|
||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||
|
||||
this.appRdioScannerService.skip(options);
|
||||
this.rdioScannerService.skip(options);
|
||||
|
||||
this.updateDimmer();
|
||||
}
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.appRdioScannerService.stop();
|
||||
this.rdioScannerService.stop();
|
||||
}
|
||||
|
||||
private eventHandler(event: RdioScannerEvent): void {
|
||||
|
|
@ -338,7 +300,7 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
|||
if (password) {
|
||||
this.authForm.get('password')?.setValue(password);
|
||||
|
||||
this.appRdioScannerService.authenticate(password);
|
||||
this.rdioScannerService.authenticate(password);
|
||||
|
||||
} else {
|
||||
this.auth = event.auth;
|
||||
|
|
@ -385,6 +347,10 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
if ('expired' in event && event.expired === true) {
|
||||
this.authForm.get('password')?.setErrors({ expired: true });
|
||||
}
|
||||
|
||||
if ('holdSys' in event) {
|
||||
this.holdSys = event.holdSys || false;
|
||||
}
|
||||
|
|
@ -423,6 +389,10 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
|||
this.updateDimmer();
|
||||
}
|
||||
|
||||
if ('tooMany' in event && event.tooMany === true) {
|
||||
this.authForm.get('password')?.setErrors({ tooMany: true });
|
||||
}
|
||||
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
</mat-toolbar>
|
||||
<div #scrollableSearch>
|
||||
<div>
|
||||
<app-rdio-scanner-search #searchComponent></app-rdio-scanner-search>
|
||||
<rdio-scanner-search #searchComponent></rdio-scanner-search>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -26,16 +26,16 @@
|
|||
</mat-toolbar>
|
||||
<div #scrollableSelect>
|
||||
<div>
|
||||
<app-rdio-scanner-select></app-rdio-scanner-select>
|
||||
<rdio-scanner-select></rdio-scanner-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content>
|
||||
<app-rdio-scanner-main
|
||||
<rdio-scanner-main
|
||||
(openSearchPanel)="scrollableSearch.scrollTo(0, 0); searchPanel.open(); searchComponent.searchCalls()"
|
||||
(openSelectPanel)="scrollableSelect.scrollTo(0, 0); selectPanel.open();"
|
||||
(toggleFullscreen)="toggleFullscreen()">
|
||||
</app-rdio-scanner-main>
|
||||
</rdio-scanner-main>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
|
|
@ -45,7 +45,7 @@ $background-color: rgb(30, 30, 30);
|
|||
|
||||
.mat-toolbar {
|
||||
background-color: inherit;
|
||||
box-shadow: 0 8px 20px rgba($background-color, 0.9);
|
||||
box-shadow: 0 4px 8px rgba($background-color, 0.8);
|
||||
color: currentColor;
|
||||
z-index: 2;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -20,17 +20,17 @@
|
|||
import { Component, ElementRef, HostListener, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { MatSidenav } from '@angular/material/sidenav';
|
||||
import { RdioScannerConfig, RdioScannerEvent, RdioScannerLivefeedMode } from './rdio-scanner';
|
||||
import { AppRdioScannerService } from './rdio-scanner.service';
|
||||
import { RdioScannerService } from './rdio-scanner.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rdio-scanner',
|
||||
selector: 'rdio-scanner',
|
||||
styleUrls: ['./rdio-scanner.component.scss'],
|
||||
templateUrl: './rdio-scanner.component.html',
|
||||
})
|
||||
export class AppRdioScannerComponent implements OnDestroy {
|
||||
export class RdioScannerComponent implements OnDestroy {
|
||||
private config: RdioScannerConfig | undefined;
|
||||
|
||||
private eventSubscription = this.appRdioScannerService.event.subscribe((event: RdioScannerEvent) => this.eventHandler(event));
|
||||
private eventSubscription = this.rdioScannerService.event.subscribe((event: RdioScannerEvent) => this.eventHandler(event));
|
||||
|
||||
private livefeedMode: RdioScannerLivefeedMode = RdioScannerLivefeedMode.Offline;
|
||||
|
||||
|
|
@ -39,8 +39,8 @@ export class AppRdioScannerComponent implements OnDestroy {
|
|||
@ViewChild('selectPanel') private selectPanel: MatSidenav | undefined;
|
||||
|
||||
constructor(
|
||||
private appRdioScannerService: AppRdioScannerService,
|
||||
private ngElementRef: ElementRef,
|
||||
private rdioScannerService: RdioScannerService,
|
||||
) { }
|
||||
|
||||
@HostListener('window:beforeunload', ['$event'])
|
||||
|
|
@ -53,11 +53,11 @@ export class AppRdioScannerComponent implements OnDestroy {
|
|||
}
|
||||
|
||||
start(): void {
|
||||
this.appRdioScannerService.startLivefeed();
|
||||
this.rdioScannerService.startLivefeed();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.appRdioScannerService.stopLivefeed();
|
||||
this.rdioScannerService.stopLivefeed();
|
||||
|
||||
this.searchPanel?.close();
|
||||
this.selectPanel?.close();
|
||||
|
|
@ -67,15 +67,7 @@ export class AppRdioScannerComponent implements OnDestroy {
|
|||
this.eventSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.f', ['$event'])
|
||||
@HostListener('document:keydown.tab', ['$event'])
|
||||
toggleFullscreen(event?: KeyboardEvent): void {
|
||||
if (event && !this.config?.keyboardShortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
event?.preventDefault();
|
||||
|
||||
toggleFullscreen(): void {
|
||||
if (document.fullscreenElement) {
|
||||
const el: {
|
||||
exitFullscreen?: () => void;
|
||||
|
|
@ -124,36 +116,4 @@ export class AppRdioScannerComponent implements OnDestroy {
|
|||
this.livefeedMode = event.livefeedMode;
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.arrowleft', ['$event'])
|
||||
private keyLeftArrow(event?: KeyboardEvent): void {
|
||||
if (event && !this.config?.keyboardShortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
event?.preventDefault();
|
||||
|
||||
if (this.selectPanel?.opened) {
|
||||
this.selectPanel?.close();
|
||||
|
||||
} else {
|
||||
this.searchPanel?.open();
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.arrowright', ['$event'])
|
||||
private keyRightArrow(event?: KeyboardEvent): void {
|
||||
if (event && !this.config?.keyboardShortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
event?.preventDefault();
|
||||
|
||||
if (this.searchPanel?.opened) {
|
||||
this.searchPanel?.close();
|
||||
|
||||
} else {
|
||||
this.selectPanel?.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -20,24 +20,24 @@
|
|||
import { FullscreenOverlayContainer, OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { AppSharedModule } from '../../shared/shared.module';
|
||||
import { AppRdioScannerComponent } from './rdio-scanner.component';
|
||||
import { AppRdioScannerService } from './rdio-scanner.service';
|
||||
import { AppRdioScannerMainComponent } from './main/main.component';
|
||||
import { AppRdioScannerSearchComponent } from './search/search.component';
|
||||
import { AppRdioScannerSelectComponent } from './select/select.component';
|
||||
import { RdioScannerComponent } from './rdio-scanner.component';
|
||||
import { RdioScannerService } from './rdio-scanner.service';
|
||||
import { RdioScannerMainComponent } from './main/main.component';
|
||||
import { RdioScannerSearchComponent } from './search/search.component';
|
||||
import { RdioScannerSelectComponent } from './select/select.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppRdioScannerComponent,
|
||||
AppRdioScannerMainComponent,
|
||||
AppRdioScannerSearchComponent,
|
||||
AppRdioScannerSelectComponent,
|
||||
RdioScannerComponent,
|
||||
RdioScannerMainComponent,
|
||||
RdioScannerSearchComponent,
|
||||
RdioScannerSelectComponent,
|
||||
],
|
||||
exports: [AppRdioScannerComponent],
|
||||
exports: [RdioScannerComponent],
|
||||
imports: [AppSharedModule],
|
||||
providers: [
|
||||
AppRdioScannerService,
|
||||
RdioScannerService,
|
||||
{ provide: OverlayContainer, useClass: FullscreenOverlayContainer },
|
||||
],
|
||||
})
|
||||
export class AppRdioScannerModule { }
|
||||
export class RdioScannerModule { }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -49,13 +49,16 @@ enum WebsocketCallFlag {
|
|||
enum WebsocketCommand {
|
||||
Call = 'CAL',
|
||||
Config = 'CFG',
|
||||
Expired = 'XPR',
|
||||
ListCall = 'LCL',
|
||||
LivefeedMap = 'LFM',
|
||||
Max = 'MAX',
|
||||
Pin = 'PIN',
|
||||
Ver = 'VER',
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AppRdioScannerService implements OnDestroy {
|
||||
export class RdioScannerService implements OnDestroy {
|
||||
static LOCAL_STORAGE_KEY = 'rdio-scanner';
|
||||
|
||||
event = new EventEmitter<RdioScannerEvent>();
|
||||
|
|
@ -74,7 +77,6 @@ export class AppRdioScannerService implements OnDestroy {
|
|||
private config: RdioScannerConfig = {
|
||||
dimmerDelay: false,
|
||||
groups: {},
|
||||
keyboardShortcuts: true,
|
||||
keypadBeeps: false,
|
||||
systems: [],
|
||||
tags: {},
|
||||
|
|
@ -331,10 +333,18 @@ export class AppRdioScannerService implements OnDestroy {
|
|||
}
|
||||
|
||||
loadAndDownload(id: string): void {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getCall(id, WebsocketCallFlag.Download);
|
||||
}
|
||||
|
||||
loadAndPlay(id: string): void {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.skipDelay) {
|
||||
this.skipDelay.unsubscribe();
|
||||
|
||||
|
|
@ -765,16 +775,18 @@ export class AppRdioScannerService implements OnDestroy {
|
|||
if (Array.isArray(message)) {
|
||||
switch (message[0]) {
|
||||
case WebsocketCommand.Call:
|
||||
if (message[2] === WebsocketCallFlag.Download) {
|
||||
this.download(message[1]);
|
||||
if (message[1] !== null) {
|
||||
if (message[2] === WebsocketCallFlag.Download) {
|
||||
this.download(message[1]);
|
||||
|
||||
} else if (message[2] === WebsocketCallFlag.Play && message[1]?.id === this.playbackPending) {
|
||||
this.playbackPending = undefined;
|
||||
} else if (message[2] === WebsocketCallFlag.Play && message[1]?.id === this.playbackPending) {
|
||||
this.playbackPending = undefined;
|
||||
|
||||
this.queue(this.transformCall(message[1]), { priority: true });
|
||||
this.queue(this.transformCall(message[1]), { priority: true });
|
||||
|
||||
} else {
|
||||
this.queue(this.transformCall(message[1]));
|
||||
} else {
|
||||
this.queue(this.transformCall(message[1]));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -785,7 +797,6 @@ export class AppRdioScannerService implements OnDestroy {
|
|||
this.config = {
|
||||
dimmerDelay: typeof config.dimmerDelay === 'number' ? config.dimmerDelay : 5000,
|
||||
groups: typeof config.groups !== null && typeof config.groups === 'object' ? config.groups : {},
|
||||
keyboardShortcuts: config.keyboardShortcuts,
|
||||
keypadBeeps: config.keypadBeeps !== null && typeof config.keypadBeeps === 'object' ? config.keypadBeeps : {},
|
||||
systems: Array.isArray(config.systems) ? config.systems.slice() : [],
|
||||
tags: typeof config.tags !== null && typeof config.tags === 'object' ? config.tags : {},
|
||||
|
|
@ -808,6 +819,11 @@ export class AppRdioScannerService implements OnDestroy {
|
|||
|
||||
break;
|
||||
|
||||
case WebsocketCommand.Expired:
|
||||
this.event.emit({ auth: true, expired: true });
|
||||
|
||||
break;
|
||||
|
||||
case WebsocketCommand.ListCall:
|
||||
this.playbackList = message[1];
|
||||
|
||||
|
|
@ -823,6 +839,11 @@ export class AppRdioScannerService implements OnDestroy {
|
|||
|
||||
break;
|
||||
|
||||
case WebsocketCommand.Max:
|
||||
this.event.emit({ auth: true, tooMany: true });
|
||||
|
||||
break;
|
||||
|
||||
case WebsocketCommand.Pin:
|
||||
this.event.emit({ auth: true });
|
||||
|
||||
|
|
@ -893,7 +914,7 @@ export class AppRdioScannerService implements OnDestroy {
|
|||
}
|
||||
|
||||
private rebuildLivefeedMap(): void {
|
||||
this.livefeedMap = this.config.systems.reduce((sysMap, sys) => {
|
||||
const livefeedMap = this.config.systems.reduce((sysMap, sys) => {
|
||||
const tgs = sys.talkgroups.map((tg) => tg.id.toString());
|
||||
|
||||
sysMap[sys.id] = sys.talkgroups.reduce((tgMap, tg) => {
|
||||
|
|
@ -907,6 +928,14 @@ export class AppRdioScannerService implements OnDestroy {
|
|||
return sysMap;
|
||||
}, {} as RdioScannerLivefeedMap);
|
||||
|
||||
if (this.livefeedMapPriorToHoldSystem != null) {
|
||||
this.livefeedMapPriorToHoldSystem = livefeedMap;
|
||||
} else if (this.livefeedMapPriorToHoldTalkgroup != null) {
|
||||
this.livefeedMapPriorToHoldTalkgroup = livefeedMap;
|
||||
} else {
|
||||
this.livefeedMap = livefeedMap;
|
||||
}
|
||||
|
||||
this.storeLivefeedMap();
|
||||
|
||||
this.rebuildGroups();
|
||||
|
|
@ -919,7 +948,7 @@ export class AppRdioScannerService implements OnDestroy {
|
|||
}
|
||||
|
||||
private restoreLivefeed(): void {
|
||||
const map = window?.localStorage?.getItem(AppRdioScannerService.LOCAL_STORAGE_KEY);
|
||||
const map = window?.localStorage?.getItem(RdioScannerService.LOCAL_STORAGE_KEY);
|
||||
|
||||
if (map) {
|
||||
try {
|
||||
|
|
@ -948,11 +977,11 @@ export class AppRdioScannerService implements OnDestroy {
|
|||
}
|
||||
|
||||
private storeLivefeedMap(): void {
|
||||
window?.localStorage?.setItem(AppRdioScannerService.LOCAL_STORAGE_KEY, JSON.stringify(this.livefeedMap));
|
||||
window?.localStorage?.setItem(RdioScannerService.LOCAL_STORAGE_KEY, JSON.stringify(this.livefeedMap));
|
||||
}
|
||||
|
||||
private transformCall(call: RdioScannerCall): RdioScannerCall {
|
||||
if (Array.isArray(this.config?.systems)) {
|
||||
if (call && Array.isArray(this.config?.systems)) {
|
||||
call.systemData = this.config.systems.find((system) => system.id === call.system);
|
||||
|
||||
if (Array.isArray(call.systemData?.talkgroups)) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -73,7 +73,6 @@ export interface RdioScannerCallSource {
|
|||
export interface RdioScannerConfig {
|
||||
dimmerDelay: number | false;
|
||||
groups: { [key: string]: { [key: number]: number[] } };
|
||||
keyboardShortcuts: boolean;
|
||||
keypadBeeps: RdioScannerKeypadBeeps | false;
|
||||
systems: RdioScannerSystem[];
|
||||
tags: { [key: string]: { [key: number]: number[] } };
|
||||
|
|
@ -83,6 +82,7 @@ export interface RdioScannerEvent {
|
|||
auth?: boolean;
|
||||
config?: RdioScannerConfig;
|
||||
call?: RdioScannerCall;
|
||||
expired?: boolean;
|
||||
groups?: RdioScannerGroup[];
|
||||
holdSys?: boolean;
|
||||
holdTg?: boolean;
|
||||
|
|
@ -94,6 +94,7 @@ export interface RdioScannerEvent {
|
|||
playbackPending?: string;
|
||||
queue?: number;
|
||||
time?: number;
|
||||
tooMany?: boolean;
|
||||
}
|
||||
|
||||
export enum RdioScannerGroupStatus {
|
||||
|
|
@ -150,7 +151,7 @@ export interface RdioScannerSystem {
|
|||
led?: 'blue' | 'cyan' | 'green' | 'magenta' | 'red' | 'white' | 'yellow';
|
||||
order?: number;
|
||||
talkgroups: RdioScannerTalkgroup[];
|
||||
units?: RdioScannerUnit[];
|
||||
units: RdioScannerUnit[];
|
||||
}
|
||||
|
||||
export interface RdioScannerTalkgroup {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -31,14 +31,14 @@ import {
|
|||
RdioScannerSystem,
|
||||
RdioScannerTalkgroup,
|
||||
} from '../rdio-scanner';
|
||||
import { AppRdioScannerService } from '../rdio-scanner.service';
|
||||
import { RdioScannerService } from '../rdio-scanner.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rdio-scanner-search',
|
||||
selector: 'rdio-scanner-search',
|
||||
styleUrls: ['./search.component.scss'],
|
||||
templateUrl: './search.component.html',
|
||||
})
|
||||
export class AppRdioScannerSearchComponent implements OnDestroy {
|
||||
export class RdioScannerSearchComponent implements OnDestroy {
|
||||
call: RdioScannerCall | undefined;
|
||||
callPending: string | undefined;
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
|||
|
||||
private config: RdioScannerConfig | undefined;
|
||||
|
||||
private eventSubscription = this.appRdioScannerService.event.subscribe((event: RdioScannerEvent) => this.eventHandler(event));
|
||||
private eventSubscription = this.rdioScannerService.event.subscribe((event: RdioScannerEvent) => this.eventHandler(event));
|
||||
|
||||
private limit = 200;
|
||||
|
||||
|
|
@ -77,18 +77,18 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
|||
@ViewChild(MatPaginator, { read: MatPaginator }) private paginator: MatPaginator | undefined;
|
||||
|
||||
constructor(
|
||||
private appRdioScannerService: AppRdioScannerService,
|
||||
private rdioScannerService: RdioScannerService,
|
||||
private ngChangeDetectorRef: ChangeDetectorRef,
|
||||
private ngFormBuilder: FormBuilder,
|
||||
) { }
|
||||
|
||||
download(id: string): void {
|
||||
this.appRdioScannerService.loadAndDownload(id);
|
||||
this.rdioScannerService.loadAndDownload(id);
|
||||
}
|
||||
|
||||
formChangeHandler(): void {
|
||||
if (this.livefeedPlayback) {
|
||||
this.appRdioScannerService.stopPlaybackMode();
|
||||
this.rdioScannerService.stopPlaybackMode();
|
||||
}
|
||||
|
||||
this.paginator?.firstPage();
|
||||
|
|
@ -127,16 +127,14 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
|||
.filter((tag) => this.config?.systems
|
||||
.some((system) => system.talkgroups
|
||||
.some((talkgroup) => talkgroup.group === selectedGroup && talkgroup.tag === tag)))
|
||||
.sort()
|
||||
: selectedTalkgroup
|
||||
? [selectedTalkgroup.tag]
|
||||
: selectedSystem
|
||||
? Object.keys(this.config.tags)
|
||||
.filter((tag) => selectedSystem.talkgroups
|
||||
.some((talkgroup) => talkgroup.tag === tag))
|
||||
.sort()
|
||||
: Object.keys(this.config.tags)
|
||||
.sort();
|
||||
this.optionsTag.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.optionsTalkgroup = selectedGroup
|
||||
? selectedSystem
|
||||
|
|
@ -176,18 +174,16 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
|||
.filter((group) => Object.keys(this.config?.groups[group] || {})
|
||||
.map((system) => +system)
|
||||
.includes(selectedSystem.id))
|
||||
.sort()
|
||||
: Object.keys(this.config.groups)
|
||||
.sort();
|
||||
this.optionsGroup.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.optionsTag = selectedSystem
|
||||
? Object.keys(this.config.tags)
|
||||
.filter((tag) => Object.keys(this.config?.tags[tag] || {})
|
||||
.map((system) => +system)
|
||||
.includes(selectedSystem.id))
|
||||
.sort()
|
||||
: Object.keys(this.config.tags)
|
||||
.sort();
|
||||
this.optionsTag.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.optionsTalkgroup = selectedSystem
|
||||
? selectedSystem.talkgroups
|
||||
|
|
@ -223,16 +219,14 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
|||
.filter((group) => this.config?.systems
|
||||
.some((system) => system.talkgroups
|
||||
.some((talkgroup) => talkgroup.group === group && talkgroup.tag === selectedTag)))
|
||||
.sort()
|
||||
: selectedTalkgroup
|
||||
? [selectedTalkgroup.group]
|
||||
: selectedSystem
|
||||
? Object.keys(this.config.groups)
|
||||
.filter((group) => selectedSystem.talkgroups
|
||||
.some((talkgroup) => talkgroup.group === group))
|
||||
.sort()
|
||||
: Object.keys(this.config.groups)
|
||||
.sort();
|
||||
this.optionsGroup.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.optionsSystem = selectedTag
|
||||
? this.config.systems
|
||||
|
|
@ -283,26 +277,22 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
|||
this.optionsGroup = selectedTalkgroup
|
||||
? Object.keys(this.config.groups)
|
||||
.filter((group) => group === selectedTalkgroup.group)
|
||||
.sort()
|
||||
: selectedSystem
|
||||
? Object.keys(this.config.groups)
|
||||
.filter((group) => selectedSystem.talkgroups
|
||||
.some((talkgroup) => talkgroup.group === group))
|
||||
.sort()
|
||||
: Object.keys(this.config.groups)
|
||||
.sort();
|
||||
this.optionsGroup.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.optionsTag = selectedTalkgroup
|
||||
? Object.keys(this.config.tags)
|
||||
.filter((tag) => tag === selectedTalkgroup.tag)
|
||||
.sort()
|
||||
: selectedSystem
|
||||
? Object.keys(this.config.tags)
|
||||
.filter((tag) => selectedSystem.talkgroups
|
||||
.some((talkgroup) => talkgroup.tag === tag))
|
||||
.sort()
|
||||
: Object.keys(this.config.tags)
|
||||
.sort();
|
||||
this.optionsTag.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.form.patchValue({
|
||||
group: selectedGroup ? this.optionsGroup.findIndex((group) => group === selectedGroup) : -1,
|
||||
|
|
@ -317,7 +307,7 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
|||
}
|
||||
|
||||
play(id: string): void {
|
||||
this.appRdioScannerService.loadAndPlay(id);
|
||||
this.rdioScannerService.loadAndPlay(id);
|
||||
}
|
||||
|
||||
refreshResults(): void {
|
||||
|
|
@ -353,11 +343,6 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
|||
talkgroup: -1,
|
||||
});
|
||||
|
||||
this.optionsGroup = Object.keys(this.config?.groups || []);
|
||||
this.optionsSystem = (this.config?.systems || []).map((system) => system.label);
|
||||
this.optionsTag = Object.keys(this.config?.tags || []);
|
||||
this.optionsTalkgroup = [];
|
||||
|
||||
this.paginator?.firstPage();
|
||||
|
||||
this.formChangeHandler();
|
||||
|
|
@ -420,15 +405,15 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
|||
|
||||
this.form.disable();
|
||||
|
||||
this.appRdioScannerService.searchCalls(options);
|
||||
this.rdioScannerService.searchCalls(options);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.livefeedPlayback) {
|
||||
this.appRdioScannerService.stopPlaybackMode();
|
||||
this.rdioScannerService.stopPlaybackMode();
|
||||
|
||||
} else {
|
||||
this.appRdioScannerService.stop();
|
||||
this.rdioScannerService.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -455,7 +440,12 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
|||
if ('config' in event) {
|
||||
this.config = event.config;
|
||||
|
||||
this.resetForm();
|
||||
this.callPending = undefined;
|
||||
|
||||
this.optionsGroup = Object.keys(this.config?.groups || []).sort((a, b) => a.localeCompare(b));
|
||||
this.optionsSystem = (this.config?.systems || []).map((system) => system.label);
|
||||
this.optionsTag = Object.keys(this.config?.tags || []).sort((a, b) => a.localeCompare(b));
|
||||
this.optionsTalkgroup = [];
|
||||
}
|
||||
|
||||
if ('livefeedMode' in event) {
|
||||
|
|
|
|||
|
|
@ -12,23 +12,28 @@
|
|||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset *ngFor="let system of systems" class="fieldset">
|
||||
<legend>
|
||||
{{ system.label }}
|
||||
</legend>
|
||||
<div>
|
||||
<button *ngFor="let talkgroup of system.talkgroups" class="rdio-button"
|
||||
[ngClass]="{ off: !(map[system.id] && map[system.id][talkgroup.id]), on: map[system.id] && map[system.id][talkgroup.id] }"
|
||||
(click)="avoid({ system: system, talkgroup: talkgroup })">
|
||||
{{ talkgroup.label }}
|
||||
</button>
|
||||
<ng-container *ngIf="system.talkgroups.length > 1">
|
||||
<button class="rdio-button-mini all-off" (click)="avoid({ system: system, status: false })">
|
||||
OFF
|
||||
<ng-container *ngFor="let system of systems">
|
||||
<fieldset *ngIf="system.talkgroups.length" class="fieldset">
|
||||
<legend>
|
||||
{{ system.label }}
|
||||
</legend>
|
||||
<div>
|
||||
<button *ngFor="let talkgroup of system.talkgroups" class="rdio-button"
|
||||
[ngClass]="{ off: !(map[system.id] && map[system.id][talkgroup.id]), on: map[system.id] && map[system.id][talkgroup.id] }"
|
||||
(click)="avoid({ system: system, talkgroup: talkgroup })">
|
||||
{{ talkgroup.label }}
|
||||
</button>
|
||||
<button class="rdio-button-mini all-on" (click)="avoid({ system: system, status: true })">
|
||||
ON
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</fieldset>
|
||||
<ng-container *ngIf="system.talkgroups.length > 1">
|
||||
<button class="rdio-button-mini all-off" (click)="avoid({ system: system, status: false })">
|
||||
OFF
|
||||
</button>
|
||||
<button class="rdio-button-mini all-on" (click)="avoid({ system: system, status: true })">
|
||||
ON
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</fieldset>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="systems?.length === 0">
|
||||
No talkgroup configured!
|
||||
</ng-container>
|
||||
|
|
@ -39,25 +39,34 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
}
|
||||
|
||||
.rdio-button {
|
||||
width: calc(25% - 4px);
|
||||
}
|
||||
|
||||
.rdio-button-mini {
|
||||
width: calc(12.5% - 4px);
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.rdio-button {
|
||||
width: calc(20% - 8px);
|
||||
width: calc(20% - 4px);
|
||||
}
|
||||
|
||||
.rdio-button-mini {
|
||||
width: calc(10% - 8px);
|
||||
width: calc(10% - 4px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.rdio-button {
|
||||
width: calc(10% - 8px);
|
||||
width: calc(10% - 4px);
|
||||
}
|
||||
|
||||
.rdio-button-mini {
|
||||
width: calc(5% - 8px);
|
||||
width: calc(5% - 4px);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -19,31 +19,31 @@
|
|||
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { RdioScannerAvoidOptions, RdioScannerBeepStyle, RdioScannerEvent, RdioScannerGroup, RdioScannerLivefeedMap, RdioScannerSystem } from '../rdio-scanner';
|
||||
import { AppRdioScannerService } from '../rdio-scanner.service';
|
||||
import { RdioScannerService } from '../rdio-scanner.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rdio-scanner-select',
|
||||
selector: 'rdio-scanner-select',
|
||||
styleUrls: [
|
||||
'../common.scss',
|
||||
'./select.component.scss',
|
||||
],
|
||||
templateUrl: './select.component.html',
|
||||
})
|
||||
export class AppRdioScannerSelectComponent implements OnDestroy {
|
||||
export class RdioScannerSelectComponent implements OnDestroy {
|
||||
groups: RdioScannerGroup[] | undefined;
|
||||
|
||||
map: RdioScannerLivefeedMap = {};
|
||||
|
||||
systems: RdioScannerSystem[] | undefined;
|
||||
|
||||
private eventSubscription = this.appRdioScannerService.event.subscribe((event: RdioScannerEvent) => this.eventHandler(event));
|
||||
private eventSubscription = this.rdioScannerService.event.subscribe((event: RdioScannerEvent) => this.eventHandler(event));
|
||||
|
||||
constructor(private appRdioScannerService: AppRdioScannerService) { }
|
||||
constructor(private rdioScannerService: RdioScannerService) { }
|
||||
|
||||
avoid(options?: RdioScannerAvoidOptions): void {
|
||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||
|
||||
this.appRdioScannerService.avoid(options);
|
||||
this.rdioScannerService.avoid(options);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
|
@ -51,14 +51,14 @@ export class AppRdioScannerSelectComponent implements OnDestroy {
|
|||
}
|
||||
|
||||
toggleGroup(label: string): void {
|
||||
this.appRdioScannerService.beep();
|
||||
this.rdioScannerService.beep();
|
||||
|
||||
this.appRdioScannerService.toggleGroup(label);
|
||||
this.rdioScannerService.toggleGroup(label);
|
||||
}
|
||||
|
||||
private eventHandler(event: RdioScannerEvent): void {
|
||||
if (event.config) {
|
||||
this.systems = this.sortSystems(event.config.systems);
|
||||
this.systems = event.config.systems;
|
||||
}
|
||||
|
||||
if (event.groups) {
|
||||
|
|
@ -69,21 +69,4 @@ export class AppRdioScannerSelectComponent implements OnDestroy {
|
|||
this.map = event.map;
|
||||
}
|
||||
}
|
||||
|
||||
private sortSystems(systems: RdioScannerSystem[]): RdioScannerSystem[] {
|
||||
return systems.sort((sysA, sysB) => {
|
||||
if (typeof sysA.order === 'number' && typeof sysB.order !== 'number') {
|
||||
return -1;
|
||||
|
||||
} else if (typeof sysA.order !== 'number' && typeof sysB.order === 'number') {
|
||||
return 1;
|
||||
|
||||
} else if (typeof sysA.order === 'number' && typeof sysB.order === 'number') {
|
||||
return sysA.order - sysB.order;
|
||||
|
||||
} else {
|
||||
return sysA.id - sysB.id;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<h1 class="mat-display-2">Rdio Scanner server administration</h1>
|
||||
<div>
|
||||
<mat-card>
|
||||
<rdio-scanner-admin></rdio-scanner-admin>
|
||||
</mat-card>
|
||||
</div>
|
||||
35
client/src/app/pages/rdio-scanner/admin/admin.component.scss
Normal file
35
client/src/app/pages/rdio-scanner/admin/admin.component.scss
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
:host {
|
||||
background: #1e1e1e;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
padding: 16px;
|
||||
|
||||
> div {
|
||||
margin: auto;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
> h1 {
|
||||
color: #2d2d2d;
|
||||
letter-spacing: 0.01rem;
|
||||
margin: 0;
|
||||
padding: 16px 0;
|
||||
text-align: center;
|
||||
text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.83);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 719px) {
|
||||
:host > h1 {
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.mat-card {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
}
|
||||
27
client/src/app/pages/rdio-scanner/admin/admin.component.ts
Normal file
27
client/src/app/pages/rdio-scanner/admin/admin.component.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-page',
|
||||
styleUrls: ['./admin.component.scss'],
|
||||
templateUrl: './admin.component.html',
|
||||
})
|
||||
export class RdioScannerAdminPageComponent { }
|
||||
34
client/src/app/pages/rdio-scanner/admin/admin.module.ts
Normal file
34
client/src/app/pages/rdio-scanner/admin/admin.module.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RdioScannerAdminModule } from '../../../components/rdio-scanner/admin';
|
||||
import { AppSharedModule } from '../../../shared/shared.module';
|
||||
import { RdioScannerAdminPageComponent } from './admin.component';
|
||||
import { routes } from './admin.routes';
|
||||
|
||||
@NgModule({
|
||||
declarations: [RdioScannerAdminPageComponent],
|
||||
exports: [RdioScannerAdminPageComponent],
|
||||
imports: [
|
||||
RdioScannerAdminModule,
|
||||
AppSharedModule.forChild({routerRoutes: routes }),
|
||||
],
|
||||
})
|
||||
export class RdioScannerAdminPageModule { }
|
||||
28
client/src/app/pages/rdio-scanner/admin/admin.routes.ts
Normal file
28
client/src/app/pages/rdio-scanner/admin/admin.routes.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Routes } from '@angular/router';
|
||||
import { RdioScannerAdminPageComponent } from './admin.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: RdioScannerAdminPageComponent,
|
||||
},
|
||||
];
|
||||
22
client/src/app/pages/rdio-scanner/index.ts
Normal file
22
client/src/app/pages/rdio-scanner/index.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
export * from './rdio-scanner.component';
|
||||
export * from './rdio-scanner.module';
|
||||
export * from './rdio-scanner.routes';
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -17,17 +17,10 @@
|
|||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
const callFactory = require('./call');
|
||||
const systemFactory = require('./system');
|
||||
|
||||
class Models {
|
||||
constructor(ctx = {}) {
|
||||
this.call = callFactory(ctx);
|
||||
|
||||
this.system = systemFactory(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Models;
|
||||
@Component({
|
||||
selector: 'rdio-scanner-main-page',
|
||||
template: '<rdio-scanner></rdio-scanner>',
|
||||
})
|
||||
export class RdioScannerMainPageComponent { }
|
||||
26
client/src/app/pages/rdio-scanner/rdio-scanner.component.ts
Normal file
26
client/src/app/pages/rdio-scanner/rdio-scanner.component.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-page',
|
||||
template: '<router-outlet></router-outlet>',
|
||||
})
|
||||
export class RdioScannerPageComponent { }
|
||||
38
client/src/app/pages/rdio-scanner/rdio-scanner.module.ts
Normal file
38
client/src/app/pages/rdio-scanner/rdio-scanner.module.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RdioScannerModule } from '../../components/rdio-scanner';
|
||||
import { AppSharedModule } from '../../shared/shared.module';
|
||||
import { RdioScannerPageComponent } from './rdio-scanner.component';
|
||||
import { RdioScannerMainPageComponent } from './rdio-scanner-main.component';
|
||||
import { routes } from './rdio-scanner.routes';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
RdioScannerPageComponent,
|
||||
RdioScannerMainPageComponent,
|
||||
],
|
||||
exports: [RdioScannerPageComponent],
|
||||
imports: [
|
||||
RdioScannerModule,
|
||||
AppSharedModule.forChild({routerRoutes: routes }),
|
||||
],
|
||||
})
|
||||
export class RdioScannerPageModule { }
|
||||
39
client/src/app/pages/rdio-scanner/rdio-scanner.routes.ts
Normal file
39
client/src/app/pages/rdio-scanner/rdio-scanner.routes.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Routes } from '@angular/router';
|
||||
import { RdioScannerMainPageComponent } from './rdio-scanner-main.component';
|
||||
import { RdioScannerPageComponent } from './rdio-scanner.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: RdioScannerPageComponent,
|
||||
children: [
|
||||
{
|
||||
component: RdioScannerMainPageComponent,
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
loadChildren: () => import('./admin/admin.module').then((module) => module.RdioScannerAdminPageModule),
|
||||
path: 'admin',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -17,36 +17,50 @@
|
|||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
|
||||
@NgModule({
|
||||
exports: [
|
||||
DragDropModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatDatepickerModule,
|
||||
MatDialogModule,
|
||||
MatExpansionModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatListModule,
|
||||
MatNativeDateModule,
|
||||
MatPaginatorModule,
|
||||
MatProgressBarModule,
|
||||
MatRadioModule,
|
||||
MatSelectModule,
|
||||
MatSidenavModule,
|
||||
MatSlideToggleModule,
|
||||
MatSnackBarModule,
|
||||
MatTableModule,
|
||||
MatToolbarModule,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -20,9 +20,15 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ExtraOptions, RouterModule, Routes } from '@angular/router';
|
||||
import { AppLocaleModule } from './locale/locale.module';
|
||||
import { AppMaterialModule } from './material/material.module';
|
||||
|
||||
export interface AppSharedModuleConfig {
|
||||
routerExtraOptions?: ExtraOptions;
|
||||
routerRoutes?: Routes;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
exports: [
|
||||
AppLocaleModule,
|
||||
|
|
@ -30,14 +36,25 @@ import { AppMaterialModule } from './material/material.module';
|
|||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule,
|
||||
],
|
||||
})
|
||||
export class AppSharedModule {
|
||||
static forRoot(): ModuleWithProviders<AppSharedModule> {
|
||||
static forChild(config: AppSharedModuleConfig = {}): ModuleWithProviders<AppSharedModule> {
|
||||
return {
|
||||
ngModule: AppSharedModule,
|
||||
providers: [
|
||||
...RouterModule.forChild(config.routerRoutes || []).providers || [],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
static forRoot(config: AppSharedModuleConfig = {}): ModuleWithProviders<AppSharedModule> {
|
||||
return {
|
||||
ngModule: AppSharedModule,
|
||||
providers: [
|
||||
...AppLocaleModule.forRoot().providers || [],
|
||||
...RouterModule.forRoot(config.routerRoutes || [], config.routerExtraOptions || {}).providers || [],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
<link rel="apple-touch-icon" sizes="192x192" href="assets/icons/icon-192x192.png">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<link rel="stylesheet" href="///fonts.googleapis.com/css?family=Roboto:400,500|Material+Icons">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Icons&family=Roboto:wght@300;400;500&display=block">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title>Rdio Scanner</title>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -27,5 +27,4 @@ if (environment.production) {
|
|||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch((error) => console.error(error));
|
||||
platformBrowserDynamic().bootstrapModule(AppModule).catch((error) => console.error(error));
|
||||
|
|
|
|||
|
|
@ -57,4 +57,4 @@
|
|||
"purpose": "maskable any"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue