diff --git a/.gitmodules b/.gitmodules index 331c6dc..a028157 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,7 @@ path = daemon/SDL url = https://github.com/libsdl-org/SDL.git branch = SDL2 +[submodule "daemon/rc2-core"] + path = daemon/rc2-core + url = https://github.com/W3AXL/rc2-core + branch = main diff --git a/README.md b/README.md index 7878c6d..9520f51 100644 --- a/README.md +++ b/README.md @@ -4,83 +4,29 @@ RadioConsole2 aims to be an open-source and expandable software console for controlling radios and radio systems remotely via WebRTC. ## Overview -RC2 consists of two parts - the GUI console (`rc2-console`) and the individual radio control daemons (`rc2-daemon`), one per each radio to be connected to the console. RC2 now also supports connection to a [DVMProject FNE](https://github.com/DVMProject/dvmhost) directly using the [rc2-dvm daemon](https://github.com/W3AXL/rc2-dvm). +RC2 consists of two parts - the GUI console (`rc2-console`) and the individual radio control daemons (`rc2-daemon`), one per each radio endpoint to be connected to the console. + +RC2 now also supports connection to a [DVMProject FNE](https://github.com/DVMProject/dvmhost) directly using the [rc2-dvm daemon](https://github.com/W3AXL/rc2-dvm). + +RC2 is designed to be expandable and customizable, easy to develop for, and robust enough to serve as your main radio base station. ![](docs/media/screenshot-1.1.0-beta.1-lots.png) -## Installation -### Daemon Installation -1. Download the appropriate `rc2-daemon` from the [latest release on the releases page](https://github.com/W3AXL/RadioConsole2/releases). Download the zipfile for the appropriate OS architecture you will be running the daemons on. The daemon runs as a portable executable binary and does not require an installer. -2. Install SDL2 - this is the library used for manipulating sound devices from the daemon and is not bundled by default. You will receive a runtime error if the library is not properly installed. Eventually I will figure out a better way to automate the installation of this library, but for now it has to be done manually. - - **For Windows** - download the latest release of the [SDL2 library](https://github.com/libsdl-org/SDL) and place the `SDL2.dll` library in the same folder as your `daemon.exe`. - - **For Linux** - install the latest version of SDL2 and libSDL2-dev from your package manager (example - `sudo apt install libsdl2-dev libsdl2-2.0-0` on Debian-based systems) -3. Test your `daemon` installation - - Try to query your PC's audio devices by running `./daemon list-audio` from a command prompt/terminal in the directory you downloaded the daemon to. If everything is installed correctly, you should see a list of audio input & output devices printed to the terminal. -### Console GUI Installation -The `rc2-console` GUI is a portable electron application. Simply extract the exe from the [latest release on the releases page](https://github.com/W3AXL/RadioConsole2/releases) to a location of your choosing and run. -## Configuration -### Configuring the radio control `daemon` -Use the [`config.example.toml`](https://github.com/W3AXL/RadioConsole2/blob/main/daemon/config.example.toml) file as a template for your daemon configuration. See the descriptions for each section below: -#### `[info]` -These parameters are displayed in the console when you hover over a radio card. -- `name`: the name of this daemon -- `desc`: a description for this daemon -#### `[network]` -These parameters control how the daemon interacts with the network -- `ip`: the address to listen on for connections from the console. Use 0.0.0.0 to listen on all addresses. -- `port`: the port to listen on for connections from the console. -#### `[radio]` -These parameters control how the radio is configured and controlled. -- `type`: the type of radio the daemon is connected to. Currently, only `sb9600` or `none` are supported, however in the future other radio models as well as generic CM108 interface will be implemented. -- `rxOnly`: `true` or `false`. This is used to tell the daemon the radio it's connected to cannot transmit. Useful for things like scanners. -#### `[none]` -These options are used when the radio type is set to `none`. -- `zone`: The name of the zone to display on the console. -- `chan`: The name of the channel to display on the console. -#### `[sb9600]` -These options are used when the radio type is set to `sb9600`. -- `head`: The type of control head the SB9600 radio is using. Valid options are currently `W9` for Astro Spectra/XTL5000, or `M3` for MCS2000. -- `port`: The PC's serial port the SB9600 RIB is connected to. For windows, use the format `COMx`. For linux, use the format `/dev/ttyXXX`. -#### `[audio]` -These parameters specify which audio devices to use for TX & RX audio. Get a list of device names by running the daemon with the `list-audio` command: `./daemon list-audio`. -#### `[lookups]` -These lists control how text on the radio's display is translated to text on the console. Very handy for radios with limited display sizes like the W9 control head. They translate the lookup string in the first item to the longer text in the second. For instance, you could translate a single-line radio displaying "Z1 CHAN1" or "Z2 CHAN2" to zone text of "Zone 1"/"Zone 2" and channel text of "Channel 1"/"Channel 2" using the following `zoneLookup` and `chanLookup` entries: -```toml -[lookups] -zoneLookup = [ - ["Z1", "Zone 1"], - ["Z2", "Zone 2"] -] -chanLookup = [ - ["CHAN1", "Channel 1"], - ["CHAN2", "Channel 2"] -] -``` -- `zoneLookup`: Lookup list used for zone text matching -- `chanLookup`: Lookup list used for channel text matching -#### `[softkeys]` -This section defines the softkeys available on the console UI, and what buttons on the radio control head map to these softkeys. At first this may seem confusing - see the Wiki entry on button bindings for more information and available options. -- `buttonBinding`: This section defines the buttons on the radio's control head and what functions are programmed to each button. -- `softkeyList`: This section defines what softkeys are made availble on the console UI. The keys defined here map to the button bindings defined in `buttonBinding` above. -### Running the Daemon -Once your configuration file has been created, you can run the daemon using the `-c` flag to specify the config file to use. For example: `./daemon -c config.toml`. You may optionally enable logging, debug printing, and other features with additional command line switches. Run `./daemon -h` to get a full list of available command line flags. +## Documentation + +All RC2 documentation is now hosted at [ReadTheDocs](https://rc2.readthedocs.io). + +## Downloading + +The latest release can be found on the right side of the Github Repo main page, or by [following this link](https://github.com/W3AXL/RadioConsole2/releases) + +You will find both `rc2-daemon` and `rc2-console` available for download. You will need both for a full RC2 system, [see the documentation](https://rc2.readthedocs.io) for more information. + +## Support -When you run the daemon, you should see some output as the daemon connects to the radio and updates its zone/channel/status information. -### Configuring the Console UI -- First, run the rc2-console.exe file. You will be presented with an empty console window like below. Once you have your daemon(s) running as explained above, you can add new radios to the console. Click the pencil icon to bring up the edit window\ - \ - ![](docs/media/tut-mainwindow-edit.png) +If you encounter issues using RC2, you should first consider joining [our Discord channel on the DVMProject discord server](https://discord.gg/Y9CF6zNtjr). If you don't wish to use +Discord, you can always [create an issue using our bug issue template](https://github.com/W3AXL/RadioConsole2/issues/new/choose). -- Use the plus (+) button to add a new radio definition. Specify the address of the PC running the daemon and the port you configured above. A new radio card will automatically be added to the main console window.\ - \ - ![](docs/media/tut-radios-add.png) \ - ![](docs/media/tut-radio-new.png) \ - In addition to the radio's address and port, you can specify the background color of the radio card and the default panning (left to right) of the radio's RX audio. +## Development -- Click the signal bars on the radio card to connect to the daemon. You can also click the power icon in the top right which will connect to all daemons currently configured.\ - \ - ![](docs/media/tut-mainwindow-connect.png) \ - \ - If everything goes well, you should see the bars go green and the radio text get populated from the daemon: \ - \ - ![](docs/media/tut-mainwindow-connected.png) +See the development section of the documentatioion at [ReadTheDocs/Development](https://rc2.readthedocs.io/en/latest/development.html) for information on getting started developing for RC2 \ No newline at end of file diff --git a/console/client.js b/console/client.js index 195d71c..7d3db8b 100644 --- a/console/client.js +++ b/console/client.js @@ -140,6 +140,9 @@ const rtcConf = { // RTT (round-trip time) parameters for RTC connection rttLimit: 0.25, rttSize: 25, + + // Periodic WebRTC latency check time (ms) + statCheckTime: 3000, // Whether to disable FEC and enable CBR (this actually causes more latency annoyingly) cbr: false @@ -186,11 +189,14 @@ const radioCardTemplate = document.querySelector('#card-template'); const alertTemplate = document.querySelector("#alert-dialog-template"); // Radio JSON validation -const validColors = ["red","amber","green","blue","purple"]; +const validColors = ["red","amber", "yellow", "green", "teal", "blue", "purple"]; // Extension websocket connection var extensionWs = null; +// Whether we're currently editing a radio +var editingRadioIdx = -1; + /*********************************************************************************** State variables ***********************************************************************************/ @@ -550,9 +556,11 @@ function deselectRadios() { * Populate radio cards based on the radios in radios[] and bind their buttons */ function populateRadios() { + console.debug("Populating radio cards from initial config"); // Add a card for each radio in the list radios.forEach((radio, index) => { - console.log("Adding radio " + radio.name); + console.info("Adding radio " + radio.name); + console.debug(radio); // Add the radio card addRadioCard("radio" + String(index), radio.name, radio.color); // Update edit list @@ -580,6 +588,9 @@ function clearRadios() { * @param {string} name Name to display in header */ function addRadioCard(id, name, color) { + // Log + console.debug(`Adding card for radio ${name} (id ${id})`); + // New, much easier way to add new cards var newCard = radioCardTemplate.content.cloneNode(true); newCard.querySelector(".radio-card").classList.add(color); @@ -603,37 +614,228 @@ function addRadioCard(id, name, color) { $("#main-layout").append(newCard); } -function addRadioToEditTable(radio) { - $("#edit-radios-table tr:last").after(` - ${radio.name}${radio.address}${radio.port} - `); +/** + * Add a radio to the edit radios table + * @param {Radio} radio radio object to add + * @param {int} index optional index in the table to overwrite + */ +function addRadioToEditTable(radio, index = null) { + // Get nice pretty display value for pan + let panValue = "C"; + if (radio.pan != 0) + { + const panPercent = Math.abs(radio.pan / 1.0).toFixed(2) * 100; + if (radio.pan < 0) + { + panValue = `L ${panPercent}%`; + } + else + { + panValue = `R ${panPercent}%`; + } + } + // Create HTML content + const tableRowHtml = ` + ${radio.name} + ${radio.address} + ${radio.port} + ${radio.color} + ${panValue} + + +   + + + + + ` + if (index != null) + { + console.debug(`Updating edit table row ${index} for radio ${radio.name}`); + $(`#edit-radios-table tr:eq(${index})`).html(tableRowHtml); + } + else + { + console.debug(`Adding edit table row for radio ${radio.name} to end of table`); + $("#edit-radios-table tr:last").after(`${tableRowHtml}`); + } +} + +/** + * Show the radio dialog for a new radio (empty) + */ +function showAddRadioDialog() +{ + window.electronAPI.showRadioConfig(null); +} + +/** + * Show the radio dialog for an existing radio + * @param {int} editRow + * @param {str} name + */ +function editRadio(editRow, name) +{ + // Find the radio + const idx = radios.findIndex((radio) => radio.name == name); + // Verify found + if (idx < 0) + { + alert(`Unable to edit radio ${name}: could not find radio in list`); + return; + } + // Get radio config + const radioConfig = config.Radios[idx] + // Flag editing + editingRadioIdx = idx; + console.info(`Now editing radio ${radioConfig.name}`); + console.debug(radioConfig); + // Show window + window.electronAPI.showRadioConfig(radioConfig); } +/** + * Delete a radio + * @param {int} editRow row in the table + * @param {str} name name of the radio + */ function deleteRadio(editRow, name) { - var found = false; - console.warn(`Removing radio ${name}`); - // Remove from config and radio objects - radios.forEach((radio, index) => { - if (radio.name == name) { - // Update list objects - config.Radios.splice(index, 1); - radios.splice(index, 1); - // Remove radio card - console.debug("Removing radio card by identifier: " + `.radio-card:contains("${name}")`); - $(`.radio-card:contains("${name}")`).remove(); - // Update List - $(editRow).closest("tr").remove(); - // Save config - saveConfig(); - // update flag - found = true; - } - }); - if (!found) { - alert("Failed to delete radio!"); + // Find the radio + const idx = radios.findIndex((radio) => radio.name == name); + // Verify found + if (idx < 0) + { + alert(`Unable to delete radio ${name}: could not find radio in list`); + return; } + // Log + console.info(`Removing radio ${name})`) + console.debug(config.Radios[idx]); + // Remove from config and radio list + config.Radios.splice(idx, 1); + radios.splice(idx, 1); + // Remove card + $(`.radio-card:contains("${name}")`).remove(); + // Remove row in radio table + $(editRow).closest("tr").remove(); + // Save config + saveConfig(); } +/** + * Handle radio edit dialog cancel + */ +window.electronAPI.cancelRadioConfig(() => { + if (editingRadioIdx >= 0) + { + console.debug("Clearing edit radio flag, edit cancelled"); + editingRadioIdx = -1; + } +}); + +/** + * New handler for getting new radio configurations from the radio config window + */ +window.electronAPI.saveRadioConfig((event, radioConfig) => { + // Debug print + console.debug('Got new radio config from radio edit window!'); + console.debug(radioConfig); + + // Handle edit of an existing radio first + if (editingRadioIdx >= 0) + { + console.info(`Updating radio at index ${editingRadioIdx}`); + console.debug(radioConfig); + + // Store index + const idx = editingRadioIdx; + // Clear flag + editingRadioIdx = -1; + + // Update radio config at index + config.Radios[idx] = radioConfig; + saveConfig(); + + // Disconnect radio if connected + if (radios[idx].status.State != 'Disconnected') + { + disconnectRadio(idx); + } + + // Update radio in main list + radios[idx].name = radioConfig.name; + radios[idx].address = radioConfig.address; + radios[idx].port = radioConfig.port; + radios[idx].color = radioConfig.color; + radios[idx].pan = radioConfig.pan; + + // Find the table row for this radio and get its index + let editTableRow = $(`#edit-radios-table tr:contains('${radioConfig.name}')`); + const editTableIndex = editTableRow.index(); + + // Update the row at the index + addRadioToEditTable(radioConfig, editTableIndex); + + // Update card + updateRadioCard(idx); + + // Return + return; + } + + // Validate radio doesn't already exist + if (config.Radios.some(radio => radio.name === radioConfig.name)) + { + alert(`Radio with name ${radioConfig.name} already exists!`); + return; + } + if (config.Radios.some(radio => radio.address === radioConfig.address) && config.Radios.some(radio => radio.port === radioConfig.port)) + { + alert(`Radio at destination ${radioConfig.address}:${radioConfig.port} already exists!`); + return; + } + // Validate color selection + if (!validColors.includes(radioConfig.color)) + { + alert(`Invalid radio color selected: ${radioConfig.color}`); + return; + } + + // Save new radio + config.Radios.push(radioConfig); + saveConfig(); + + // Copy config to a new radio object (this gets added to our current radios) + var newRadio = radioConfig; + + // Populate defaults + newRadio.status = { State: 'Disconnected' }; + newRadio.rtc = {}; + newRadio.wsConn = null; + newRadio.audioSrc = null; + + // Get the index for this new radio (will be at the end of the list) + const newRadioIdx = radios.length; + + // Append to config + radios.push(newRadio); + + // Populate new radio + console.log("Adding radio " + newRadio.name); + + // Add the radio card + addRadioCard("radio" + String(newRadioIdx), newRadio.name, newRadio.color); + + // Populate its text + updateRadioCard(newRadioIdx); + + // Update edit list + addRadioToEditTable(newRadio); + + // Clear form + newRadioClear(); +}); + function stopClick(event, obj) { event.stopPropagation(); event.preventDefault(); @@ -646,10 +848,26 @@ function updateRadioCard(idx) { // Get card object var radioCard = $("#radio" + String(idx)); - // Update card name & description (we limit the header name to 14 characters) - radioCard.find(".radio-name").html(radio.status.Name ? radio.status.Name.substring(0,14) : `Radio ${idx}`); + // Update card name & description + radioCard.find(".radio-name").html(radio.status.Name ? radio.status.Name : radio.name); radioCard.find(".radio-name").attr("title", radio.status.Description); + // Update color if changed + if (!radioCard.hasClass(radio.color)) + { + const cardClasses = radioCard.attr('class').split(/\s+/); + cardClasses.forEach((className) => { + if (validColors.some(color => color === className)) + { + const oldColor = className + console.debug(`Updating radio card color from ${oldColor} to ${radio.color}`); + radioCard.removeClass(oldColor); + radioCard.addClass(radio.color); + } + }) + + } + // Limit zone & channel text to 27/18 characters // TODO: figure out dynamic scaling of channel/zone text so we don't have to do this if (radio.status.ZoneName != null) { @@ -829,24 +1047,6 @@ function startPtt(micActive) { alertStopTimeout = null; } - // Old logic that doesn't use the ACK below - // Unmute mic after timeout, if requested - /**if (micActive) { - setTimeout( unmuteMic, audio.micUnmuteDelay); - } - // Play TPT - playSound("sound-ptt"); - // Send radio keyup after latency timeout - setTimeout( function() { - radios[selectedRadioIdx].wsConn.send(JSON.stringify( - { - "radio": { - "command": "startTx" - } - } - )); - }, radios[selectedRadioIdx].rtc.txLatency);**/ - // Flag that we want the mic to unmute or not txUnmuteMic = micActive; // Send the command @@ -1112,6 +1312,8 @@ function startAlert(mode) { // Start PTT if we need to, otherwise PTT was overridden if (!pttActive) { startPtt(false); + // Ensure mic doesn't unmute (should be covered by the above false but it gets weird sometimes) + txUnmuteMic = false; } else { alertPttOverride = true; } @@ -1143,6 +1345,8 @@ function sendAlert() { sendAlert(); }, 50); } else { + // Ensure mic is muted + muteMic(); console.debug("Radio transmitting, starting alert tone"); alertStartTimeout = setTimeout(() => { audio.tones.start(); @@ -1401,8 +1605,6 @@ async function readConfig() { } // Default mute (not muted) radios[idx].mute = false; - // Default name (used for logging until we get the proper name) - radios[idx].name = `Radio ${idx}`; }); // Populate radio cards @@ -1456,58 +1658,6 @@ function newRadioClear() { $('#new-radio-pan').val(0); } -function newRadioAdd() { - // Get values - const newRadioAddress = $('#new-radio-address').val(); - const newRadioPort = $('#new-radio-port').val(); - const newRadioColor = $('#new-radio-color').val(); - const newRadioPan = $('#new-radio-pan').val(); - - // Create the new radio entry - var newRadio = { - address: newRadioAddress, - port: newRadioPort, - color: newRadioColor, - pan: newRadioPan, - }; - - // Validate - if (!validColors.includes(newRadio.color)) { - console.warn(`Color ${newRadio.color} not valid, defaulting to blue`); - radios[idx].color = "blue"; - } - - // Save config - config.Radios.push(newRadio); - saveConfig(); - - // Populate default values - newRadio.status = { State: 'Disconnected' }; - newRadio.rtc = {}; - newRadio.wsConn = null; - newRadio.audioSrc = null; - - // Get the index - var newRadioIdx = radios.length; - - // Default name (used for logging until we get the proper name) - newRadio.name = `Radio ${newRadioIdx}`; - - // Append to config - radios.push(newRadio); - - // Populate new radio - console.log("Adding radio " + newRadio.name); - // Add the radio card - addRadioCard("radio" + String(newRadioIdx), newRadio.name, newRadio.color); - // Populate its text - updateRadioCard(newRadioIdx); - // Update edit list - addRadioToEditTable(newRadio); - // Clear form - newRadioClear(); -} - /*********************************************************************************** WebRTC Functions @@ -1972,7 +2122,7 @@ function checkRoundTripTime(idx) { }) setTimeout(function() { checkRoundTripTime(idx) - }, 1000); + }, rtcConf.statCheckTime); }) } else { console.warn(`Peer connection closed, stopping RTT monitoring for radio ${idx}`); @@ -2041,7 +2191,7 @@ function startAudioDevices() { // Create gain node for output volume and connect it to the default output device audio.outputGain = audio.context.createGain(); - audio.outputGain.gain.value = 0.75; + audio.outputGain.gain.value = Math.pow($("#console-volume").val() / 100, 2); audio.outputGain.connect(audio.context.destination); // Start audio input @@ -2159,7 +2309,7 @@ function audioMeterCallback() { return; } - // Draw stuff + // Update meters radios.forEach((radio, idx) => { // Ignore radios with no connected audio if (radios[idx].audioSrc == null) { @@ -2244,8 +2394,11 @@ function zeroAudioMeters() function volumeSlider() { // Convert 0-100 to 0-1 for multiplication with audio, using an inverse-square curve for better "logarithmic" volume const newVol = Math.pow($("#console-volume").val() / 100, 2); - // Set gain node to new value - audio.outputGain.gain.value = newVol; + // Set gain node to new value if it exists + if (audio.outputGain != null) + { + audio.outputGain.gain.value = newVol; + } // Set volume of each ui html sound const uiSounds = document.getElementsByClassName("ui-audio"); for (var i = 0; i < uiSounds.length; i++) { @@ -2777,23 +2930,37 @@ function connectRadio(idx) { * @param {function} callback callback function to execute once connected */ function waitForWebSockets(sockets, callback=null) { - setTimeout( - function() { - socketsReady = 0; - sockets.forEach((socket) => { - if (socket.readyState === 1) - { - socketsReady++; - } - }) - if (socketsReady === sockets.length) - { - callback(); - } else { + // Starting variables + socketsReady = 0; + cancel = false; + // Iterate over each socket in our list + sockets.forEach((socket) => { + if (socket.readyState === WebSocket.OPEN) + { + socketsReady++; + } + // If any of our sockets closed or are closing, we cancel the wait + else if (socket.readyState == WebSocket.CLOSING || socket.readyState == WebSocket.CLOSED) + { + console.warn(`Websocket ${socket} closed, cancelling waitForWebsockets`); + cancel = true; + } + }); + // Check if we should cancel listening + if (cancel) { return; } + // Check if all sockets are ready + if (socketsReady === sockets.length) + { + callback(); + } + else + { + setTimeout( + function() { waitForWebSockets(sockets, callback); - } - }, - 5); // 5 ms timeout + }, + 5 ); + } } /** @@ -2802,7 +2969,7 @@ function waitForWebSockets(sockets, callback=null) { */ function onConnectWebsocket(idx) { //$("#navbar-status").html("Websocket connected"); - console.log(`Websocket connected for radio ${radios[idx].name}`); + console.log(`Websockets connected for radio ${radios[idx].name}`); // Query radio status console.log(`Querying radio ${radios[idx].name} status`); radios[idx].wsConn.send(JSON.stringify( @@ -3031,8 +3198,19 @@ function extensionConnect() { extensionWs.close(); return; } + // Prepare URL + const wsUrl = `ws://${config.Extension.address}:${config.Extension.port}`; + // Verify valid address + try { + const url = new URL(wsUrl); + } + catch (_) + { + alert("Invalid extension URL, cannot open connection!"); + return; + } // Create the connection - extensionWs = new WebSocket(`ws://${config.Extension.address}:${config.Extension.port}`); + extensionWs = new WebSocket(wsUrl); // Create websocket extensionWs.onerror = function(event) { handleExtensionError(event) }; extensionWs.onmessage = function(event) { recvExtensionMessage(event) }; diff --git a/console/css/custom.css b/console/css/custom.css index eafe3a5..b7e882a 100644 --- a/console/css/custom.css +++ b/console/css/custom.css @@ -39,23 +39,69 @@ --color-btn-dark: #1A1A1A; --color-btn-pressed: #3D3D3D; - /* Dynamic Card Colors */ - --color-card-darkred: #291C1C; - --color-card-midred: #523838; - --color-card-lightred: #B87D7D; - --color-card-darkamber: #292015; - --color-card-midamber: #52402B; - --color-card-lightamber: #B88E60; - --color-card-darkgreen: #1C291E; - --color-card-midgreen: #38523B; - --color-card-lightgreen: #7DB885; - --color-card-darkblue: #1C2529; - --color-card-midblue: #384b52; - --color-card-lightblue: #7DA8B8; - --color-card-darkpurple: #1F1C29; - --color-card-midpurple: #3F3852; - --color-card-lightpurple: #8D7DB8; - + /* Dynamic Card Color Values */ + --value-card-darkest: 5%; + --value-card-dark: 15%; + --value-card-mid: 30%; + --value-card-light: 60%; + --value-card-text: 90%; + --sat-card-text: 50%; + /* Red */ + --hue-card-red: 0; + --sat-card-red: 25%; + --color-card-red-black: hsl(var(--hue-card-red), var(--sat-card-red), var(--value-card-darkest)); + --color-card-red-dark: hsl(var(--hue-card-red), var(--sat-card-red), var(--value-card-dark)); + --color-card-red-mid: hsl(var(--hue-card-red), var(--sat-card-red), var(--value-card-mid)); + --color-card-red-light: hsl(var(--hue-card-red), var(--sat-card-red), var(--value-card-light)); + --color-card-red-text: hsl(var(--hue-card-red), var(--sat-card-text), var(--value-card-text)); + /* Amber */ + --hue-card-amber: 20; + --sat-card-amber: 40%; + --color-card-amber-black: hsl(var(--hue-card-amber), var(--sat-card-amber), var(--value-card-darkest)); + --color-card-amber-dark: hsl(var(--hue-card-amber), var(--sat-card-amber), var(--value-card-dark)); + --color-card-amber-mid: hsl(var(--hue-card-amber), var(--sat-card-amber), var(--value-card-mid)); + --color-card-amber-light: hsl(var(--hue-card-amber), var(--sat-card-amber), var(--value-card-light)); + --color-card-amber-text: hsl(var(--hue-card-amber), var(--sat-card-text), var(--value-card-text)); + /* Yellow */ + --hue-card-yellow: 50; + --sat-card-yellow: 40%; + --color-card-yellow-black: hsl(var(--hue-card-yellow), var(--sat-card-yellow), var(--value-card-darkest)); + --color-card-yellow-dark: hsl(var(--hue-card-yellow), var(--sat-card-yellow), var(--value-card-dark)); + --color-card-yellow-mid: hsl(var(--hue-card-yellow), var(--sat-card-yellow), var(--value-card-mid)); + --color-card-yellow-light: hsl(var(--hue-card-yellow), var(--sat-card-yellow), var(--value-card-light)); + --color-card-yellow-text: hsl(var(--hue-card-yellow), var(--sat-card-text), var(--value-card-text)); + /* Green */ + --hue-card-green: 110; + --sat-card-green: 25%; + --color-card-green-black: hsl(var(--hue-card-green), var(--sat-card-green), var(--value-card-darkest)); + --color-card-green-dark: hsl(var(--hue-card-green), var(--sat-card-green), var(--value-card-dark)); + --color-card-green-mid: hsl(var(--hue-card-green), var(--sat-card-green), var(--value-card-mid)); + --color-card-green-light: hsl(var(--hue-card-green), var(--sat-card-green), var(--value-card-light)); + --color-card-green-text: hsl(var(--hue-card-green), var(--sat-card-text), var(--value-card-text)); + /* Teal */ + --hue-card-teal: 170; + --sat-card-teal: 25%; + --color-card-teal-black: hsl(var(--hue-card-teal), var(--sat-card-teal), var(--value-card-darkest)); + --color-card-teal-dark: hsl(var(--hue-card-teal), var(--sat-card-teal), var(--value-card-dark)); + --color-card-teal-mid: hsl(var(--hue-card-teal), var(--sat-card-teal), var(--value-card-mid)); + --color-card-teal-light: hsl(var(--hue-card-teal), var(--sat-card-teal), var(--value-card-light)); + --color-card-teal-text: hsl(var(--hue-card-teal), var(--sat-card-text), var(--value-card-text)); + /* Blue */ + --hue-card-blue: 210; + --sat-card-blue: 25%; + --color-card-blue-black: hsl(var(--hue-card-blue), var(--sat-card-blue), var(--value-card-darkest)); + --color-card-blue-dark: hsl(var(--hue-card-blue), var(--sat-card-blue), var(--value-card-dark)); + --color-card-blue-mid: hsl(var(--hue-card-blue), var(--sat-card-blue), var(--value-card-mid)); + --color-card-blue-light: hsl(var(--hue-card-blue), var(--sat-card-blue), var(--value-card-light)); + --color-card-blue-text: hsl(var(--hue-card-blue), var(--sat-card-text), var(--value-card-text)); + /* Purple */ + --hue-card-purple: 270; + --sat-card-purple: 25%; + --color-card-purple-black: hsl(var(--hue-card-purple), var(--sat-card-purple), var(--value-card-darkest)); + --color-card-purple-dark: hsl(var(--hue-card-purple), var(--sat-card-purple), var(--value-card-dark)); + --color-card-purple-mid: hsl(var(--hue-card-purple), var(--sat-card-purple), var(--value-card-mid)); + --color-card-purple-light: hsl(var(--hue-card-purple), var(--sat-card-purple), var(--value-card-light)); + --color-card-purple-text: hsl(var(--hue-card-purple), var(--sat-card-text), var(--value-card-text)); } @font-face { @@ -476,19 +522,25 @@ ion-icon { /* Dynamic Header Colors */ .radio-card.red .header { - background-color: var(--color-card-midred); + background-color: var(--color-card-red-mid); } .radio-card.amber .header { - background-color: var(--color-card-midamber); + background-color: var(--color-card-amber-mid); +} +.radio-card.yellow .header { + background-color: var(--color-card-yellow-mid); } .radio-card.green .header { - background-color: var(--color-card-midgreen); + background-color: var(--color-card-green-mid); +} +.radio-card.teal .header { + background-color: var(--color-card-teal-mid); } .radio-card.blue .header { - background-color: var(--color-card-midblue); + background-color: var(--color-card-blue-mid); } .radio-card.purple .header { - background-color: var(--color-card-midpurple); + background-color: var(--color-card-purple-mid); } /* Hover highlight */ @@ -502,25 +554,64 @@ ion-icon { opacity: 0.75; } -/* Dynamic Header Text Color */ -.radio-card.red .header h2 { - color: var(--color-card-lightred); +/* Dynamic Header/Zone Text Color */ +.radio-card.red .header h2, +.radio-card.red #zone-text { + color: var(--color-card-red-light); +} +.radio-card.amber .header h2, +.radio-card.amber #zone-text { + color: var(--color-card-amber-light); } -.radio-card.amber .header h2 { - color: var(--color-card-lightamber); +.radio-card.yellow .header h2, +.radio-card.yellow #zone-text { + color: var(--color-card-yellow-light); } -.radio-card.green .header h2 { - color: var(--color-card-lightgreen); +.radio-card.green .header h2, +.radio-card.green #zone-text { + color: var(--color-card-green-light); } -.radio-card.blue .header h2 { - color: var(--color-card-lightblue); +.radio-card.teal .header h2, +.radio-card.teal #zone-text { + color: var(--color-card-teal-light); } -.radio-card.purple .header h2 { - color: var(--color-card-lightpurple); +.radio-card.blue .header h2, +.radio-card.blue #zone-text { + color: var(--color-card-blue-light); +} +.radio-card.purple .header h2, +.radio-card.purple #zone-text { + color: var(--color-card-purple-light); } -.radio-card.selected .header h2 { - color: var(--color-txt-light); +/* Selected Card Header/Zone Text Color */ +.radio-card.red.selected .header h2, +.radio-card.red.selected #zone-text { + color: var(--color-card-red-text); +} +.radio-card.amber.selected .header h2, +.radio-card.amber.selected #zone-text { + color: var(--color-card-amber-text); +} +.radio-card.yellow.selected .header h2, +.radio-card.yellow.selected #zone-text { + color: var(--color-card-yellow-text); +} +.radio-card.green.selected .header h2, +.radio-card.green.selected #zone-text { + color: var(--color-card-green-text); +} +.radio-card.teal.selected .header h2, +.radio-card.teal.selected #zone-text { + color: var(--color-card-teal-text); +} +.radio-card.blue.selected .header h2, +.radio-card.blue.selected #zone-text { + color: var(--color-card-blue-text); +} +.radio-card.purple.selected .header h2, +.radio-card.purple.selected #zone-text { + color: var(--color-card-purple-text); } .radio-card .header h2 { @@ -680,23 +771,31 @@ ion-icon { .radio-card.red .panning-dropdown, .radio-card.red .dtmf-dropdown { - background-color: var(--color-card-midred); + background-color: var(--color-card-red-mid); } .radio-card.amber .panning-dropdown, .radio-card.amber .dtmf-dropdown { - background-color: var(--color-card-midamber); + background-color: var(--color-card-amber-mid); +} +.radio-card.yellow .panning-dropdown, +.radio-card.yellow .dtmf-dropdown { + background-color: var(--color-card-yellow-mid); } .radio-card.green .panning-dropdown, .radio-card.green .dtmf-dropdown { - background-color: var(--color-card-midgreen); + background-color: var(--color-card-green-mid); +} +.radio-card.teal .panning-dropdown, +.radio-card.teal .dtmf-dropdown { + background-color: var(--color-card-teal-mid); } .radio-card.blue .panning-dropdown, .radio-card.blue .dtmf-dropdown { - background-color: var(--color-card-midblue); + background-color: var(--color-card-blue-mid); } .radio-card.purple .panning-dropdown, .radio-card.purple .dtmf-dropdown { - background-color: var(--color-card-midpurple); + background-color: var(--color-card-purple-mid); } .panning-dropdown.closed, @@ -737,19 +836,25 @@ ion-icon { /* Dynamic Content Color */ .radio-card.red .content { - background-color: var(--color-card-darkred); + background-color: var(--color-card-red-dark); } .radio-card.amber .content { - background-color: var(--color-card-darkamber); + background-color: var(--color-card-amber-dark); +} +.radio-card.yellow .content { + background-color: var(--color-card-yellow-dark); } .radio-card.green .content { - background-color: var(--color-card-darkgreen); + background-color: var(--color-card-green-dark); +} +.radio-card.teal .content { + background-color: var(--color-card-teal-dark); } .radio-card.blue .content { - background-color: var(--color-card-darkblue); + background-color: var(--color-card-blue-dark); } .radio-card.purple .content { - background-color: var(--color-card-darkpurple); + background-color: var(--color-card-purple-dark); } /* Icon Stack */ @@ -818,28 +923,33 @@ ion-icon { float: left; } -/* Dynamic Zone Text Color */ -.radio-card.red #zone-text { - color: var(--color-card-lightred); +.radio-card #channel-text { + font-family: "Iosevka Bold"; + font-size: 28px; + margin-top: -4px; } -.radio-card.amber #zone-text { - color: var(--color-card-lightamber); + +/* Dynamic Channel Text Color */ +.radio-card.red #channel-text { + color: var(--color-card-red-text); } -.radio-card.green #zone-text { - color: var(--color-card-lightgreen); +.radio-card.amber #channel-text { + color: var(--color-card-amber-text); } -.radio-card.blue #zone-text { - color: var(--color-card-lightblue); +.radio-card.yellow #channel-text { + color: var(--color-card-yellow-text); } -.radio-card.purple #zone-text { - color: var(--color-card-lightpurple); +.radio-card.green #channel-text { + color: var(--color-card-green-text); } - -.radio-card #channel-text { - font-family: "Iosevka Bold"; - font-size: 28px; - color: var(--color-txt-light); - margin-top: -4px; +.radio-card.teal #channel-text { + color: var(--color-card-teal-text); +} +.radio-card.blue #channel-text { + color: var(--color-card-blue-text); +} +.radio-card.purple #channel-text { + color: var(--color-card-purple-text); } .radio-card.selected #channel-text { @@ -879,19 +989,25 @@ ion-icon { /* Dynamic Icon Color */ .radio-card.red .audio-bar { - color: var(--color-card-midred); + color: var(--color-card-red-mid); } .radio-card.amber .audio-bar { - color: var(--color-card-midamber); + color: var(--color-card-amber-mid); +} +.radio-card.yellow .audio-bar { + color: var(--color-card-yellow-mid); } .radio-card.green .audio-bar { - color: var(--color-card-midgreen); + color: var(--color-card-green-mid); +} +.radio-card.teal .audio-bar { + color: var(--color-card-teal-mid); } .radio-card.blue .audio-bar { - color: var(--color-card-midblue); + color: var(--color-card-blue-mid); } .radio-card.purple .audio-bar { - color: var(--color-card-midpurple); + color: var(--color-card-purple-mid); } /******************************** @@ -987,15 +1103,15 @@ ion-icon { } #ptt { - background-color: var(--color-card-midred) !important; - color: var(--color-card-lightred); + background-color: var(--color-card-red-mid) !important; + color: var(--color-card-red-light); clip-path: polygon(15% 0, 100% 0, 100% 70%, 85% 100%, 0 100%, 0 30%); width: 64px; } #ptt.pressed, #ptt:active:hover { - background-color: var(--color-card-darkred) !important; + background-color: var(--color-card-red-dark) !important; color: var(--color-txt-light); } @@ -1006,14 +1122,14 @@ ion-icon { #alert-bar-icon.pressed, #alert-bar-icon:active:hover { - background-color: var(--color-card-darkamber) !important; + background-color: var(--color-card-amber-dark) !important; color: var(--color-txt-light); } #alert-bar-icon, .alert-btn-icon { - background-color: var(--color-card-midamber) !important; - color: var(--color-card-lightamber); + background-color: var(--color-card-amber-mid) !important; + color: var(--color-card-amber-light); clip-path: polygon(15% 0, 100% 0, 100% 70%, 85% 100%, 0 100%, 0 30%); width: 64px; } @@ -1053,8 +1169,8 @@ ion-icon { .popup { position: fixed; - width: 480px; - top: -16px; + width: 540px; + top: -48px; left: 0; right: 0; margin: 5% auto; diff --git a/console/dialogs/edit-radio-preload.js b/console/dialogs/edit-radio-preload.js new file mode 100644 index 0000000..8191196 --- /dev/null +++ b/console/dialogs/edit-radio-preload.js @@ -0,0 +1,10 @@ +const { contextBridge, ipcRenderer } = require('electron/renderer') + +contextBridge.exposeInMainWorld('electronAPI', { + // Save/show config + populateRadioConfig: (radioConfig) => ipcRenderer.on('populateRadioConfig', radioConfig), + // Add new radio + saveRadioConfig: (radioConfig) => ipcRenderer.invoke('saveRadioConfig', radioConfig), + // Cancel edit + cancelRadioConfig: (data) => ipcRenderer.invoke('cancelRadioConfig', data), +}); \ No newline at end of file diff --git a/console/dialogs/edit-radio.html b/console/dialogs/edit-radio.html new file mode 100644 index 0000000..f487bf9 --- /dev/null +++ b/console/dialogs/edit-radio.html @@ -0,0 +1,139 @@ + + + + Configure Radios + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/console/midi-preload.js b/console/dialogs/midi-preload.js similarity index 100% rename from console/midi-preload.js rename to console/dialogs/midi-preload.js diff --git a/console/midi.html b/console/dialogs/midi.html similarity index 97% rename from console/midi.html rename to console/dialogs/midi.html index 5e0f850..4c3a222 100644 --- a/console/midi.html +++ b/console/dialogs/midi.html @@ -4,16 +4,16 @@ Configure MIDI Interface - + - + - +