mirror of
https://github.com/chuot/rdio-scanner.git
synced 2026-08-16 09:53:01 -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
|
# 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.
|
- 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>"
|
LABEL maintainer="Chrystian Huot <chrystian.huot@saubeo.solutions>"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV APP_DATA=/app/data \
|
ENV APP_DATA=/app/data \
|
||||||
APP_PORT=3000
|
APP_PORT=3000 \
|
||||||
RUN apk --no-cache --no-progress add ffmpeg sqlite tzdata && \
|
DOCKER=1
|
||||||
|
RUN apk --no-cache --no-progress add ffmpeg openssl sqlite tzdata && \
|
||||||
mkdir -p "${APP_DATA}"
|
mkdir -p "${APP_DATA}"
|
||||||
COPY LICENSE .
|
COPY LICENSE .
|
||||||
COPY --from=client-build /app/client/dist/rdio-scanner/. client/.
|
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.
|
[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
|
## Main features
|
||||||
|
|
||||||
- It's a progressive web app that also works great on mobile devices.
|
|
||||||
- Simplistic interface, but strong usability.
|
- Simplistic interface, but strong usability.
|
||||||
|
- Administrative dashboard for easy server configuration.
|
||||||
- Configurable systems and talkgroups.
|
- Configurable systems and talkgroups.
|
||||||
- Talkgroups from any systems can be grouped together for better selection of live streams.
|
- Talkgroups from any systems can be grouped together for better selection of live streams.
|
||||||
- Access control via password with possible restriction to systems/talkgroups.
|
- 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
|
- [docs/examples](./docs/examples) - Some miscellaneous examples
|
||||||
- [CONTRIBUTING.md](./CONTRIBUTING.md) - Contribution guidelines
|
- [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
|
## 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.
|
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
|
## 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.
|
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": "",
|
"root": "",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"prefix": "app",
|
"prefix": "rdio-scanner",
|
||||||
"architect": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-devkit/build-angular:browser",
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "rdio-scanner:build",
|
"browserTarget": "rdio-scanner:build",
|
||||||
"proxyConfig": "proxy.conf.js"
|
"proxyConfig": "src/proxy.conf.js"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
|
|
|
||||||
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>",
|
"author": "Chrystian Huot <chrystian.huot@saubeo.solutions>",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^11.2.8",
|
"@angular/animations": "^11.2.14",
|
||||||
"@angular/cdk": "^11.2.7",
|
"@angular/cdk": "^11.2.12",
|
||||||
"@angular/common": "~11.2.8",
|
"@angular/common": "~11.2.14",
|
||||||
"@angular/compiler": "~11.2.8",
|
"@angular/compiler": "~11.2.14",
|
||||||
"@angular/core": "~11.2.8",
|
"@angular/core": "~11.2.14",
|
||||||
"@angular/forms": "~11.2.8",
|
"@angular/forms": "~11.2.14",
|
||||||
"@angular/material": "^11.2.7",
|
"@angular/material": "^11.2.12",
|
||||||
"@angular/platform-browser": "~11.2.8",
|
"@angular/platform-browser": "~11.2.14",
|
||||||
"@angular/platform-browser-dynamic": "~11.2.8",
|
"@angular/platform-browser-dynamic": "~11.2.14",
|
||||||
"@angular/service-worker": "~11.2.8",
|
"@angular/router": "^11.2.14",
|
||||||
|
"@angular/service-worker": "^11.2.14",
|
||||||
"rxjs": "~6.6.7",
|
"rxjs": "~6.6.7",
|
||||||
"tslib": "^2.1.0",
|
"tslib": "^2.2.0",
|
||||||
"zone.js": "~0.11.4"
|
"zone.js": "~0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "~0.1102.7",
|
"@angular-devkit/build-angular": "~0.1102.13",
|
||||||
"@angular/cli": "^11.2.7",
|
"@angular/cli": "^11.2.10",
|
||||||
"@angular/compiler-cli": "~11.2.8",
|
"@angular/compiler-cli": "~11.2.11",
|
||||||
"@types/node": "~14.14.37",
|
"@types/node": "~14.14.41",
|
||||||
"codelyzer": "^6.0.1",
|
|
||||||
"ts-node": "~9.1.1",
|
"ts-node": "~9.1.1",
|
||||||
"tslint": "~6.1.3",
|
|
||||||
"typescript": "~4.1.5"
|
"typescript": "~4.1.5"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0"
|
||||||
|
},
|
||||||
"license": "LICENSE",
|
"license": "LICENSE",
|
||||||
"name": "rdio-scanner-client",
|
"name": "rdio-scanner-client",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|
@ -32,5 +34,5 @@
|
||||||
"build": "ng build --base-href ./ --prod",
|
"build": "ng build --base-href ./ --prod",
|
||||||
"start": "ng serve"
|
"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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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 { ServiceWorkerModule } from '@angular/service-worker';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { AppComponent } from './app.component';
|
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 { AppSharedModule } from './shared/shared.module';
|
||||||
|
import { routes } from './app.routes';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent],
|
||||||
imports: [
|
imports: [
|
||||||
AppRdioScannerModule,
|
RdioScannerModule,
|
||||||
AppSharedModule.forRoot(),
|
AppSharedModule.forRoot({
|
||||||
|
routerExtraOptions: { enableTracing: false },
|
||||||
|
routerRoutes: routes,
|
||||||
|
}),
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
ServiceWorkerModule.register('ngsw-worker.js', {
|
||||||
|
enabled: environment.production,
|
||||||
|
registrationStrategy: 'registerWhenStable:30000',
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
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,
|
||||||
.rdio-button-mini {
|
.rdio-button-mini {
|
||||||
background: rgb(45, 45, 45);
|
background: rgb(45, 45, 45);
|
||||||
border: none;
|
border-style: solid;
|
||||||
border-radius: 4px;
|
border-width: 1px;
|
||||||
box-shadow: 1px 1px 2px rgb(255, 255, 255) inset,
|
border-bottom-color: rgba(0, 0, 0, 0.87);
|
||||||
-1px -1px 1px rgb(0, 0, 0) inset, 0 0 2px 2px rgb(0, 0, 0);
|
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;
|
box-sizing: border-box;
|
||||||
color: rgb(255, 255, 255);
|
color: rgb(255, 255, 255);
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
line-height: 40px;
|
line-height: 18px;
|
||||||
|
margin: 2px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 2px 8px;
|
||||||
position: relative;
|
position: relative;
|
||||||
text-overflow: ellipsis;
|
text-overflow: clip;
|
||||||
text-shadow: 0 0 4px rgb(0, 0, 0);
|
text-shadow: 0 0 4px rgb(0, 0, 0);
|
||||||
white-space: nowrap;
|
white-space: normal;
|
||||||
width: auto;
|
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
|
|
@ -30,7 +27,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
top: 4px;
|
top: 2px;
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
transform-origin: bottom center;
|
transform-origin: bottom center;
|
||||||
}
|
}
|
||||||
|
|
@ -39,45 +36,36 @@
|
||||||
&.on::after,
|
&.on::after,
|
||||||
&.partial::after {
|
&.partial::after {
|
||||||
content: "";
|
content: "";
|
||||||
border-radius: 4px;
|
|
||||||
display: block;
|
display: block;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 6px;
|
right: 4px;
|
||||||
top: 6px;
|
top: 4px;
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.off::after {
|
&.off::after {
|
||||||
background: rgb(230, 50, 50);
|
background: #e63232;
|
||||||
box-shadow: 1px 1px 1px rgb(255, 255, 255) inset,
|
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset,
|
||||||
0 0 3px 2px rgb(230, 50, 50);
|
0 0 3px 1px rgb(230, 50, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.on::after {
|
&.on::after {
|
||||||
background: rgb(100, 230, 50);
|
background: rgb(100, 230, 50);
|
||||||
box-shadow: 1px 1px 1px rgb(255, 255, 255) inset,
|
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset,
|
||||||
0 0 3px 2px rgb(100, 230, 50);
|
0 0 3px 1px rgb(100, 230, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.partial::after {
|
&.partial::after {
|
||||||
background: rgb(230, 165, 50);
|
background: rgb(230, 165, 50);
|
||||||
box-shadow: 1px 1px 1px rgb(255, 255, 255) inset,
|
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset,
|
||||||
0 0 3px 2px rgb(230, 165, 50);
|
0 0 3px 1px rgb(230, 165, 50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rdio-button {
|
.rdio-button {
|
||||||
color: rgb(250, 250, 250);
|
color: rgb(250, 250, 250);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
min-width: 64px;
|
min-width: 80px;
|
||||||
text-transform: uppercase;
|
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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,12 @@
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input #password matInput type="password" autocomplete="off" formControlName="password"
|
<input #password matInput type="password" autocomplete="off" formControlName="password"
|
||||||
placeholder="Unlock code" (blur)="authFocus()">
|
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>
|
</mat-form-field>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -106,9 +112,11 @@
|
||||||
[ngClass]="{ off: livefeedOffline && !playbackMode, on: livefeedOnline, partial: playbackMode }">
|
[ngClass]="{ off: livefeedOffline && !playbackMode, on: livefeedOnline, partial: playbackMode }">
|
||||||
LIVE<br>FEED
|
LIVE<br>FEED
|
||||||
</button>
|
</button>
|
||||||
|
<div class="spacer"></div>
|
||||||
<button class="rdio-button" (click)="holdSystem()" [ngClass]="{ off: !holdSys, on: holdSys }">
|
<button class="rdio-button" (click)="holdSystem()" [ngClass]="{ off: !holdSys, on: holdSys }">
|
||||||
HOLD<br>SYS
|
HOLD<br>SYS
|
||||||
</button>
|
</button>
|
||||||
|
<div class="spacer"></div>
|
||||||
<button class="rdio-button" (click)="holdTalkgroup()" [ngClass]="{ off: !holdTg, on: holdTg }">
|
<button class="rdio-button" (click)="holdTalkgroup()" [ngClass]="{ off: !holdTg, on: holdTg }">
|
||||||
HOLD<br>TG
|
HOLD<br>TG
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -117,9 +125,11 @@
|
||||||
<button class="rdio-button" (click)="replay()">
|
<button class="rdio-button" (click)="replay()">
|
||||||
REPLAY<br>LAST
|
REPLAY<br>LAST
|
||||||
</button>
|
</button>
|
||||||
|
<div class="spacer"></div>
|
||||||
<button class="rdio-button" (click)="skip()">
|
<button class="rdio-button" (click)="skip()">
|
||||||
SKIP<br>NEXT
|
SKIP<br>NEXT
|
||||||
</button>
|
</button>
|
||||||
|
<div class="spacer"></div>
|
||||||
<button class="rdio-button" (click)="avoid()">
|
<button class="rdio-button" (click)="avoid()">
|
||||||
AVOID
|
AVOID
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -128,9 +138,11 @@
|
||||||
<button class="rdio-button" (click)="showSearchPanel()">
|
<button class="rdio-button" (click)="showSearchPanel()">
|
||||||
SEARCH<br>CALL
|
SEARCH<br>CALL
|
||||||
</button>
|
</button>
|
||||||
|
<div class="spacer"></div>
|
||||||
<button class="rdio-button" (click)="pause()" [ngClass]="{ off: !livefeedPaused, on: livefeedPaused }">
|
<button class="rdio-button" (click)="pause()" [ngClass]="{ off: !livefeedPaused, on: livefeedPaused }">
|
||||||
PAUSE
|
PAUSE
|
||||||
</button>
|
</button>
|
||||||
|
<div class="spacer"></div>
|
||||||
<button class="rdio-button" (click)="showSelectPanel()">
|
<button class="rdio-button" (click)="showSelectPanel()">
|
||||||
SELECT<br>TG
|
SELECT<br>TG
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,26 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.rdio-control {
|
.rdio-control {
|
||||||
.rdio-button {
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.rdio-button {
|
||||||
|
flex: 1;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
display: block;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rdio-display {
|
.rdio-display {
|
||||||
background: rgb(209, 238, 238);
|
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);
|
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);
|
color: rgba(0, 0, 0, 0.8);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
@ -55,7 +60,7 @@
|
||||||
|
|
||||||
* {
|
* {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: clip;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,7 +153,6 @@
|
||||||
.rdio-status {
|
.rdio-status {
|
||||||
.led {
|
.led {
|
||||||
background: rgb(80, 80, 80);
|
background: rgb(80, 80, 80);
|
||||||
border-radius: 2px;
|
|
||||||
display: block;
|
display: block;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
margin-left: auto;
|
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
|
* 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
|
* 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 { FormBuilder } from '@angular/forms';
|
||||||
import { MatInput } from '@angular/material/input';
|
import { MatInput } from '@angular/material/input';
|
||||||
import { Subscription, timer } from 'rxjs';
|
import { Subscription, timer } from 'rxjs';
|
||||||
|
|
@ -31,19 +31,19 @@ import {
|
||||||
RdioScannerLivefeedMap,
|
RdioScannerLivefeedMap,
|
||||||
RdioScannerLivefeedMode,
|
RdioScannerLivefeedMode,
|
||||||
} from '../rdio-scanner';
|
} 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({
|
@Component({
|
||||||
selector: 'app-rdio-scanner-main',
|
selector: 'rdio-scanner-main',
|
||||||
styleUrls: [
|
styleUrls: [
|
||||||
'../common.scss',
|
'../common.scss',
|
||||||
'./main.component.scss',
|
'./main.component.scss',
|
||||||
],
|
],
|
||||||
templateUrl: './main.component.html',
|
templateUrl: './main.component.html',
|
||||||
})
|
})
|
||||||
export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
export class RdioScannerMainComponent implements OnDestroy, OnInit {
|
||||||
auth = false;
|
auth = false;
|
||||||
authForm = this.ngFormBuilder.group({ password: [] });
|
authForm = this.ngFormBuilder.group({ password: [] });
|
||||||
|
|
||||||
|
|
@ -98,10 +98,10 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
||||||
|
|
||||||
private dimmerTimer: Subscription | undefined;
|
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(
|
constructor(
|
||||||
private appRdioScannerService: AppRdioScannerService,
|
private rdioScannerService: RdioScannerService,
|
||||||
private ngChangeDetectorRef: ChangeDetectorRef,
|
private ngChangeDetectorRef: ChangeDetectorRef,
|
||||||
private ngFormBuilder: FormBuilder,
|
private ngFormBuilder: FormBuilder,
|
||||||
) { }
|
) { }
|
||||||
|
|
@ -109,7 +109,7 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
||||||
authenticate(password = this.authForm.value.password): void {
|
authenticate(password = this.authForm.value.password): void {
|
||||||
this.authForm.disable();
|
this.authForm.disable();
|
||||||
|
|
||||||
this.appRdioScannerService.authenticate(password);
|
this.rdioScannerService.authenticate(password);
|
||||||
}
|
}
|
||||||
|
|
||||||
authFocus(): void {
|
authFocus(): void {
|
||||||
|
|
@ -118,12 +118,7 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('document:keydown.a', ['$event'])
|
avoid(options?: RdioScannerAvoidOptions): void {
|
||||||
avoid(options?: RdioScannerAvoidOptions, event?: KeyboardEvent): void {
|
|
||||||
if (event && !this.config?.keyboardShortcuts) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.auth) {
|
if (this.auth) {
|
||||||
this.authFocus();
|
this.authFocus();
|
||||||
|
|
||||||
|
|
@ -131,82 +126,67 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
||||||
const call = this.call || this.callPrevious;
|
const call = this.call || this.callPrevious;
|
||||||
|
|
||||||
if (options || call) {
|
if (options || call) {
|
||||||
this.appRdioScannerService.avoid(options);
|
this.rdioScannerService.avoid(options);
|
||||||
|
|
||||||
if (call && !this.map[call.system][call.talkgroup]) {
|
if (call && !this.map[call.system][call.talkgroup]) {
|
||||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Deactivate);
|
this.rdioScannerService.beep(RdioScannerBeepStyle.Deactivate);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
this.rdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateDimmer();
|
this.updateDimmer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('document:keydown.s', ['$event'])
|
holdSystem(): void {
|
||||||
holdSystem(event?: KeyboardEvent): void {
|
|
||||||
if (event && !this.config?.keyboardShortcuts) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.auth) {
|
if (this.auth) {
|
||||||
this.authFocus();
|
this.authFocus();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (this.call || this.callPrevious) {
|
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 {
|
} else {
|
||||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
this.rdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateDimmer();
|
this.updateDimmer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('document:keydown.t', ['$event'])
|
holdTalkgroup(): void {
|
||||||
holdTalkgroup(event?: KeyboardEvent): void {
|
|
||||||
if (event && !this.config?.keyboardShortcuts) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.auth) {
|
if (this.auth) {
|
||||||
this.authFocus();
|
this.authFocus();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (this.call || this.callPrevious) {
|
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 {
|
} else {
|
||||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
this.rdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateDimmer();
|
this.updateDimmer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('document:keydown.l', ['$event'])
|
livefeed(): void {
|
||||||
livefeed(event?: KeyboardEvent): void {
|
|
||||||
if (event && !this.config?.keyboardShortcuts) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.auth) {
|
if (this.auth) {
|
||||||
this.authFocus();
|
this.authFocus();
|
||||||
|
|
||||||
} else {
|
} 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();
|
this.updateDimmer();
|
||||||
}
|
}
|
||||||
|
|
@ -222,49 +202,36 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
||||||
this.syncClock();
|
this.syncClock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('document:keydown.space', ['$event'])
|
pause(): void {
|
||||||
@HostListener('document:keydown.p', ['$event'])
|
|
||||||
pause(event?: KeyboardEvent): void {
|
|
||||||
if (event && !this.config?.keyboardShortcuts) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event?.preventDefault();
|
|
||||||
|
|
||||||
if (this.auth) {
|
if (this.auth) {
|
||||||
this.authFocus();
|
this.authFocus();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (this.livefeedPaused) {
|
if (this.livefeedPaused) {
|
||||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Deactivate).then(() => this.appRdioScannerService.pause());
|
this.rdioScannerService.beep(RdioScannerBeepStyle.Deactivate).then(() => this.rdioScannerService.pause());
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||||
|
|
||||||
this.appRdioScannerService.pause();
|
this.rdioScannerService.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateDimmer();
|
this.updateDimmer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('document:keydown.r', ['$event'])
|
replay(): void {
|
||||||
replay(event?: KeyboardEvent): void {
|
|
||||||
if (event && !this.config?.keyboardShortcuts) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.auth) {
|
if (this.auth) {
|
||||||
this.authFocus();
|
this.authFocus();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (!this.livefeedPaused && (this.call || this.callPrevious)) {
|
if (!this.livefeedPaused && (this.call || this.callPrevious)) {
|
||||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||||
|
|
||||||
this.appRdioScannerService.replay();
|
this.rdioScannerService.replay();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
this.rdioScannerService.beep(RdioScannerBeepStyle.Denied);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateDimmer();
|
this.updateDimmer();
|
||||||
|
|
@ -280,7 +247,7 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
||||||
this.authFocus();
|
this.authFocus();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.appRdioScannerService.beep();
|
this.rdioScannerService.beep();
|
||||||
|
|
||||||
this.openSearchPanel.emit();
|
this.openSearchPanel.emit();
|
||||||
}
|
}
|
||||||
|
|
@ -295,32 +262,27 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
||||||
this.authFocus();
|
this.authFocus();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.appRdioScannerService.beep();
|
this.rdioScannerService.beep();
|
||||||
|
|
||||||
this.openSelectPanel.emit();
|
this.openSelectPanel.emit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('document:keydown.n', ['$event'])
|
skip(options?: { delay?: boolean }): void {
|
||||||
skip(options?: { delay?: boolean }, event?: KeyboardEvent): void {
|
|
||||||
if (event && !this.config?.keyboardShortcuts) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.auth) {
|
if (this.auth) {
|
||||||
this.authFocus();
|
this.authFocus();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||||
|
|
||||||
this.appRdioScannerService.skip(options);
|
this.rdioScannerService.skip(options);
|
||||||
|
|
||||||
this.updateDimmer();
|
this.updateDimmer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
this.appRdioScannerService.stop();
|
this.rdioScannerService.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
private eventHandler(event: RdioScannerEvent): void {
|
private eventHandler(event: RdioScannerEvent): void {
|
||||||
|
|
@ -338,7 +300,7 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
||||||
if (password) {
|
if (password) {
|
||||||
this.authForm.get('password')?.setValue(password);
|
this.authForm.get('password')?.setValue(password);
|
||||||
|
|
||||||
this.appRdioScannerService.authenticate(password);
|
this.rdioScannerService.authenticate(password);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.auth = event.auth;
|
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) {
|
if ('holdSys' in event) {
|
||||||
this.holdSys = event.holdSys || false;
|
this.holdSys = event.holdSys || false;
|
||||||
}
|
}
|
||||||
|
|
@ -423,6 +389,10 @@ export class AppRdioScannerMainComponent implements OnDestroy, OnInit {
|
||||||
this.updateDimmer();
|
this.updateDimmer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('tooMany' in event && event.tooMany === true) {
|
||||||
|
this.authForm.get('password')?.setErrors({ tooMany: true });
|
||||||
|
}
|
||||||
|
|
||||||
this.updateDisplay();
|
this.updateDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</mat-toolbar>
|
</mat-toolbar>
|
||||||
<div #scrollableSearch>
|
<div #scrollableSearch>
|
||||||
<div>
|
<div>
|
||||||
<app-rdio-scanner-search #searchComponent></app-rdio-scanner-search>
|
<rdio-scanner-search #searchComponent></rdio-scanner-search>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,16 +26,16 @@
|
||||||
</mat-toolbar>
|
</mat-toolbar>
|
||||||
<div #scrollableSelect>
|
<div #scrollableSelect>
|
||||||
<div>
|
<div>
|
||||||
<app-rdio-scanner-select></app-rdio-scanner-select>
|
<rdio-scanner-select></rdio-scanner-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-sidenav>
|
</mat-sidenav>
|
||||||
<mat-sidenav-content>
|
<mat-sidenav-content>
|
||||||
<app-rdio-scanner-main
|
<rdio-scanner-main
|
||||||
(openSearchPanel)="scrollableSearch.scrollTo(0, 0); searchPanel.open(); searchComponent.searchCalls()"
|
(openSearchPanel)="scrollableSearch.scrollTo(0, 0); searchPanel.open(); searchComponent.searchCalls()"
|
||||||
(openSelectPanel)="scrollableSelect.scrollTo(0, 0); selectPanel.open();"
|
(openSelectPanel)="scrollableSelect.scrollTo(0, 0); selectPanel.open();"
|
||||||
(toggleFullscreen)="toggleFullscreen()">
|
(toggleFullscreen)="toggleFullscreen()">
|
||||||
</app-rdio-scanner-main>
|
</rdio-scanner-main>
|
||||||
</mat-sidenav-content>
|
</mat-sidenav-content>
|
||||||
</mat-sidenav-container>
|
</mat-sidenav-container>
|
||||||
|
|
@ -45,7 +45,7 @@ $background-color: rgb(30, 30, 30);
|
||||||
|
|
||||||
.mat-toolbar {
|
.mat-toolbar {
|
||||||
background-color: inherit;
|
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;
|
color: currentColor;
|
||||||
z-index: 2;
|
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
|
* 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
|
* 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 { Component, ElementRef, HostListener, OnDestroy, ViewChild } from '@angular/core';
|
||||||
import { MatSidenav } from '@angular/material/sidenav';
|
import { MatSidenav } from '@angular/material/sidenav';
|
||||||
import { RdioScannerConfig, RdioScannerEvent, RdioScannerLivefeedMode } from './rdio-scanner';
|
import { RdioScannerConfig, RdioScannerEvent, RdioScannerLivefeedMode } from './rdio-scanner';
|
||||||
import { AppRdioScannerService } from './rdio-scanner.service';
|
import { RdioScannerService } from './rdio-scanner.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-rdio-scanner',
|
selector: 'rdio-scanner',
|
||||||
styleUrls: ['./rdio-scanner.component.scss'],
|
styleUrls: ['./rdio-scanner.component.scss'],
|
||||||
templateUrl: './rdio-scanner.component.html',
|
templateUrl: './rdio-scanner.component.html',
|
||||||
})
|
})
|
||||||
export class AppRdioScannerComponent implements OnDestroy {
|
export class RdioScannerComponent implements OnDestroy {
|
||||||
private config: RdioScannerConfig | undefined;
|
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;
|
private livefeedMode: RdioScannerLivefeedMode = RdioScannerLivefeedMode.Offline;
|
||||||
|
|
||||||
|
|
@ -39,8 +39,8 @@ export class AppRdioScannerComponent implements OnDestroy {
|
||||||
@ViewChild('selectPanel') private selectPanel: MatSidenav | undefined;
|
@ViewChild('selectPanel') private selectPanel: MatSidenav | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private appRdioScannerService: AppRdioScannerService,
|
|
||||||
private ngElementRef: ElementRef,
|
private ngElementRef: ElementRef,
|
||||||
|
private rdioScannerService: RdioScannerService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@HostListener('window:beforeunload', ['$event'])
|
@HostListener('window:beforeunload', ['$event'])
|
||||||
|
|
@ -53,11 +53,11 @@ export class AppRdioScannerComponent implements OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
start(): void {
|
start(): void {
|
||||||
this.appRdioScannerService.startLivefeed();
|
this.rdioScannerService.startLivefeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
this.appRdioScannerService.stopLivefeed();
|
this.rdioScannerService.stopLivefeed();
|
||||||
|
|
||||||
this.searchPanel?.close();
|
this.searchPanel?.close();
|
||||||
this.selectPanel?.close();
|
this.selectPanel?.close();
|
||||||
|
|
@ -67,15 +67,7 @@ export class AppRdioScannerComponent implements OnDestroy {
|
||||||
this.eventSubscription.unsubscribe();
|
this.eventSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('document:keydown.f', ['$event'])
|
toggleFullscreen(): void {
|
||||||
@HostListener('document:keydown.tab', ['$event'])
|
|
||||||
toggleFullscreen(event?: KeyboardEvent): void {
|
|
||||||
if (event && !this.config?.keyboardShortcuts) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event?.preventDefault();
|
|
||||||
|
|
||||||
if (document.fullscreenElement) {
|
if (document.fullscreenElement) {
|
||||||
const el: {
|
const el: {
|
||||||
exitFullscreen?: () => void;
|
exitFullscreen?: () => void;
|
||||||
|
|
@ -124,36 +116,4 @@ export class AppRdioScannerComponent implements OnDestroy {
|
||||||
this.livefeedMode = event.livefeedMode;
|
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
|
* 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
|
* 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 { FullscreenOverlayContainer, OverlayContainer } from '@angular/cdk/overlay';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { AppSharedModule } from '../../shared/shared.module';
|
import { AppSharedModule } from '../../shared/shared.module';
|
||||||
import { AppRdioScannerComponent } from './rdio-scanner.component';
|
import { RdioScannerComponent } from './rdio-scanner.component';
|
||||||
import { AppRdioScannerService } from './rdio-scanner.service';
|
import { RdioScannerService } from './rdio-scanner.service';
|
||||||
import { AppRdioScannerMainComponent } from './main/main.component';
|
import { RdioScannerMainComponent } from './main/main.component';
|
||||||
import { AppRdioScannerSearchComponent } from './search/search.component';
|
import { RdioScannerSearchComponent } from './search/search.component';
|
||||||
import { AppRdioScannerSelectComponent } from './select/select.component';
|
import { RdioScannerSelectComponent } from './select/select.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppRdioScannerComponent,
|
RdioScannerComponent,
|
||||||
AppRdioScannerMainComponent,
|
RdioScannerMainComponent,
|
||||||
AppRdioScannerSearchComponent,
|
RdioScannerSearchComponent,
|
||||||
AppRdioScannerSelectComponent,
|
RdioScannerSelectComponent,
|
||||||
],
|
],
|
||||||
exports: [AppRdioScannerComponent],
|
exports: [RdioScannerComponent],
|
||||||
imports: [AppSharedModule],
|
imports: [AppSharedModule],
|
||||||
providers: [
|
providers: [
|
||||||
AppRdioScannerService,
|
RdioScannerService,
|
||||||
{ provide: OverlayContainer, useClass: FullscreenOverlayContainer },
|
{ 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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -49,13 +49,16 @@ enum WebsocketCallFlag {
|
||||||
enum WebsocketCommand {
|
enum WebsocketCommand {
|
||||||
Call = 'CAL',
|
Call = 'CAL',
|
||||||
Config = 'CFG',
|
Config = 'CFG',
|
||||||
|
Expired = 'XPR',
|
||||||
ListCall = 'LCL',
|
ListCall = 'LCL',
|
||||||
LivefeedMap = 'LFM',
|
LivefeedMap = 'LFM',
|
||||||
|
Max = 'MAX',
|
||||||
Pin = 'PIN',
|
Pin = 'PIN',
|
||||||
|
Ver = 'VER',
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppRdioScannerService implements OnDestroy {
|
export class RdioScannerService implements OnDestroy {
|
||||||
static LOCAL_STORAGE_KEY = 'rdio-scanner';
|
static LOCAL_STORAGE_KEY = 'rdio-scanner';
|
||||||
|
|
||||||
event = new EventEmitter<RdioScannerEvent>();
|
event = new EventEmitter<RdioScannerEvent>();
|
||||||
|
|
@ -74,7 +77,6 @@ export class AppRdioScannerService implements OnDestroy {
|
||||||
private config: RdioScannerConfig = {
|
private config: RdioScannerConfig = {
|
||||||
dimmerDelay: false,
|
dimmerDelay: false,
|
||||||
groups: {},
|
groups: {},
|
||||||
keyboardShortcuts: true,
|
|
||||||
keypadBeeps: false,
|
keypadBeeps: false,
|
||||||
systems: [],
|
systems: [],
|
||||||
tags: {},
|
tags: {},
|
||||||
|
|
@ -331,10 +333,18 @@ export class AppRdioScannerService implements OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAndDownload(id: string): void {
|
loadAndDownload(id: string): void {
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.getCall(id, WebsocketCallFlag.Download);
|
this.getCall(id, WebsocketCallFlag.Download);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAndPlay(id: string): void {
|
loadAndPlay(id: string): void {
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.skipDelay) {
|
if (this.skipDelay) {
|
||||||
this.skipDelay.unsubscribe();
|
this.skipDelay.unsubscribe();
|
||||||
|
|
||||||
|
|
@ -765,16 +775,18 @@ export class AppRdioScannerService implements OnDestroy {
|
||||||
if (Array.isArray(message)) {
|
if (Array.isArray(message)) {
|
||||||
switch (message[0]) {
|
switch (message[0]) {
|
||||||
case WebsocketCommand.Call:
|
case WebsocketCommand.Call:
|
||||||
if (message[2] === WebsocketCallFlag.Download) {
|
if (message[1] !== null) {
|
||||||
this.download(message[1]);
|
if (message[2] === WebsocketCallFlag.Download) {
|
||||||
|
this.download(message[1]);
|
||||||
|
|
||||||
} else if (message[2] === WebsocketCallFlag.Play && message[1]?.id === this.playbackPending) {
|
} else if (message[2] === WebsocketCallFlag.Play && message[1]?.id === this.playbackPending) {
|
||||||
this.playbackPending = undefined;
|
this.playbackPending = undefined;
|
||||||
|
|
||||||
this.queue(this.transformCall(message[1]), { priority: true });
|
this.queue(this.transformCall(message[1]), { priority: true });
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.queue(this.transformCall(message[1]));
|
this.queue(this.transformCall(message[1]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
@ -785,7 +797,6 @@ export class AppRdioScannerService implements OnDestroy {
|
||||||
this.config = {
|
this.config = {
|
||||||
dimmerDelay: typeof config.dimmerDelay === 'number' ? config.dimmerDelay : 5000,
|
dimmerDelay: typeof config.dimmerDelay === 'number' ? config.dimmerDelay : 5000,
|
||||||
groups: typeof config.groups !== null && typeof config.groups === 'object' ? config.groups : {},
|
groups: typeof config.groups !== null && typeof config.groups === 'object' ? config.groups : {},
|
||||||
keyboardShortcuts: config.keyboardShortcuts,
|
|
||||||
keypadBeeps: config.keypadBeeps !== null && typeof config.keypadBeeps === 'object' ? config.keypadBeeps : {},
|
keypadBeeps: config.keypadBeeps !== null && typeof config.keypadBeeps === 'object' ? config.keypadBeeps : {},
|
||||||
systems: Array.isArray(config.systems) ? config.systems.slice() : [],
|
systems: Array.isArray(config.systems) ? config.systems.slice() : [],
|
||||||
tags: typeof config.tags !== null && typeof config.tags === 'object' ? config.tags : {},
|
tags: typeof config.tags !== null && typeof config.tags === 'object' ? config.tags : {},
|
||||||
|
|
@ -808,6 +819,11 @@ export class AppRdioScannerService implements OnDestroy {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case WebsocketCommand.Expired:
|
||||||
|
this.event.emit({ auth: true, expired: true });
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
case WebsocketCommand.ListCall:
|
case WebsocketCommand.ListCall:
|
||||||
this.playbackList = message[1];
|
this.playbackList = message[1];
|
||||||
|
|
||||||
|
|
@ -823,6 +839,11 @@ export class AppRdioScannerService implements OnDestroy {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case WebsocketCommand.Max:
|
||||||
|
this.event.emit({ auth: true, tooMany: true });
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
case WebsocketCommand.Pin:
|
case WebsocketCommand.Pin:
|
||||||
this.event.emit({ auth: true });
|
this.event.emit({ auth: true });
|
||||||
|
|
||||||
|
|
@ -893,7 +914,7 @@ export class AppRdioScannerService implements OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private rebuildLivefeedMap(): void {
|
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());
|
const tgs = sys.talkgroups.map((tg) => tg.id.toString());
|
||||||
|
|
||||||
sysMap[sys.id] = sys.talkgroups.reduce((tgMap, tg) => {
|
sysMap[sys.id] = sys.talkgroups.reduce((tgMap, tg) => {
|
||||||
|
|
@ -907,6 +928,14 @@ export class AppRdioScannerService implements OnDestroy {
|
||||||
return sysMap;
|
return sysMap;
|
||||||
}, {} as RdioScannerLivefeedMap);
|
}, {} as RdioScannerLivefeedMap);
|
||||||
|
|
||||||
|
if (this.livefeedMapPriorToHoldSystem != null) {
|
||||||
|
this.livefeedMapPriorToHoldSystem = livefeedMap;
|
||||||
|
} else if (this.livefeedMapPriorToHoldTalkgroup != null) {
|
||||||
|
this.livefeedMapPriorToHoldTalkgroup = livefeedMap;
|
||||||
|
} else {
|
||||||
|
this.livefeedMap = livefeedMap;
|
||||||
|
}
|
||||||
|
|
||||||
this.storeLivefeedMap();
|
this.storeLivefeedMap();
|
||||||
|
|
||||||
this.rebuildGroups();
|
this.rebuildGroups();
|
||||||
|
|
@ -919,7 +948,7 @@ export class AppRdioScannerService implements OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private restoreLivefeed(): void {
|
private restoreLivefeed(): void {
|
||||||
const map = window?.localStorage?.getItem(AppRdioScannerService.LOCAL_STORAGE_KEY);
|
const map = window?.localStorage?.getItem(RdioScannerService.LOCAL_STORAGE_KEY);
|
||||||
|
|
||||||
if (map) {
|
if (map) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -948,11 +977,11 @@ export class AppRdioScannerService implements OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private storeLivefeedMap(): void {
|
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 {
|
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);
|
call.systemData = this.config.systems.find((system) => system.id === call.system);
|
||||||
|
|
||||||
if (Array.isArray(call.systemData?.talkgroups)) {
|
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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -73,7 +73,6 @@ export interface RdioScannerCallSource {
|
||||||
export interface RdioScannerConfig {
|
export interface RdioScannerConfig {
|
||||||
dimmerDelay: number | false;
|
dimmerDelay: number | false;
|
||||||
groups: { [key: string]: { [key: number]: number[] } };
|
groups: { [key: string]: { [key: number]: number[] } };
|
||||||
keyboardShortcuts: boolean;
|
|
||||||
keypadBeeps: RdioScannerKeypadBeeps | false;
|
keypadBeeps: RdioScannerKeypadBeeps | false;
|
||||||
systems: RdioScannerSystem[];
|
systems: RdioScannerSystem[];
|
||||||
tags: { [key: string]: { [key: number]: number[] } };
|
tags: { [key: string]: { [key: number]: number[] } };
|
||||||
|
|
@ -83,6 +82,7 @@ export interface RdioScannerEvent {
|
||||||
auth?: boolean;
|
auth?: boolean;
|
||||||
config?: RdioScannerConfig;
|
config?: RdioScannerConfig;
|
||||||
call?: RdioScannerCall;
|
call?: RdioScannerCall;
|
||||||
|
expired?: boolean;
|
||||||
groups?: RdioScannerGroup[];
|
groups?: RdioScannerGroup[];
|
||||||
holdSys?: boolean;
|
holdSys?: boolean;
|
||||||
holdTg?: boolean;
|
holdTg?: boolean;
|
||||||
|
|
@ -94,6 +94,7 @@ export interface RdioScannerEvent {
|
||||||
playbackPending?: string;
|
playbackPending?: string;
|
||||||
queue?: number;
|
queue?: number;
|
||||||
time?: number;
|
time?: number;
|
||||||
|
tooMany?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RdioScannerGroupStatus {
|
export enum RdioScannerGroupStatus {
|
||||||
|
|
@ -150,7 +151,7 @@ export interface RdioScannerSystem {
|
||||||
led?: 'blue' | 'cyan' | 'green' | 'magenta' | 'red' | 'white' | 'yellow';
|
led?: 'blue' | 'cyan' | 'green' | 'magenta' | 'red' | 'white' | 'yellow';
|
||||||
order?: number;
|
order?: number;
|
||||||
talkgroups: RdioScannerTalkgroup[];
|
talkgroups: RdioScannerTalkgroup[];
|
||||||
units?: RdioScannerUnit[];
|
units: RdioScannerUnit[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RdioScannerTalkgroup {
|
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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -31,14 +31,14 @@ import {
|
||||||
RdioScannerSystem,
|
RdioScannerSystem,
|
||||||
RdioScannerTalkgroup,
|
RdioScannerTalkgroup,
|
||||||
} from '../rdio-scanner';
|
} from '../rdio-scanner';
|
||||||
import { AppRdioScannerService } from '../rdio-scanner.service';
|
import { RdioScannerService } from '../rdio-scanner.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-rdio-scanner-search',
|
selector: 'rdio-scanner-search',
|
||||||
styleUrls: ['./search.component.scss'],
|
styleUrls: ['./search.component.scss'],
|
||||||
templateUrl: './search.component.html',
|
templateUrl: './search.component.html',
|
||||||
})
|
})
|
||||||
export class AppRdioScannerSearchComponent implements OnDestroy {
|
export class RdioScannerSearchComponent implements OnDestroy {
|
||||||
call: RdioScannerCall | undefined;
|
call: RdioScannerCall | undefined;
|
||||||
callPending: string | undefined;
|
callPending: string | undefined;
|
||||||
|
|
||||||
|
|
@ -68,7 +68,7 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
||||||
|
|
||||||
private config: RdioScannerConfig | undefined;
|
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;
|
private limit = 200;
|
||||||
|
|
||||||
|
|
@ -77,18 +77,18 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
||||||
@ViewChild(MatPaginator, { read: MatPaginator }) private paginator: MatPaginator | undefined;
|
@ViewChild(MatPaginator, { read: MatPaginator }) private paginator: MatPaginator | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private appRdioScannerService: AppRdioScannerService,
|
private rdioScannerService: RdioScannerService,
|
||||||
private ngChangeDetectorRef: ChangeDetectorRef,
|
private ngChangeDetectorRef: ChangeDetectorRef,
|
||||||
private ngFormBuilder: FormBuilder,
|
private ngFormBuilder: FormBuilder,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
download(id: string): void {
|
download(id: string): void {
|
||||||
this.appRdioScannerService.loadAndDownload(id);
|
this.rdioScannerService.loadAndDownload(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
formChangeHandler(): void {
|
formChangeHandler(): void {
|
||||||
if (this.livefeedPlayback) {
|
if (this.livefeedPlayback) {
|
||||||
this.appRdioScannerService.stopPlaybackMode();
|
this.rdioScannerService.stopPlaybackMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.paginator?.firstPage();
|
this.paginator?.firstPage();
|
||||||
|
|
@ -127,16 +127,14 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
||||||
.filter((tag) => this.config?.systems
|
.filter((tag) => this.config?.systems
|
||||||
.some((system) => system.talkgroups
|
.some((system) => system.talkgroups
|
||||||
.some((talkgroup) => talkgroup.group === selectedGroup && talkgroup.tag === tag)))
|
.some((talkgroup) => talkgroup.group === selectedGroup && talkgroup.tag === tag)))
|
||||||
.sort()
|
|
||||||
: selectedTalkgroup
|
: selectedTalkgroup
|
||||||
? [selectedTalkgroup.tag]
|
? [selectedTalkgroup.tag]
|
||||||
: selectedSystem
|
: selectedSystem
|
||||||
? Object.keys(this.config.tags)
|
? Object.keys(this.config.tags)
|
||||||
.filter((tag) => selectedSystem.talkgroups
|
.filter((tag) => selectedSystem.talkgroups
|
||||||
.some((talkgroup) => talkgroup.tag === tag))
|
.some((talkgroup) => talkgroup.tag === tag))
|
||||||
.sort()
|
|
||||||
: Object.keys(this.config.tags)
|
: Object.keys(this.config.tags)
|
||||||
.sort();
|
this.optionsTag.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
this.optionsTalkgroup = selectedGroup
|
this.optionsTalkgroup = selectedGroup
|
||||||
? selectedSystem
|
? selectedSystem
|
||||||
|
|
@ -176,18 +174,16 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
||||||
.filter((group) => Object.keys(this.config?.groups[group] || {})
|
.filter((group) => Object.keys(this.config?.groups[group] || {})
|
||||||
.map((system) => +system)
|
.map((system) => +system)
|
||||||
.includes(selectedSystem.id))
|
.includes(selectedSystem.id))
|
||||||
.sort()
|
|
||||||
: Object.keys(this.config.groups)
|
: Object.keys(this.config.groups)
|
||||||
.sort();
|
this.optionsGroup.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
this.optionsTag = selectedSystem
|
this.optionsTag = selectedSystem
|
||||||
? Object.keys(this.config.tags)
|
? Object.keys(this.config.tags)
|
||||||
.filter((tag) => Object.keys(this.config?.tags[tag] || {})
|
.filter((tag) => Object.keys(this.config?.tags[tag] || {})
|
||||||
.map((system) => +system)
|
.map((system) => +system)
|
||||||
.includes(selectedSystem.id))
|
.includes(selectedSystem.id))
|
||||||
.sort()
|
|
||||||
: Object.keys(this.config.tags)
|
: Object.keys(this.config.tags)
|
||||||
.sort();
|
this.optionsTag.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
this.optionsTalkgroup = selectedSystem
|
this.optionsTalkgroup = selectedSystem
|
||||||
? selectedSystem.talkgroups
|
? selectedSystem.talkgroups
|
||||||
|
|
@ -223,16 +219,14 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
||||||
.filter((group) => this.config?.systems
|
.filter((group) => this.config?.systems
|
||||||
.some((system) => system.talkgroups
|
.some((system) => system.talkgroups
|
||||||
.some((talkgroup) => talkgroup.group === group && talkgroup.tag === selectedTag)))
|
.some((talkgroup) => talkgroup.group === group && talkgroup.tag === selectedTag)))
|
||||||
.sort()
|
|
||||||
: selectedTalkgroup
|
: selectedTalkgroup
|
||||||
? [selectedTalkgroup.group]
|
? [selectedTalkgroup.group]
|
||||||
: selectedSystem
|
: selectedSystem
|
||||||
? Object.keys(this.config.groups)
|
? Object.keys(this.config.groups)
|
||||||
.filter((group) => selectedSystem.talkgroups
|
.filter((group) => selectedSystem.talkgroups
|
||||||
.some((talkgroup) => talkgroup.group === group))
|
.some((talkgroup) => talkgroup.group === group))
|
||||||
.sort()
|
|
||||||
: Object.keys(this.config.groups)
|
: Object.keys(this.config.groups)
|
||||||
.sort();
|
this.optionsGroup.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
this.optionsSystem = selectedTag
|
this.optionsSystem = selectedTag
|
||||||
? this.config.systems
|
? this.config.systems
|
||||||
|
|
@ -283,26 +277,22 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
||||||
this.optionsGroup = selectedTalkgroup
|
this.optionsGroup = selectedTalkgroup
|
||||||
? Object.keys(this.config.groups)
|
? Object.keys(this.config.groups)
|
||||||
.filter((group) => group === selectedTalkgroup.group)
|
.filter((group) => group === selectedTalkgroup.group)
|
||||||
.sort()
|
|
||||||
: selectedSystem
|
: selectedSystem
|
||||||
? Object.keys(this.config.groups)
|
? Object.keys(this.config.groups)
|
||||||
.filter((group) => selectedSystem.talkgroups
|
.filter((group) => selectedSystem.talkgroups
|
||||||
.some((talkgroup) => talkgroup.group === group))
|
.some((talkgroup) => talkgroup.group === group))
|
||||||
.sort()
|
|
||||||
: Object.keys(this.config.groups)
|
: Object.keys(this.config.groups)
|
||||||
.sort();
|
this.optionsGroup.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
this.optionsTag = selectedTalkgroup
|
this.optionsTag = selectedTalkgroup
|
||||||
? Object.keys(this.config.tags)
|
? Object.keys(this.config.tags)
|
||||||
.filter((tag) => tag === selectedTalkgroup.tag)
|
.filter((tag) => tag === selectedTalkgroup.tag)
|
||||||
.sort()
|
|
||||||
: selectedSystem
|
: selectedSystem
|
||||||
? Object.keys(this.config.tags)
|
? Object.keys(this.config.tags)
|
||||||
.filter((tag) => selectedSystem.talkgroups
|
.filter((tag) => selectedSystem.talkgroups
|
||||||
.some((talkgroup) => talkgroup.tag === tag))
|
.some((talkgroup) => talkgroup.tag === tag))
|
||||||
.sort()
|
|
||||||
: Object.keys(this.config.tags)
|
: Object.keys(this.config.tags)
|
||||||
.sort();
|
this.optionsTag.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
this.form.patchValue({
|
this.form.patchValue({
|
||||||
group: selectedGroup ? this.optionsGroup.findIndex((group) => group === selectedGroup) : -1,
|
group: selectedGroup ? this.optionsGroup.findIndex((group) => group === selectedGroup) : -1,
|
||||||
|
|
@ -317,7 +307,7 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
play(id: string): void {
|
play(id: string): void {
|
||||||
this.appRdioScannerService.loadAndPlay(id);
|
this.rdioScannerService.loadAndPlay(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshResults(): void {
|
refreshResults(): void {
|
||||||
|
|
@ -353,11 +343,6 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
||||||
talkgroup: -1,
|
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.paginator?.firstPage();
|
||||||
|
|
||||||
this.formChangeHandler();
|
this.formChangeHandler();
|
||||||
|
|
@ -420,15 +405,15 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
||||||
|
|
||||||
this.form.disable();
|
this.form.disable();
|
||||||
|
|
||||||
this.appRdioScannerService.searchCalls(options);
|
this.rdioScannerService.searchCalls(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
if (this.livefeedPlayback) {
|
if (this.livefeedPlayback) {
|
||||||
this.appRdioScannerService.stopPlaybackMode();
|
this.rdioScannerService.stopPlaybackMode();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.appRdioScannerService.stop();
|
this.rdioScannerService.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -455,7 +440,12 @@ export class AppRdioScannerSearchComponent implements OnDestroy {
|
||||||
if ('config' in event) {
|
if ('config' in event) {
|
||||||
this.config = event.config;
|
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) {
|
if ('livefeedMode' in event) {
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,28 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset *ngFor="let system of systems" class="fieldset">
|
<ng-container *ngFor="let system of systems">
|
||||||
<legend>
|
<fieldset *ngIf="system.talkgroups.length" class="fieldset">
|
||||||
{{ system.label }}
|
<legend>
|
||||||
</legend>
|
{{ system.label }}
|
||||||
<div>
|
</legend>
|
||||||
<button *ngFor="let talkgroup of system.talkgroups" class="rdio-button"
|
<div>
|
||||||
[ngClass]="{ off: !(map[system.id] && map[system.id][talkgroup.id]), on: map[system.id] && map[system.id][talkgroup.id] }"
|
<button *ngFor="let talkgroup of system.talkgroups" class="rdio-button"
|
||||||
(click)="avoid({ system: system, talkgroup: talkgroup })">
|
[ngClass]="{ off: !(map[system.id] && map[system.id][talkgroup.id]), on: map[system.id] && map[system.id][talkgroup.id] }"
|
||||||
{{ talkgroup.label }}
|
(click)="avoid({ system: system, talkgroup: talkgroup })">
|
||||||
</button>
|
{{ talkgroup.label }}
|
||||||
<ng-container *ngIf="system.talkgroups.length > 1">
|
|
||||||
<button class="rdio-button-mini all-off" (click)="avoid({ system: system, status: false })">
|
|
||||||
OFF
|
|
||||||
</button>
|
</button>
|
||||||
<button class="rdio-button-mini all-on" (click)="avoid({ system: system, status: true })">
|
<ng-container *ngIf="system.talkgroups.length > 1">
|
||||||
ON
|
<button class="rdio-button-mini all-off" (click)="avoid({ system: system, status: false })">
|
||||||
</button>
|
OFF
|
||||||
</ng-container>
|
</button>
|
||||||
</div>
|
<button class="rdio-button-mini all-on" (click)="avoid({ system: system, status: true })">
|
||||||
</fieldset>
|
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;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
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) {
|
@media (min-width: 720px) {
|
||||||
.rdio-button {
|
.rdio-button {
|
||||||
width: calc(20% - 8px);
|
width: calc(20% - 4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rdio-button-mini {
|
.rdio-button-mini {
|
||||||
width: calc(10% - 8px);
|
width: calc(10% - 4px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1200px) {
|
||||||
.rdio-button {
|
.rdio-button {
|
||||||
width: calc(10% - 8px);
|
width: calc(10% - 4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rdio-button-mini {
|
.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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -19,31 +19,31 @@
|
||||||
|
|
||||||
import { Component, OnDestroy } from '@angular/core';
|
import { Component, OnDestroy } from '@angular/core';
|
||||||
import { RdioScannerAvoidOptions, RdioScannerBeepStyle, RdioScannerEvent, RdioScannerGroup, RdioScannerLivefeedMap, RdioScannerSystem } from '../rdio-scanner';
|
import { RdioScannerAvoidOptions, RdioScannerBeepStyle, RdioScannerEvent, RdioScannerGroup, RdioScannerLivefeedMap, RdioScannerSystem } from '../rdio-scanner';
|
||||||
import { AppRdioScannerService } from '../rdio-scanner.service';
|
import { RdioScannerService } from '../rdio-scanner.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-rdio-scanner-select',
|
selector: 'rdio-scanner-select',
|
||||||
styleUrls: [
|
styleUrls: [
|
||||||
'../common.scss',
|
'../common.scss',
|
||||||
'./select.component.scss',
|
'./select.component.scss',
|
||||||
],
|
],
|
||||||
templateUrl: './select.component.html',
|
templateUrl: './select.component.html',
|
||||||
})
|
})
|
||||||
export class AppRdioScannerSelectComponent implements OnDestroy {
|
export class RdioScannerSelectComponent implements OnDestroy {
|
||||||
groups: RdioScannerGroup[] | undefined;
|
groups: RdioScannerGroup[] | undefined;
|
||||||
|
|
||||||
map: RdioScannerLivefeedMap = {};
|
map: RdioScannerLivefeedMap = {};
|
||||||
|
|
||||||
systems: RdioScannerSystem[] | undefined;
|
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 {
|
avoid(options?: RdioScannerAvoidOptions): void {
|
||||||
this.appRdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||||
|
|
||||||
this.appRdioScannerService.avoid(options);
|
this.rdioScannerService.avoid(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
|
@ -51,14 +51,14 @@ export class AppRdioScannerSelectComponent implements OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleGroup(label: string): void {
|
toggleGroup(label: string): void {
|
||||||
this.appRdioScannerService.beep();
|
this.rdioScannerService.beep();
|
||||||
|
|
||||||
this.appRdioScannerService.toggleGroup(label);
|
this.rdioScannerService.toggleGroup(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
private eventHandler(event: RdioScannerEvent): void {
|
private eventHandler(event: RdioScannerEvent): void {
|
||||||
if (event.config) {
|
if (event.config) {
|
||||||
this.systems = this.sortSystems(event.config.systems);
|
this.systems = event.config.systems;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.groups) {
|
if (event.groups) {
|
||||||
|
|
@ -69,21 +69,4 @@ export class AppRdioScannerSelectComponent implements OnDestroy {
|
||||||
this.map = event.map;
|
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
|
* 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
|
* 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');
|
@Component({
|
||||||
const systemFactory = require('./system');
|
selector: 'rdio-scanner-main-page',
|
||||||
|
template: '<rdio-scanner></rdio-scanner>',
|
||||||
class Models {
|
})
|
||||||
constructor(ctx = {}) {
|
export class RdioScannerMainPageComponent { }
|
||||||
this.call = callFactory(ctx);
|
|
||||||
|
|
||||||
this.system = systemFactory(ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Models;
|
|
||||||
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
|
* 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
|
* 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
|
* 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
|
* 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 { NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatNativeDateModule } from '@angular/material/core';
|
import { MatNativeDateModule } from '@angular/material/core';
|
||||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
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 { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatListModule } from '@angular/material/list';
|
||||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||||
|
import { MatRadioModule } from '@angular/material/radio';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||||
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
exports: [
|
exports: [
|
||||||
|
DragDropModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
|
MatCheckboxModule,
|
||||||
MatDatepickerModule,
|
MatDatepickerModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatExpansionModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
|
MatListModule,
|
||||||
MatNativeDateModule,
|
MatNativeDateModule,
|
||||||
MatPaginatorModule,
|
MatPaginatorModule,
|
||||||
MatProgressBarModule,
|
MatProgressBarModule,
|
||||||
|
MatRadioModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
MatSidenavModule,
|
MatSidenavModule,
|
||||||
MatSlideToggleModule,
|
MatSlideToggleModule,
|
||||||
|
MatSnackBarModule,
|
||||||
MatTableModule,
|
MatTableModule,
|
||||||
MatToolbarModule,
|
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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -20,9 +20,15 @@
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { ExtraOptions, RouterModule, Routes } from '@angular/router';
|
||||||
import { AppLocaleModule } from './locale/locale.module';
|
import { AppLocaleModule } from './locale/locale.module';
|
||||||
import { AppMaterialModule } from './material/material.module';
|
import { AppMaterialModule } from './material/material.module';
|
||||||
|
|
||||||
|
export interface AppSharedModuleConfig {
|
||||||
|
routerExtraOptions?: ExtraOptions;
|
||||||
|
routerRoutes?: Routes;
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
exports: [
|
exports: [
|
||||||
AppLocaleModule,
|
AppLocaleModule,
|
||||||
|
|
@ -30,14 +36,25 @@ import { AppMaterialModule } from './material/material.module';
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
RouterModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppSharedModule {
|
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 {
|
return {
|
||||||
ngModule: AppSharedModule,
|
ngModule: AppSharedModule,
|
||||||
providers: [
|
providers: [
|
||||||
...AppLocaleModule.forRoot().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="apple-touch-icon" sizes="192x192" href="assets/icons/icon-192x192.png">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
<link rel="manifest" href="manifest.webmanifest">
|
<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 charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
<title>Rdio Scanner</title>
|
<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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -27,5 +27,4 @@ if (environment.production) {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
platformBrowserDynamic().bootstrapModule(AppModule).catch((error) => console.error(error));
|
||||||
.catch((error) => console.error(error));
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -19,22 +19,21 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const CallUpload = require('./call-upload');
|
const server = 'http://localhost:3000';
|
||||||
const TrunkRecorderCallUpload = require('./trunk-recorder-call-upload');
|
|
||||||
|
|
||||||
class API {
|
const PROXY_CONFIG = [
|
||||||
constructor(ctx = {}) {
|
{
|
||||||
this.config = ctx.config;
|
bypass: (req) => req.upgrade ? null : '/',
|
||||||
|
context: '/',
|
||||||
this.controller = ctx.controller;
|
secure: false,
|
||||||
|
target: server,
|
||||||
this.router = ctx.router;
|
ws: true,
|
||||||
|
},
|
||||||
this.routes = {
|
{
|
||||||
callUpload: new CallUpload(this),
|
context: ['/api'],
|
||||||
trunkRecorderCallUpload: new TrunkRecorderCallUpload(this),
|
secure: false,
|
||||||
};
|
target: server,
|
||||||
}
|
}
|
||||||
}
|
];
|
||||||
|
|
||||||
module.exports = API
|
module.exports = PROXY_CONFIG;
|
||||||
|
|
@ -7,7 +7,7 @@ $custom-typography: mat-typography-config($font-family: $font-family);
|
||||||
@include mat-core($custom-typography);
|
@include mat-core($custom-typography);
|
||||||
|
|
||||||
$primary: mat-palette($mat-grey, 800, 50);
|
$primary: mat-palette($mat-grey, 800, 50);
|
||||||
$accent: mat-palette($mat-indigo);
|
$accent: mat-palette($mat-green);
|
||||||
|
|
||||||
$theme: mat-light-theme($primary, $accent);
|
$theme: mat-light-theme($primary, $accent);
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue