diff --git a/src/languages/en.json b/src/languages/en.json index 50d5402..232a268 100644 --- a/src/languages/en.json +++ b/src/languages/en.json @@ -89,7 +89,8 @@ "other": { "title": "Other settings", "tooltip": "Other settings for the program that don't fit in the other categories.", - "trackerVisualizationFPS": "Tracker visualization FPS" + "trackerVisualizationFPS": "Tracker visualization FPS", + "pairing": "Manage GX(6/2) pairing" }, "updates": { "title": "Update checking", @@ -215,6 +216,27 @@ } } }, + "pairing": { + "title": "GX(6/2) Tracker Pairing", + "text": "Manage the pairing of your trackers connected with the GX(6/2) communication dongles.", + "card": { + "title": "COM port:", + "id": { + "title": { + "0": "Port ID 0", + "1": "Port ID 1" + }, + "status": { + "title": "Status:", + "paired": "Paired", + "unpaired": "Unpaired" + }, + "tracker": "Tracker:", + "manage": "Pair/Unpair" + }, + "manage": "Pair/Unpair all" + } + }, "dialogs": { "connectionFailed": { "title": "Connection failed", diff --git a/src/main.ts b/src/main.ts index 834fea8..5c1e6de 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,14 @@ import { app, BrowserWindow, ipcMain, shell, dialog, Menu } from "electron"; // @ts-ignore (for development) -import { HaritoraX, TrackerModel, SensorMode, FPSMode, SensorAutoCorrection, MagStatus } from "haritorax-interpreter"; +import { + HaritoraX, + TrackerModel, + SensorMode, + FPSMode, + SensorAutoCorrection, + MagStatus, +} from "haritorax-interpreter; import { autoDetect } from "@serialport/bindings-cpp"; const Binding = autoDetect(); import fs, { PathLike } from "fs"; @@ -305,28 +312,7 @@ const createWindow = async () => { await fs.promises.writeFile(configPath, "{}"); firstLaunch = true; } - mainWindow = new BrowserWindow({ - title: "SlimeTora: Main", - autoHideMenuBar: true, - width: 900, - height: 700, - webPreferences: { - contextIsolation: true, - nodeIntegration: true, - preload: path.join(__dirname, "preload.mjs"), - spellcheck: false, - sandbox: false, // fixes startup crashes due to GPU process, shouldn't be too large of a security risk as we're not loading any external content/connect to internet - }, - icon: path.join(__dirname, "static/images/icon.ico"), - }); - - mainWindow.loadURL( - format({ - pathname: path.join(__dirname, "static/html/index.html"), - protocol: "file:", - slashes: true, - }) - ); + mainWindow = createBrowserWindow("SlimeTora: Main", "index.html", "en", null, 900, 700); mainWindow.webContents.on("did-finish-load", async () => { mainWindow.webContents.send("localize", resources); @@ -346,11 +332,6 @@ const createWindow = async () => { if (appUpdatesEnabled) await checkForAppUpdates(); if (translationsUpdatesEnabled) await checkForTranslationUpdates(); }); - - mainWindow.webContents.setWindowOpenHandler(({ url }) => { - shell.openExternal(url); - return { action: "deny" }; - }); }; const closeApp = () => { @@ -375,16 +356,21 @@ app.on("window-all-closed", closeApp); * Renderer handlers */ -function onboarding(language: string) { - log("Showing onboarding screen"); - - let onboardingWindow = new BrowserWindow({ - title: "SlimeTora: Onboarding", +function createBrowserWindow( + title: string, + htmlFile: string, + query: string | ParsedUrlQueryInput, + parent: BrowserWindow, + width: number = 950, + height: number = 750 +): BrowserWindow { + let window = new BrowserWindow({ + title: title, autoHideMenuBar: true, - width: 950, - height: 750, + width: width, + height: height, modal: true, // prevent interaction with "parent" window until closed - parent: mainWindow, + parent: parent, webPreferences: { contextIsolation: true, nodeIntegration: true, @@ -395,19 +381,31 @@ function onboarding(language: string) { icon: path.join(__dirname, "static/images/icon.ico"), }); - onboardingWindow.loadURL( + window.loadURL( format({ - pathname: path.join(__dirname, "static/html/onboarding.html"), + pathname: path.join(__dirname, `static/html/${htmlFile}`), protocol: "file:", slashes: true, - query: { language: language }, + query: query, }) ); - onboardingWindow.webContents.setWindowOpenHandler(({ url }) => { + window.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: "deny" }; }); + + return window; +} + +function onboarding(language: string) { + log("Showing onboarding screen"); + createBrowserWindow("SlimeTora: Onboarding", "onboarding.html", { language: language }, mainWindow); +} + +function pairing() { + log("Showing pairing screen"); + createBrowserWindow("SlimeTora: Pairing", "pairing.html", null, mainWindow); } async function showMessage( @@ -472,6 +470,10 @@ ipcMain.on("show-onboarding", (_event, language) => { onboarding(language); }); +ipcMain.on("show-pairing", () => { + pairing(); +}); + ipcMain.handle("translate", async (_event, arg: string) => { return await translate(arg); }); @@ -548,30 +550,13 @@ ipcMain.on("open-logs-folder", async () => { }); ipcMain.on("open-tracker-settings", (_event, arg: string) => { - let trackerSettingsWindow = new BrowserWindow({ - title: `SlimeTora: ${arg} settings`, - autoHideMenuBar: true, - width: 850, - height: 650, - modal: true, // prevent interaction with "parent" window until closed - parent: mainWindow, - webPreferences: { - contextIsolation: true, - nodeIntegration: true, - preload: path.join(__dirname, "preload.mjs"), - spellcheck: false, - sandbox: false, // fixes startup crashes due to GPU process, shouldn't be too large of a security risk as we're not loading any external content/connect to internet - }, - icon: path.join(__dirname, "static/images/icon.ico"), - }); - - trackerSettingsWindow.loadURL( - format({ - pathname: path.join(__dirname, "static/html/settings.html"), - protocol: "file:", - slashes: true, - query: { trackerName: arg }, - }) + let trackerSettingsWindow = createBrowserWindow( + `SlimeTora: ${arg} settings`, + "settings.html", + { trackerName: arg }, + mainWindow, + 850, + 650 ); trackerSettingsWindow.webContents.on("did-finish-load", () => { @@ -704,7 +689,7 @@ function shouldInitializeNewDevice(): boolean { } function initializeDevice(forceDisableLogging: boolean = false) { - const trackerType = wiredTrackerEnabled ? TrackerModel.Wired: TrackerModel.Wireless; + const trackerType = wiredTrackerEnabled ? TrackerModel.Wired : TrackerModel.Wireless; const effectiveLoggingMode = forceDisableLogging ? 1 : loggingMode; log(`Creating new HaritoraX ${trackerType} instance with logging mode ${effectiveLoggingMode}...`, "connection"); const loggingOptions = { @@ -721,7 +706,7 @@ function initializeDevice(forceDisableLogging: boolean = false) { async function notifyConnectedDevices(): Promise { const activeTrackers = Array.from(new Set(device.getActiveTrackers())); if (activeTrackers.length === 0) return; - for (const trackerName of activeTrackers) await addTracker(trackerName); + for (const trackerName of activeTrackers) await addTracker(trackerName as string); log("Connected devices: " + JSON.stringify(activeTrackers), "connection"); } @@ -921,6 +906,7 @@ import { } from "@slimevr/firmware-protocol"; import { EmulatedTracker } from "@slimevr/tracker-emulation"; import BetterQuaternion from "quaternion"; +import { ParsedUrlQueryInput } from "querystring"; // For haritorax-interpreter // Used to handle errors coming from haritorax-interpreter and display them to the user if wanted @@ -1037,9 +1023,8 @@ async function processQueue() { const trackerTimeouts: { [key: string]: NodeJS.Timeout } = {}; const resetTrackerTimeout = (trackerName: string) => { - if (trackerTimeouts[trackerName]) { - clearTimeout(trackerTimeouts[trackerName]); - } + if (trackerTimeouts[trackerName]) clearTimeout(trackerTimeouts[trackerName]); + trackerTimeouts[trackerName] = setTimeout(() => { device.emit("disconnect", trackerName); log(`Tracker "${trackerName}" assumed disconnected due to inactivity.`, "tracker"); diff --git a/src/preload.mts b/src/preload.mts index 515790d..f5c6ad6 100644 --- a/src/preload.mts +++ b/src/preload.mts @@ -99,6 +99,7 @@ declare global { startConnection: () => void; stopConnection: () => void; showOnboarding: () => void; + showPairing: () => void; openLogsFolder: () => void; openSupport: () => void; saveSettings: () => void; diff --git a/src/static/css/index.css b/src/static/css/index.css index 898231d..63a82cc 100644 --- a/src/static/css/index.css +++ b/src/static/css/index.css @@ -21850,7 +21850,7 @@ br { flex-wrap: wrap; } -.card-header-title { +.card-header-title.with-padding { padding-right: 0.4rem; } diff --git a/src/static/css/index.scss b/src/static/css/index.scss index 63935d5..a3bae40 100644 --- a/src/static/css/index.scss +++ b/src/static/css/index.scss @@ -313,7 +313,7 @@ br { flex-wrap: wrap; } -.card-header-title { +.card-header-title.with-padding { padding-right: 0.4rem; } diff --git a/src/static/html/index.html b/src/static/html/index.html index bfaca15..d4a10c3 100644 --- a/src/static/html/index.html +++ b/src/static/html/index.html @@ -305,6 +305,17 @@

Other s style="max-width: 30%" /> +
+
+ +
@@ -510,6 +521,64 @@

+
+

Virtual feet trackers

+
+
+ Help icon +
+ This enables the use of the trackers' ankle motion detection (ToF/distance sensors) + to create virtual feet trackers for use in SlimeVR. +
+
+
+
+
+ + +
+ +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+

diff --git a/src/static/html/pairing.html b/src/static/html/pairing.html new file mode 100644 index 0000000..787f0a8 --- /dev/null +++ b/src/static/html/pairing.html @@ -0,0 +1,134 @@ + + + + + + + + + + + +
+
+

+ GX(6/2) Tracker Pairing +

+

+ Manage the pairing of your trackers connected with the GX(6/2) communication dongles. +

+
+ +
+
+
+
+

+ COM port: +

+ COM3 +
+
+
+
+
+ +
+
+
+

+ Port ID 0 +

+
+
+
+

+ Status: +

+ Unpaired +
+
+

+ Tracker: +

+ None +
+
+ +
+
+ +
+
+
+

+ Port ID 1 +

+
+
+
+

+ Status: +

+ Unpaired +
+
+

+ Tracker: +

+ None +
+
+ +
+
+
+
+
+ +
+
+
+
+
+ + diff --git a/src/static/js/index.ts b/src/static/js/index.ts index 4196ac2..5961472 100644 --- a/src/static/js/index.ts +++ b/src/static/js/index.ts @@ -749,7 +749,7 @@ async function addDeviceToList(deviceID: string) { : `
-

+

Device:

${deviceName}
@@ -1067,7 +1067,7 @@ function addEventListeners() { devices.forEach((device) => { const deviceNameElement = device.querySelector("#device-name"); const deviceIDElement = device.querySelector("#device-id"); - + if (deviceNameElement) { const deviceName = deviceNameElement.textContent; if (deviceName.includes("HaritoraX") && deviceName === device.id) { @@ -1076,7 +1076,7 @@ function addEventListeners() { deviceNameElement.textContent = "HaritoraXW-XXXXXX"; } } - + if (deviceIDElement) { const deviceID = deviceIDElement.textContent; if (deviceID.includes("HaritoraX")) { @@ -1093,11 +1093,11 @@ function addEventListeners() { const deviceNameElement = device.querySelector("#device-name"); const deviceIDElement = device.querySelector("#device-id"); const originalDeviceName = settings.trackers?.[device.id]?.name ?? device.id; - + if (deviceNameElement) { deviceNameElement.textContent = originalDeviceName; } - + if (deviceIDElement) { deviceIDElement.textContent = device.id; } @@ -1248,7 +1248,7 @@ function addEventListeners() { trackerVisualizationFPS: trackerVisualizationFPS, }, }); - + refreshDeviceList(); }); @@ -1460,11 +1460,16 @@ function selectLanguage(language: string) { } function showOnboarding() { - window.log("Reopening onboarding screen..."); + window.log("Opening onboarding screen..."); const language: string = (document.getElementById("language-select") as HTMLSelectElement).value; window.ipc.send("show-onboarding", language); } +function showPairing() { + window.log("Opening pairing screen..."); + window.ipc.send("show-pairing", null); +} + function saveSettings() { window.log("Saving settings..."); unsavedSettings(false); @@ -1547,6 +1552,7 @@ function simulateChangeEvent(element: HTMLInputElement, value: boolean) { window.startConnection = startConnection; window.stopConnection = stopConnection; window.showOnboarding = showOnboarding; +window.showPairing = showPairing; window.saveSettings = saveSettings; window.openTrackerSettings = async (deviceID: string) => { diff --git a/src/static/js/pairing.ts b/src/static/js/pairing.ts new file mode 100644 index 0000000..8d4a27f --- /dev/null +++ b/src/static/js/pairing.ts @@ -0,0 +1,26 @@ +document.addEventListener("DOMContentLoaded", async () => { + const i18nElements = document.querySelectorAll("[data-i18n]"); + const translationPromises: Promise[] = []; + + i18nElements.forEach((element) => { + const key = element.getAttribute("data-i18n"); + const translationPromise = window.translate(key).then((translation) => { + if (translation && translation !== key) { + // could be a slight security risk, but makes it so much easier to format text + element.innerHTML = translation; + } + }); + translationPromises.push(translationPromise); + }); + + await Promise.all(translationPromises); +}); + +async function getSetting(key: string, defaultValue: any) { + const exists = await window.ipc.invoke("has-setting", key); + window.log(`Setting "${key}" exists with value: ${exists}`); + return exists ? await window.ipc.invoke("get-setting", key) : defaultValue; +} + +// Required to prevent variable conflicts from other files +export {};