diff --git a/backend/extern/afv-native b/backend/extern/afv-native index 904f896c..c34bb1c0 160000 --- a/backend/extern/afv-native +++ b/backend/extern/afv-native @@ -1 +1 @@ -Subproject commit 904f896c672109a9e30cdc3f1df4043000cfd385 +Subproject commit c34bb1c05fd6ba77353dba500a33e627a5ced28e diff --git a/backend/include/Helpers.hpp b/backend/include/Helpers.hpp index a581edd4..5a142024 100644 --- a/backend/include/Helpers.hpp +++ b/backend/include/Helpers.hpp @@ -20,7 +20,7 @@ class Helpers { * @param frequency The frequency value to be cleaned up. * @return The cleaned up frequency value. */ - inline static int CleanUpFrequency(int frequency) + static int CleanUpFrequency(int frequency) { // We don't clean up an unset frequency if (std::abs(frequency) == OBS_FREQUENCY) { @@ -37,7 +37,7 @@ class Helpers { * @return A string representation of the frequency in a human-readable format (e.g. "122.800" * for 122800000 Hz) */ - inline static std::string ConvertHzToHumanString(unsigned int frequencyHz) + static std::string ConvertHzToHumanString(unsigned int frequencyHz) { std::string temp = std::to_string(frequencyHz / 1000); return temp.substr(0, 3) + "." + temp.substr(3, 7); @@ -52,8 +52,7 @@ class Helpers { * opposite of currentValue if the property is "toggle", or the currentValue if * incoming value is undefined or invalid. */ - inline static bool ConvertBoolOrToggleToBool( - const nlohmann::json& incomingValue, bool currentValue) + static bool ConvertBoolOrToggleToBool(const nlohmann::json& incomingValue, bool currentValue) { if (incomingValue.is_null()) { TRACK_LOG_INFO("ConvertBoolOrToggleToBool: Incoming value wasn't specified, returning " @@ -94,10 +93,11 @@ class NapiHelpers { NapiHelpers::callbackAvailable = true; } - inline static void callElectron( + static void callElectron( const std::string& eventName, const std::string& data = "", const std::string& data2 = "") { - if (!NapiHelpers::callbackAvailable || NapiHelpers::callbackRef == nullptr) { + if (!NapiHelpers::callbackAvailable || NapiHelpers::callbackRef == nullptr + || NapiHelpers::_requestExit.load()) { return; } @@ -111,10 +111,8 @@ class NapiHelpers { }); } - inline static void sendErrorToElectron(const std::string& message) - { - callElectron("error", message); - } + static void sendErrorToElectron(const std::string& message) { callElectron("error", message); } inline static std::mutex _callElectronMutex; -}; \ No newline at end of file + inline static std::atomic _requestExit = false; +}; diff --git a/backend/include/RadioHelper.hpp b/backend/include/RadioHelper.hpp index 5828fc5d..7410ff0d 100644 --- a/backend/include/RadioHelper.hpp +++ b/backend/include/RadioHelper.hpp @@ -1,7 +1,9 @@ #pragma once +#include "Helpers.hpp" #include "Shared.hpp" #include "sdk.hpp" +#include class RadioState { public: @@ -22,8 +24,8 @@ class RadioHelper { * @return true The state was set successfully * @return false The state was not set successfully */ - inline static bool SetRadioState( - const std::shared_ptr& mApiServer, const RadioState& newState) + static bool SetRadioState(const std::shared_ptr& mApiServer, const RadioState& newState, + const std::string& stationCallsign = "") { if (!mClient->IsVoiceConnected()) { TRACK_LOG_TRACE("Voice is not connected, not setting radio state"); @@ -56,6 +58,7 @@ class RadioHelper { mClient->SetOnHeadset(newState.frequency, newState.headset); + if (!oldRxValue && newState.rx) { // When turning on RX, we refresh the transceivers auto states = mClient->getRadioState(); @@ -66,14 +69,17 @@ class RadioHelper { } // Included for legacy reasons in case some older client depends on this message. - mApiServer->handleAFVEventForWebsocket(sdk::types::Event::kFrequencyStateUpdate, {}, {}); + mApiServer->handleAFVEventForWebsocket( + sdk::types::Event::kFrequencyStateUpdate, std::nullopt, std::nullopt); // New event that only notifies of the change to this specific station. - auto stateJson = mApiServer->buildStationStateJson(std::nullopt, newState.frequency); + std::optional optionalCallsign + = stationCallsign.empty() ? std::nullopt : std::optional(stationCallsign); + auto stateJson = mApiServer->buildStationStateJson(optionalCallsign, newState.frequency); mApiServer->publishStationState(stateJson); NapiHelpers::callElectron("station-state-update", stateJson.dump()); return true; } }; -; \ No newline at end of file +; diff --git a/backend/include/Shared.hpp b/backend/include/Shared.hpp index fe14b508..9e48b141 100644 --- a/backend/include/Shared.hpp +++ b/backend/include/Shared.hpp @@ -23,11 +23,12 @@ #define VERSION_CHECK_ENDPOINT "/pierr3/TrackAudio/main/MANDATORY_VERSION" #define OBS_FREQUENCY 199998000 // 199.998 -#define UNICOM_FREQUENCY = 122800000 // 122.800 +#define UNICOM_FREQUENCY 122800000 // 122.800 +#define GUARD_FREQUENCY 121500000 // 121.500 #define API_SERVER_PORT 49080 -constexpr semver::version VERSION = semver::version { 1, 3, 0, semver::prerelease::beta, 2 }; +constexpr semver::version VERSION = semver::version { 1, 3, 0, semver::prerelease::beta, 3 }; // NOLINTNEXTLINE const std::string CLIENT_NAME = std::string("TrackAudio-") + VERSION.to_string(); @@ -35,7 +36,7 @@ const std::string CLIENT_NAME = std::string("TrackAudio-") + VERSION.to_string() static std::unique_ptr mClient = nullptr; struct FileSystem { - inline static std::filesystem::path GetStateFolderPath() + static std::filesystem::path GetStateFolderPath() { return std::filesystem::path(sago::getStateDir()) / "trackaudio"; } @@ -71,7 +72,7 @@ struct UserSettings { // NOLINTNEXTLINE inline static CSimpleIniA ini; - inline static void load() + static void load() { try { _load(); @@ -80,7 +81,7 @@ struct UserSettings { } } - inline static void save() + static void save() { std::string settingsFilePath = (FileSystem::GetStateFolderPath() / "settings.ini").string(); @@ -103,7 +104,7 @@ struct UserSettings { } protected: - inline static void _load() + static void _load() { std::string settingsFilePath = (FileSystem::GetStateFolderPath() / "settings.ini").string(); ini.SetUnicode(); diff --git a/backend/src/main.cpp b/backend/src/main.cpp index ccb19038..2acc65d5 100644 --- a/backend/src/main.cpp +++ b/backend/src/main.cpp @@ -1,9 +1,8 @@ #include "LogFactory.h" +#include "afv-native/afv/dto/StationTransceiver.h" #include "afv-native/atcClientWrapper.h" #include "afv-native/event.h" #include "afv-native/hardwareType.h" -#include "spdlog/sinks/rotating_file_sink.h" -#include "spdlog/spdlog.h" #include #include #include @@ -39,7 +38,7 @@ struct MainThreadShared { inline static std::unique_ptr inputHandler = nullptr; }; - +namespace { Napi::Array GetAudioApis(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); @@ -123,6 +122,28 @@ void Disconnect(const Napi::CallbackInfo& /*info*/) sdk::types::Event::kDisconnectFrequencyStateUpdate, {}, {}); } +void SetGuardAndUnicomTransceivers() +{ + const auto transceivers = mClient->GetTransceivers(); + const auto states = mClient->getRadioState(); + + std::vector guardAndUnicomTransceivers; + for (const auto& [frequency, state] : states) { + if (frequency == UNICOM_FREQUENCY || frequency == GUARD_FREQUENCY || !state.rx) { + continue; + } + + if (transceivers.find(state.stationName) != transceivers.end()) { + for (const auto& transceiver : transceivers.at(state.stationName)) { + guardAndUnicomTransceivers.push_back(transceiver); + } + } + } + + mClient->SetManualTransceivers(UNICOM_FREQUENCY, guardAndUnicomTransceivers); + mClient->SetManualTransceivers(GUARD_FREQUENCY, guardAndUnicomTransceivers); +} + void SetAudioSettings(const Napi::CallbackInfo& info) { if (mClient->IsVoiceConnected()) { @@ -155,7 +176,7 @@ Napi::Boolean AddFrequency(const Napi::CallbackInfo& info) return Napi::Boolean::New(info.Env(), false); } - RadioState newState; + RadioState newState {}; newState.frequency = frequency; newState.rx = false; @@ -164,13 +185,13 @@ Napi::Boolean AddFrequency(const Napi::CallbackInfo& info) newState.headset = true; newState.xca = false; - auto result = RadioHelper::SetRadioState(MainThreadShared::mApiServer, newState); + auto result = RadioHelper::SetRadioState(MainThreadShared::mApiServer, newState, callsign); return Napi::Boolean::New(info.Env(), result); } void RemoveFrequency(const Napi::CallbackInfo& info) { - RadioState newState; + RadioState newState {}; newState.frequency = info[0].As().Int32Value(); newState.rx = false; @@ -189,7 +210,7 @@ void Reset(const Napi::CallbackInfo& /*info*/) { mClient->reset(); } Napi::Boolean SetFrequencyState(const Napi::CallbackInfo& info) { - RadioState newState; + RadioState newState {}; newState.frequency = info[0].As().Int32Value(); newState.rx = info[1].As().Value(); @@ -199,6 +220,8 @@ Napi::Boolean SetFrequencyState(const Napi::CallbackInfo& info) newState.headset = !info[4].As().Value(); newState.xca = info[5].As().Value(); // Not used + // SetGuardAndUnicomTransceivers(); + auto result = RadioHelper::SetRadioState(MainThreadShared::mApiServer, newState); return Napi::Boolean::New(info.Env(), result); } @@ -323,7 +346,22 @@ void SetRadioGain(const Napi::CallbackInfo& info) float gain = info[0].As().FloatValue(); UserSession::currentRadioGain = gain; - mClient->SetRadioGainAll(gain); + auto states = mClient->getRadioState(); + for (const auto& state : states) { + if (state.second.Frequency == UNICOM_FREQUENCY + || state.second.Frequency == GUARD_FREQUENCY) { + continue; + } + mClient->SetRadioGain(state.first, gain); + } +} + +void SetFrequencyRadioGain(const Napi::CallbackInfo& info) +{ + int frequency = info[0].As().Int32Value(); + float gain = info[1].As().FloatValue(); + + mClient->SetRadioGain(frequency, gain); } Napi::String Version(const Napi::CallbackInfo& info) @@ -427,7 +465,7 @@ void RequestPttKeyName(const Napi::CallbackInfo& info) } // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast,readability-function-cognitive-complexity) -static void HandleAfvEvents(afv_native::ClientEventType eventType, void* data, void* data2) +void HandleAfvEvents(afv_native::ClientEventType eventType, void* data, void* data2) { if (!NapiHelpers::callbackAvailable) { return; @@ -458,7 +496,7 @@ static void HandleAfvEvents(afv_native::ClientEventType eventType, void* data, v break; } } - + SetGuardAndUnicomTransceivers(); NapiHelpers::callElectron( "StationTransceiversUpdated", station, std::to_string(transceiverCount)); } @@ -486,7 +524,7 @@ static void HandleAfvEvents(afv_native::ClientEventType eventType, void* data, v } NapiHelpers::callElectron("StationDataReceived", callsign, std::to_string(frequency)); - MainThreadShared::mApiServer->publishStationAdded(callsign, frequency); + MainThreadShared::mApiServer->publishStationAdded(callsign, static_cast(frequency)); } if (eventType == afv_native::ClientEventType::VccsReceived) { @@ -508,7 +546,8 @@ static void HandleAfvEvents(afv_native::ClientEventType eventType, void* data, v } NapiHelpers::callElectron("StationDataReceived", callsign, std::to_string(frequency)); - MainThreadShared::mApiServer->publishStationAdded(callsign, frequency); + MainThreadShared::mApiServer->publishStationAdded( + callsign, static_cast(frequency)); } } @@ -728,16 +767,18 @@ Napi::Object Bootstrap(const Napi::CallbackInfo& info) Napi::Boolean Exit(const Napi::CallbackInfo& info) { TRACK_LOG_INFO("Awaiting to exit TrackAudio..."); - std::lock_guard HelperLock(NapiHelpers::_callElectronMutex); - TRACK_LOG_INFO("Exiting TrackAudio...") + NapiHelpers::_requestExit.store(true); if (mClient->IsVoiceConnected()) { + TRACK_LOG_INFO("Forcing disconnect..."); mClient->Disconnect(); } + MainThreadShared::mApiServer.reset(); MainThreadShared::mRemoteDataHandler.reset(); MainThreadShared::inputHandler.reset(); mClient.reset(); + TRACK_LOG_INFO("Exiting TrackAudio...") LogFactory::destroyLoggers(); return Napi::Boolean::New(info.Env(), true); @@ -790,6 +831,9 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) exports.Set(Napi::String::New(env, "SetPtt"), Napi::Function::New(env, SetPtt)); + exports.Set(Napi::String::New(env, "SetFrequencyRadioGain"), + Napi::Function::New(env, SetFrequencyRadioGain)); + exports.Set(Napi::String::New(env, "SetRadioGain"), Napi::Function::New(env, SetRadioGain)); exports.Set( @@ -824,5 +868,5 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) return exports; } - NODE_API_MODULE(addon, Init) +} diff --git a/backend/src/sdk.cpp b/backend/src/sdk.cpp index dc28f633..1ddd6aa8 100644 --- a/backend/src/sdk.cpp +++ b/backend/src/sdk.cpp @@ -57,7 +57,6 @@ void SDK::handleVoiceConnectedEventForWebsocket(bool isVoiceConnected) jsonMessage["value"]["connected"] = isVoiceConnected; this->broadcastOnWebsocket(jsonMessage.dump()); - return; } // NOLINTNEXTLINE @@ -330,6 +329,7 @@ void SDK::handleGetStationStates() // Collect the states for all the radios auto allRadios = mClient->getRadioState(); + stationStates.reserve(allRadios.size()); for (const auto& [frequency, state] : allRadios) { stationStates.push_back(this->buildStationStateJson(state.stationName, frequency)); } diff --git a/backend/types/index.d.ts b/backend/types/index.d.ts index 73601106..3568aa52 100644 --- a/backend/types/index.d.ts +++ b/backend/types/index.d.ts @@ -76,6 +76,7 @@ declare namespace TrackAudioAfv { export function SetCid(cid: string): void; export function SetRadioGain(gain: number): void; + export function SetFrequencyRadioGain(frequency: number, gain: number): void; export function SetPtt(activate: boolean): void; export function SetRadioEffects(type: string): void; diff --git a/fast-afv-update.sh b/fast-afv-update.sh new file mode 100755 index 00000000..2d86ecf0 --- /dev/null +++ b/fast-afv-update.sh @@ -0,0 +1,4 @@ +cd backend/extern/afv-native +git pull origin $1 +cd ../../../ +npm run build:backend-fast diff --git a/package-lock.json b/package-lock.json index 81fd1406..999aa39f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "trackaudio", - "version": "1.3.0-beta.2", + "version": "1.3.0-beta.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "trackaudio", - "version": "1.3.0-beta.2", + "version": "1.3.0-beta.3", "hasInstallScript": true, "dependencies": { "@electron-toolkit/preload": "^3.0.1", @@ -8196,7 +8196,7 @@ "node_modules/trackaudio-afv": { "version": "1.0.0", "resolved": "file:backend/trackaudio-afv-1.0.0.tgz", - "integrity": "sha512-tjVkginiREmDG2jumOlvXa1P8xViKT1EnJtAeFJEuMlzw49DiplusOl2Nh7xvnjQr6ePJ3doiE9Pw0dKZz4MOg==", + "integrity": "sha512-v8yOyEKXGA1HLz5JqE0UN0DjqS0wIulCMT8cwkIf5g1J0pPT7wTy1jkZ2KRLm7EXHatxVxdtxLTBW7xsPBkF/Q==", "license": "GPL-3.0-only", "dependencies": { "bindings": "^1.5.0", diff --git a/package.json b/package.json index e899c71d..73c0a790 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "trackaudio", "productName": "TrackAudio", - "version": "1.3.0-beta.2", + "version": "1.3.0-beta.3", "description": "A next generation Audio-For-VATSIM ATC Client for macOS, Linux and Windows", "main": "./out/main/index.js", "author": "github.com/pierr3", diff --git a/src/main/config.ts b/src/main/config.ts index 4ea020cb..809aa3a4 100644 --- a/src/main/config.ts +++ b/src/main/config.ts @@ -1,9 +1,7 @@ import { dialog } from 'electron'; import Store from 'electron-store'; -export type RadioEffects = 'on' | 'input' | 'output' | 'off'; - -export type AlwaysOnTopMode = 'never' | 'always' | 'inMiniMode'; +import { AlwaysOnTopMode, Configuration } from '../shared/config.type'; // Used to check for older settings that need upgrading. This should get // increased any time the Configuration object has a breaking change. @@ -26,26 +24,6 @@ export const defaultConfiguration = { alwaysOnTop: 'never' as AlwaysOnTopMode }; -export interface Configuration { - version?: number; - - audioApi: number; - audioInputDeviceId: string; - headsetOutputDeviceId: string; - speakerOutputDeviceId: string; - - cid: string; - password: string; - callsign: string; - - radioEffects: RadioEffects; - hardwareType: number; - radioGain: number; - - // Boolean is the prior type for this property, AlwaysOnTopMode is the updated type. - alwaysOnTop: boolean | AlwaysOnTopMode; -} - class ConfigManager { private static _instance: ConfigManager | null = null; private _currentConfiguration: Configuration | null = null; diff --git a/src/main/index.ts b/src/main/index.ts index 43f6f972..96d64f02 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -4,7 +4,9 @@ import Store from 'electron-store'; import { join } from 'path'; import { AfvEventTypes, TrackAudioAfv } from 'trackaudio-afv'; import icon from '../../resources/AppIcon/icon.png?asset'; -import configManager, {AlwaysOnTopMode, RadioEffects} from './config'; + +import configManager, {RadioEffects} from './config'; +import { AlwaysOnTopMode } from '../shared/config.type'; type WindowMode = 'mini' | 'maxi'; @@ -433,9 +435,14 @@ ipcMain.handle('set-radio-gain', (_, radioGain: number) => { TrackAudioAfv.SetRadioGain(radioGain); }); + +ipcMain.handle('set-frequency-radio-gain', (_, frequency: number, radioGain: number) => { + TrackAudioAfv.SetFrequencyRadioGain(frequency, radioGain); +}); ipcMain.handle('set-radio-effects', (_, radioEffects: RadioEffects) => { configManager.updateConfig({ radioEffects }); TrackAudioAfv.SetRadioEffects(radioEffects); + }); ipcMain.handle('set-hardware-type', (_, hardwareType: number) => { diff --git a/src/preload/bindings.ts b/src/preload/bindings.ts index bdfc4a0b..6494da2f 100644 --- a/src/preload/bindings.ts +++ b/src/preload/bindings.ts @@ -1,5 +1,7 @@ import { ipcRenderer, IpcRendererEvent } from 'electron'; -import {AlwaysOnTopMode, RadioEffects} from '../main/config'; + +import { AlwaysOnTopMode } from '../shared/config.type'; + export const api = { /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -65,6 +67,8 @@ export const api = { SetupPtt: (pttIndex: number) => ipcRenderer.invoke('setup-ptt', pttIndex), + SetFrequencyRadioGain: (frequency: number, gain: number) => + ipcRenderer.invoke('set-frequency-radio-gain', frequency, gain), SetRadioGain: (gain: number) => ipcRenderer.invoke('set-radio-gain', gain), SetRadioEffects: (type: RadioEffects) => ipcRenderer.invoke('set-radio-effects', type), diff --git a/src/renderer/src/components/navbar.tsx b/src/renderer/src/components/navbar.tsx index bb301e26..0f0fb5af 100644 --- a/src/renderer/src/components/navbar.tsx +++ b/src/renderer/src/components/navbar.tsx @@ -1,6 +1,5 @@ import clsx from 'clsx'; import React, { useEffect, useState } from 'react'; -import { Configuration } from '../../../../src/main/config'; import { checkIfCallsignIsRelief, getCleanCallsign } from '../helpers/CallsignHelper'; import useErrorStore from '../store/errorStore'; import useSessionStore from '../store/sessionStore'; @@ -9,6 +8,7 @@ import '../style/navbar.scss'; import Clock from './clock'; import MiniModeToggleButton from './MiniModeToggleButton'; import SettingsModal from './settings-modal/settings-modal'; +import { Configuration } from 'src/shared/config.type'; const Navbar: React.FC = () => { const [showModal, setShowModal] = useState(false); diff --git a/src/renderer/src/components/radio/radio-container.tsx b/src/renderer/src/components/radio/radio-container.tsx index 29b61617..fdc22cf8 100644 --- a/src/renderer/src/components/radio/radio-container.tsx +++ b/src/renderer/src/components/radio/radio-container.tsx @@ -1,15 +1,27 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import Radio from './radio'; import useRadioState from '../../store/radioStore'; +import UnicomGuardBar from './unicom-guard'; const RadioContainer: React.FC = () => { const radios = useRadioState((state) => state.radios); + + const filteredRadios = useMemo(() => { + return radios.filter( + (radio) => radio.frequency !== 0 && radio.frequency !== 122.8e6 && radio.frequency !== 121.5e6 + ); + }, [radios]); + return ( <>
+
+ +
+
-
- {radios.map((radio) => ( +
+ {filteredRadios.map((radio) => ( ))}
diff --git a/src/renderer/src/components/radio/unicom-guard.tsx b/src/renderer/src/components/radio/unicom-guard.tsx new file mode 100644 index 00000000..ab37d5a9 --- /dev/null +++ b/src/renderer/src/components/radio/unicom-guard.tsx @@ -0,0 +1,307 @@ +import useRadioState, { RadioType } from '@renderer/store/radioStore'; +import '../../style/UnicomGuard.scss'; +import { useEffect, useMemo, useState } from 'react'; +import clsx from 'clsx'; +import useSessionStore from '@renderer/store/sessionStore'; +import useErrorStore from '@renderer/store/errorStore'; +import { GuardFrequency, UnicomFrequency } from '../../../../shared/common'; + +const UnicomGuardBar = () => { + const [radios, setRadioState, addRadio, removeRadio] = useRadioState((state) => [ + state.radios, + state.setRadioState, + state.addRadio, + state.removeRadio + ]); + const [isConnected, isAtc] = useSessionStore((state) => [ + state.isConnected, + state.isAtc + ]); + + const [localRadioGain, setLocalRadioGain] = useState(50); + + const postError = useErrorStore((state) => state.postError); + + const unicom = useMemo(() => { + return radios.find((radio) => radio.frequency === UnicomFrequency); + }, [radios, isConnected]); + + const guard = useMemo(() => { + return radios.find((radio) => radio.frequency === GuardFrequency); + }, [radios, isConnected]); + + const clickRx = (radio: RadioType | undefined) => { + if (!radio) return; + const newState = !radio.rx; + + window.api + .setFrequencyState( + radio.frequency, + newState, + newState ? radio.tx : false, + false, + radio.onSpeaker, + false + ) + .then((ret) => { + if (!ret) { + postError('Invalid action on invalid radio: RX.'); + return; + } + setRadioState(radio.frequency, { + rx: newState, + tx: !newState ? false : radio.tx, + xc: !false, + crossCoupleAcross: false, + onSpeaker: radio.onSpeaker + }); + }) + .catch((err: unknown) => { + console.error(err); + }); + }; + + const clickTx = (radio: RadioType | undefined) => { + if (!radio) return; + const newState = !radio.tx; + + window.api + .setFrequencyState( + radio.frequency, + newState ? true : radio.rx, // If tx is true, rx must be true + newState, + false, + radio.onSpeaker, + false + ) + .then((ret) => { + if (!ret) { + postError('Invalid action on invalid radio: TX.'); + return; + } + setRadioState(radio.frequency, { + rx: !radio.rx && newState ? true : radio.rx, + tx: newState, + xc: false, + crossCoupleAcross: false, + onSpeaker: radio.onSpeaker + }); + }) + .catch((err: unknown) => { + console.error(err); + }); + }; + + const clickSpK = (radio: RadioType | undefined) => { + if (!radio) return; + const newState = !radio.onSpeaker; + window.api + .setFrequencyState( + radio.frequency, + radio.rx, + radio.tx, + radio.xc, + newState, + radio.crossCoupleAcross + ) + .then((ret) => { + if (!ret) { + postError('Invalid action on invalid radio: OnSPK.'); + return; + } + setRadioState(radio.frequency, { + rx: radio.rx, + tx: radio.tx, + xc: radio.xc, + crossCoupleAcross: radio.crossCoupleAcross, + onSpeaker: newState + }); + }) + .catch((err: unknown) => { + console.error(err); + }); + }; + + useEffect(() => { + if (!isConnected) { + void window.api.removeFrequency(UnicomFrequency).then((ret) => { + if (!ret) { + return; + } + removeRadio(UnicomFrequency); + }); + void window.api.removeFrequency(GuardFrequency).then((ret) => { + if (!ret) { + return; + } + removeRadio(GuardFrequency); + }); + } else { + void window.api.addFrequency(UnicomFrequency, 'UNICOM').then((ret) => { + if (!ret) { + console.error('Failed to add UNICOM frequency'); + return; + } + console.log('Adding unicom frequency'); + addRadio(UnicomFrequency, 'UNICOM', 'UNICOM'); + void window.api.SetFrequencyRadioGain(UnicomFrequency, localRadioGain / 100); + }); + void window.api.addFrequency(GuardFrequency, 'GUARD').then((ret) => { + if (!ret) { + console.error('Failed to add GUARD frequency'); + return; + } + console.log('Adding guard frequency'); + addRadio(GuardFrequency, 'GUARD', 'GUARD'); + void window.api.SetFrequencyRadioGain(GuardFrequency, localRadioGain / 100); + }); + } + }, [isConnected]); + + useEffect(() => { + const storedGain = window.localStorage.getItem('unicomRadioGain'); + const gainToSet = storedGain?.length ? parseInt(storedGain) : 50; + setLocalRadioGain(gainToSet); + }, []); + + const updateRadioGainValue = (newGain: number) => { + if (!unicom || !guard) return; + window.api + .SetFrequencyRadioGain(unicom.frequency, newGain / 100) + .then(() => { + void window.api.SetFrequencyRadioGain(guard.frequency, newGain / 100); + setLocalRadioGain(newGain); + }) + .catch((err: unknown) => { + console.error(err); + }); + + window.localStorage.setItem('unicomRadioGain', newGain.toString()); + }; + + const handleRadioGainChange = (event: React.ChangeEvent) => { + updateRadioGainValue(event.target.valueAsNumber); + }; + + const handleRadioGainMouseWheel = (event: React.WheelEvent) => { + const newValue = Math.min(Math.max(localRadioGain + (event.deltaY > 0 ? -1 : 1), 0), 100); + + updateRadioGainValue(newValue); + }; + + return ( +
+ + + UNICOM + + + + + + + + + + + GUARD + + + + + + + + + + +
+ ); +}; + +export default UnicomGuardBar; diff --git a/src/renderer/src/components/settings-modal/settings-modal.tsx b/src/renderer/src/components/settings-modal/settings-modal.tsx index ba4766f2..0ab0502f 100644 --- a/src/renderer/src/components/settings-modal/settings-modal.tsx +++ b/src/renderer/src/components/settings-modal/settings-modal.tsx @@ -2,7 +2,8 @@ import clsx from 'clsx'; import React, { useEffect, useState } from 'react'; import { AudioApi, AudioDevice } from 'trackaudio-afv'; import { useDebouncedCallback } from 'use-debounce'; -import {AlwaysOnTopMode, Configuration, RadioEffects} from '../../../../../src/main/config'; + +import { AlwaysOnTopMode, Configuration } from '../../../../shared/config.type'; import useRadioState from '../../store/radioStore'; import useUtilStore from '../../store/utilStore'; import AudioApis from './audio-apis'; diff --git a/src/renderer/src/store/radioStore.ts b/src/renderer/src/store/radioStore.ts index 3da1f1ab..c1bfef98 100644 --- a/src/renderer/src/store/radioStore.ts +++ b/src/renderer/src/store/radioStore.ts @@ -2,6 +2,7 @@ import { create } from 'zustand'; import useSessionStore from './sessionStore'; import { radioCompare } from '../helpers/RadioHelper'; import { getCallsignParts } from '../helpers/CallsignHelper'; +import { GuardFrequency, UnicomFrequency } from '../../../shared/common'; export interface RadioType { frequency: number; @@ -78,9 +79,11 @@ const useRadioState = create((set) => ({ pttIsOn: false, addRadio: (frequency, callsign, stationCallsign) => { if (RadioHelper.doesRadioExist(useRadioState.getState().radios, frequency)) { - postMessage( - 'Frequency already exists in local client, but maybe not in AFV, delete it and try again' - ); + if (frequency !== UnicomFrequency && frequency !== GuardFrequency) { + postMessage( + 'Frequency already exists in local client, but maybe not in AFV, delete it and try again' + ); + } return; } diff --git a/src/renderer/src/style/UnicomGuard.scss b/src/renderer/src/style/UnicomGuard.scss new file mode 100644 index 00000000..7a76aa2d --- /dev/null +++ b/src/renderer/src/style/UnicomGuard.scss @@ -0,0 +1,69 @@ +@import 'variables'; + +.unicon-overall-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.unicom-bar-container { + height: 28px; + line-height: 25px; + width: fit-content; + margin: 10px; + background-color: lighten($disabled, 10%); + border-radius: $card-border-radius; + border-color: lighten($disabled, 20%); + outline: lighten($disabled, 25%) solid 2px; + padding-left: 10px; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; // Added to center items horizontally + +} + +.hide-unicom-container { + @media screen and (max-width: 465px) { + display: none; + } +} + +.unicom-line-item { + margin-right: 10px; +} + +.unicom-text { + display: inline-block; + vertical-align: middle; +} + +.sm-button { + height: 19px !important; + width: 30px !important; + min-height: 0 !important; + padding-left: 3px !important; + padding-right: 3px !important; + padding-top: 2px !important; + padding-bottom: 2px !important; + margin: 0 !important; + line-height: 2px !important; + font-size: 14px !important; + outline: darken($info, 15%) solid 1px !important; + margin-left: 5px !important; + + &.btn-success { + outline: darken($success, 15%) solid 1px !important; + } + + &.btn-warning { + outline: darken($warning, 15%) solid 1px !important; + } +} + +.unicom-volume-bar { + margin-right: 10px; + width: 50px !important; +} diff --git a/src/renderer/src/style/app.scss b/src/renderer/src/style/app.scss index 7cdb6084..52775911 100644 --- a/src/renderer/src/style/app.scss +++ b/src/renderer/src/style/app.scss @@ -233,9 +233,7 @@ button:disabled { float: left; height: 100%; width: 65%; - padding-top: 0; - padding-bottom: 0; - padding-right: 0; + padding: 0; } // When changing the max-width property make sure to update the miniModeWidthBreakpoint constant in @@ -385,8 +383,7 @@ select:disabled { font-size: $mini-font-size; } -.radio-list -{ +.radio-list { max-height: 80vh; overflow-y: auto; } @@ -421,4 +418,21 @@ select:disabled { .exit-mini-mode-container.hidden { opacity: 0; -} \ No newline at end of file +} + +::-webkit-scrollbar { + width: 5px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgb(50, 50, 58); /* Color of the scrollbar thumb */ + border-radius: 2px; /* Rounded corners of the scrollbar thumb */ +} + +::-webkit-scrollbar-thumb:hover { + background: lighten(rgb(50, 50, 58), 10%) /* Color of the scrollbar thumb on hover */ +} diff --git a/src/shared/common.ts b/src/shared/common.ts new file mode 100644 index 00000000..444ace10 --- /dev/null +++ b/src/shared/common.ts @@ -0,0 +1,2 @@ +export const UnicomFrequency = 122800000; +export const GuardFrequency = 121500000; diff --git a/src/shared/config.type.ts b/src/shared/config.type.ts new file mode 100644 index 00000000..0cc87cce --- /dev/null +++ b/src/shared/config.type.ts @@ -0,0 +1,20 @@ +export type AlwaysOnTopMode = 'never' | 'always' | 'inMiniMode'; + +export interface Configuration { + version?: number; + + audioApi: number; + audioInputDeviceId: string; + headsetOutputDeviceId: string; + speakerOutputDeviceId: string; + + cid: string; + password: string; + callsign: string; + + hardwareType: number; + radioGain: number; + + // Boolean is the prior type for this property, AlwaysOnTopMode is the updated type. + alwaysOnTop: boolean | AlwaysOnTopMode; +} diff --git a/tsconfig.node.json b/tsconfig.node.json index eabf67bd..c3ad7893 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,6 +1,6 @@ { "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", - "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"], + "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/shared/**/*"], "compilerOptions": { "sourceMap": true, "composite": true, diff --git a/tsconfig.web.json b/tsconfig.web.json index 464fb277..eb373d9a 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -5,6 +5,7 @@ "src/renderer/src/**/*", "src/preload/*.d.ts", "src/renderer/src/**/*.tsx", + "src/shared/**/*" ], "compilerOptions": { "sourceMap": true,