new v6-beta with backend rewritten in go
|
|
@ -1,7 +0,0 @@
|
|||
dist
|
||||
client/dist
|
||||
client/node_modules
|
||||
server/config.json*
|
||||
server/database.sqlite*
|
||||
server/dist
|
||||
server/node_modules
|
||||
3
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
|||
.DS_Store
|
||||
.vscode
|
||||
*.token
|
||||
/dist
|
||||
/build
|
||||
/dist
|
||||
|
|
|
|||
12
.setversion
|
|
@ -1,12 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
dir=$(dirname $0)
|
||||
oldver=$(awk '/"version":/{ gsub(/"/, "", $2); print $2 }' $dir/package.json)
|
||||
newver=${1:-${oldver}}
|
||||
|
||||
sed -i -re "s/^(\s*\"version\":).*$/\1 \"$newver\"/" \
|
||||
$dir/package.json \
|
||||
$dir/client/package.json \
|
||||
$dir/server/package.json
|
||||
sed -i -re "s/(version\s*=\s*')[^']*('.*)/\1$newver\2/" \
|
||||
$dir/server/lib/rdio-scanner/version.js
|
||||
64
CHANGELOG.md
|
|
@ -1,11 +1,33 @@
|
|||
# Version 5.2
|
||||
# Change log
|
||||
|
||||
## Version 6.0
|
||||
|
||||
- Backend server rewritten in Go for better performance and ease of installation.
|
||||
- New toggle by tags option to toggle talkgroups by their tag in addition to their group.
|
||||
- Buttons on the select panel now sound differently depending on their state.
|
||||
- You can now filter calls by date and time on the search panel.
|
||||
- Installable as a service from the command line.
|
||||
- Let's Encrypt automatic generation of certificates from the command line.
|
||||
- A bunch of minor fixes and improvements.
|
||||
|
||||
### BREAKING CHANGES SINCE V5
|
||||
|
||||
[Rdio Scanner](https://github.com/chuot/rdio-scanner) is now distributed as a precompiled executable in a zip file, which also contains documentation on how it works.
|
||||
|
||||
The backend server has been completely rewritten in GO language. Therefore, all the subpackages used in v5 had to be replaced with new ones. These new subpackages do not necessarily have the same functionality as those of v5.
|
||||
|
||||
- No more polling mode for _dirwatch_, which in a way is a good thing as polling was disastrous for CPU consumption. The alternative is to install a local instance and use the downstream feature to feed your main instance.
|
||||
- Due to the polling situation, the Docker version of Rdio Scanner is no longer offered. Since it is now so easy to install v6 on various platforms, that was a no-brainer.
|
||||
- Default database name changed from _database.sqlite_ to _rdio-scanner.db_. You will need to rename your database file with the new name if you want to convert it. Otherwise, a new database will be created.
|
||||
|
||||
## Version 5.2
|
||||
|
||||
- Change to how the server reports version.
|
||||
- Fix cmd.js exiting on inexistant session token keystore.
|
||||
- Fix issue with iframe.
|
||||
- Node modules updated for security fixes.
|
||||
|
||||
_v5.2.1_
|
||||
_v.5.2.1_
|
||||
|
||||
- Fix talkgroup header on the search panel (issue #47).
|
||||
- Update dirwatch meta tags #DATE, #TIME and #ZTIME for SDRSharp compatibility (issue #48).
|
||||
|
|
@ -53,7 +75,7 @@ _V5.2.9_
|
|||
- Fix bad code for server options parsing.
|
||||
- Increase dirwatch polling interval from 1000ms to 2500ms.
|
||||
|
||||
# Version 5.1
|
||||
## Version 5.1
|
||||
|
||||
This one is a big one... **Be sure to backup your config.json and your database.sqlite before updating.**
|
||||
|
||||
|
|
@ -85,7 +107,7 @@ _v5.1.2_
|
|||
- Remove the autoJsonMap from the sequelize dialectOptions.
|
||||
- Client updated to angular 12.
|
||||
|
||||
# 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.sortTalkgroupswhich by default is false. Sort talkgroups based on their ID.
|
||||
|
|
@ -118,7 +140,7 @@ _v5.0.5_
|
|||
- Improve documentation in regards to minimal Node.js LTS version.
|
||||
- Add python to build requirements (to be able to build SQLite node module).
|
||||
|
||||
# Version 4.9
|
||||
## Version 4.9
|
||||
|
||||
- Add basic duplicate call detection and rejection.
|
||||
- Add keyboard shortcuts for the main buttons.
|
||||
|
|
@ -136,7 +158,7 @@ _v4.9.2_
|
|||
|
||||
- Add Config.options.disableKeyboardShortcuts to make everyone a happy camper.
|
||||
|
||||
# Version 4.8
|
||||
## Version 4.8
|
||||
|
||||
- Add downstream.system.id_as property to allow export system with a different id.
|
||||
- Add system.order for system list ordering on the client side.
|
||||
|
|
@ -174,7 +196,7 @@ _v4.8.5_
|
|||
- Fix broken dirwatch.delay.
|
||||
- Node modules update.
|
||||
|
||||
# Version 4.7
|
||||
## Version 4.7
|
||||
|
||||
- New dirWatch.type='sdr-trunk'.
|
||||
- New search panel layout with new group and tag filters.
|
||||
|
|
@ -212,7 +234,7 @@ _v4.7.6_
|
|||
- Skip next button no longer emit a denied beeps sequence when pushed while there's no audio playing.
|
||||
- Node modules update.
|
||||
|
||||
# Version 4.6
|
||||
## Version 4.6
|
||||
|
||||
- Fix documentation in regards to load-rrd in install-github.md.
|
||||
- Fix database absolute path in config.json.
|
||||
|
|
@ -221,12 +243,12 @@ _v4.7.6_
|
|||
- Config.options.keypadBeeps now with presets instead of full pattern declaration.
|
||||
- Bug fixes.
|
||||
|
||||
# Version 4.5
|
||||
## Version 4.5
|
||||
|
||||
- Config.options.keyBeep which by default is true.
|
||||
- Bug fixes.
|
||||
|
||||
# Version 4.4
|
||||
## Version 4.4
|
||||
|
||||
- Config.systems.talkgroups.patches to group many talkgroups (patches) into one talkgroup.id.
|
||||
- Config.options now groups allowDownloads, disableAudioConversion, pruneDays, useDimmer, useGroup and useLed options instead of having them spread all over the config file.
|
||||
|
|
@ -234,7 +256,7 @@ _v4.7.6_
|
|||
- Fix annoying bug when next call queued to play is still played even though offline continuous play mode is turned off.
|
||||
- Talkgroup ID is displayed no matter what and unit ID is displayed only if known.
|
||||
|
||||
# Version 4.3
|
||||
## Version 4.3
|
||||
|
||||
- Add metatags to converted audio files.
|
||||
- Automatic database migration on startup.
|
||||
|
|
@ -246,18 +268,18 @@ _v4.7.6_
|
|||
- Fix SSL certificate handling.
|
||||
- Rewritten documentation.
|
||||
|
||||
# Version 4.2
|
||||
## Version 4.2
|
||||
|
||||
- Fix possible race conditions....
|
||||
- Added websocket keepalive which helps mobile clients when switching from/to wifi/wan.
|
||||
- Better playback offline mode animations and queue count.
|
||||
- New dirWatch.mask option to simplify meta data import.
|
||||
|
||||
# Version 4.1
|
||||
## Version 4.1
|
||||
|
||||
- New offline playback mode.
|
||||
|
||||
# Version 4.0
|
||||
## Version 4.0
|
||||
|
||||
- GraphQL replaced by a pure websocket command and control system.
|
||||
- `server/.env` replaced by a `server/config.json`.
|
||||
|
|
@ -271,7 +293,7 @@ _v4.7.6_
|
|||
- Customizable LED colors by systems/talkgroups.
|
||||
- Dimmable display based on active call.
|
||||
|
||||
## Upgrading from version 3
|
||||
### Upgrading from version 3
|
||||
|
||||
- Your `server/.env` file will be used to create the new `server/config.json` file. Then the `server/.env` will be deleted.
|
||||
- The `rdioScannerSystems` table will be used to create the _rdioScanner.systems_ within `server/config.json`. Then the `rdioScannerSystems` table will be purged.
|
||||
|
|
@ -280,12 +302,12 @@ _v4.7.6_
|
|||
|
||||
> YOU SHOULD BACKUP YOUR `SERVER/.ENV` FILE AND YOUR DATABASE PRIOR TO UPGRADING, JUST IN CASE. WE'VE TESTED THE UPGRADE PROCESS MANY TIMES, BUT WE CAN'T KNOW FOR SURE IF IT'S GOING TO WORK WELL ON YOUR SIDE.
|
||||
|
||||
# Version 3.1
|
||||
## Version 3.1
|
||||
|
||||
- Client now on Angular 9.
|
||||
- Display listeners count on the server's end.
|
||||
|
||||
# Version 3.0
|
||||
## Version 3.0
|
||||
|
||||
- Unit aliases support, display names instead of unit ID.
|
||||
- Download calls from the search panel.
|
||||
|
|
@ -293,18 +315,18 @@ _v4.7.6_
|
|||
|
||||
> Note that you can only update from version 2.0 and above. You have to do a fresh install if your actual version is prior to version 2.0.
|
||||
|
||||
# Version 2.5
|
||||
## Version 2.5
|
||||
|
||||
- New group toggle on the select panel.
|
||||
|
||||
# Version 2.1
|
||||
## Version 2.1
|
||||
|
||||
- Various speed improvements for searching stored calls.
|
||||
|
||||
# Version 2.0
|
||||
## Version 2.0
|
||||
|
||||
- Ditched meteor in favour of GraphQL.
|
||||
|
||||
# Version 1.0
|
||||
## Version 1.0
|
||||
|
||||
- First public version.
|
||||
|
|
|
|||
30
Dockerfile
|
|
@ -1,30 +0,0 @@
|
|||
FROM --platform=${BUILDPLATFORM:-amd64} node:lts-alpine as client-build
|
||||
WORKDIR /app/client
|
||||
COPY client/. .
|
||||
RUN NG_CLI_ANALYTICS=0 npm ci --loglevel=error --no-progress && \
|
||||
npm run build
|
||||
|
||||
FROM node:lts-alpine as server-build
|
||||
WORKDIR /app/server
|
||||
COPY server/. .
|
||||
RUN apk add --no-cache --no-progress --quiet python make gcc g++ linux-headers && \
|
||||
npm config set jobs max && \
|
||||
npm ci --loglevel=error --no-progress && \
|
||||
npm run build && \
|
||||
npm prune --production
|
||||
|
||||
FROM node:lts-alpine
|
||||
LABEL maintainer="Chrystian Huot <chrystian.huot@saubeo.solutions>"
|
||||
WORKDIR /app
|
||||
ENV APP_DATA=/app/data \
|
||||
APP_PORT=3000 \
|
||||
DOCKER=1
|
||||
RUN apk --no-cache --no-progress add ffmpeg openssl sqlite tzdata && \
|
||||
mkdir -p "${APP_DATA}"
|
||||
COPY LICENSE .
|
||||
COPY --from=client-build /app/client/dist/rdio-scanner/. client/.
|
||||
COPY --from=server-build /app/server/node_modules/. server/node_modules/.
|
||||
COPY --from=server-build /app/server/dist/index.js server/.
|
||||
VOLUME [ "/app/data" ]
|
||||
EXPOSE ${APP_PORT}
|
||||
ENTRYPOINT [ "node", "server" ]
|
||||
106
Makefile
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
################################################################################
|
||||
## 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/>
|
||||
################################################################################
|
||||
|
||||
app := rdio-scanner
|
||||
ver := 6.0.0
|
||||
|
||||
client := $(wildcard client/*.json client/*.ts)
|
||||
server := $(wildcard server/*.go)
|
||||
|
||||
build = @cd server && GOOS=$(1) GOARCH=$(2) go build -o ../dist/$(1)-$(2)/$(3)
|
||||
pandoc = @test -d dist/$(1)-$(2) || mkdir -p dist/$(1)-$(2) && pandoc -f markdown -o dist/$(1)-$(2)/$(3) --resource-path docs:docs/platforms $(4) docs/webapp.md CHANGELOG.md
|
||||
zip = @cd dist/$(1)-$(2) && zip -q ../$(app)-$(1)-$(2)-v$(ver).zip * && cd .. && rm -fr $(1)-$(2)
|
||||
|
||||
.PHONY: all clean dist setver
|
||||
.PHONY: darwin darwin-amd64 darwin-arm64
|
||||
.PHONY: freebsd freebsd-amd64
|
||||
.PHONY: linux linux-386 linux-amd64 linux-arm linux-arm64
|
||||
.PHONY: windows windows-amd64
|
||||
|
||||
all: clean dist
|
||||
|
||||
clean:
|
||||
@rm -fr client/node_modules dist server/webapp
|
||||
|
||||
dist: darwin freebsd linux windows
|
||||
|
||||
setver:
|
||||
@sed -i -re "s/^(\s*\"version\":).*$$/\1 \"$(ver)\"/" client/package.json
|
||||
@sed -i -re "s/^(const\s+Version\s+=).*$$/\1 \"$(ver)\"/" server/version.go
|
||||
@sed -i -re "s/v[0-9]\.[0-9]\.[0-9]/v$(ver)/" docs/platforms/*.md
|
||||
|
||||
webapp: server/webapp/index.html
|
||||
|
||||
server/webapp/index.html: $(client)
|
||||
@cd client && test -d node_modules || npm ci --loglevel=error --no-progress
|
||||
@cd client && npm run build
|
||||
|
||||
darwin: darwin-amd64 darwin-arm64
|
||||
darwin-amd64: webapp dist/$(app)-darwin-amd64-v$(ver).zip
|
||||
darwin-arm64: webapp dist/$(app)-darwin-arm64-v$(ver).zip
|
||||
|
||||
dist/$(app)-darwin-amd64-v$(ver).zip: $(server)
|
||||
$(call pandoc,darwin,amd64,rdio-scanner.pdf,docs/platforms/darwin.md,CHANGELOG.md)
|
||||
$(call build,darwin,amd64,$(app))
|
||||
$(call zip,darwin,amd64,$(app))
|
||||
|
||||
dist/$(app)-darwin-arm64-v$(ver).zip: $(server)
|
||||
$(call pandoc,darwin,arm64,rdio-scanner.pdf,docs/platforms/darwin.md,CHANGELOG.md)
|
||||
$(call build,darwin,arm64,$(app))
|
||||
$(call zip,darwin,arm64,$(app))
|
||||
|
||||
freebsd: freebsd-amd64
|
||||
freebsd-amd64: webapp dist/$(app)-freebsd-amd64-v$(ver).zip
|
||||
|
||||
dist/$(app)-freebsd-amd64-v$(ver).zip: $(server)
|
||||
$(call pandoc,freebsd,amd64,rdio-scanner.pdf,docs/platforms/freebsd.md,CHANGELOG.md)
|
||||
$(call build,freebsd,amd64,$(app))
|
||||
$(call zip,freebsd,amd64,$(app))
|
||||
|
||||
linux: linux-386 linux-amd64 linux-arm linux-arm64
|
||||
linux-386: webapp dist/$(app)-linux-386-v$(ver).zip
|
||||
linux-amd64: webapp dist/$(app)-linux-amd64-v$(ver).zip
|
||||
linux-arm: webapp dist/$(app)-linux-arm-v$(ver).zip
|
||||
linux-arm64: webapp dist/$(app)-linux-arm64-v$(ver).zip
|
||||
|
||||
dist/$(app)-linux-386-v$(ver).zip: $(server)
|
||||
$(call pandoc,linux,386,rdio-scanner.pdf,docs/platforms/linux.md,CHANGELOG.md)
|
||||
$(call build,linux,386,$(app))
|
||||
$(call zip,linux,386,$(app))
|
||||
|
||||
dist/$(app)-linux-amd64-v$(ver).zip: $(server)
|
||||
$(call pandoc,linux,amd64,rdio-scanner.pdf,docs/platforms/linux.md,CHANGELOG.md)
|
||||
$(call build,linux,amd64,$(app))
|
||||
$(call zip,linux,amd64,$(app))
|
||||
|
||||
dist/$(app)-linux-arm-v$(ver).zip: $(server)
|
||||
$(call pandoc,linux,arm,rdio-scanner.pdf,docs/platforms/linux.md,CHANGELOG.md)
|
||||
$(call build,linux,arm,$(app))
|
||||
$(call zip,linux,arm,$(app))
|
||||
|
||||
dist/$(app)-linux-arm64-v$(ver).zip: $(server)
|
||||
$(call pandoc,linux,arm64,rdio-scanner.pdf,docs/platforms/linux.md,CHANGELOG.md)
|
||||
$(call build,linux,arm64,$(app))
|
||||
$(call zip,linux,arm64,$(app))
|
||||
|
||||
windows: windows-amd64
|
||||
windows-amd64: webapp dist/$(app)-windows-amd64-v$(ver).zip
|
||||
|
||||
dist/$(app)-windows-amd64-v$(ver).zip: $(server)
|
||||
$(call pandoc,windows,amd64,rdio-scanner.pdf,docs/platforms/windows.md,CHANGELOG.md)
|
||||
$(call build,windows,amd64,$(app).exe)
|
||||
$(call zip,windows,amd64,$(app))
|
||||
79
README.md
|
|
@ -1,71 +1,54 @@
|
|||
# Rdio Scanner
|
||||
\
|
||||
|
||||
[Rdio Scanner](https://github.com/chuot/rdio-scanner) is a progressive web interface for listening to audio files generated by various recorders. Its interface tries to reproduce the user experience of a real police scanner, while adding its own touch.
|
||||
# What is it ?
|
||||
|
||||
The number of available recorders continues to grow. Among them, [Rdio Scanner](https://github.com/chuot/rdio-scanner) has proven to be compatible and even very useful when used in conjunction with [Trunk Recorder](https://github.com/robotastic/trunk-recorder), [RTLSDR-Airband](https://github.com/szpajder/RTLSDR-Airband), [SDRTrunk](https://github.com/DSheirer/sdrtrunk), [csdr](https://github.com/ha7ilm/csdr) and many others.
|
||||
[Rdio Scanner](https://github.com/chuot/rdio-scanner) is an open source software that ingest and distribute audio files generated by various software-defined radio recorders. Its interface tries to reproduce the user experience of a real police scanner, while adding its own touch.
|
||||
|
||||
The client web app is responsive and also works well on mobile devices.
|
||||
You can listen to [Rdio Scanner](https://github.com/chuot/rdio-scanner) on any modern browsers using the integrated web app.
|
||||
|
||||
## How it works
|
||||
# Recorders compatibility
|
||||
|
||||
[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) works with any radio recorder, as long as they can create audio files separated by conversations or transmissions.
|
||||
|
||||
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.
|
||||
Here is a list of recorders known to work with [Rdio Scanner](https://github.com/chuot/rdio-scanner):
|
||||
|
||||
## Main features
|
||||
| Recorder | API | Dirwatch |
|
||||
| -------------------------------------------------------------- | --- | -------- |
|
||||
| [Trunk Recorder](https://github.com/robotastic/trunk-recorder) | X | X |
|
||||
| [RTLSDR-Airband](https://github.com/szpajder/RTLSDR-Airband) | | X |
|
||||
| [sdrtrunk](https://github.com/DSheirer/sdrtrunk) | | X |
|
||||
| [voxcall](https://github.com/aaknitt/voxcall) | X | |
|
||||
|
||||
- Simplistic interface, but strong usability.
|
||||
- Administrative dashboard for easy server configuration.
|
||||
- Configurable systems and talkgroups.
|
||||
- Talkgroups from any systems can be grouped together for better selection of live streams.
|
||||
- Access control via password with possible restriction to systems/talkgroups.
|
||||
- Downloadable audio files from the database archives.
|
||||
- Folder monitoring for new audio files to be ingested.
|
||||
- Generic API to which you can push your audio files.
|
||||
- Redistribute systems/talkgroups to other [Rdio Scanner](https://github.com/chuot/rdio-scanner) instances.
|
||||
- Automatic pruning of the database after a configurable number of days.
|
||||
- Unit ID alias where you can display a label instead of its numeric value.
|
||||
- Configurable LED colors based on system or talkgroup.
|
||||
# Quick start
|
||||
|
||||
## Screenshots
|
||||
ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rdio-scanner) FROM ITS OFFICIAL REPOSITORY AT **[HTTPS://GITHUB.COM/CHUOT/RDIO-SCANNER](https://github.com/chuot/rdio-scanner)**.
|
||||
|
||||

|
||||
1. Download the the latest precompiled version of [Rdio Scanner](https://github.com/chuot/rdio-scanner) from the [releases tab](https://github.com/chuot/rdio-scanner/releases).
|
||||
2. Extract the contents of the archive somewhere on your computer.
|
||||
3. Run the [Rdio Scanner](https://github.com/chuot/rdio-scanner) executable.
|
||||
4. Access the administrative dashboard to finalize the configuration.
|
||||
|
||||

|
||||
More detailed instructions are available in the `rdio-scanner.pdf` file provided in the precompiled archives.
|
||||
|
||||

|
||||
# Need help ?
|
||||
|
||||
## Quickstart
|
||||
You can ask your questions or post your comments on the [Gitter Rdio Scanner Lobby](https://gitter.im/rdio-scanner/Lobby) at **[https://gitter.im/rdio-scanner/Lobby](https://gitter.im/rdio-scanner/Lobby)** where the community keeps growing.
|
||||
|
||||
- [docs/install-docker.md](./docs/install-docker.md) - Installation from the Docker image (easiest)
|
||||
- [docs/install-github.md](./docs/install-github.md) - Installation from the Github repository (longer and harder)
|
||||
- [docs/config.md](./docs/config.md) - The complete reference to the `config.json` file
|
||||
- [docs/webapp.md](./docs/webapp.md) - How to operate the frontend interface
|
||||
- [docs/api.md](./docs/api.md) - [Rdio Scanner](https://github.com/chuot/rdio-scanner) API's reference
|
||||
- [docs/examples](./docs/examples) - Some miscellaneous examples
|
||||
- [CONTRIBUTING.md](./CONTRIBUTING.md) - Contribution guidelines
|
||||
# Show your appreciation, support the author
|
||||
|
||||
> Raspberry Pi users can use this [script](https://github.com/chuot/rdio-scanner-pi-setup) to simplify the installation procedure.
|
||||
If you like [Rdio Scanner](https://github.com/chuot/rdio-scanner), **[consider starring the GitHub repository](https://github.com/chuot/rdio-scanner)** to show you appreciation to the author for his hard work. It cost nothing but is really appreciated.
|
||||
|
||||
## Questions / Comments
|
||||
If you use [Rdio Scanner](https://github.com/chuot/rdio-scanner) for commercial purposes or derive income from it, **[consider sponsoring the project](https://github.com/sponsors/chuot)** to help support continued development.
|
||||
|
||||
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.
|
||||
# Improve your experience on the go
|
||||
|
||||
## Native app
|
||||
You can enjoy your [Rdio Scanner](https://github.com/chuot/rdio-scanner) on the go on your mobile device with the native app.
|
||||
|
||||
[Rdio Scanner](https://github.com/chuot/rdio-scanner) is now the perfect software-defined radio companion. Go get the native client here:
|
||||
|
||||
[](https://apps.apple.com/us/app/rdio-scanner/id1563065667#?platform=iphone)
|
||||
[](https://play.google.com/store/apps/details?id=solutions.saubeo.rdioScanner)
|
||||
[](https://apps.apple.com/us/app/rdio-scanner/id1563065667#?platform=iphone)
|
||||
[](https://play.google.com/store/apps/details?id=solutions.saubeo.rdioScanner)
|
||||
|
||||
# Commercial support
|
||||
|
||||
Commercial support is available at https://saubeo.solutions/.
|
||||
Commercial support is available at **[https://saubeo.solutions/](https://saubeo.solutions)**.
|
||||
|
||||
## Show your appreciation / Support the author
|
||||
|
||||
If you like [Rdio Scanner](https://github.com/chuot/rdio-scanner) please consider **starring the repository** to show you appreciation to the author for his hard work. It cost nothing but is really appreciated.
|
||||
|
||||
If you use [Rdio Scanner](https://github.com/chuot/rdio-scanner) for commercial purposes or derive income from it, please consider [sponsoring this project](https://github.com/sponsors/chuot) to help support continued development.
|
||||
|
||||
**Happy Rdio scanning !**
|
||||
**Happy Rdio scanning !**
|
||||
|
|
@ -11,7 +11,6 @@
|
|||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major version
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
||||
|
|
|
|||
2
client/.gitignore
vendored
|
|
@ -12,7 +12,6 @@
|
|||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
|
|
@ -32,6 +31,7 @@ speed-measure-plugin*.json
|
|||
.history/*
|
||||
|
||||
# misc
|
||||
/.angular/cache
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
|
|
|
|||
|
|
@ -20,15 +20,12 @@
|
|||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"allowedCommonJsDependencies": [
|
||||
"@angular/common/locales/en-CA"
|
||||
],
|
||||
"outputPath": "dist/rdio-scanner",
|
||||
"outputPath": "../server/webapp",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
|
|
@ -37,51 +34,55 @@
|
|||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"serviceWorker": true,
|
||||
"ngswConfigPath": "ngsw-config.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "1500kb",
|
||||
"maximumError": "1500kb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "3kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "7kb",
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
],
|
||||
"serviceWorker": true,
|
||||
"ngswConfigPath": "ngsw-config.json"
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "rdio-scanner:build",
|
||||
"proxyConfig": "src/proxy.conf.js"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "rdio-scanner:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "rdio-scanner:build:development",
|
||||
"proxyConfig": "src/proxy.conf.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
|
|
@ -89,13 +90,14 @@
|
|||
"browserTarget": "rdio-scanner:build"
|
||||
}
|
||||
},
|
||||
"rdio-scanner": {
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
|
|
@ -106,33 +108,9 @@
|
|||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "rdio-scanner:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "rdio-scanner:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}},
|
||||
}
|
||||
},
|
||||
"defaultProject": "rdio-scanner"
|
||||
}
|
||||
}
|
||||
22698
client/package-lock.json
generated
|
|
@ -1,41 +1,43 @@
|
|||
{
|
||||
"author": "Chrystian Huot <chrystian.huot@saubeo.solutions>",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^12.2.0",
|
||||
"@angular/cdk": "^12.2.0",
|
||||
"@angular/common": "~12.2.0",
|
||||
"@angular/compiler": "~12.2.0",
|
||||
"@angular/core": "~12.2.0",
|
||||
"@angular/forms": "~12.2.0",
|
||||
"@angular/material": "^12.2.0",
|
||||
"@angular/platform-browser": "~12.2.0",
|
||||
"@angular/platform-browser-dynamic": "~12.2.0",
|
||||
"@angular/router": "^12.2.0",
|
||||
"@angular/service-worker": "^12.2.0",
|
||||
"rxjs": "~6.5.5",
|
||||
"tslib": "^2.3.0",
|
||||
"@angular/animations": "^13.0.2",
|
||||
"@angular/cdk": "^13.0.2",
|
||||
"@angular/common": "~13.0.2",
|
||||
"@angular/compiler": "~13.0.2",
|
||||
"@angular/core": "~13.0.2",
|
||||
"@angular/forms": "~13.0.2",
|
||||
"@angular/material": "^13.0.2",
|
||||
"@angular/platform-browser": "~13.0.2",
|
||||
"@angular/platform-browser-dynamic": "~13.0.2",
|
||||
"@angular/router": "^13.0.2",
|
||||
"@angular/service-worker": "^13.0.2",
|
||||
"rxjs": "~7.4.0",
|
||||
"tslib": "^2.3.1",
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~12.2.0",
|
||||
"@angular/cli": "^12.2.0",
|
||||
"@angular/compiler-cli": "~12.2.0",
|
||||
"@types/node": "~16.4.13",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||
"@typescript-eslint/parser": "^4.29.0",
|
||||
"eslint": "^7.32.0",
|
||||
"ts-node": "~10.1.0",
|
||||
"typescript": "~4.2.4"
|
||||
"@angular-devkit/build-angular": "~13.0.3",
|
||||
"@angular/cli": "^13.0.3",
|
||||
"@angular/compiler-cli": "~13.0.2",
|
||||
"@types/node": "~16.11.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.5.0",
|
||||
"@typescript-eslint/parser": "^5.5.0",
|
||||
"ajv-keywords": "^5.1.0",
|
||||
"eslint": "^8.3.0",
|
||||
"npm-check-updates": "^12.0.2",
|
||||
"ts-node": "~10.4.0",
|
||||
"typescript": "~4.4.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
"node": ">=16.0"
|
||||
},
|
||||
"license": "LICENSE",
|
||||
"name": "rdio-scanner-client",
|
||||
"name": "rdio-scanner",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "ng build --base-href ./ --configuration production",
|
||||
"start": "ng serve"
|
||||
},
|
||||
"version": "5.2.9"
|
||||
"version": "6.0.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ 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';
|
||||
import { firstValueFrom, timer } from 'rxjs';
|
||||
import { AppUpdateService } from '../../../shared/update/update.service';
|
||||
|
||||
export interface Access {
|
||||
_id?: string;
|
||||
|
|
@ -52,7 +53,7 @@ export interface ApiKey {
|
|||
|
||||
export interface DirWatch {
|
||||
_id?: string;
|
||||
delay?: number;
|
||||
// delay?: number;
|
||||
deleteAfter?: boolean;
|
||||
directory?: string;
|
||||
disabled?: boolean;
|
||||
|
|
@ -63,7 +64,7 @@ export interface DirWatch {
|
|||
systemId?: number;
|
||||
talkgroupId?: number;
|
||||
type?: string;
|
||||
usePolling?: boolean;
|
||||
// usePolling?: boolean;
|
||||
}
|
||||
|
||||
export interface Downstream {
|
||||
|
|
@ -91,6 +92,7 @@ export interface Options {
|
|||
keypadBeeps?: string;
|
||||
pruneDays?: number;
|
||||
sortTalkgroups?: boolean;
|
||||
tagsToggle?: boolean;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
|
|
@ -207,6 +209,7 @@ export class RdioScannerAdminService implements OnDestroy {
|
|||
}
|
||||
|
||||
constructor(
|
||||
appUpdateService: AppUpdateService,
|
||||
private matSnackBar: MatSnackBar,
|
||||
private ngFormBuilder: FormBuilder,
|
||||
private ngHttpClient: HttpClient,
|
||||
|
|
@ -222,11 +225,11 @@ export class RdioScannerAdminService implements OnDestroy {
|
|||
|
||||
async changePassword(currentPassword: string, newPassword: string): Promise<void> {
|
||||
try {
|
||||
const res = await this.ngHttpClient.post<{ passwordNeedChange: boolean }>(
|
||||
const res = await firstValueFrom(this.ngHttpClient.post<{ passwordNeedChange: boolean }>(
|
||||
this.getUrl(url.password),
|
||||
{ currentPassword, newPassword },
|
||||
{ headers: this.getHeaders(), responseType: 'json' },
|
||||
).toPromise();
|
||||
));
|
||||
|
||||
this._passwordNeedChange = res.passwordNeedChange;
|
||||
|
||||
|
|
@ -242,13 +245,13 @@ export class RdioScannerAdminService implements OnDestroy {
|
|||
|
||||
async getConfig(): Promise<Config> {
|
||||
try {
|
||||
const res = await this.ngHttpClient.get<{
|
||||
const res = await firstValueFrom(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;
|
||||
|
|
@ -260,42 +263,42 @@ export class RdioScannerAdminService implements OnDestroy {
|
|||
|
||||
} catch (error) {
|
||||
this.errorHandler(error);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
getLeds(): string[] {
|
||||
return ['blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow'];
|
||||
}
|
||||
|
||||
async getLogs(options: LogsQueryOptions): Promise<LogsQuery | null> {
|
||||
async getLogs(options: LogsQueryOptions): Promise<LogsQuery | undefined> {
|
||||
try {
|
||||
const res = await this.ngHttpClient.post<LogsQuery>(
|
||||
const res = await firstValueFrom(this.ngHttpClient.post<LogsQuery>(
|
||||
this.getUrl(url.logs),
|
||||
options,
|
||||
{ headers: this.getHeaders(), responseType: 'json' },
|
||||
).toPromise();
|
||||
));
|
||||
|
||||
return res;
|
||||
|
||||
} catch (error) {
|
||||
this.errorHandler(error);
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async login(password: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await this.ngHttpClient.post<{
|
||||
const res = await firstValueFrom(this.ngHttpClient.post<{
|
||||
passwordNeedChange: boolean,
|
||||
token: string
|
||||
}>(
|
||||
this.getUrl(url.login),
|
||||
{ password },
|
||||
{ headers: this.getHeaders(), responseType: 'json' },
|
||||
).toPromise();
|
||||
));
|
||||
|
||||
this.token = res.token;
|
||||
|
||||
|
|
@ -319,11 +322,11 @@ export class RdioScannerAdminService implements OnDestroy {
|
|||
|
||||
async logout(): Promise<boolean> {
|
||||
try {
|
||||
await this.ngHttpClient.post(
|
||||
this.ngHttpClient.post(
|
||||
this.getUrl(url.logout),
|
||||
null,
|
||||
{ headers: this.getHeaders(), responseType: 'text' },
|
||||
).toPromise();
|
||||
);
|
||||
|
||||
this.configWebSocketClose();
|
||||
|
||||
|
|
@ -342,11 +345,11 @@ export class RdioScannerAdminService implements OnDestroy {
|
|||
|
||||
async saveConfig(config: Config): Promise<Config> {
|
||||
try {
|
||||
const res = await this.ngHttpClient.put<{ config: Config }>(
|
||||
const res = await firstValueFrom(this.ngHttpClient.put<{ config: Config }>(
|
||||
this.getUrl(url.config),
|
||||
config,
|
||||
{ headers: this.getHeaders(), responseType: 'json' },
|
||||
).toPromise();
|
||||
));
|
||||
|
||||
return res.config;
|
||||
|
||||
|
|
@ -396,7 +399,7 @@ export class RdioScannerAdminService implements OnDestroy {
|
|||
newDirWatchForm(dirWatch?: DirWatch): FormGroup {
|
||||
return this.ngFormBuilder.group({
|
||||
_id: [dirWatch?._id],
|
||||
delay: [dirWatch?.delay, Validators.min(0)],
|
||||
// delay: [dirWatch?.delay, Validators.min(0)],
|
||||
deleteAfter: [dirWatch?.deleteAfter],
|
||||
directory: [dirWatch?.directory, [Validators.required, this.validateDirectory()]],
|
||||
disabled: [dirWatch?.disabled],
|
||||
|
|
@ -407,7 +410,7 @@ export class RdioScannerAdminService implements OnDestroy {
|
|||
systemId: [dirWatch?.systemId, this.validateDirwatchSystemId()],
|
||||
talkgroupId: [dirWatch?.talkgroupId, this.validateDirwatchTalkgroupId()],
|
||||
type: [dirWatch?.type],
|
||||
usePolling: [dirWatch?.usePolling],
|
||||
// usePolling: [dirWatch?.usePolling],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -441,7 +444,7 @@ export class RdioScannerAdminService implements OnDestroy {
|
|||
_id: [system?._id],
|
||||
autoPopulate: [system?.autoPopulate],
|
||||
blacklists: [system?.blacklists, this.validateBlacklists()],
|
||||
id: [system?.id, [Validators.required, Validators.min(0), this.validateId()]],
|
||||
id: [system?.id, [Validators.required, Validators.min(1), this.validateId()]],
|
||||
label: [system?.label, Validators.required],
|
||||
led: [system?.led],
|
||||
order: [system?.order],
|
||||
|
|
@ -454,7 +457,7 @@ export class RdioScannerAdminService implements OnDestroy {
|
|||
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()]],
|
||||
id: [talkgroup?.id, [Validators.required, Validators.min(1), this.validateId()]],
|
||||
label: [talkgroup?.label, Validators.required],
|
||||
led: [talkgroup?.led],
|
||||
name: [talkgroup?.name, Validators.required],
|
||||
|
|
@ -480,6 +483,7 @@ export class RdioScannerAdminService implements OnDestroy {
|
|||
keypadBeeps: [options?.keypadBeeps, Validators.required],
|
||||
pruneDays: [options?.pruneDays, [Validators.required, Validators.min(0)]],
|
||||
sortTalkgroups: [options?.sortTalkgroups],
|
||||
tagsToggle: [options?.tagsToggle],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -492,7 +496,7 @@ export class RdioScannerAdminService implements OnDestroy {
|
|||
this.configWebSocket.close();
|
||||
|
||||
this.configWebSocket = undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private configWebSocketReconnect(): void {
|
||||
|
|
@ -531,7 +535,11 @@ export class RdioScannerAdminService implements OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
private errorHandler(error: HttpErrorResponse): void {
|
||||
private errorHandler(error: unknown): void {
|
||||
if (!(error instanceof HttpErrorResponse)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error.status === 401) {
|
||||
this.token = '';
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
with this access code exceeds the defined limit.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="number" matInput formControlName="limit" placeholder="Limit">
|
||||
<input type="number" min="0" step="1" matInput formControlName="limit" placeholder="Limit">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@
|
|||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- <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
|
||||
|
|
@ -165,10 +165,10 @@
|
|||
settle before ingesting it.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="number" matInput formControlName="delay" placeholder="Delay">
|
||||
<input type="number" min="0" step="1" matInput formControlName="delay" placeholder="Delay">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
</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
|
||||
|
|
@ -178,7 +178,7 @@
|
|||
<div>
|
||||
<mat-slide-toggle color="primary" formControlName="usePolling"></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="row bottom">
|
||||
<button type="button" mat-button color="warn" (click)="remove(i)">
|
||||
Delete dirwatch
|
||||
|
|
|
|||
|
|
@ -64,7 +64,8 @@ export class RdioScannerAdminDirWatchComponent implements OnChanges {
|
|||
}
|
||||
|
||||
add(): void {
|
||||
const dirWatch = this.adminService.newDirWatchForm({ usePolling: this.docker });
|
||||
// const dirWatch = this.adminService.newDirWatchForm({ usePolling: this.docker });
|
||||
const dirWatch = this.adminService.newDirWatchForm();
|
||||
|
||||
dirWatch.markAllAsTouched();
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
inactive.</span>
|
||||
</p>
|
||||
<mat-form-field>
|
||||
<input matInput formControlName="dimmerDelay" type="number">
|
||||
<input type="number" min="0" step="1" matInput formControlName="dimmerDelay">
|
||||
<mat-error *ngIf="form?.get('dimmerDelay')?.hasError('required')">
|
||||
Dimmer delay is required
|
||||
</mat-error>
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
a start time of +/- this delay in milliseconds.</span>
|
||||
</p>
|
||||
<mat-form-field>
|
||||
<input matInput formControlName="duplicateDetectionTimeFrame" type="number">
|
||||
<input type="number" min="0" step="1" matInput formControlName="duplicateDetectionTimeFrame">
|
||||
<mat-error *ngIf="form?.get('duplicateDetectionTimeFrame')?.hasError('required')">
|
||||
Duplicate call time frame is required
|
||||
</mat-error>
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
disable.</span>
|
||||
</p>
|
||||
<mat-form-field>
|
||||
<input matInput formControlName="pruneDays" type="number">
|
||||
<input type="number" min="0" step="1" matInput formControlName="pruneDays">
|
||||
<mat-error *ngIf="form?.get('pruneDays')?.hasError('required')">
|
||||
Prune days is required
|
||||
</mat-error>
|
||||
|
|
@ -97,4 +97,13 @@
|
|||
<mat-slide-toggle color="primary" formControlName="sortTalkgroups"></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Toggle by tags</span><br>
|
||||
<span class="mat-caption">Allow toggling talkgroups by their tag.</span>
|
||||
</p>
|
||||
<div>
|
||||
<mat-slide-toggle color="primary" formControlName="tagsToggle"></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
<span class="mat-caption">System identifier in decimal format.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="number" matInput formControlName="id" placeholder="Id">
|
||||
<input type="number" min="1" step="1" matInput formControlName="id" placeholder="Id">
|
||||
<mat-error *ngIf="form.get('id')?.hasError('duplicate')">
|
||||
Id is already defined
|
||||
</mat-error>
|
||||
|
|
@ -58,8 +58,7 @@
|
|||
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>
|
||||
<textarea type="text" matInput formControlName="blacklists" placeholder="Blacklists"></textarea>
|
||||
<mat-error *ngIf="form?.get('blacklists')?.hasError('invalid')">
|
||||
Comma separated list of talkgroup Ids
|
||||
</mat-error>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<span class="mat-caption">Talkgroup identifier in decimal format.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="number" matInput formControlName="id" placeholder="Id">
|
||||
<input type="number" min="1" step="1" matInput formControlName="id" placeholder="Id">
|
||||
<mat-error *ngIf="form?.get('id')?.hasError('duplicate')">
|
||||
Id is already defined
|
||||
</mat-error>
|
||||
|
|
@ -94,7 +94,7 @@
|
|||
<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">
|
||||
<input type="number" min="0" matInput formControlName="frequency" placeholder="Frequency">
|
||||
<mat-error *ngIf="form?.get('frequency')?.errors">
|
||||
Invalid frequency
|
||||
</mat-error>
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
<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>
|
||||
<textarea type="text" matInput formControlName="patches" placeholder="Patches"></textarea>
|
||||
<mat-error *ngIf="form?.get('patches')?.hasError('invalid')">
|
||||
Comma separated list of talkgroup Ids
|
||||
</mat-error>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<span class="mat-caption">Unit identifier in decimal format.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<input type="number" matInput formControlName="id" placeholder="Id">
|
||||
<input type="number" min="0" step="1" matInput formControlName="id" placeholder="Id">
|
||||
<mat-error *ngIf="form?.get('id')?.hasError('duplicate')">
|
||||
Id is already defined
|
||||
</mat-error>
|
||||
|
|
|
|||
|
|
@ -61,11 +61,8 @@
|
|||
<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>
|
||||
<input matInput type="datetime-local" formControlName="date" [max]="logsQuery?.dateStop"
|
||||
[min]="logsQuery?.dateStart" placeholder="Choose a date" (change)="formHandler()">
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export class RdioScannerAdminLogsComponent {
|
|||
|
||||
logs = new BehaviorSubject(new Array<Log | null>(10));
|
||||
|
||||
logsQuery: LogsQuery | null = null;
|
||||
logsQuery: LogsQuery | undefined = undefined;
|
||||
|
||||
logsQueryPending = false;
|
||||
|
||||
|
|
@ -105,8 +105,8 @@ export class RdioScannerAdminLogsComponent {
|
|||
options.level = this.form.value.level;
|
||||
}
|
||||
|
||||
if (this.form.value.date instanceof Date) {
|
||||
options.date = this.form.value.date;
|
||||
if (typeof this.form.value.date === 'string') {
|
||||
options.date = new Date(Date.parse(this.form.value.date));
|
||||
}
|
||||
|
||||
this.logsQueryPending = true;
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export class RdioScannerAdminTodosComponent implements OnDestroy, OnInit {
|
|||
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.',
|
||||
message: 'No apikeys or dirwatch defined. Please set at least one to allow ingesting audio files.',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export class RdioScannerAdminImportExportConfigComponent {
|
|||
this.config.emit(config);
|
||||
|
||||
} catch (error) {
|
||||
this.matSnackBar.open(error.message, '', { duration: 5000 });
|
||||
this.matSnackBar.open(error as string, '', { duration: 5000 });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export class RdioScannerMainComponent implements OnDestroy, OnInit {
|
|||
callTag = 'Tag';
|
||||
callTalkgroup = 'Talkgroup';
|
||||
callTalkgroupId = '0';
|
||||
callTalkgroupName = `Rdio Scanner ${packageInfo.name === 'rdio-scanner-client' ? 'v'.concat(packageInfo.version) : ''}`;
|
||||
callTalkgroupName = `Rdio Scanner ${packageInfo.name === 'rdio-scanner' ? 'v'.concat(packageInfo.version) : ''}`;
|
||||
callTime = 0;
|
||||
callUnit = '0';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
<svg id="livetype" width="119.66" height="40" version="1.1" viewBox="0 0 119.66 40" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
|
||||
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" fill="#a6a6a6"/>
|
||||
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
|
||||
<g fill="#fff">
|
||||
<path id="_Path_" d="m24.769 20.301a4.9488 4.9488 0 0 1 2.3566-4.1521 5.0657 5.0657 0 0 0-3.9912-2.1577c-1.6792-0.17626-3.3072 1.0048-4.1629 1.0048-0.87227 0-2.1898-0.98733-3.6085-0.95814a5.3153 5.3153 0 0 0-4.4729 2.7279c-1.934 3.3484-0.49141 8.2695 1.3612 10.976 0.9269 1.3254 2.0102 2.8058 3.4276 2.7533 1.3871-0.05753 1.9051-0.88448 3.5794-0.88448 1.6588 0 2.1448 0.88448 3.591 0.8511 1.4884-0.02416 2.4261-1.3312 3.3205-2.6691a10.962 10.962 0 0 0 1.5184-3.0925 4.782 4.782 0 0 1-2.9192-4.3992z" data-name="<Path>"/>
|
||||
<path d="m22.037 12.211a4.8725 4.8725 0 0 0 1.1145-3.4906 4.9575 4.9575 0 0 0-3.2076 1.6596 4.6363 4.6363 0 0 0-1.1437 3.3614 4.099 4.099 0 0 0 3.2368-1.5304z" data-name="<Path>"/>
|
||||
<path d="m42.302 27.14h-4.7334l-1.1367 3.3564h-2.0049l4.4834-12.418h2.083l4.4834 12.418h-2.0391zm-4.2432-1.5488h3.752l-1.8496-5.4473h-0.05176z"/>
|
||||
<path d="m55.16 25.97c0 2.8135-1.5059 4.6211-3.7783 4.6211a3.0693 3.0693 0 0 1-2.8486-1.584h-0.043v4.4844h-1.8584v-12.049h1.7989v1.5059h0.03418a3.2116 3.2116 0 0 1 2.8828-1.6006c2.2978 1e-5 3.8125 1.8164 3.8125 4.6221zm-1.9102 0c0-1.833-0.94727-3.0381-2.3926-3.0381-1.4199 0-2.375 1.2305-2.375 3.0381 0 1.8242 0.95508 3.0459 2.375 3.0459 1.4453 0 2.3926-1.1963 2.3926-3.0459z"/>
|
||||
<path d="m65.125 25.97c0 2.8135-1.5059 4.6211-3.7783 4.6211a3.0693 3.0693 0 0 1-2.8486-1.584h-0.043v4.4844h-1.8584v-12.049h1.7988v1.5059h0.03418a3.2116 3.2116 0 0 1 2.8828-1.6006c2.2979 0 3.8125 1.8164 3.8125 4.6221zm-1.9102 0c0-1.833-0.94727-3.0381-2.3926-3.0381-1.4199 0-2.375 1.2305-2.375 3.0381 0 1.8242 0.95508 3.0459 2.375 3.0459 1.4453 0 2.3926-1.1963 2.3926-3.0459z"/>
|
||||
<path d="m71.71 27.036c0.1377 1.2314 1.334 2.04 2.9688 2.04 1.5664 0 2.6934-0.80859 2.6934-1.919 0-0.96387-0.67969-1.541-2.2891-1.9365l-1.6094-0.3877c-2.2803-0.55078-3.3389-1.6172-3.3389-3.3477 0-2.1426 1.8672-3.6143 4.5186-3.6143 2.624 0 4.4228 1.4717 4.4834 3.6143h-1.876c-0.1123-1.2393-1.1367-1.9873-2.6338-1.9873s-2.5215 0.75684-2.5215 1.8584c0 0.87793 0.6543 1.3945 2.2549 1.79l1.3682 0.33594c2.5478 0.60254 3.6064 1.626 3.6064 3.4424 0 2.3232-1.8506 3.7783-4.794 3.7783-2.7539 0-4.6133-1.4209-4.7334-3.667z"/>
|
||||
<path d="m83.346 19.3v2.1426h1.7217v1.4717h-1.7217v4.9912c0 0.77539 0.34473 1.1367 1.1016 1.1367a5.8075 5.8075 0 0 0 0.61133-0.043v1.4629a5.1035 5.1035 0 0 1-1.0322 0.08594c-1.833 0-2.5478-0.68848-2.5478-2.4443v-5.1894h-1.3164v-1.4717h1.3164v-2.1426z"/>
|
||||
<path d="m86.065 25.97c0-2.8486 1.6777-4.6387 4.294-4.6387 2.625 0 4.2949 1.79 4.2949 4.6387 0 2.8564-1.6611 4.6387-4.2949 4.6387-2.6329 0-4.294-1.7822-4.294-4.6387zm6.6953 0c0-1.9541-0.89551-3.1074-2.4014-3.1074s-2.4004 1.1621-2.4004 3.1074c0 1.9619 0.89453 3.1064 2.4004 3.1064s2.4013-1.1445 2.4013-3.1064z"/>
|
||||
<path d="m96.186 21.442h1.7725v1.541h0.043a2.1594 2.1594 0 0 1 2.1777-1.6357 2.8662 2.8662 0 0 1 0.63672 0.06934v1.7383a2.5979 2.5979 0 0 0-0.835-0.1123 1.8726 1.8726 0 0 0-1.9365 2.083v5.3701h-1.8584z"/>
|
||||
<path d="m109.38 27.837c-0.25 1.6436-1.8506 2.7715-3.8984 2.7715-2.6338 0-4.2686-1.7646-4.2686-4.5957 0-2.8398 1.6436-4.6816 4.1904-4.6816 2.5049 0 4.0801 1.7207 4.0801 4.4658v0.63672h-6.3945v0.1123a2.358 2.358 0 0 0 2.4356 2.5644 2.0483 2.0483 0 0 0 2.0908-1.2734zm-6.2822-2.7022h4.5264a2.1773 2.1773 0 0 0-2.2207-2.2978 2.292 2.292 0 0 0-2.3057 2.2979z"/>
|
||||
<path d="m37.826 8.731a2.6396 2.6396 0 0 1 2.8076 2.9648c0 1.9062-1.0303 3.002-2.8076 3.002h-2.1553v-5.9668zm-1.2285 5.123h1.125a1.8759 1.8759 0 0 0 1.9678-2.146 1.881 1.881 0 0 0-1.9678-2.1338h-1.125z"/>
|
||||
<path d="m41.681 12.444a2.1332 2.1332 0 1 1 4.2471 0 2.1336 2.1336 0 1 1-4.2471 0zm3.333 0c0-0.97607-0.43848-1.5469-1.208-1.5469-0.77246 0-1.207 0.5708-1.207 1.5469 0 0.98389 0.43457 1.5503 1.207 1.5503 0.76954-1e-5 1.208-0.57032 1.208-1.5503z"/>
|
||||
<path d="m51.573 14.698h-0.92187l-0.93066-3.3164h-0.07031l-0.92676 3.3164h-0.91309l-1.2412-4.5029h0.90137l0.80664 3.436h0.06641l0.92578-3.436h0.85254l0.92578 3.436h0.07031l0.80273-3.436h0.88867z"/>
|
||||
<path d="m53.854 10.195h0.85546v0.71533h0.06641a1.348 1.348 0 0 1 1.3438-0.80225 1.4646 1.4646 0 0 1 1.5586 1.6748v2.915h-0.88867v-2.6918c0-0.72363-0.31445-1.0835-0.97168-1.0835a1.0329 1.0329 0 0 0-1.0752 1.1411v2.6343h-0.88867z"/>
|
||||
<path d="m59.094 8.437h0.88867v6.2607h-0.88867z"/>
|
||||
<path d="m61.218 12.444a2.1335 2.1335 0 1 1 4.2476 0 2.1338 2.1338 0 1 1-4.2476 0zm3.333 0c0-0.97607-0.43848-1.5469-1.208-1.5469-0.77246 0-1.207 0.5708-1.207 1.5469 0 0.98389 0.43457 1.5503 1.207 1.5503 0.76953-1e-5 1.208-0.57032 1.208-1.5503z"/>
|
||||
<path d="m66.401 13.424c0-0.81055 0.60352-1.2778 1.6748-1.3442l1.2197-0.07031v-0.38867c0-0.47559-0.31445-0.74414-0.92187-0.74414-0.49609 0-0.83984 0.18213-0.93848 0.50049h-0.86035c0.09082-0.77344 0.81836-1.2695 1.8398-1.2695 1.1289 0 1.7656 0.562 1.7656 1.5132v3.0767h-0.85547v-0.63281h-0.07031a1.515 1.515 0 0 1-1.3525 0.707 1.3603 1.3603 0 0 1-1.501-1.3476zm2.8945-0.38477v-0.37646l-1.0996 0.07031c-0.62012 0.0415-0.90137 0.25244-0.90137 0.64941 0 0.40527 0.35156 0.64111 0.835 0.64111a1.0615 1.0615 0 0 0 1.166-0.98437z"/>
|
||||
<path d="m71.348 12.444c0-1.4228 0.73145-2.3242 1.8691-2.3242a1.484 1.484 0 0 1 1.3809 0.79h0.06641v-2.4731h0.88867v6.2607h-0.85156v-0.71143h-0.07031a1.5628 1.5628 0 0 1-1.4141 0.78564c-1.1455 5e-5 -1.8692-0.90134-1.8692-2.3276zm0.918 0c0 0.95508 0.4502 1.5298 1.2031 1.5298 0.749 0 1.2119-0.583 1.2119-1.5259 0-0.93848-0.46777-1.5298-1.2119-1.5298-0.74808 0-1.2032 0.57861-1.2032 1.5259z"/>
|
||||
<path d="m79.23 12.444a2.1332 2.1332 0 1 1 4.2471 0 2.1336 2.1336 0 1 1-4.2471 0zm3.333 0c0-0.97607-0.43848-1.5469-1.208-1.5469-0.77246 0-1.207 0.5708-1.207 1.5469 0 0.98389 0.43457 1.5503 1.207 1.5503 0.76953-1e-5 1.208-0.57032 1.208-1.5503z"/>
|
||||
<path d="m84.669 10.195h0.85547v0.71533h0.06641a1.348 1.348 0 0 1 1.3438-0.80225 1.4646 1.4646 0 0 1 1.5586 1.6748v2.915h-0.88867v-2.6918c0-0.72363-0.31445-1.0835-0.97168-1.0835a1.0329 1.0329 0 0 0-1.0752 1.1411v2.6343h-0.88867z"/>
|
||||
<path d="m93.515 9.0737v1.1416h0.97559v0.74854h-0.97559v2.3154c0 0.47168 0.19434 0.67822 0.63672 0.67822a2.9666 2.9666 0 0 0 0.33887-0.02051v0.74023a2.9155 2.9155 0 0 1-0.4834 0.04541c-0.98828 0-1.3818-0.34766-1.3818-1.2158v-2.543h-0.71484v-0.74854h0.71484v-1.1416z"/>
|
||||
<path d="m95.705 8.437h0.88086v2.4814h0.07031a1.3856 1.3856 0 0 1 1.373-0.80664 1.4834 1.4834 0 0 1 1.5508 1.6787v2.9072h-0.88956v-2.688c0-0.71924-0.335-1.0835-0.96289-1.0835a1.0519 1.0519 0 0 0-1.1338 1.1416v2.6299h-0.88867z"/>
|
||||
<path d="m104.76 13.482a1.828 1.828 0 0 1-1.9512 1.3027 2.0453 2.0453 0 0 1-2.0801-2.3242 2.0768 2.0768 0 0 1 2.0762-2.3525c1.2529 0 2.0088 0.856 2.0088 2.27v0.31009h-3.1797v0.0498a1.1902 1.1902 0 0 0 1.1992 1.29 1.0793 1.0793 0 0 0 1.0713-0.5459zm-3.126-1.4512h2.2744a1.0865 1.0865 0 0 0-1.1084-1.1665 1.1516 1.1516 0 0 0-1.166 1.1665z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.2 KiB |
|
|
@ -17,13 +17,10 @@
|
|||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
const PROXY_CONFIG = {
|
||||
"/": {
|
||||
"bypass": (req) => req.upgrade ? null : '/',
|
||||
"secure": false,
|
||||
"target": "http://localhost:3000",
|
||||
"ws": true,
|
||||
},
|
||||
};
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
module.exports = PROXY_CONFIG;
|
||||
@Component({
|
||||
selector: 'app-store-badge',
|
||||
templateUrl: './app-store-badge.component.svg',
|
||||
})
|
||||
export class AppStoreBadgeComponent {}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
<svg width="135" height="40" version="1.1" viewBox="0 0 135 40" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<clipPath id="clipPath54-3">
|
||||
<path d="m205.1 176.1-0.743 0.711c-2.91 3.079-4.628 7.86-4.628 14.051v-1.457 221.16 0.051-0.055-1.461c0 6.704 2.003 11.754 5.359 14.782l123.9-123.9-123.89-123.89m-5.371 234.53c0 0.551 0.015 1.086 0.043 1.613-0.028-0.523-0.043-1.062-0.043-1.613m0.043 1.637c0 7e-3 0 0.015 3e-3 0.023-3e-3 -8e-3 -3e-3 -0.016-3e-3 -0.023m3e-3 0.07v0.012-0.012"/>
|
||||
</clipPath>
|
||||
<linearGradient id="linearGradient68-7" x2="1" gradientTransform="matrix(-167.82 -167.82 167.82 -167.82 318 412.9)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00a0ff" offset="0"/>
|
||||
<stop stop-color="#00a1ff" offset=".0065744"/>
|
||||
<stop stop-color="#00beff" offset=".2601"/>
|
||||
<stop stop-color="#00d2ff" offset=".5122"/>
|
||||
<stop stop-color="#00dfff" offset=".7604"/>
|
||||
<stop stop-color="#00e3ff" offset="1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clipPath78-1">
|
||||
<path d="m370.28 257.22 0.942 0.535 48.929 27.801c4.665 2.652 7.774 5.867 9.325 9.269-1.547-3.402-4.657-6.621-9.325-9.273l-48.929-27.801-0.942-0.531m0.012 1.465-41.305 41.312 41.301 41.305 49.863-28.328c6.332-3.598 9.887-8.235 10.481-12.973v-0.019c-0.594-4.727-4.149-9.368-10.481-12.965l-49.859-28.332"/>
|
||||
</clipPath>
|
||||
<linearGradient id="linearGradient88-7" x2="1" gradientTransform="matrix(-241.97 0 0 -241.97 438.34 299.99)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#ffe000" offset="0"/>
|
||||
<stop stop-color="#ffbd00" offset=".4087"/>
|
||||
<stop stop-color="#ffa500" offset=".7754"/>
|
||||
<stop stop-color="#ff9c00" offset="1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clipPath98-0">
|
||||
<path d="m213.93 171.39c-3.461 0-6.481 1.125-8.836 3.25l4e-3 4e-3c2.355-2.125 5.379-3.254 8.84-3.254 0.3 0 0.605 8e-3 0.914 0.023-0.313-0.015-0.622-0.023-0.922-0.023m4e-3 1.465c-3.461-4e-3 -6.481 1.125-8.836 3.25v3e-3l123.89 123.89 41.305-41.312-145.15-82.473c-4.016-2.277-7.821-3.355-11.203-3.355m-8.871 1.812c-0.219 0.199-0.434 0.41-0.645 0.629l0.645-0.629"/>
|
||||
</clipPath>
|
||||
<linearGradient id="linearGradient104-2" x2="1" gradientTransform="matrix(-227.58 -227.58 227.58 -227.58 348.27 277.04)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#ff3a44" offset="0"/>
|
||||
<stop stop-color="#c31162" offset="1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clipPath114-8">
|
||||
<path d="m328.98 299.99-123.9 123.9c2.355 2.121 5.371 3.246 8.828 3.246 3.391 0 7.199-1.082 11.223-3.363l145.15-82.473-41.301-41.305m42.235 42.238-146.08 83c-4.024 2.282-7.832 3.364-11.223 3.364h-0.043 0.059c3.383 0 7.187-1.078 11.207-3.36l146.08-83.004m-157.38 86.364"/>
|
||||
</clipPath>
|
||||
<linearGradient id="linearGradient126-9" x2="1" gradientTransform="matrix(101.62 -101.62 101.62 101.62 172.97 498.24)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#32a071" offset="0"/>
|
||||
<stop stop-color="#2da771" offset=".0685"/>
|
||||
<stop stop-color="#15cf74" offset=".4762"/>
|
||||
<stop stop-color="#06e775" offset=".8009"/>
|
||||
<stop stop-color="#00f076" offset="1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clipPath138-5">
|
||||
<path d="m370.28 257.22 0.942 0.535-0.942-0.535"/>
|
||||
</clipPath>
|
||||
<linearGradient id="linearGradient148-2" x2="1" gradientTransform="matrix(-241.97 0 0 -241.97 438.34 299.99)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#ccb300" offset="0"/>
|
||||
<stop stop-color="#cc9700" offset=".4087"/>
|
||||
<stop stop-color="#cc8400" offset=".7754"/>
|
||||
<stop stop-color="#cc7d00" offset="1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clipPath158-3">
|
||||
<path d="m213.94 171.39c-3.461 0-6.485 1.129-8.84 3.254l-4e-3 -4e-3c-8e-3 8e-3 -0.02 0.019-0.031 0.027l-0.645 0.629c-0.02 0.02-0.039 0.043-0.063 0.066l0.743 0.743c2.355-2.125 5.375-3.254 8.836-3.25 3.382 0 7.187 1.078 11.203 3.355l145.15 82.473 0.93-0.93-0.942-0.535-145.14-82.469c-3.66-2.074-7.141-3.156-10.285-3.336-0.309-0.015-0.614-0.023-0.914-0.023"/>
|
||||
</clipPath>
|
||||
<linearGradient id="linearGradient164-1" x2="1" gradientTransform="matrix(-227.58 -227.58 227.58 -227.58 348.27 277.04)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#cc2e36" offset="0"/>
|
||||
<stop stop-color="#9c0e4e" offset="1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clipPath176-3">
|
||||
<path d="m204.36 175.36c-2.91 3.079-4.628 7.856-4.628 14.051v1.457c0-6.191 1.718-10.972 4.628-14.051l0.743-0.711-0.743-0.746"/>
|
||||
</clipPath>
|
||||
<linearGradient id="linearGradient190-9" x2="1" gradientTransform="matrix(-167.82 -167.82 167.82 -167.82 318 412.9)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#008de0" offset="0"/>
|
||||
<stop stop-color="#008de0" offset=".0065744"/>
|
||||
<stop stop-color="#00a7e0" offset=".2601"/>
|
||||
<stop stop-color="#00b8e0" offset=".5122"/>
|
||||
<stop stop-color="#00c4e0" offset=".7604"/>
|
||||
<stop stop-color="#00c7e0" offset="1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clipPath200-3">
|
||||
<path d="m204.36 175.36 0.743 0.746v-3e-3l-0.743-0.743"/>
|
||||
</clipPath>
|
||||
<linearGradient id="linearGradient206-1" x2="1" gradientTransform="matrix(-227.58 -227.58 227.58 -227.58 348.27 277.04)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#e0333c" offset="0"/>
|
||||
<stop stop-color="#ab0f56" offset="1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clipPath218-3">
|
||||
<path d="m371.22 257.75-0.93 0.93 49.859 28.332c6.332 3.597 9.887 8.238 10.481 12.965 0-1.735-0.387-3.469-1.156-5.157-1.551-3.402-4.66-6.617-9.325-9.269l-48.929-27.801"/>
|
||||
</clipPath>
|
||||
<linearGradient id="linearGradient228-0" x2="1" gradientTransform="matrix(-241.97 0 0 -241.97 438.34 299.99)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#e0c500" offset="0"/>
|
||||
<stop stop-color="#e0a600" offset=".4087"/>
|
||||
<stop stop-color="#e09100" offset=".7754"/>
|
||||
<stop stop-color="#e08900" offset="1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clipPath240-4">
|
||||
<path d="m430.63 300c-0.594 4.738-4.149 9.375-10.481 12.973l-49.863 28.328 0.93 0.93h4e-3l48.929-27.801c6.981-3.969 10.477-9.199 10.481-14.43"/>
|
||||
</clipPath>
|
||||
<linearGradient id="linearGradient250-6" x2="1" gradientTransform="matrix(-241.97 0 0 -241.97 438.34 299.99)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#ffe840" offset="0"/>
|
||||
<stop stop-color="#ffce40" offset=".4087"/>
|
||||
<stop stop-color="#ffbc40" offset=".7754"/>
|
||||
<stop stop-color="#ffb540" offset="1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clipPath262-7">
|
||||
<path d="m199.73 409.1v1.461 0.055 0.012c0 0.551 0.015 1.09 0.043 1.613v0.024c0 7e-3 0 0.015 3e-3 0.023v0.047 0.012c0.301 5.332 1.957 9.492 4.582 12.269l0.731-0.734c-3.356-3.028-5.359-8.078-5.359-14.782m5.339 16.215 0.028 0.028c-8e-3 -0.012-0.02-0.02-0.028-0.028"/>
|
||||
</clipPath>
|
||||
<linearGradient id="linearGradient276-5" x2="1" gradientTransform="matrix(-167.82 -167.82 167.82 -167.82 318 412.9)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40b8ff" offset="0"/>
|
||||
<stop stop-color="#40b9ff" offset=".0065744"/>
|
||||
<stop stop-color="#40ceff" offset=".2601"/>
|
||||
<stop stop-color="#40ddff" offset=".5122"/>
|
||||
<stop stop-color="#40e7ff" offset=".7604"/>
|
||||
<stop stop-color="#40eaff" offset="1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clipPath288-3">
|
||||
<path d="m370.28 341.3-145.15 82.473c-4.024 2.281-7.832 3.363-11.223 3.363-3.457 0-6.473-1.125-8.828-3.246l-0.731 0.734c0.231 0.242 0.465 0.477 0.711 0.699 8e-3 8e-3 0.02 0.016 0.028 0.024 2.336 2.105 5.32 3.23 8.746 3.25h0.031 0.043c3.391 0 7.199-1.082 11.223-3.364l146.08-83-4e-3 -3e-3 -0.93-0.93"/>
|
||||
</clipPath>
|
||||
<linearGradient id="linearGradient300-7" x2="1" gradientTransform="matrix(101.62 -101.62 101.62 101.62 172.97 498.24)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#65b895" offset="0"/>
|
||||
<stop stop-color="#62bd95" offset=".0685"/>
|
||||
<stop stop-color="#50db97" offset=".4762"/>
|
||||
<stop stop-color="#44ed98" offset=".8009"/>
|
||||
<stop stop-color="#40f498" offset="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="m130 40h-125c-2.75 0-5-2.25-5-5v-30c0-2.75 2.25-5 5-5h125c2.75 0 5 2.25 5 5v30c0 2.75-2.25 5-5 5"/>
|
||||
<path d="m130 0h-125c-2.75 0-5 2.25-5 5v30c0 2.75 2.25 5 5 5h125c2.75 0 5-2.25 5-5v-30c0-2.75-2.25-5-5-5m0 0.8c2.316 0 4.2 1.8844 4.2 4.2v30c0 2.316-1.884 4.2004-4.2 4.2004h-125c-2.3156 0-4.2-1.8844-4.2-4.2004v-30c0-2.3156 1.8844-4.2 4.2-4.2h125" fill="#a6a6a6"/>
|
||||
<g fill="#fff">
|
||||
<path d="m47.418 10.243c0 0.8378-0.2481 1.505-0.745 2.0027-0.5644 0.5918-1.3 0.8879-2.2043 0.8879-0.866 0-1.6023-0.3-2.2078-0.9008-0.6066-0.6004-0.9093-1.3449-0.9093-2.2332 0-0.8887 0.3027-1.6328 0.9093-2.2336 0.6055-0.6004 1.3418-0.9008 2.2078-0.9008 0.4297 0 0.8411 0.084 1.2317 0.2516 0.3906 0.168 0.7039 0.391 0.9383 0.6703l-0.5274 0.5277c-0.3972-0.4746-0.9441-0.7117-1.6426-0.7117-0.6316 0-1.1777 0.2219-1.6386 0.666-0.461 0.4446-0.6914 1.0211-0.6914 1.7305s0.2304 1.2859 0.6914 1.7305c0.4609 0.4441 1.007 0.666 1.6386 0.666 0.67 0 1.2286-0.2231 1.6758-0.6699 0.2903-0.2911 0.4582-0.6957 0.5031-1.2153h-2.1789v-0.7207h2.9075c0.0281 0.1567 0.0418 0.3071 0.0418 0.4528"/>
|
||||
<path d="m47.418 10.243h-0.1c-8e-4 0.8187-0.2399 1.4546-0.7157 1.932l-0.0011 8e-4 -4e-4 8e-4c-0.5465 0.5714-1.2489 0.8558-2.1321 0.857-0.8429-0.0012-1.5484-0.2887-2.1375-0.8715-0.5878-0.584-0.8785-1.2969-0.8796-2.1625 0.0011-0.866 0.2918-1.5785 0.8796-2.1621 0.5891-0.5832 1.2946-0.8711 2.1375-0.8723 0.4168 0 0.8137 0.0813 1.1922 0.2434 0.3789 0.1633 0.6782 0.3773 0.9012 0.643l0.0766-0.0645-0.0707-0.0707-0.5274 0.5281 0.0707 0.0703 0.077-0.064c-0.4157-0.4981-0.9992-0.7492-1.7196-0.7477-0.655-8e-4 -1.2312 0.2332-1.7078 0.6942-0.4812 0.4625-0.723 1.0714-0.7222 1.8023-8e-4 0.7309 0.241 1.3398 0.7222 1.8023 0.4766 0.461 1.0528 0.695 1.7078 0.6942 0.6922 0.0012 1.2821-0.2336 1.7465-0.6992 0.3102-0.311 0.486-0.7418 0.5321-1.2774l0.0093-0.1086h-2.1879v-0.5207h2.8075v-0.1l-0.0985 0.0176c0.0274 0.1516 0.0403 0.2957 0.0403 0.4352h0.2c0-0.152-0.0141-0.3082-0.0434-0.4707l-0.0148-0.0821h-3.0911v0.9207h2.2789v-0.1l-0.0996-0.0086c-0.0441 0.5032-0.2043 0.8821-0.4742 1.1532-0.4297 0.4281-0.957 0.6394-1.6051 0.6406-0.6082-8e-4 -1.1242-0.2102-1.5691-0.6379-0.4406-0.4262-0.6598-0.9703-0.6609-1.6586 0.0011-0.6883 0.2203-1.2324 0.6609-1.6586 0.4449-0.4277 0.9609-0.6371 1.5691-0.6379 0.6762 0.0016 1.1868 0.225 1.5661 0.6762l0.0703 0.0836 0.6043-0.6051 0.0648-0.0648-0.059-0.07c-0.2457-0.2929-0.573-0.5254-0.9754-0.698-0.4031-0.1731-0.8285-0.2598-1.2711-0.2598-0.889-8e-4 -1.6562 0.3117-2.2785 0.9301-0.6246 0.6172-0.9394 1.393-0.9386 2.3043-8e-4 0.9109 0.314 1.6871 0.939 2.3047 0.6219 0.618 1.3891 0.9301 2.2781 0.9293 0.9258 8e-4 1.6938-0.3067 2.2766-0.9188l-0.0723-0.0691 0.0707 0.0707c0.518-0.5176 0.775-1.2168 0.7743-2.0734h-0.1"/>
|
||||
<path d="m52.028 7.7371h-2.7324v1.9024h2.4641v0.721h-2.4641v1.9024h2.7324v0.7371h-3.5027v-6h3.5027v0.7371"/>
|
||||
<path d="m52.028 7.7371v-0.1h-2.8324v2.1024h2.4641v0.5207l-2.4641 3e-4v2.1024h2.7324v0.5371h-3.3027v-5.8h3.3027v0.6371h0.1v-0.1 0.1h0.1v-0.8371h-3.7027v6.2h3.7027v-0.9371h-2.7324v-1.7024h2.4641v-0.921h-2.4641v-1.7024h2.7324v-0.1h-0.1"/>
|
||||
<path d="m55.279 13h-0.7715v-5.2629h-1.6758v-0.7371h4.1231v0.7371h-1.6758v5.2629"/>
|
||||
<path d="m55.279 13v-0.1h-0.6715v-5.2629h-1.6758v-0.5371h3.9231v0.5371h-1.6758v5.3629h0.1v-0.1 0.1h0.1v-5.1629h1.6758v-0.9371h-4.3231v0.9371h1.6758v5.2629h0.9715v-0.1h-0.1"/>
|
||||
<path d="m59.938 13v-6h0.7707v6h-0.7707"/>
|
||||
<path d="m59.938 13h0.1v-5.9h0.5707v5.8h-0.6707v0.1h0.1-0.1v0.1h0.8707v-6.2h-0.9707v6.2h0.1v-0.1"/>
|
||||
<path d="m64.128 13h-0.7715v-5.2629h-1.6757v-0.7371h4.123v0.7371h-1.6758v5.2629"/>
|
||||
<path d="m64.128 13v-0.1h-0.6715v-5.2629h-1.6757v-0.5371h3.923v0.5371h-1.6758v5.3629h0.1v-0.1 0.1h0.1v-5.1629h1.6758v-0.9371h-4.323v0.9371h1.6757v5.2629h0.9715v-0.1h-0.1"/>
|
||||
<path d="m69.779 11.722c0.4441 0.4496 0.9875 0.6742 1.6301 0.6742s1.1863-0.2246 1.6297-0.6742c0.4445-0.4496 0.6672-1.0243 0.6672-1.7223s-0.2227-1.2727-0.6672-1.7223c-0.4434-0.4496-0.9871-0.6742-1.6297-0.6742s-1.186 0.2246-1.6301 0.6742c-0.4434 0.4496-0.666 1.0243-0.666 1.7223s0.2226 1.2727 0.666 1.7223zm3.8301 0.5023c-0.5899 0.6066-1.3235 0.9094-2.2 0.9094-0.8774 0-1.6106-0.3028-2.1992-0.9094-0.5903-0.6059-0.884-1.3476-0.884-2.2246s0.2937-1.6188 0.884-2.2246c0.5886-0.6067 1.3218-0.9098 2.1992-0.9098 0.8719 0 1.6035 0.3047 2.1961 0.9137 0.5918 0.609 0.8879 1.3492 0.8879 2.2207 0 0.877-0.2954 1.6187-0.884 2.2246"/>
|
||||
<path d="m69.779 11.722-0.0711 0.0703c0.4606 0.4676 1.0352 0.7051 1.7012 0.7039 0.6656 0.0012 1.241-0.2363 1.7008-0.7039 0.4636-0.468 0.6968-1.0735 0.6961-1.7926 7e-4 -0.7191-0.2325-1.3246-0.6961-1.7926-0.4598-0.4676-1.0352-0.7051-1.7008-0.7039-0.666-0.0012-1.2406 0.2363-1.7012 0.7039-0.4625 0.468-0.6961 1.0735-0.6949 1.7926-0.0012 0.7191 0.2324 1.3246 0.6949 1.7926l0.1422-0.1406c-0.4242-0.4317-0.6363-0.9747-0.6371-1.652 8e-4 -0.6773 0.2129-1.2203 0.6371-1.652 0.4281-0.4316 0.9394-0.6433 1.559-0.6445 0.6191 0.0012 1.1316 0.2129 1.5586 0.6445 0.425 0.4317 0.6371 0.9747 0.6379 1.652-8e-4 0.6773-0.2129 1.2203-0.6379 1.652-0.427 0.4316-0.9395 0.6433-1.5586 0.6445-0.6196-0.0012-1.1309-0.2129-1.559-0.6445zm3.8301 0.5023-0.0715-0.0695c-0.5734 0.5875-1.2746 0.8777-2.1285 0.8789-0.8543-0.0012-1.5555-0.2914-2.1278-0.8789l-0.0027-0.0031 0.0027 0.0031c-0.5722-0.5895-0.8546-1.3004-0.8554-2.1551 8e-4 -0.8547 0.2832-1.5656 0.8554-2.1551 0.5723-0.5875 1.2735-0.8781 2.1278-0.8793 0.8484 0.0012 1.548 0.293 2.1246 0.8836 0.5742 0.5922 0.8582 1.302 0.8594 2.1508-0.0012 0.8547-0.2844 1.5656-0.8555 2.1551l0.0715 0.0695 0.0719 0.0695c0.6062-0.6226 0.9128-1.3949 0.9121-2.2941 7e-4 -0.8941-0.3071-1.6648-0.9161-2.2902-0.6093-0.6278-1.3726-0.9453-2.2679-0.9442-0.9-0.0011-1.6656 0.3149-2.2711 0.9403v-4e-4c-0.6074 0.623-0.9129 1.3953-0.9121 2.2945-8e-4 0.8992 0.3047 1.6715 0.9121 2.2945l0.0027 0.0028-0.0027-0.0032c0.6055 0.6254 1.3711 0.9407 2.2711 0.9399 0.8996 8e-4 1.6652-0.3145 2.2719-0.9399l-0.0719-0.0695"/>
|
||||
<path d="m75.575 13v-6h0.9383l2.916 4.6676h0.0332l-0.0332-1.1563v-3.5113h0.7715v6h-0.8047l-3.0508-4.8938h-0.0332l0.0332 1.1567v3.7371h-0.7703"/>
|
||||
<path d="m75.575 13h0.1v-5.9h0.7828l2.916 4.6676h0.1914l-0.0359-1.2578v-3.4098h0.5715v5.8h-0.6492l-3.0508-4.8938h-0.1914l0.0359 1.2583v3.6355h-0.6703v0.1h0.1-0.1v0.1h0.8703v-3.8387l-0.0332-1.1578-0.1 0.0027v0.1h0.0332v-0.1l-0.0848 0.0532 3.0801 4.9406h0.9602v-6.2h-0.9715v3.6125l0.0332 1.1578 0.1-0.0027v-0.1h-0.0332v0.1l0.0848-0.0531-2.9454-4.7145h-1.0937v6.2h0.1v-0.1"/>
|
||||
<path d="m106.94 30h1.866v-12.501h-1.866zm16.807-7.998-2.139 5.4199h-0.064l-2.22-5.4199h-2.01l3.329 7.5753-1.898 4.2137h1.947l5.13-11.789zm-10.582 6.5781c-0.612 0-1.464-0.3055-1.464-1.0617 0-0.9649 1.062-1.3348 1.978-1.3348 0.821 0 1.206 0.1769 1.705 0.4184-0.145 1.1578-1.142 1.9781-2.219 1.9781zm0.225-6.8516c-1.351 0-2.75 0.5953-3.329 1.9141l1.656 0.6914c0.354-0.6914 1.013-0.9168 1.705-0.9168 0.965 0 1.946 0.5793 1.962 1.6086v0.1285c-0.338-0.193-1.061-0.4824-1.946-0.4824-1.785 0-3.603 0.9808-3.603 2.8144 0 1.6727 1.464 2.7504 3.104 2.7504 1.255 0 1.946-0.5629 2.381-1.2222h0.064v0.9648h1.801v-4.793c0-2.2191-1.656-3.4578-3.795-3.4578zm-11.532 1.7953h-2.654v-4.2851h2.654c1.395 0 2.187 1.155 2.187 2.1425 0 0.9688-0.792 2.1426-2.187 2.1426zm-0.048-6.025h-4.471v12.501h1.865v-4.7363h2.606c2.068 0 4.101-1.4973 4.101-3.8825 0-2.3851-2.033-3.8824-4.101-3.8824zm-24.381 11.084c-1.2891 0-2.3684-1.0793-2.3684-2.5617 0-1.498 1.0793-2.5937 2.3684-2.5937 1.2727 0 2.2715 1.0957 2.2715 2.5937 0 1.4824-0.9988 2.5617-2.2715 2.5617zm2.1426-5.8804h-0.0645c-0.4187-0.4993-1.2246-0.9504-2.2394-0.9504-2.1266 0-4.0758 1.8687-4.0758 4.2691 0 2.3844 1.9492 4.2371 4.0758 4.2371 1.0148 0 1.8207-0.4512 2.2394-0.9664h0.0645v0.6121c0 1.6274-0.8699 2.4973-2.2715 2.4973-1.1438 0-1.8527-0.8219-2.143-1.5145l-1.6269 0.6766c0.4672 1.1277 1.7078 2.5133 3.7699 2.5133 2.191 0 4.0437-1.2891 4.0437-4.4305v-7.6363h-1.7722zm3.0609 7.298h1.8688v-12.502h-1.8688zm4.6238-4.1242c-0.0484-1.6434 1.2727-2.4809 2.2231-2.4809 0.7416 0 1.3696 0.3703 1.5786 0.9024zm5.7997-1.4176c-0.354-0.9508-1.434-2.7066-3.6411-2.7066-2.191 0-4.0113 1.7238-4.0113 4.2531 0 2.3844 1.8043 4.2531 4.2207 4.2531 1.9497 0 3.0777-1.1922 3.5447-1.8848l-1.45-0.9668c-0.483 0.709-1.144 1.1762-2.0947 1.1762-0.9504 0-1.6269-0.4351-2.0621-1.2887l5.6868-2.3523zm-45.308-1.4016v1.8043h4.3175c-0.1289 1.0149-0.4672 1.7559-0.9828 2.2715-0.6281 0.6285-1.6109 1.3211-3.3347 1.3211-2.6583 0-4.7364-2.1426-4.7364-4.8008s2.0781-4.8011 4.7364-4.8011c1.4339 0 2.4808 0.564 3.2542 1.289l1.2727-1.2726c-1.0793-1.0313-2.5133-1.8207-4.5269-1.8207-3.6411 0-6.702 2.9644-6.702 6.6054s3.0609 6.6051 6.702 6.6051c1.9656 0 3.4476-0.6441 4.6074-1.8527 1.1922-1.1922 1.5629-2.8676 1.5629-4.2207 0-0.4192-0.0321-0.8055-0.0969-1.1278zm11.079 5.5258c-1.2891 0-2.4008-1.0633-2.4008-2.5777 0-1.5305 1.1117-2.5777 2.4008-2.5777 1.2886 0 2.4003 1.0472 2.4003 2.5777 0 1.5144-1.1117 2.5777-2.4003 2.5777zm0-6.8308c-2.3524 0-4.2696 1.7882-4.2696 4.2531 0 2.4488 1.9172 4.2531 4.2696 4.2531 2.3519 0 4.2691-1.8043 4.2691-4.2531 0-2.4649-1.9172-4.2531-4.2691-4.2531zm9.3132 6.8308c-1.2886 0-2.4004-1.0633-2.4004-2.5777 0-1.5305 1.1118-2.5777 2.4004-2.5777 1.2887 0 2.4004 1.0472 2.4004 2.5777 0 1.5144-1.1117 2.5777-2.4004 2.5777zm0-6.8308c-2.3523 0-4.2691 1.7882-4.2691 4.2531 0 2.4488 1.9168 4.2531 4.2691 4.2531 2.352 0 4.2692-1.8043 4.2692-4.2531 0-2.4649-1.9172-4.2531-4.2692-4.2531"/>
|
||||
</g>
|
||||
<g transform="matrix(.1 0 0 -.1 -10 50)">
|
||||
<g clip-path="url(#clipPath54-3)">
|
||||
<path d="m205.1 176.1-0.743 0.711c-2.91 3.079-4.628 7.86-4.628 14.051v-1.457 221.16 0.051-0.055-1.461c0 6.704 2.003 11.754 5.359 14.782l123.9-123.9-123.89-123.89m-5.371 234.53c0 0.551 0.015 1.086 0.043 1.613-0.028-0.523-0.043-1.062-0.043-1.613m0.043 1.637c0 7e-3 0 0.015 3e-3 0.023-3e-3 -8e-3 -3e-3 -0.016-3e-3 -0.023m3e-3 0.07v0.012-0.012" fill="url(#linearGradient68-7)"/>
|
||||
</g>
|
||||
<g clip-path="url(#clipPath78-1)">
|
||||
<path d="m370.28 257.22 0.942 0.535 48.929 27.801c4.665 2.652 7.774 5.867 9.325 9.269-1.547-3.402-4.657-6.621-9.325-9.273l-48.929-27.801-0.942-0.531m0.012 1.465-41.305 41.312 41.301 41.305 49.863-28.328c6.332-3.598 9.887-8.235 10.481-12.973v-0.019c-0.594-4.727-4.149-9.368-10.481-12.965l-49.859-28.332" fill="url(#linearGradient88-7)"/>
|
||||
</g>
|
||||
<g clip-path="url(#clipPath98-0)">
|
||||
<path d="m213.93 171.39c-3.461 0-6.481 1.125-8.836 3.25l4e-3 4e-3c2.355-2.125 5.379-3.254 8.84-3.254 0.3 0 0.605 8e-3 0.914 0.023-0.313-0.015-0.622-0.023-0.922-0.023m4e-3 1.465c-3.461-4e-3 -6.481 1.125-8.836 3.25v3e-3l123.89 123.89 41.305-41.312-145.15-82.473c-4.016-2.277-7.821-3.355-11.203-3.355m-8.871 1.812c-0.219 0.199-0.434 0.41-0.645 0.629l0.645-0.629" fill="url(#linearGradient104-2)"/>
|
||||
</g>
|
||||
<g clip-path="url(#clipPath114-8)">
|
||||
<path d="m328.98 299.99-123.9 123.9c2.355 2.121 5.371 3.246 8.828 3.246 3.391 0 7.199-1.082 11.223-3.363l145.15-82.473-41.301-41.305m42.235 42.238-146.08 83c-4.024 2.282-7.832 3.364-11.223 3.364h-0.043 0.059c3.383 0 7.187-1.078 11.207-3.36l146.08-83.004m-157.38 86.364" fill="url(#linearGradient126-9)"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="m11.485 32.859c0.3144-0.018 0.6625-0.1262 1.0285-0.3336l14.514-8.2469-14.514 8.2469c-0.3657 0.2078-0.7141 0.3156-1.0285 0.3336m-0.9758-0.3227-0.0031-0.0027c0.0011 8e-4 0.0023 0.0019 0.0031 0.0027m-0.0676-0.0656-0.0063-0.0062v-4e-4c0.0024 0.0023 0.0043 0.0046 0.0063 0.0066"/>
|
||||
<g transform="matrix(.1 0 0 -.1 -10 50)">
|
||||
<g clip-path="url(#clipPath138-5)">
|
||||
<path d="m370.28 257.22 0.942 0.535-0.942-0.535" fill="url(#linearGradient148-2)"/>
|
||||
</g>
|
||||
<g clip-path="url(#clipPath158-3)">
|
||||
<path d="m213.94 171.39c-3.461 0-6.485 1.129-8.84 3.254l-4e-3 -4e-3c-8e-3 8e-3 -0.02 0.019-0.031 0.027l-0.645 0.629c-0.02 0.02-0.039 0.043-0.063 0.066l0.743 0.743c2.355-2.125 5.375-3.254 8.836-3.25 3.382 0 7.187 1.078 11.203 3.355l145.15 82.473 0.93-0.93-0.942-0.535-145.14-82.469c-3.66-2.074-7.141-3.156-10.285-3.336-0.309-0.015-0.614-0.023-0.914-0.023" fill="url(#linearGradient164-1)"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="m10.436 32.464c-0.291-0.3079-0.4628-0.786-0.4628-1.4051v-4e-4c0 0.6195 0.1718 1.0972 0.4628 1.4051v4e-4"/>
|
||||
<g transform="matrix(.1 0 0 -.1 -10 50)">
|
||||
<g clip-path="url(#clipPath176-3)">
|
||||
<path d="m204.36 175.36c-2.91 3.079-4.628 7.856-4.628 14.051v1.457c0-6.191 1.718-10.972 4.628-14.051l0.743-0.711-0.743-0.746" fill="url(#linearGradient190-9)"/>
|
||||
</g>
|
||||
<g clip-path="url(#clipPath200-3)">
|
||||
<path d="m204.36 175.36 0.743 0.746v-3e-3l-0.743-0.743" fill="url(#linearGradient206-1)"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="m32.947 20.518c0.0769-0.1688 0.1156-0.3422 0.1156-0.5157v-7e-4c0 0.1738-0.0387 0.3472-0.1156 0.5164"/>
|
||||
<g transform="matrix(.1 0 0 -.1 -10 50)">
|
||||
<g clip-path="url(#clipPath218-3)">
|
||||
<path d="m371.22 257.75-0.93 0.93 49.859 28.332c6.332 3.597 9.887 8.238 10.481 12.965 0-1.735-0.387-3.469-1.156-5.157-1.551-3.402-4.66-6.617-9.325-9.269l-48.929-27.801" fill="url(#linearGradient228-0)"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="m33.063 20.001v-4e-4c-4e-4 -0.5231-0.35-1.0461-1.0481-1.443l-4.8929-2.7801h-4e-4l4e-4 -3e-4 4.8929 2.78c0.6989 0.3969 1.0489 0.9207 1.0481 1.4438" fill="#404040"/>
|
||||
<g transform="matrix(.1 0 0 -.1 -10 50)">
|
||||
<g clip-path="url(#clipPath240-4)">
|
||||
<path d="m430.63 300c-0.594 4.738-4.149 9.375-10.481 12.973l-49.863 28.328 0.93 0.93h4e-3l48.929-27.801c6.981-3.969 10.477-9.199 10.481-14.43" fill="url(#linearGradient250-6)"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="m9.9727 8.9379v-0.0012 0.0012m0.0043-0.1625v-0.0024 0.0024m3e-4 -0.0047v-0.0047 0.0047m0-0.0059c0.0579-1.0273 0.6165-1.6207 1.4067-1.6242-0.3426 2e-3 -0.641 0.1145-0.8746 0.325v-4e-4l-0.0028 0.0028c-0.0246 0.0222-0.048 0.0457-0.0711 0.0699-0.2625 0.2777-0.4281 0.6937-0.4582 1.2269" fill="#404040"/>
|
||||
<g transform="matrix(.1 0 0 -.1 -10 50)">
|
||||
<g clip-path="url(#clipPath262-7)">
|
||||
<path d="m199.73 409.1v1.461 0.055 0.012c0 0.551 0.015 1.09 0.043 1.613v0.024c0 7e-3 0 0.015 3e-3 0.023v0.047 0.012c0.301 5.332 1.957 9.492 4.582 12.269l0.731-0.734c-3.356-3.028-5.359-8.078-5.359-14.782m5.339 16.215 0.028 0.028c-8e-3 -0.012-0.02-0.02-0.028-0.028" fill="url(#linearGradient276-5)"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="m11.384 7.1406h0.0031-0.0031" fill="#404040"/>
|
||||
<g transform="matrix(.1 0 0 -.1 -10 50)">
|
||||
<g clip-path="url(#clipPath288-3)">
|
||||
<path d="m370.28 341.3-145.15 82.473c-4.024 2.281-7.832 3.363-11.223 3.363-3.457 0-6.473-1.125-8.828-3.246l-0.731 0.734c0.231 0.242 0.465 0.477 0.711 0.699 8e-3 8e-3 0.02 0.016 0.028 0.024 2.336 2.105 5.32 3.23 8.746 3.25h0.031 0.043c3.391 0 7.199-1.082 11.223-3.364l146.08-83-4e-3 -3e-3 -0.93-0.93" fill="url(#linearGradient300-7)"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2019-2021 Chrystian Huot
|
||||
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -17,8 +17,10 @@
|
|||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
export const version = '5.2.9';
|
||||
|
||||
export default version;
|
||||
@Component({
|
||||
selector: 'google-play-badge',
|
||||
templateUrl: './google-play-badge.component.svg',
|
||||
})
|
||||
export class GooglePlayBadgeComponent {}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<div>
|
||||
<div>
|
||||
<p>Do you know that Rdio Scanner works natively on {{ deviceType }} devices?</p>
|
||||
</div>
|
||||
<div>
|
||||
<ng-container *ngIf="isAndroid">
|
||||
<a href="https://play.google.com/store/apps/details?id=solutions.saubeo.rdioScanner" target="_blank">
|
||||
<google-play-badge></google-play-badge>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isApple">
|
||||
<a href="https://apps.apple.com/us/app/rdio-scanner/id1563065667#?platform=iphone" target="_blank">
|
||||
<app-store-badge></app-store-badge>
|
||||
</a>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="countdown">{{ countdown }}</div>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* 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, Optional, OnInit } from '@angular/core';
|
||||
import { MatSnackBarRef } from '@angular/material/snack-bar';
|
||||
|
||||
@Component({
|
||||
selector: 'RdioScannerNative',
|
||||
styleUrls: ['./native.scss'],
|
||||
templateUrl: './native.component.html',
|
||||
})
|
||||
export class RdioScannerNativeComponent implements OnInit {
|
||||
countdown: number = 10;
|
||||
|
||||
deviceType: string = 'mobile';
|
||||
|
||||
isAndroid: boolean = false;
|
||||
isApple: boolean = false;
|
||||
|
||||
constructor(@Optional() private matSnackBarRef: MatSnackBarRef<RdioScannerNativeComponent>) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const ua = navigator.userAgent;
|
||||
|
||||
if (ua.includes('Android')) {
|
||||
this.deviceType = 'Android';
|
||||
this.isAndroid = true;
|
||||
this.wait();
|
||||
|
||||
} else if (ua.includes('iPad') || ua.includes('iPhone')) {
|
||||
this.deviceType = 'Apple';
|
||||
this.isApple = true;
|
||||
this.wait();
|
||||
|
||||
} else {
|
||||
this.matSnackBarRef?.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
private wait(): void {
|
||||
setTimeout(() => {
|
||||
this.countdown--;
|
||||
|
||||
if (this.countdown < 1) {
|
||||
this.matSnackBarRef?.dismiss();
|
||||
|
||||
} else {
|
||||
this.wait();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,30 +17,19 @@
|
|||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { AppSharedModule } from '../../../shared/shared.module';
|
||||
import { RdioScannerNativeComponent } from './native.component';
|
||||
import { AppStoreBadgeComponent } from './badges/app-store-badge.component';
|
||||
import { GooglePlayBadgeComponent } from './badges/google-play-badge.component';
|
||||
|
||||
import Sequelize from 'sequelize';
|
||||
|
||||
class RdioScannerTag extends Sequelize.Model { }
|
||||
|
||||
export function tagFactory(ctx) {
|
||||
RdioScannerTag.init(tagFactory.schema, {
|
||||
modelName: 'rdioScannerTag',
|
||||
sequelize: ctx.sequelize,
|
||||
timestamps: false,
|
||||
});
|
||||
|
||||
return RdioScannerTag;
|
||||
}
|
||||
|
||||
tagFactory.schema = {
|
||||
_id: {
|
||||
type: Sequelize.DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
label: {
|
||||
type: Sequelize.DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
};
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppStoreBadgeComponent,
|
||||
GooglePlayBadgeComponent,
|
||||
RdioScannerNativeComponent,
|
||||
],
|
||||
exports: [RdioScannerNativeComponent],
|
||||
imports: [AppSharedModule],
|
||||
})
|
||||
export class RdioScannerNativeModule { }
|
||||
23
client/src/app/components/rdio-scanner/native/native.scss
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
:host {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
> *:first-child {
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.countdown {
|
||||
bottom: 4px;
|
||||
color: rgba(0, 0, 0, 0.67);
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
}
|
||||
}
|
||||
|
|
@ -17,17 +17,19 @@
|
|||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component, ElementRef, HostListener, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatSidenav } from '@angular/material/sidenav';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RdioScannerEvent, RdioScannerLivefeedMode } from './rdio-scanner';
|
||||
import { RdioScannerService } from './rdio-scanner.service';
|
||||
import { RdioScannerNativeComponent } from './native/native.component';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner',
|
||||
styleUrls: ['./rdio-scanner.component.scss'],
|
||||
templateUrl: './rdio-scanner.component.html',
|
||||
})
|
||||
export class RdioScannerComponent implements OnDestroy {
|
||||
export class RdioScannerComponent implements OnDestroy, OnInit {
|
||||
private eventSubscription = this.rdioScannerService.event.subscribe((event: RdioScannerEvent) => this.eventHandler(event));
|
||||
|
||||
private livefeedMode: RdioScannerLivefeedMode = RdioScannerLivefeedMode.Offline;
|
||||
|
|
@ -37,6 +39,7 @@ export class RdioScannerComponent implements OnDestroy {
|
|||
@ViewChild('selectPanel') private selectPanel: MatSidenav | undefined;
|
||||
|
||||
constructor(
|
||||
private matSnackBar: MatSnackBar,
|
||||
private ngElementRef: ElementRef,
|
||||
private rdioScannerService: RdioScannerService,
|
||||
) { }
|
||||
|
|
@ -65,6 +68,29 @@ export class RdioScannerComponent implements OnDestroy {
|
|||
this.eventSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
/*
|
||||
* BEGIN OF RED TAPE:
|
||||
*
|
||||
* By modifying, deleting or disabling the following lines, you are harming
|
||||
* the open source project and its author. Rdio Scanner represents a lot of
|
||||
* investment in time, support, testing and hardware.
|
||||
*
|
||||
* Be respectful, sponsor the project if you can, use native apps when possible.
|
||||
*
|
||||
*/
|
||||
setTimeout(() => {
|
||||
const ua: String = navigator.userAgent;
|
||||
|
||||
if (ua.includes('Android') || ua.includes('iPad') || ua.includes('iPhone')) {
|
||||
this.matSnackBar.openFromComponent(RdioScannerNativeComponent, { panelClass: 'snackbar-white' });
|
||||
}
|
||||
}, 10000);
|
||||
/**
|
||||
* END OF RED TAPE.
|
||||
*/
|
||||
}
|
||||
|
||||
toggleFullscreen(): void {
|
||||
if (document.fullscreenElement) {
|
||||
const el: {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { AppSharedModule } from '../../shared/shared.module';
|
|||
import { RdioScannerComponent } from './rdio-scanner.component';
|
||||
import { RdioScannerService } from './rdio-scanner.service';
|
||||
import { RdioScannerMainComponent } from './main/main.component';
|
||||
import { RdioScannerNativeModule } from './native/native.module';
|
||||
import { RdioScannerSearchComponent } from './search/search.component';
|
||||
import { RdioScannerSelectComponent } from './select/select.component';
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ import { RdioScannerSelectComponent } from './select/select.component';
|
|||
RdioScannerSelectComponent,
|
||||
],
|
||||
exports: [RdioScannerComponent],
|
||||
imports: [AppSharedModule],
|
||||
imports: [AppSharedModule, RdioScannerNativeModule],
|
||||
providers: [
|
||||
RdioScannerService,
|
||||
{ provide: OverlayContainer, useClass: FullscreenOverlayContainer },
|
||||
|
|
|
|||
|
|
@ -26,10 +26,11 @@ import {
|
|||
RdioScannerAvoidOptions,
|
||||
RdioScannerBeepStyle,
|
||||
RdioScannerCall,
|
||||
RdioScannerCategory,
|
||||
RdioScannerCategoryStatus,
|
||||
RdioScannerCategoryType,
|
||||
RdioScannerConfig,
|
||||
RdioScannerEvent,
|
||||
RdioScannerGroup,
|
||||
RdioScannerGroupStatus,
|
||||
RdioScannerLivefeedMap,
|
||||
RdioScannerLivefeedMode,
|
||||
RdioScannerPlaybackList,
|
||||
|
|
@ -74,16 +75,17 @@ export class RdioScannerService implements OnDestroy {
|
|||
private callPrevious: RdioScannerCall | undefined;
|
||||
private callQueue: RdioScannerCall[] = [];
|
||||
|
||||
private categories: RdioScannerCategory[] = [];
|
||||
|
||||
private config: RdioScannerConfig = {
|
||||
dimmerDelay: false,
|
||||
groups: {},
|
||||
keypadBeeps: false,
|
||||
systems: [],
|
||||
tags: {},
|
||||
tagsToggle: false,
|
||||
};
|
||||
|
||||
private groups: RdioScannerGroup[] = [];
|
||||
|
||||
private livefeedMap = {} as RdioScannerLivefeedMap;
|
||||
private livefeedMapPriorToHoldSystem: RdioScannerLivefeedMap | undefined;
|
||||
private livefeedMapPriorToHoldTalkgroup: RdioScannerLivefeedMap | undefined;
|
||||
|
|
@ -91,14 +93,14 @@ export class RdioScannerService implements OnDestroy {
|
|||
private livefeedPaused = false;
|
||||
|
||||
private playbackList: RdioScannerPlaybackList | undefined;
|
||||
private playbackPending: string | undefined;
|
||||
private playbackPending: number | undefined;
|
||||
|
||||
private skipDelay: Subscription | undefined;
|
||||
|
||||
private websocket: WebSocket | undefined;
|
||||
|
||||
constructor(
|
||||
private appUpdateService: AppUpdateService,
|
||||
appUpdateService: AppUpdateService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {
|
||||
this.bootstrapAudio();
|
||||
|
|
@ -162,7 +164,7 @@ export class RdioScannerService implements OnDestroy {
|
|||
this.cleanQueue();
|
||||
}
|
||||
|
||||
this.rebuildGroups();
|
||||
this.rebuildCategories();
|
||||
|
||||
this.storeLivefeedMap();
|
||||
|
||||
|
|
@ -171,7 +173,7 @@ export class RdioScannerService implements OnDestroy {
|
|||
}
|
||||
|
||||
this.event.emit({
|
||||
groups: this.groups,
|
||||
categories: this.categories,
|
||||
holdSys: false,
|
||||
holdTg: false,
|
||||
map: this.livefeedMap,
|
||||
|
|
@ -253,7 +255,7 @@ export class RdioScannerService implements OnDestroy {
|
|||
this.cleanQueue();
|
||||
}
|
||||
|
||||
this.rebuildGroups();
|
||||
this.rebuildCategories();
|
||||
|
||||
if (typeof options?.resubscribe !== 'boolean' || options.resubscribe) {
|
||||
if (this.livefeedMode === RdioScannerLivefeedMode.Online) {
|
||||
|
|
@ -262,7 +264,7 @@ export class RdioScannerService implements OnDestroy {
|
|||
}
|
||||
|
||||
this.event.emit({
|
||||
groups: this.groups,
|
||||
categories: this.categories,
|
||||
holdSys: !!this.livefeedMapPriorToHoldSystem,
|
||||
holdTg: false,
|
||||
map: this.livefeedMap,
|
||||
|
|
@ -305,7 +307,7 @@ export class RdioScannerService implements OnDestroy {
|
|||
this.cleanQueue();
|
||||
}
|
||||
|
||||
this.rebuildGroups();
|
||||
this.rebuildCategories();
|
||||
|
||||
if (typeof options?.resubscribe !== 'boolean' || options.resubscribe) {
|
||||
if (this.livefeedMode === RdioScannerLivefeedMode.Online) {
|
||||
|
|
@ -314,7 +316,7 @@ export class RdioScannerService implements OnDestroy {
|
|||
}
|
||||
|
||||
this.event.emit({
|
||||
groups: this.groups,
|
||||
categories: this.categories,
|
||||
holdSys: false,
|
||||
holdTg: !!this.livefeedMapPriorToHoldTalkgroup,
|
||||
map: this.livefeedMap,
|
||||
|
|
@ -335,7 +337,7 @@ export class RdioScannerService implements OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
loadAndDownload(id: string): void {
|
||||
loadAndDownload(id: number): void {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -343,7 +345,7 @@ export class RdioScannerService implements OnDestroy {
|
|||
this.getCall(id, WebsocketCallFlag.Download);
|
||||
}
|
||||
|
||||
loadAndPlay(id: string): void {
|
||||
loadAndPlay(id: number): void {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -573,10 +575,8 @@ export class RdioScannerService implements OnDestroy {
|
|||
this.stop();
|
||||
}
|
||||
|
||||
toggleGroup(label: string): void {
|
||||
const group = this.groups.find((gr) => gr.label === label);
|
||||
|
||||
if (group) {
|
||||
toggleCategory(category: RdioScannerCategory): void {
|
||||
if (category) {
|
||||
if (this.livefeedMapPriorToHoldSystem) {
|
||||
this.livefeedMapPriorToHoldSystem = undefined;
|
||||
}
|
||||
|
|
@ -585,17 +585,20 @@ export class RdioScannerService implements OnDestroy {
|
|||
this.livefeedMapPriorToHoldTalkgroup = undefined;
|
||||
}
|
||||
|
||||
const status = group.status === RdioScannerGroupStatus.On ? false : true;
|
||||
const status = category.status === RdioScannerCategoryStatus.On ? false : true;
|
||||
|
||||
this.config?.systems.forEach((sys) => {
|
||||
sys.talkgroups?.forEach((tg) => {
|
||||
if (tg.group === label) {
|
||||
if (category.type == RdioScannerCategoryType.Group && tg.group === category.label) {
|
||||
this.livefeedMap[sys.id][tg.id] = status;
|
||||
|
||||
} else if (category.type == RdioScannerCategoryType.Tag && tg.tag === category.label) {
|
||||
this.livefeedMap[sys.id][tg.id] = status;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.rebuildGroups();
|
||||
this.rebuildCategories();
|
||||
|
||||
if (this.call && !this.livefeedMap[this.call.system] &&
|
||||
this.livefeedMap[this.call.system][this.call.talkgroup]) {
|
||||
|
|
@ -612,7 +615,7 @@ export class RdioScannerService implements OnDestroy {
|
|||
this.cleanQueue();
|
||||
|
||||
this.event.emit({
|
||||
groups: this.groups,
|
||||
categories: this.categories,
|
||||
holdSys: false,
|
||||
holdTg: false,
|
||||
map: this.livefeedMap,
|
||||
|
|
@ -718,8 +721,8 @@ export class RdioScannerService implements OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
private getCall(id: string, flags?: WebsocketCallFlag): void {
|
||||
this.sendtoWebsocket(WebsocketCommand.Call, id, flags);
|
||||
private getCall(id: number, flags?: WebsocketCallFlag): void {
|
||||
this.sendtoWebsocket(WebsocketCommand.Call, `${id}`, flags);
|
||||
}
|
||||
|
||||
private getPlaybackQueueCount(id = this.call?.id || this.callPrevious?.id): number {
|
||||
|
|
@ -772,7 +775,7 @@ export class RdioScannerService implements OnDestroy {
|
|||
message = JSON.parse(message);
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`Invalid control message received, ${error.message}`);
|
||||
console.warn(`Invalid control message received, ${error}`);
|
||||
}
|
||||
|
||||
if (Array.isArray(message)) {
|
||||
|
|
@ -803,6 +806,7 @@ export class RdioScannerService implements OnDestroy {
|
|||
keypadBeeps: config.keypadBeeps !== null && typeof config.keypadBeeps === 'object' ? config.keypadBeeps : {},
|
||||
systems: Array.isArray(config.systems) ? config.systems.slice() : [],
|
||||
tags: typeof config.tags !== null && typeof config.tags === 'object' ? config.tags : {},
|
||||
tagsToggle: typeof config.tagsToggle !== null && typeof config.tagsToggle === 'boolean' ? config.tagsToggle : false,
|
||||
};
|
||||
|
||||
this.rebuildLivefeedMap();
|
||||
|
|
@ -813,8 +817,8 @@ export class RdioScannerService implements OnDestroy {
|
|||
|
||||
this.event.emit({
|
||||
auth: false,
|
||||
categories: this.categories,
|
||||
config: this.config,
|
||||
groups: this.groups,
|
||||
holdSys: !!this.livefeedMapPriorToHoldSystem,
|
||||
holdTg: !!this.livefeedMapPriorToHoldTalkgroup,
|
||||
map: this.livefeedMap,
|
||||
|
|
@ -901,8 +905,8 @@ export class RdioScannerService implements OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
private rebuildGroups(): void {
|
||||
this.groups = Object.keys(this.config.groups || []).map((label) => {
|
||||
private rebuildCategories(): void {
|
||||
this.categories = Object.keys(this.config.groups || []).map((label) => {
|
||||
const allOff = Object.keys(this.config.groups[label]).map((sys) => +sys)
|
||||
.every((sys: number) => this.config.groups[label] && this.config.groups[label][sys]
|
||||
.every((tg) => this.livefeedMap[sys] && !this.livefeedMap[sys][tg]));
|
||||
|
|
@ -911,10 +915,28 @@ export class RdioScannerService implements OnDestroy {
|
|||
.every((sys: number) => this.config.groups[label] && this.config.groups[label][sys]
|
||||
.every((tg) => this.livefeedMap[sys] && this.livefeedMap[sys][tg]));
|
||||
|
||||
const status = allOff ? RdioScannerGroupStatus.Off : allOn ? RdioScannerGroupStatus.On : RdioScannerGroupStatus.Partial;
|
||||
const status = allOff ? RdioScannerCategoryStatus.Off : allOn ? RdioScannerCategoryStatus.On : RdioScannerCategoryStatus.Partial;
|
||||
|
||||
return { label, status };
|
||||
}).sort((a, b) => a.label.localeCompare(b.label));
|
||||
return { label, status, type: RdioScannerCategoryType.Group };
|
||||
})
|
||||
|
||||
if (this.config.tagsToggle) {
|
||||
this.categories = this.categories.concat(Object.keys(this.config.tags || []).map((label) => {
|
||||
const allOff = Object.keys(this.config.tags[label]).map((sys) => +sys)
|
||||
.every((sys: number) => this.config.tags[label] && this.config.tags[label][sys]
|
||||
.every((tg) => this.livefeedMap[sys] && !this.livefeedMap[sys][tg]));
|
||||
|
||||
const allOn = Object.keys(this.config.tags[label]).map((sys) => +sys)
|
||||
.every((sys: number) => this.config.tags[label] && this.config.tags[label][sys]
|
||||
.every((tg) => this.livefeedMap[sys] && this.livefeedMap[sys][tg]));
|
||||
|
||||
const status = allOff ? RdioScannerCategoryStatus.Off : allOn ? RdioScannerCategoryStatus.On : RdioScannerCategoryStatus.Partial;
|
||||
|
||||
return { label, status, type: RdioScannerCategoryType.Tag };
|
||||
}))
|
||||
}
|
||||
|
||||
this.categories.sort((a, b) => a.label.localeCompare(b.label));
|
||||
}
|
||||
|
||||
private rebuildLivefeedMap(): void {
|
||||
|
|
@ -940,7 +962,7 @@ export class RdioScannerService implements OnDestroy {
|
|||
|
||||
this.storeLivefeedMap();
|
||||
|
||||
this.rebuildGroups();
|
||||
this.rebuildCategories();
|
||||
}
|
||||
|
||||
private reconnectWebsocket(): void {
|
||||
|
|
@ -964,7 +986,7 @@ export class RdioScannerService implements OnDestroy {
|
|||
|
||||
private sendtoWebsocket(command: string, payload?: unknown, flags?: string): void {
|
||||
if (this.websocket?.readyState === 1) {
|
||||
const message: (string | unknown)[] = [command];
|
||||
const message: unknown[] = [command];
|
||||
|
||||
if (payload) {
|
||||
message.push(payload);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export interface RdioScannerCall {
|
|||
dateTime: Date;
|
||||
frequencies?: RdioScannerCallFrequency[];
|
||||
frequency?: number;
|
||||
id: string;
|
||||
id: number;
|
||||
source?: number;
|
||||
sources?: RdioScannerCallSource[];
|
||||
system: number;
|
||||
|
|
@ -70,20 +70,38 @@ export interface RdioScannerCallSource {
|
|||
src?: number;
|
||||
}
|
||||
|
||||
export interface RdioScannerCategory {
|
||||
label: string;
|
||||
status: RdioScannerCategoryStatus;
|
||||
type: RdioScannerCategoryType;
|
||||
}
|
||||
|
||||
export enum RdioScannerCategoryStatus {
|
||||
Off = 'off',
|
||||
On = 'on',
|
||||
Partial = 'partial',
|
||||
}
|
||||
|
||||
export enum RdioScannerCategoryType {
|
||||
Group = 'group',
|
||||
Tag = 'tag',
|
||||
}
|
||||
|
||||
export interface RdioScannerConfig {
|
||||
dimmerDelay: number | false;
|
||||
groups: { [key: string]: { [key: number]: number[] } };
|
||||
keypadBeeps: RdioScannerKeypadBeeps | false;
|
||||
systems: RdioScannerSystem[];
|
||||
tags: { [key: string]: { [key: number]: number[] } };
|
||||
tagsToggle: boolean;
|
||||
}
|
||||
|
||||
export interface RdioScannerEvent {
|
||||
auth?: boolean;
|
||||
config?: RdioScannerConfig;
|
||||
categories?: RdioScannerCategory[];
|
||||
call?: RdioScannerCall;
|
||||
config?: RdioScannerConfig;
|
||||
expired?: boolean;
|
||||
groups?: RdioScannerGroup[];
|
||||
holdSys?: boolean;
|
||||
holdTg?: boolean;
|
||||
linked?: boolean;
|
||||
|
|
@ -91,23 +109,12 @@ export interface RdioScannerEvent {
|
|||
map?: RdioScannerLivefeedMap;
|
||||
pause?: boolean;
|
||||
playbackList?: RdioScannerPlaybackList;
|
||||
playbackPending?: string;
|
||||
playbackPending?: number;
|
||||
queue?: number;
|
||||
time?: number;
|
||||
tooMany?: boolean;
|
||||
}
|
||||
|
||||
export enum RdioScannerGroupStatus {
|
||||
Off = 'off',
|
||||
On = 'on',
|
||||
Partial = 'partial',
|
||||
}
|
||||
|
||||
export interface RdioScannerGroup {
|
||||
label: string;
|
||||
status: RdioScannerGroupStatus;
|
||||
}
|
||||
|
||||
export interface RdioScannerKeypadBeeps {
|
||||
[RdioScannerBeepStyle.Activate]: RdioScannerBeep[];
|
||||
[RdioScannerBeepStyle.Deactivate]: RdioScannerBeep[];
|
||||
|
|
|
|||
|
|
@ -3,18 +3,17 @@
|
|||
<ng-container matColumnDef="control">
|
||||
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<button *ngIf="downloadMode.checked && row" mat-icon-button (click)="download(row.id)">
|
||||
<button *ngIf="downloadMode.checked && row" mat-icon-button (click)="download(+row.id)">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="row && !downloadMode.checked && !paused && row?.id !== call?.id && row?.id !== callPending"
|
||||
mat-icon-button (click)="play(row.id)">
|
||||
<button *ngIf="row && !downloadMode.checked && !paused && row?.id != call?.id && row?.id != callPending"
|
||||
mat-icon-button (click)="play(+row.id)">
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="row && !downloadMode.checked && row?.id === callPending" mat-icon-button>
|
||||
<button *ngIf="row && !downloadMode.checked && row?.id == callPending" mat-icon-button>
|
||||
<mat-icon class="spin">cached</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="row && !downloadMode.checked && row?.id === call?.id" mat-icon-button (click)="stop()">
|
||||
<button *ngIf="row && !downloadMode.checked && row?.id == call?.id" mat-icon-button (click)="stop()">
|
||||
<mat-icon>stop</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
|
|
@ -70,8 +69,9 @@
|
|||
<mat-slide-toggle #downloadMode color="primary" labelPosition="before">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</mat-slide-toggle>
|
||||
<mat-paginator [disabled]="livefeedPlayback || resultsPending" [length]="playbackList?.count" [hidePageSize]="true"
|
||||
[pageSize]="results.value.length" [showFirstLastButtons]="true" (page)="refreshResults()">
|
||||
<mat-paginator [disabled]="livefeedPlayback || resultsPending" [length]="playbackList?.count"
|
||||
[hidePageSize]="true" [pageSize]="results.value.length" [showFirstLastButtons]="true"
|
||||
(page)="refreshResults()">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
|
@ -94,17 +94,14 @@
|
|||
<mat-label>
|
||||
Date
|
||||
</mat-label>
|
||||
<input formControlName="date" [matDatepicker]="date" matInput [max]="playbackList?.dateStop"
|
||||
[min]="playbackList?.dateStart" placeholder="Choose a date" readonly (click)="date.open()"
|
||||
(dateChange)="formChangeHandler()">
|
||||
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
|
||||
<mat-datepicker #date></mat-datepicker>
|
||||
<input matInput type="datetime-local" formControlName="date" [max]="playbackList?.dateStop"
|
||||
[min]="playbackList?.dateStart" placeholder="Choose a date" (change)="formChangeHandler()">
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>
|
||||
System
|
||||
</mat-label>
|
||||
<mat-select formControlName="system" (selectionChange)="formSystemHandler()">
|
||||
<mat-select formControlName="system" (selectionChange)="formChangeHandler()">
|
||||
<mat-option [value]="-1">
|
||||
All Systems
|
||||
</mat-option>
|
||||
|
|
@ -117,7 +114,7 @@
|
|||
<mat-label>
|
||||
Talkgroup
|
||||
</mat-label>
|
||||
<mat-select formControlName="talkgroup" (selectionChange)="formTalkgroupHandler()">
|
||||
<mat-select formControlName="talkgroup" (selectionChange)="formChangeHandler()">
|
||||
<mat-option [value]="-1">
|
||||
All Talkgroups
|
||||
</mat-option>
|
||||
|
|
@ -130,7 +127,7 @@
|
|||
<mat-label>
|
||||
Group
|
||||
</mat-label>
|
||||
<mat-select formControlName="group" (selectionChange)="formGroupHandler()">
|
||||
<mat-select formControlName="group" (selectionChange)="formChangeHandler()">
|
||||
<mat-option [value]="-1">
|
||||
All Groups
|
||||
</mat-option>
|
||||
|
|
@ -143,7 +140,7 @@
|
|||
<mat-label>
|
||||
Tag
|
||||
</mat-label>
|
||||
<mat-select formControlName="tag" (selectionChange)="formTagHandler()">
|
||||
<mat-select formControlName="tag" (selectionChange)="formChangeHandler()">
|
||||
<mat-option [value]="-1">
|
||||
All Tags
|
||||
</mat-option>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import { RdioScannerService } from '../rdio-scanner.service';
|
|||
})
|
||||
export class RdioScannerSearchComponent implements OnDestroy {
|
||||
call: RdioScannerCall | undefined;
|
||||
callPending: string | undefined;
|
||||
callPending: number | undefined;
|
||||
|
||||
form = this.ngFormBuilder.group({
|
||||
date: [null],
|
||||
|
|
@ -82,7 +82,7 @@ export class RdioScannerSearchComponent implements OnDestroy {
|
|||
private ngFormBuilder: FormBuilder,
|
||||
) { }
|
||||
|
||||
download(id: string): void {
|
||||
download(id: number): void {
|
||||
this.rdioScannerService.loadAndDownload(id);
|
||||
}
|
||||
|
||||
|
|
@ -93,223 +93,89 @@ export class RdioScannerSearchComponent implements OnDestroy {
|
|||
|
||||
this.paginator?.firstPage();
|
||||
|
||||
this.refreshFilters();
|
||||
|
||||
this.searchCalls();
|
||||
}
|
||||
|
||||
formGroupHandler(): void {
|
||||
if (!this.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedGroup = this.getSelectedGroup();
|
||||
|
||||
const selectedSystem = this.getSelectedSystem();
|
||||
|
||||
const selectedTag = this.getSelectedTag();
|
||||
|
||||
const selectedTalkgroup = this.getSelectedTalkgroup();
|
||||
|
||||
this.optionsSystem = selectedGroup
|
||||
? this.config.systems
|
||||
.filter((system) => system.talkgroups
|
||||
.some((talkgroup) => talkgroup.group === selectedGroup))
|
||||
.map((system) => system.label)
|
||||
: selectedTag
|
||||
? this.config.systems
|
||||
.filter((system) => system.talkgroups
|
||||
.some((talkgroup) => talkgroup.tag === selectedTag))
|
||||
.map((system) => system.label)
|
||||
: this.config.systems
|
||||
.map((system) => system.label);
|
||||
|
||||
this.optionsTag = selectedGroup
|
||||
? Object.keys(this.config.tags)
|
||||
.filter((tag) => this.config?.systems
|
||||
.some((system) => system.talkgroups
|
||||
.some((talkgroup) => talkgroup.group === selectedGroup && talkgroup.tag === tag)))
|
||||
: selectedTalkgroup
|
||||
? [selectedTalkgroup.tag]
|
||||
: selectedSystem
|
||||
? Object.keys(this.config.tags)
|
||||
.filter((tag) => selectedSystem.talkgroups
|
||||
.some((talkgroup) => talkgroup.tag === tag))
|
||||
: Object.keys(this.config.tags)
|
||||
this.optionsTag.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.optionsTalkgroup = selectedGroup
|
||||
? selectedSystem
|
||||
? selectedSystem.talkgroups
|
||||
.filter((talkgroup) => talkgroup.group === selectedGroup)
|
||||
.map((talkgroup) => talkgroup.label)
|
||||
: []
|
||||
: selectedSystem
|
||||
? selectedSystem.talkgroups
|
||||
.map((talkgroup) => talkgroup.label)
|
||||
: [];
|
||||
|
||||
this.form.patchValue({
|
||||
system: selectedSystem ? this.optionsSystem.findIndex((system) => system === selectedSystem.label) : -1,
|
||||
tag: selectedTag ? this.optionsTag.findIndex((tag) => tag === selectedTag) : -1,
|
||||
talkgroup: selectedTalkgroup ? this.optionsTalkgroup.findIndex((talkgroup) => talkgroup === selectedTalkgroup.label) : -1,
|
||||
});
|
||||
|
||||
this.formChangeHandler();
|
||||
}
|
||||
|
||||
formSystemHandler(): void {
|
||||
if (!this.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedGroup = this.getSelectedGroup();
|
||||
|
||||
const selectedSystem = this.getSelectedSystem();
|
||||
|
||||
const selectedTag = this.getSelectedTag();
|
||||
|
||||
const selectedTalkgroup = this.getSelectedTalkgroup();
|
||||
|
||||
this.optionsGroup = selectedSystem
|
||||
? Object.keys(this.config.groups)
|
||||
.filter((group) => Object.keys(this.config?.groups[group] || {})
|
||||
.map((system) => +system)
|
||||
.includes(selectedSystem.id))
|
||||
: Object.keys(this.config.groups)
|
||||
this.optionsGroup.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.optionsTag = selectedSystem
|
||||
? Object.keys(this.config.tags)
|
||||
.filter((tag) => Object.keys(this.config?.tags[tag] || {})
|
||||
.map((system) => +system)
|
||||
.includes(selectedSystem.id))
|
||||
: Object.keys(this.config.tags)
|
||||
this.optionsTag.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.optionsTalkgroup = selectedSystem
|
||||
? selectedSystem.talkgroups
|
||||
.filter((talkgroup) => (selectedGroup ? talkgroup.group === selectedGroup : true)
|
||||
&& (selectedTag ? talkgroup.tag === selectedTag : true))
|
||||
.map((talkgroup) => talkgroup.label)
|
||||
: [];
|
||||
|
||||
this.form.patchValue({
|
||||
group: selectedGroup ? this.optionsGroup.findIndex((group) => group === selectedGroup) : -1,
|
||||
tag: selectedTag ? this.optionsTag.findIndex((tag) => tag === selectedTag) : -1,
|
||||
talkgroup: selectedTalkgroup ? this.optionsTalkgroup.findIndex((talkgroup) => talkgroup === selectedTalkgroup.label) : -1,
|
||||
});
|
||||
|
||||
this.formChangeHandler();
|
||||
}
|
||||
|
||||
formTagHandler(): void {
|
||||
if (!this.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedGroup = this.getSelectedGroup();
|
||||
|
||||
const selectedSystem = this.getSelectedSystem();
|
||||
|
||||
const selectedTag = this.getSelectedTag();
|
||||
|
||||
const selectedTalkgroup = this.getSelectedTalkgroup();
|
||||
|
||||
this.optionsGroup = selectedTag
|
||||
? Object.keys(this.config.groups)
|
||||
.filter((group) => this.config?.systems
|
||||
.some((system) => system.talkgroups
|
||||
.some((talkgroup) => talkgroup.group === group && talkgroup.tag === selectedTag)))
|
||||
: selectedTalkgroup
|
||||
? [selectedTalkgroup.group]
|
||||
: selectedSystem
|
||||
? Object.keys(this.config.groups)
|
||||
.filter((group) => selectedSystem.talkgroups
|
||||
.some((talkgroup) => talkgroup.group === group))
|
||||
: Object.keys(this.config.groups)
|
||||
this.optionsGroup.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.optionsSystem = selectedTag
|
||||
? this.config.systems
|
||||
.filter((system) => system.talkgroups
|
||||
.some((talkgroup) => talkgroup.tag === selectedTag))
|
||||
.map((system) => system.label)
|
||||
: selectedGroup
|
||||
? this.config.systems
|
||||
.filter((system) => system.talkgroups
|
||||
.some((talkgroup) => talkgroup.group === selectedGroup))
|
||||
.map((system) => system.label)
|
||||
: this.config.systems
|
||||
.map((system) => system.label);
|
||||
|
||||
this.optionsTalkgroup = selectedTag
|
||||
? selectedSystem
|
||||
? selectedSystem.talkgroups
|
||||
.filter((talkgroup) => talkgroup.tag === selectedTag)
|
||||
.map((talkgroup) => talkgroup.label)
|
||||
: []
|
||||
: selectedSystem
|
||||
? selectedSystem.talkgroups
|
||||
.map((talkgroup) => talkgroup.label)
|
||||
: [];
|
||||
|
||||
this.form.patchValue({
|
||||
group: selectedGroup ? this.optionsGroup.findIndex((group) => group === selectedGroup) : -1,
|
||||
system: selectedSystem ? this.optionsSystem.findIndex((system) => system === selectedSystem.label) : -1,
|
||||
talkgroup: selectedTalkgroup ? this.optionsTalkgroup.findIndex((talkgroup) => talkgroup === selectedTalkgroup.label) : -1,
|
||||
});
|
||||
|
||||
this.formChangeHandler();
|
||||
}
|
||||
|
||||
formTalkgroupHandler(): void {
|
||||
if (!this.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedGroup = this.getSelectedGroup();
|
||||
|
||||
const selectedSystem = this.getSelectedSystem();
|
||||
|
||||
const selectedTag = this.getSelectedTag();
|
||||
|
||||
const selectedTalkgroup = this.getSelectedTalkgroup();
|
||||
|
||||
this.optionsGroup = selectedTalkgroup
|
||||
? Object.keys(this.config.groups)
|
||||
.filter((group) => group === selectedTalkgroup.group)
|
||||
: selectedSystem
|
||||
? Object.keys(this.config.groups)
|
||||
.filter((group) => selectedSystem.talkgroups
|
||||
.some((talkgroup) => talkgroup.group === group))
|
||||
: Object.keys(this.config.groups)
|
||||
this.optionsGroup.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.optionsTag = selectedTalkgroup
|
||||
? Object.keys(this.config.tags)
|
||||
.filter((tag) => tag === selectedTalkgroup.tag)
|
||||
: selectedSystem
|
||||
? Object.keys(this.config.tags)
|
||||
.filter((tag) => selectedSystem.talkgroups
|
||||
.some((talkgroup) => talkgroup.tag === tag))
|
||||
: Object.keys(this.config.tags)
|
||||
this.optionsTag.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.form.patchValue({
|
||||
group: selectedGroup ? this.optionsGroup.findIndex((group) => group === selectedGroup) : -1,
|
||||
tag: selectedTag ? this.optionsTag.findIndex((tag) => tag === selectedTag) : -1,
|
||||
});
|
||||
|
||||
this.formChangeHandler();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.eventSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
play(id: string): void {
|
||||
play(id: number): void {
|
||||
this.rdioScannerService.loadAndPlay(id);
|
||||
}
|
||||
|
||||
refreshFilters(): void {
|
||||
if (!this.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedGroup = this.getSelectedGroup();
|
||||
const selectedSystem = this.getSelectedSystem();
|
||||
const selectedTag = this.getSelectedTag();
|
||||
const selectedTalkgroup = this.getSelectedTalkgroup();
|
||||
|
||||
this.optionsSystem = this.config.systems
|
||||
.filter((system) => {
|
||||
const group = selectedGroup === undefined ||
|
||||
system.talkgroups.some((talkgroup) => talkgroup.group === selectedGroup);
|
||||
const tag = selectedTag === undefined ||
|
||||
system.talkgroups.some((talkgroup) => talkgroup.tag === selectedTag);
|
||||
return group && tag;
|
||||
})
|
||||
.map((system) => system.label);
|
||||
|
||||
this.optionsTalkgroup = selectedSystem == undefined
|
||||
? []
|
||||
: selectedSystem.talkgroups
|
||||
.filter((talkgroup) => {
|
||||
const group = selectedGroup == undefined ||
|
||||
talkgroup.group === selectedGroup;
|
||||
const tag = selectedTag == undefined ||
|
||||
talkgroup.tag === selectedTag;
|
||||
return group && tag;
|
||||
})
|
||||
.map((talkgroup) => talkgroup.label);
|
||||
|
||||
this.optionsGroup = Object.keys(this.config.groups)
|
||||
.filter((group) => {
|
||||
const system: boolean = selectedSystem === undefined ||
|
||||
selectedSystem.talkgroups.some((talkgroup) => talkgroup.group === group)
|
||||
const talkgroup: boolean = selectedTalkgroup === undefined ||
|
||||
selectedTalkgroup.group === group;
|
||||
const tag: boolean = selectedTag === undefined ||
|
||||
(selectedTalkgroup !== undefined && selectedTalkgroup.tag === selectedTag) ||
|
||||
(this.config !== undefined && this.config.systems
|
||||
.flatMap((system) => system.talkgroups)
|
||||
.some((talkgroup) => talkgroup.group === group && talkgroup.tag === selectedTag))
|
||||
return system && talkgroup && tag;
|
||||
})
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
|
||||
this.optionsTag = Object.keys(this.config.tags)
|
||||
.filter((tag) => {
|
||||
const system: boolean = selectedSystem === undefined ||
|
||||
selectedSystem.talkgroups.some((talkgroup) => talkgroup.tag === tag)
|
||||
const talkgroup: boolean = selectedTalkgroup === undefined ||
|
||||
selectedTalkgroup.tag === tag;
|
||||
const group: boolean = selectedGroup === undefined ||
|
||||
(selectedTalkgroup !== undefined && selectedTalkgroup.group === selectedGroup) ||
|
||||
(this.config !== undefined && this.config.systems
|
||||
.flatMap((system) => system.talkgroups)
|
||||
.some((talkgroup) => talkgroup.tag === tag && talkgroup.group === selectedGroup))
|
||||
return system && talkgroup && group;
|
||||
})
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
|
||||
this.form.patchValue({
|
||||
group: selectedGroup ? this.optionsGroup.findIndex((group) => group === selectedGroup) : -1,
|
||||
system: selectedSystem ? this.optionsSystem.findIndex((system) => system === selectedSystem.label) : -1,
|
||||
tag: selectedTag ? this.optionsTag.findIndex((tag) => tag === selectedTag) : -1,
|
||||
talkgroup: selectedTalkgroup ? this.optionsTalkgroup.findIndex((talkgroup) => talkgroup === selectedTalkgroup.label) : -1,
|
||||
});
|
||||
}
|
||||
|
||||
refreshResults(): void {
|
||||
if (!this.paginator) {
|
||||
return;
|
||||
|
|
@ -365,8 +231,8 @@ export class RdioScannerSearchComponent implements OnDestroy {
|
|||
sort: this.form.value.sort,
|
||||
};
|
||||
|
||||
if (this.form.value.date instanceof Date) {
|
||||
options.date = this.form.value.date;
|
||||
if (typeof this.form.value.date === 'string') {
|
||||
options.date = new Date(Date.parse(this.form.value.date));
|
||||
}
|
||||
|
||||
if (this.form.value.group >= 0) {
|
||||
|
|
@ -442,9 +308,9 @@ export class RdioScannerSearchComponent implements OnDestroy {
|
|||
|
||||
this.callPending = undefined;
|
||||
|
||||
this.optionsGroup = Object.keys(this.config?.groups || []).sort((a, b) => a.localeCompare(b));
|
||||
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.optionsTag = Object.keys(this.config?.tags || []).sort((a, b) => a.localeCompare(b));
|
||||
this.optionsTalkgroup = [];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<fieldset *ngIf="groups?.length && (groups?.length || 0) >= 2" class="fieldset">
|
||||
<fieldset *ngIf="categories?.length && (categories?.length || 0) >= 2" class="fieldset">
|
||||
<div>
|
||||
<button *ngFor="let group of groups" class="rdio-button" [ngClass]="group.status"
|
||||
(click)="toggleGroup(group.label)">
|
||||
{{ group.label }}
|
||||
<button *ngFor="let category of categories" class="rdio-button" [ngClass]="category.status"
|
||||
(click)="toggle(category)">
|
||||
{{ category.label }}
|
||||
</button>
|
||||
<button class="rdio-button-mini all-off" (click)="avoid({ all: false })">
|
||||
OFF
|
||||
|
|
|
|||
|
|
@ -30,9 +30,8 @@
|
|||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
opacity: 0.8;
|
||||
padding: 0 12px;
|
||||
legend.small {
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
> div {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,15 @@
|
|||
*/
|
||||
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { RdioScannerAvoidOptions, RdioScannerBeepStyle, RdioScannerEvent, RdioScannerGroup, RdioScannerLivefeedMap, RdioScannerSystem } from '../rdio-scanner';
|
||||
import {
|
||||
RdioScannerAvoidOptions,
|
||||
RdioScannerBeepStyle,
|
||||
RdioScannerCategory,
|
||||
RdioScannerCategoryStatus,
|
||||
RdioScannerEvent,
|
||||
RdioScannerLivefeedMap,
|
||||
RdioScannerSystem,
|
||||
} from '../rdio-scanner';
|
||||
import { RdioScannerService } from '../rdio-scanner.service';
|
||||
|
||||
@Component({
|
||||
|
|
@ -30,18 +38,31 @@ import { RdioScannerService } from '../rdio-scanner.service';
|
|||
templateUrl: './select.component.html',
|
||||
})
|
||||
export class RdioScannerSelectComponent implements OnDestroy {
|
||||
groups: RdioScannerGroup[] | undefined;
|
||||
categories: RdioScannerCategory[] | undefined;
|
||||
|
||||
map: RdioScannerLivefeedMap = {};
|
||||
|
||||
systems: RdioScannerSystem[] | undefined;
|
||||
|
||||
tagsToggle: boolean | undefined;
|
||||
|
||||
private eventSubscription = this.rdioScannerService.event.subscribe((event: RdioScannerEvent) => this.eventHandler(event));
|
||||
|
||||
constructor(private rdioScannerService: RdioScannerService) { }
|
||||
|
||||
avoid(options?: RdioScannerAvoidOptions): void {
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||
if (options?.all == true)
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||
else if (options?.all == false)
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Deactivate);
|
||||
else if (
|
||||
options?.system !== undefined &&
|
||||
options?.talkgroup !== undefined &&
|
||||
this.map[options!.system.id][options!.talkgroup.id] == false
|
||||
)
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Deactivate);
|
||||
else
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||
|
||||
this.rdioScannerService.avoid(options);
|
||||
}
|
||||
|
|
@ -50,23 +71,21 @@ export class RdioScannerSelectComponent implements OnDestroy {
|
|||
this.eventSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
toggleGroup(label: string): void {
|
||||
this.rdioScannerService.beep();
|
||||
toggle(category: RdioScannerCategory): void {
|
||||
if (category.status == RdioScannerCategoryStatus.On)
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Deactivate);
|
||||
else
|
||||
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
|
||||
|
||||
this.rdioScannerService.toggleGroup(label);
|
||||
this.rdioScannerService.toggleCategory(category);
|
||||
}
|
||||
|
||||
private eventHandler(event: RdioScannerEvent): void {
|
||||
if (event.config) {
|
||||
this.tagsToggle = event.config.tagsToggle;
|
||||
this.systems = event.config.systems;
|
||||
}
|
||||
|
||||
if (event.groups) {
|
||||
this.groups = event.groups;
|
||||
}
|
||||
|
||||
if (event.map) {
|
||||
this.map = event.map;
|
||||
}
|
||||
if (event.categories) this.categories = event.categories;
|
||||
if (event.map) this.map = event.map;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* *****************************************************************************
|
||||
* 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 { registerLocaleData } from '@angular/common';
|
||||
import locale from '@angular/common/locales/en-CA';
|
||||
import { LOCALE_ID as NG_LOCALE_ID, ModuleWithProviders, NgModule } from '@angular/core';
|
||||
|
||||
const LOCALE_ID = 'en-CA'; // Because we need dates as yyyy-mm-dd
|
||||
|
||||
@NgModule()
|
||||
export class AppLocaleModule {
|
||||
static forRoot(): ModuleWithProviders<AppLocaleModule> {
|
||||
return {
|
||||
ngModule: AppLocaleModule,
|
||||
providers: [
|
||||
{ provide: NG_LOCALE_ID, useValue: LOCALE_ID },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
registerLocaleData(locale, LOCALE_ID);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ import { CommonModule } from '@angular/common';
|
|||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ExtraOptions, RouterModule, Routes } from '@angular/router';
|
||||
import { AppLocaleModule } from './locale/locale.module';
|
||||
import { AppMaterialModule } from './material/material.module';
|
||||
import { AppUpdateModule } from './update/update.module';
|
||||
|
||||
|
|
@ -32,7 +31,6 @@ export interface AppSharedModuleConfig {
|
|||
|
||||
@NgModule({
|
||||
exports: [
|
||||
AppLocaleModule,
|
||||
AppMaterialModule,
|
||||
AppUpdateModule,
|
||||
CommonModule,
|
||||
|
|
@ -55,7 +53,6 @@ export class AppSharedModule {
|
|||
return {
|
||||
ngModule: AppSharedModule,
|
||||
providers: [
|
||||
...AppLocaleModule.forRoot().providers || [],
|
||||
...AppUpdateModule.forRoot().providers || [],
|
||||
...RouterModule.forRoot(config.routerRoutes || [], config.routerExtraOptions || {}).providers || [],
|
||||
],
|
||||
|
|
|
|||
|
|
@ -28,9 +28,11 @@ export class AppUpdateService {
|
|||
private matDialog: MatDialog,
|
||||
private swUpdate: SwUpdate,
|
||||
) {
|
||||
swUpdate.available.subscribe(() => this.prompt());
|
||||
swUpdate.checkForUpdate();
|
||||
setInterval(() => swUpdate.checkForUpdate(), 5 * 60 * 1000);
|
||||
if (swUpdate.isEnabled) {
|
||||
swUpdate.versionUpdates.subscribe(() => this.prompt());
|
||||
swUpdate.checkForUpdate();
|
||||
setInterval(() => swUpdate.checkForUpdate(), 5 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
prompt(): void {
|
||||
|
|
|
|||
BIN
client/src/assets/fonts/material-icons-v97.woff2
Normal file
BIN
client/src/assets/fonts/roboto-latin-ext-w300-v29.woff2
Normal file
BIN
client/src/assets/fonts/roboto-latin-ext-w400-v29.woff2
Normal file
BIN
client/src/assets/fonts/roboto-latin-ext-w500-v29.woff2
Normal file
BIN
client/src/assets/fonts/roboto-latin-w300-v29.woff2
Normal file
BIN
client/src/assets/fonts/roboto-latin-w400-v29.woff2
Normal file
BIN
client/src/assets/fonts/roboto-latin-w500-v29.woff2
Normal file
|
|
@ -6,8 +6,6 @@
|
|||
<link rel="apple-touch-icon" sizes="192x192" href="assets/icons/icon-192x192.png">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Icons&family=Roboto:wght@300;400;500&display=block">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title>Rdio Scanner</title>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
* automatically update themselves. This includes recent versions of Safari, Chrome (including
|
||||
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
|
@ -18,16 +18,6 @@
|
|||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
|
|
@ -55,7 +45,7 @@
|
|||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
import 'zone.js'; // Included with Angular CLI.
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
|
|
|
|||
|
|
@ -21,19 +21,11 @@
|
|||
|
||||
const server = 'http://localhost:3000';
|
||||
|
||||
const PROXY_CONFIG = [
|
||||
module.exports = [
|
||||
{
|
||||
bypass: (req) => req.upgrade ? null : '/',
|
||||
context: '/',
|
||||
secure: false,
|
||||
target: server,
|
||||
ws: true,
|
||||
},
|
||||
{
|
||||
context: ['/api'],
|
||||
context: ['/**', '!/admin**', '!/ws**'],
|
||||
secure: false,
|
||||
target: server,
|
||||
ws: true
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = PROXY_CONFIG;
|
||||
]
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
@import '~@angular/material/theming';
|
||||
@import "~@angular/material/theming";
|
||||
@import "./styles/material-icons";
|
||||
@import "./styles/roboto-fonts";
|
||||
|
||||
$font-family: Roboto, 'Helvetica Neue', sans-serif;
|
||||
$font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
|
||||
$custom-typography: mat-typography-config($font-family: $font-family);
|
||||
$custom-typography: mat-typography-config(
|
||||
$font-family: $font-family,
|
||||
);
|
||||
|
||||
@include mat-core($custom-typography);
|
||||
|
||||
|
|
@ -22,3 +26,22 @@ body,
|
|||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
direction: ltr;
|
||||
display: inline-block;
|
||||
font-family: "Material Icons";
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
letter-spacing: normal;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
.snackbar-white {
|
||||
background: white;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
|
|
|
|||
21
client/src/styles/material-icons.scss
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
@font-face {
|
||||
font-family: "Material Icons";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(../assets/fonts/material-icons-v97.woff2) format("woff2");
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
direction: ltr;
|
||||
display: inline-block;
|
||||
font-display: block;
|
||||
font-family: "Material Icons";
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
letter-spacing: normal;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
}
|
||||
57
client/src/styles/roboto-fonts.scss
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(../assets/fonts/roboto-latin-ext-w300-v29.woff2) format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(../assets/fonts/roboto-latin-w300-v29.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
|
||||
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../assets/fonts/roboto-latin-ext-w400-v29.woff2) format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../assets/fonts/roboto-latin-w400-v29.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
|
||||
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(../assets/fonts/roboto-latin-ext-w500-v29.woff2) format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(../assets/fonts/roboto-latin-w500-v29.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
|
||||
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
|
|
@ -9,5 +9,7 @@
|
|||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": []
|
||||
}
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2015",
|
||||
"module": "es2020",
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom"
|
||||
],
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"strictInjectionParameters": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,33 @@
|
|||
/*
|
||||
This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience.
|
||||
It is not intended to be used to perform a compilation.
|
||||
|
||||
To learn more about this file see: https://angular.io/config/solution-tsconfig.
|
||||
*/
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"baseUrl": "./",
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"importHelpers": true,
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom"
|
||||
],
|
||||
"module": "es2020",
|
||||
"moduleResolution": "node",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"outDir": "./dist/out-tsc",
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"target": "es2017"
|
||||
},
|
||||
}
|
||||
|
|
|
|||
18
client/tsconfig.spec.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src/test.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
482
cmd.js
|
|
@ -1,482 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import path from 'path';
|
||||
|
||||
const APP_NAME = path.parse(process.argv[1]).base;
|
||||
|
||||
const ARGUMENT_CODE = '--code';
|
||||
const ARGUMENT_IDENT = '--ident';
|
||||
const ARGUMENT_IN = '--in';
|
||||
const ARGUMENT_OUT = '--out';
|
||||
const ARGUMENT_PASSWORD = '--password';
|
||||
const ARGUMENT_TOKEN = '--token';
|
||||
const ARGUMENT_URL = '--url';
|
||||
|
||||
const COMMAND_CONFIG_GET = 'config-get';
|
||||
const COMMAND_CONFIG_SET = 'config-set';
|
||||
const COMMAND_HELP = 'help';
|
||||
const COMMAND_LOGIN = 'login';
|
||||
const COMMAND_LOGOUT = 'logout';
|
||||
const COMMAND_USER_ADD = 'user-add';
|
||||
const COMMAND_USER_REMOVE = 'user-remove';
|
||||
|
||||
const DEFAULT_ADMIN_PASSWORD = 'rdio-scanner';
|
||||
const DEFAULT_URL = 'http://localhost:3000/';
|
||||
const DEFAULT_TOKEN_FILE = `.${path.parse(process.argv[1]).name}.token`;
|
||||
|
||||
const PADDING = 11;
|
||||
|
||||
const URL_API = '/api/admin';
|
||||
const URL_CONFIG = `${URL_API}/config`;
|
||||
const URL_LOGIN = `${URL_API}/login`;
|
||||
const URL_LOGOUT = `${URL_API}/logout`;
|
||||
|
||||
let tokenFile = DEFAULT_TOKEN_FILE;
|
||||
|
||||
export class Cmd {
|
||||
constructor() {
|
||||
this.command = null;
|
||||
this.inputFile = null;
|
||||
this.outputFile = null;
|
||||
this.password = process.env.RDIO_ADMIN_PASSWORD || DEFAULT_ADMIN_PASSWORD;
|
||||
this.token = null;
|
||||
this.url = new URL(DEFAULT_URL);
|
||||
|
||||
const args = process.argv.slice(2).reduce((p, c) => p.concat(c.split(/(--\w+)=(\S+)/).filter((f) => f)), []);
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case COMMAND_CONFIG_GET:
|
||||
case COMMAND_CONFIG_SET:
|
||||
case COMMAND_HELP:
|
||||
case COMMAND_LOGIN:
|
||||
case COMMAND_LOGOUT:
|
||||
case COMMAND_USER_ADD:
|
||||
case COMMAND_USER_REMOVE:
|
||||
this.command = args[i];
|
||||
break;
|
||||
|
||||
case ARGUMENT_CODE:
|
||||
this.code = args[++i];
|
||||
break;
|
||||
|
||||
case ARGUMENT_IDENT:
|
||||
this.ident = args[++i];
|
||||
break;
|
||||
|
||||
case ARGUMENT_IN:
|
||||
this.inputFile = args[++i];
|
||||
break;
|
||||
|
||||
case ARGUMENT_OUT:
|
||||
this.outputFile = args[++i];
|
||||
break;
|
||||
|
||||
case ARGUMENT_PASSWORD:
|
||||
this.password = args[++i];
|
||||
break;
|
||||
|
||||
case ARGUMENT_TOKEN:
|
||||
tokenFile = args[++i];
|
||||
break;
|
||||
|
||||
case ARGUMENT_URL:
|
||||
try {
|
||||
this.url = new URL(args[++i]);
|
||||
} catch (error) {
|
||||
exitWithError('Invalid URL');
|
||||
}
|
||||
if (!/^https?:/i.test(this.url)) {
|
||||
exitWithError('Invalid URL');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.token = readTokenFile();
|
||||
|
||||
switch (this.command) {
|
||||
case COMMAND_CONFIG_GET:
|
||||
this.configGet();
|
||||
break;
|
||||
|
||||
case COMMAND_CONFIG_SET:
|
||||
this.configSet();
|
||||
break;
|
||||
|
||||
case COMMAND_LOGIN:
|
||||
this.login();
|
||||
break;
|
||||
|
||||
case COMMAND_LOGOUT:
|
||||
this.logout();
|
||||
break;
|
||||
|
||||
case COMMAND_USER_ADD:
|
||||
this.userAdd();
|
||||
break;
|
||||
|
||||
case COMMAND_USER_REMOVE:
|
||||
this.userRemove();
|
||||
break;
|
||||
|
||||
default:
|
||||
this.usage();
|
||||
}
|
||||
}
|
||||
|
||||
async configGet() {
|
||||
if (!this.outputFile) {
|
||||
exitWithError('Missing --out <file.json> arguments.');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await httpGet(new URL(URL_CONFIG, this.url), this.token);
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
saveFile(this.outputFile, JSON.stringify(res.data.config, null, 2));
|
||||
exitWithMessage(`Server's configuration saved to ${this.outputFile}`);
|
||||
} else {
|
||||
exitWithError(res.error || res.data);
|
||||
}
|
||||
}
|
||||
|
||||
async configSet() {
|
||||
if (!this.inputFile) {
|
||||
exitWithError('Missing --in <file.json> arguments.');
|
||||
return;
|
||||
}
|
||||
|
||||
let config = readFile(this.inputFile);
|
||||
|
||||
try {
|
||||
config = JSON.parse(config);
|
||||
} catch (error) {
|
||||
exitWithError(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await httpPut(new URL(URL_CONFIG, this.url), config, this.token);
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
exitWithMessage(`Server's configuration applied from file ${this.inputFile}`);
|
||||
|
||||
} else {
|
||||
exitWithError(res.error || res.data);
|
||||
}
|
||||
}
|
||||
|
||||
async login() {
|
||||
const res = await httpPost(new URL(URL_LOGIN, this.url), { password: this.password });
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
this.token = res.data.token;
|
||||
saveTokenFile(this.token);
|
||||
exitWithMessage('Logged in.');
|
||||
|
||||
} else {
|
||||
exitWithError(res.error || res.data);
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
const res = await httpPost(new URL(URL_LOGOUT, this.url), null, this.token);
|
||||
|
||||
this.token = null;
|
||||
saveTokenFile(this.token);
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
exitWithMessage('Logged out.');
|
||||
|
||||
} else {
|
||||
exitWithError(res.error || res.data);
|
||||
}
|
||||
}
|
||||
|
||||
usage() {
|
||||
exitWithMessage([
|
||||
'Available Commands:',
|
||||
` ${COMMAND_CONFIG_GET.padEnd(PADDING)} – Retrieve server's configuration.`,
|
||||
'',
|
||||
` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_CONFIG_GET} ${ARGUMENT_OUT} <file.json>`,
|
||||
'',
|
||||
` ${COMMAND_CONFIG_SET.padEnd(PADDING)} – Set server's configuration.`,
|
||||
'',
|
||||
` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_CONFIG_SET} ${ARGUMENT_IN} <file.json>`,
|
||||
'',
|
||||
` ${COMMAND_LOGIN.padEnd(PADDING)} – Login to administrative backend.`,
|
||||
'',
|
||||
` ${''.padStart(PADDING)} $ RDIO_ADMIN_PASSWORD=<password> ./${APP_NAME} ${COMMAND_LOGIN}`,
|
||||
` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_LOGIN} ${ARGUMENT_PASSWORD} <password>`,
|
||||
'',
|
||||
` ${COMMAND_LOGOUT.padEnd(PADDING)} – Logout from administrative backend.`,
|
||||
'',
|
||||
` ${COMMAND_USER_ADD.padEnd(PADDING)} – Add a user access.`,
|
||||
'',
|
||||
` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_USER_ADD} ${ARGUMENT_IDENT} <ident> ${ARGUMENT_CODE} <code>`,
|
||||
'',
|
||||
` ${COMMAND_USER_REMOVE.padEnd(PADDING)} – Remove a user access.`,
|
||||
'',
|
||||
` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_USER_REMOVE} ${ARGUMENT_IDENT} <ident>`,
|
||||
'',
|
||||
'Global options:',
|
||||
` ${ARGUMENT_TOKEN.padEnd(PADDING)} – Session token keystore. Default is '${DEFAULT_TOKEN_FILE}'.`,
|
||||
` ${ARGUMENT_URL.padEnd(PADDING)} – Server remote address. Default is '${DEFAULT_URL}'.`,
|
||||
'',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
async userAdd() {
|
||||
if (!this.code || !this.ident) {
|
||||
exitWithError('Missing --ident <ident> or --code <code> arguments.');
|
||||
return;
|
||||
}
|
||||
|
||||
let res = await httpGet(new URL(URL_CONFIG, this.url), this.token);
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
exitWithError(res.error || res.data);
|
||||
return;
|
||||
}
|
||||
|
||||
const config = res.data.config;
|
||||
|
||||
config.access = config.access.filter((u) => u.ident.toLowerCase() !== this.ident.toLowerCase());
|
||||
|
||||
config.access = config.access.concat({
|
||||
code: this.code,
|
||||
ident: this.ident,
|
||||
systems: '*',
|
||||
});
|
||||
|
||||
res = await httpPut(new URL(URL_CONFIG, this.url), config, this.token);
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
exitWithMessage(`User ${this.ident} added.`);
|
||||
|
||||
} else {
|
||||
exitWithError(res.error || res.data);
|
||||
}
|
||||
}
|
||||
|
||||
async userRemove() {
|
||||
if (!this.ident) {
|
||||
exitWithError('Missing --ident <ident> arguments.');
|
||||
return;
|
||||
}
|
||||
|
||||
let res = await httpGet(new URL(URL_CONFIG, this.url), this.token);
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
exitWithError(res.error || res.data);
|
||||
return;
|
||||
}
|
||||
|
||||
const config = res.data.config;
|
||||
|
||||
if (config.access.findIndex((u) => u.ident.toLowerCase() === this.ident.toLowerCase()) === -1) {
|
||||
exitWithError(`Unknown user ${this.ident}`);
|
||||
return;
|
||||
}
|
||||
|
||||
config.access = config.access.filter((u) => u.ident.toLowerCase() !== this.ident.toLowerCase());
|
||||
|
||||
res = await httpPut(new URL(URL_CONFIG, this.url), config, this.token);
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
exitWithMessage(`User ${this.ident} removed.`);
|
||||
|
||||
} else {
|
||||
exitWithError(res.error || res.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function exitWithError(error) {
|
||||
process.stdout.write(`ERROR: ${error}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function exitWithMessage(message) {
|
||||
process.stdout.write(`${message}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function httpGet(url, autorization) {
|
||||
return new Promise((resolve) => {
|
||||
const result = { data: null, error: null, statusCode: null };
|
||||
const options = autorization ? { headers: { Authorization: autorization } } : {};
|
||||
|
||||
try {
|
||||
const req = (url.protocol === 'https:' ? https : http).get(url, options, (res) => {
|
||||
result.statusCode = res.statusCode;
|
||||
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk) => result.data = (result.data || '') + chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
result.data = JSON.parse(result.data);
|
||||
} catch (error) { }
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
result.error = error.code;
|
||||
resolve(result);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
result.error = error.message;
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function httpPost(url, postData, autorization) {
|
||||
return new Promise((resolve) => {
|
||||
const result = { data: null, error: null, statusCode: null };
|
||||
|
||||
postData = JSON.stringify(postData || {});
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(postData),
|
||||
},
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
if (autorization) {
|
||||
options.headers.Authorization = autorization;
|
||||
}
|
||||
|
||||
try {
|
||||
const req = (url.protocol === 'https:' ? https : http).request(url, options, (res) => {
|
||||
result.statusCode = res.statusCode;
|
||||
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk) => result.data = (result.data || '') + chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
result.data = JSON.parse(result.data);
|
||||
} catch (error) { }
|
||||
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
result.error = error.code;
|
||||
resolve(result);
|
||||
});
|
||||
req.write(postData);
|
||||
req.end();
|
||||
|
||||
} catch (error) {
|
||||
result.error = error.message;
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function httpPut(url, putData, autorization) {
|
||||
return new Promise((resolve) => {
|
||||
const result = { data: null, error: null, statusCode: null };
|
||||
|
||||
putData = JSON.stringify(putData);
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(putData),
|
||||
},
|
||||
method: 'PUT',
|
||||
};
|
||||
|
||||
if (autorization) {
|
||||
options.headers.Authorization = autorization;
|
||||
}
|
||||
|
||||
try {
|
||||
const req = (url.protocol === 'https:' ? https : http).request(url, options, (res) => {
|
||||
result.statusCode = res.statusCode;
|
||||
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk) => result.data = (result.data || '') + chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
result.data = JSON.parse(result.data);
|
||||
} catch (error) { }
|
||||
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
result.error = error.code;
|
||||
resolve(result);
|
||||
});
|
||||
req.write(putData);
|
||||
req.end();
|
||||
|
||||
} catch (error) {
|
||||
result.error = error.message;
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function readFile(file) {
|
||||
if (fs.existsSync(file)) {
|
||||
try {
|
||||
return fs.readFileSync(file);
|
||||
|
||||
} catch (error) {
|
||||
exitWithError(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readTokenFile() {
|
||||
return readFile(tokenFile);
|
||||
}
|
||||
|
||||
function saveFile(file, data) {
|
||||
try {
|
||||
fs.writeFileSync(file, data || '');
|
||||
|
||||
} catch (error) {
|
||||
exitWithError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function saveTokenFile(token) {
|
||||
saveFile(tokenFile, token);
|
||||
}
|
||||
|
||||
new Cmd();
|
||||
92
docs/api.md
|
|
@ -1,10 +1,8 @@
|
|||
# Rdio Scanner's API
|
||||
# API
|
||||
|
||||
There is two API endpoints available you can use to upload your audio files to [Rdio Scanner](https://github.com/chuot/rdio-scanner).
|
||||
|
||||
> Remember that systems/talkgroups need to be defined in `config.json` .
|
||||
|
||||
## Generic API - /api/call-upload
|
||||
## Endpoint: /api/call-upload
|
||||
|
||||
This API is used by the **downstream** feature to received audio files from other [Rdio Scanner](https://github.com/chuot/rdio-scanner) instances.
|
||||
|
||||
|
|
@ -12,57 +10,51 @@ However, this API can be used for purposes other than **downstream**, as long as
|
|||
|
||||
```bash
|
||||
$ curl https://other-rdio-scanner.example.com/api/call-upload \
|
||||
-F "audio=@/recordings/audio.wav" \
|
||||
-F "audioName=audio.wav" \
|
||||
-F "audioType=audio/x-wav" \
|
||||
-F "dateTime=1970-01-01T00:00:00.000Z" \
|
||||
-F "frequencies=[]" \
|
||||
-F "frequency=774031250" \
|
||||
-F "key=d2079382-07df-4aa9-8940-8fb9e4ef5f2e" \
|
||||
-F "audio=@/recordings/audio.wav" \
|
||||
-F "dateTime=1970-01-01T00:00:00.000Z" \
|
||||
-F "frequencies=[]" \
|
||||
-F "frequency=774031250" \
|
||||
-F "source=4424000" \
|
||||
-F "sources=[]" \
|
||||
-F "system=11" \
|
||||
-F "talkgroup=54241"
|
||||
-F "source=4424000" \
|
||||
-F "sources=[]" \
|
||||
-F "system=11" \
|
||||
-F "systemLabel=RSP25MTL" \
|
||||
-F "talkgroup=54241" \
|
||||
-F "talkgroupGroup=Fire" \
|
||||
-F "talkgroupLabel=TDB A1" \
|
||||
-F "talkgroupTag=Fire dispatch"
|
||||
Call imported successfully
|
||||
```
|
||||
|
||||
- **key** - API key on the receiving host, see `config.json`.
|
||||
- **audio** - Full path to your audio file. The path **must be prefixed** with the **@ sign**.
|
||||
- **dateTime** - Audio date and time in JSON format.
|
||||
- **frequencies** - (optional) A JSON string. Inspired by _Trunk Recorder_, the JSON structure is:
|
||||
```typescript
|
||||
{
|
||||
errorCount: number;
|
||||
freq: number; // in hertz
|
||||
len: number; // in seconds
|
||||
pos: number; // in seconds
|
||||
spikeCount: number;
|
||||
}[];
|
||||
```
|
||||
- **frequency** - (optional) The frequency at which the call was recorded.
|
||||
- **source** - (optional) The unit ID.
|
||||
- **sources** - (optional) A JSON string. Inspired by _Trunk Recorder_. the JSON string structure is:
|
||||
```typescript
|
||||
{
|
||||
pos: number; // in seconds
|
||||
src: number; // the unit ID
|
||||
}[];
|
||||
```
|
||||
- **system** - The system ID to attach this audio file.
|
||||
- **talkgroup** - The talkgroup ID to attach this audio file.
|
||||
- **audio** - full path to your audio file. The path **must be prefixed** with the **@ sign**.
|
||||
- **audioName** - [optional] file name (it can be derived from the audio field).
|
||||
- **audioType** - [optional] mime type. (it can be derived from the audio field).
|
||||
- **dateTime** - date and time in RFC3339 or unix time format.
|
||||
- **frequencies** - [optional] JSON array of objects for frequency changes throughout the conversation.
|
||||
|
||||
## Trunk Recorder API - /api/trunk-recorder-call-upload
|
||||
{
|
||||
errorCount: number;
|
||||
freq: number; // in hertz
|
||||
len: number; // in seconds
|
||||
pos: number; // in seconds
|
||||
spikeCount: number;
|
||||
}[];
|
||||
|
||||
This API is used by _Trunk Recorder upload-script_ where a JSON file is also available for metadata.
|
||||
- **frequency** - [optional] the frequency on which the audio file was recorded.
|
||||
- **key** - API key on the receiving host.
|
||||
- **source** - [optional] unit ID.
|
||||
- **sources** - [optional] JSON array of objects for unit ID changes throughout the conversation.
|
||||
|
||||
```bash
|
||||
$ curl https://rdio-scanner.other.instance/api/trunk-recorder-call-upload \
|
||||
-F "key=d2079382-07df-4aa9-8940-8fb9e4ef5f2e" \
|
||||
-F "audio=@/recordings/audio.wav" \
|
||||
-F "meta=@/recordings/audio.json" \
|
||||
-F "system=11"
|
||||
Call imported successfully
|
||||
```
|
||||
{
|
||||
pos: number; // in seconds
|
||||
src: number; // the unit ID
|
||||
}[];
|
||||
|
||||
- **key** - API key on the receiving host, see `config.json`.
|
||||
- **audio** - Full path to your audio file. The path **must be prefixed** with the **@ sign**.
|
||||
- **meta** - Full path to your audio metadata. the path **must be prefixed** with the **@ sign**.
|
||||
- **system** - The system ID to link this audio file.
|
||||
- **system** - [optional] system ID.
|
||||
- **systemLabel** - [optional] system label.
|
||||
- **talkgroup** - talkgroup ID.
|
||||
- **talkgroupGroup** - [optional] talkgroup group.
|
||||
- **talkgroupLabel** - [optional] talkgroup label.
|
||||
- **talkgroupTag** - [optional] talkgroup tag.
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
# Administrative Dashboard
|
||||
|
||||
The administrative dashboard is accessible at **http[s]://<your_instance_address>/admin**
|
||||
|
||||
The administrative dashboard contains these sections:
|
||||
|
||||
- **Config** - _Server's configurations section._
|
||||
- **Access** - _Control access to your instance._
|
||||
- **ApiKey** - _Keys for audio files ingestion._
|
||||
- **Dirwatch** - _Folder monitoring for audio files ingestion._
|
||||
- **Downstreams** - _Share your audio files with other instances._
|
||||
- **Groups** - _Talkgroup groups definitions._
|
||||
- **Options** - _Global runtime options._
|
||||
- **Systems** - _Systems and talkgroups definitions._
|
||||
- **Tags** - _Talkgroup tags definitions._
|
||||
- **Logs** - _Server's log._
|
||||
- **Tools** - _Useful tools._
|
||||
- **Talkgroups CSV** - _Easily import CSV files._
|
||||
- **Admin password** - _Manage admin password._
|
||||
- **Import/export config** - _Backup and restore your entire server configuration._
|
||||
|
||||
Each subsection contains its own built-in documentation to help you configure your [Rdio Scanner](https://github.com/chuot/rdio-scanner) instance.
|
||||
|
||||
# Admin Password
|
||||
|
||||
The default admin password is `rdio-scanner` and must should be changed ASAP. To change the admin password, go to `Administrative dashboard / Tools / Admin password`. It is also possible to reset the admin password from the command line:
|
||||
|
||||
```bash
|
||||
$ cd ~/rdio-scanner
|
||||
$ node server reset-admin-password mynewpassword
|
||||
Admin password has been reset.
|
||||
```
|
||||
|
||||
## rdio-scanner/server/config.json
|
||||
|
||||
```js
|
||||
{
|
||||
"nodejs": {
|
||||
// (string) "development" or "production". Default is "production".
|
||||
"environment": "production",
|
||||
|
||||
// (string) Which IP to bind to? Default is "0.0.0.0"
|
||||
"host": "0.0.0.0",
|
||||
|
||||
// (number) Which PORT to bind to? Default is 3000
|
||||
"port": 3000,
|
||||
|
||||
// (boolean) Run server in https mode
|
||||
"ssl": false,
|
||||
|
||||
// (string) Path to the SSL CA certificate
|
||||
// See SSL certificates section below for more details
|
||||
"sslCA": "ca.crt",
|
||||
|
||||
// (string) Path to the SSL certificate
|
||||
// See SSL certificates section below for more details
|
||||
"sslCert": "server.crt",
|
||||
|
||||
// (string) Path to the SSL privat key
|
||||
// See SSL certificates section below for more details
|
||||
"sslKey": "server.key"
|
||||
},
|
||||
|
||||
// Rdio Scanner uses sqlite for its default database.
|
||||
// It is highly recommended to stick to sqlite as it works very well.
|
||||
// Refer to https://sequelize.org/v5/manual/dialects.html for more details
|
||||
"sequelize": {
|
||||
"database": null,
|
||||
"dialect": "sqlite",
|
||||
"host": null,
|
||||
"password": null,
|
||||
"port": null,
|
||||
"storage": "database.sqlite",
|
||||
"username": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SSL certificates - nodejs.sslCA / nodejs.sslCert / nodejs.sslKey
|
||||
|
||||
[Rdio Scanner](https://github.com/chuot//rdio-scanner) will automatically create self signed certificates for you if `openssl` is available on your system and no certificates are already defined. The server runs by default in normal HTTP mode, you still to change `nodejs.ssl` to `true` in your `config.json` and restart the server to run in HTTPS mode.
|
||||
|
||||
> BEAWARE THAT IOS DOESN'T ALLOW TO CONNECTIONS TO SECURE WEBSOCKET WHEN USING A SELF SIGNED CERTIFICATE. YOU MUST USE PROPERLY SIGNED CERTIFICATES.
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
# CSDR-AM
|
||||
|
||||
The purpose of this shell script is to record multiple AM frequencies from a single RTL-SDR and creates individual audio files per conversation.
|
||||
|
||||
## Requirements
|
||||
|
||||
Tested on a Linux box.
|
||||
|
||||
* [csdr](https://github.com/ha7ilm/csdr)
|
||||
* [sox](http://sox.sourceforge.net/)
|
||||
|
||||
## Configurations
|
||||
|
||||
### Within the script itself
|
||||
|
||||
Change the values to your needs. Make sure the `centered_freq` and `sampling_rate` covers the `freqs`.
|
||||
|
||||
Note the syntax for `freqs` and `filenames`, they are arrays.
|
||||
|
||||
Each filename template use sox multiple output mode syntax for file numbering (`%6n`). The static part is reused by `dirWatch` to distinguish which `talkgroup` to import to.
|
||||
|
||||
```bash
|
||||
audio_dir=./audio_files/air
|
||||
device_id=00000001
|
||||
center_freq=119.4e6
|
||||
gain=38.6
|
||||
sampling_rate=2400000
|
||||
squelch=2e-4
|
||||
|
||||
freqs=(118.9e6 119.1e6 119.9e6)
|
||||
|
||||
filenames=(118900000-%6n 119100000-%6n 119900000-%6n)
|
||||
```
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
##
|
||||
## #############################################################################
|
||||
## 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/>
|
||||
## #############################################################################
|
||||
##
|
||||
|
||||
##
|
||||
## Begin of configurable values
|
||||
##
|
||||
|
||||
audio_dir=./audio_files/air
|
||||
device_id=00000001
|
||||
center_freq=119.4e6
|
||||
gain=38.6
|
||||
sampling_rate=2400000
|
||||
squelch=2e-4
|
||||
|
||||
## List of frequencies covered by your RTL
|
||||
|
||||
freqs=(118.9e6 119.1e6 119.9e6)
|
||||
|
||||
## Filename tempaltes for each frequencies
|
||||
## See sox documentation for multiple files output
|
||||
|
||||
filenames=(118900000-%6n 119100000-%6n 119900000-%6n)
|
||||
|
||||
##
|
||||
## End of configurable values
|
||||
##
|
||||
|
||||
for i in ${!freqs[@]}; do
|
||||
mkfifo /tmp/csdr_${device_id}_${freqs[i]}_iq.fifo
|
||||
|
||||
mkfifo /tmp/csdr_${device_id}_${freqs[i]}_squelch.fifo
|
||||
done
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
test -d ${audio_dir} || mkdir -p ${audio_dir} || exit 1
|
||||
|
||||
trap "rm -f /tmp/csdr_${device_id}_*.fifo" EXIT
|
||||
|
||||
for i in ${!freqs[@]}; do
|
||||
cat /tmp/csdr_${device_id}_${freqs[i]}_iq.fifo |
|
||||
csdr convert_u8_f |
|
||||
csdr shift_addition_cc $(csdr =\(${center_freq}-${freqs[i]}\)/${sampling_rate}) |
|
||||
csdr fir_decimate_cc 50 0.005 HAMMING |
|
||||
csdr squelch_and_smeter_cc --fifo /tmp/csdr_${device_id}_${freqs[i]}_squelch.fifo --outfifo /dev/null 6e-5 600 |
|
||||
csdr amdemod_cf |
|
||||
csdr agc_ff |
|
||||
csdr limit_ff |
|
||||
csdr convert_f_s16 |
|
||||
sox -c 1 -r 44100 -t s16 - "${audio_dir}/${filenames[i]}.wav" silence 1 0.50 0.1% 1 2.0 0.1% : newfile : restart &
|
||||
done
|
||||
|
||||
for i in ${!freqs[@]}; do
|
||||
echo ${squelch} >/tmp/csdr_${device_id}_${freqs[i]}_squelch.fifo
|
||||
done
|
||||
|
||||
server="rtl_sdr -d ${device_id} -f ${center_freq} -g ${gain} -s ${sampling_rate} -"
|
||||
|
||||
for i in ${!freqs[@]}; do
|
||||
if [ $i == $((${#freqs[@]} - 1)) ]; then
|
||||
server+=" > /tmp/csdr_${device_id}_${freqs[i]}_iq.fifo"
|
||||
else
|
||||
server+=" | tee /tmp/csdr_${device_id}_${freqs[i]}_iq.fifo"
|
||||
fi
|
||||
done
|
||||
|
||||
eval ${server}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
server:
|
||||
environment:
|
||||
- TZ=America/Toronto
|
||||
image: docker.io/chuot/rdio-scanner:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- /home/radio/rdio-scanner:/app/data:z
|
||||
- /home/radio/SDRTrunk/recordings:/recordings:z
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
[Unit]
|
||||
Description=Rdio Scanner
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/home/radio/rdio-scanner/server
|
||||
ExecStart=/usr/bin/node index.js
|
||||
Restart=always
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=Rdio Scanner
|
||||
User=radio
|
||||
Group=radio
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# RTLSDR-Airband
|
||||
|
||||
Here's an example with RTLSDR-Airband:
|
||||
|
||||
## RTLSDR-Airband's configuration
|
||||
|
||||
See the `rtl_airband.conf` file for an example.
|
||||
|
||||
## Rdio Scanner's configuration
|
||||
|
||||
In you Rdio Scanner dirwatch configuration, use those parameters:
|
||||
|
||||
- Directory: `/tmp/radio/CYMX`
|
||||
- Extension: `mp3`
|
||||
- Type: `Default`
|
||||
- System: _choose the appropriate one_
|
||||
- Mask: `cymx_#TG_#DATE_#TIME_#HZ`
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
fft_size = 512;
|
||||
localtime = true;
|
||||
multiple_demod_threads = true;
|
||||
devices: (
|
||||
{
|
||||
type = "rtlsdr";
|
||||
serial = "00000081";
|
||||
gain = 38.6;
|
||||
centerfreq = 119.4;
|
||||
correction = 0;
|
||||
channels:
|
||||
(
|
||||
{
|
||||
freq = 118.9;
|
||||
outputs: (
|
||||
{
|
||||
type = "file";
|
||||
directory = "/tmp/radio/CYMX";
|
||||
filename_template = "cymx_118900";
|
||||
split_on_transmission = true;
|
||||
include_freq = true;
|
||||
}
|
||||
);
|
||||
},
|
||||
{
|
||||
freq = 119.1;
|
||||
outputs: (
|
||||
{
|
||||
type = "file";
|
||||
directory = "/tmp/radio/CYMX";
|
||||
filename_template = "cymx_119100";
|
||||
split_on_transmission = true;
|
||||
include_freq = true;
|
||||
}
|
||||
);
|
||||
},
|
||||
{
|
||||
freq = 119.9;
|
||||
outputs: (
|
||||
{
|
||||
type = "file";
|
||||
directory = "/tmp/radio/CYMX";
|
||||
filename_template = "cymx_119900";
|
||||
split_on_transmission = true;
|
||||
include_freq = true;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# If your Trunk Recorder instance is running on the same host
|
||||
# as Rdio Scanner, you can use a dirwatch to ingest audio files
|
||||
# instead of relying on this script.
|
||||
#
|
||||
# If on the contrary you Trunk Recorder instance runs on another
|
||||
# host, then this script is the way to go.
|
||||
#
|
||||
# Use this script with Trunk Recorder uploadScript system property
|
||||
# in its config.json file.
|
||||
#
|
||||
# {
|
||||
# ...
|
||||
# "uploadScript": "upload-call.sh 7537"
|
||||
# ...
|
||||
# }
|
||||
#
|
||||
# Note that you have to pass the Rdio Scanner system Id as the
|
||||
# first argument. Trunk Recorder will then add the full path
|
||||
# of the audio file as the second argument.
|
||||
|
||||
# Change this API key to the one configured in your Rdio Scanner
|
||||
# instance.
|
||||
apikey=b29eb8b9-9bcd-4e6e-bb4f-d244ada12736
|
||||
|
||||
basename="${2%.*}"
|
||||
|
||||
# Change the URL to match your Rdio Scanner host.
|
||||
curl -sS http://127.0.0.1:3000/api/trunk-recorder-call-upload \
|
||||
-F "key=${apikey}" \
|
||||
-F "audio=@${basename}.wav;type=audio/wav" \
|
||||
-F "meta=@${basename}.json;type=application/json" \
|
||||
-F "system=${1:-0}"
|
||||
|
||||
# The audio file and its JSON file will then be deleted.
|
||||
rm -f ${basename}.*
|
||||
BIN
docs/images/admin-login.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/images/admin-todos.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
docs/images/app-store-badge.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7 KiB |
BIN
docs/images/google-play-badge.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
BIN
docs/images/rdio-scanner.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |