From c84acea993fc6aba832ae9a0d4fab1c712a3530e Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:28:27 -0500 Subject: [PATCH] Galaxy Pair Locally --- .../the_pond/assets/components/router.js | 2 + .../the_pond/assets/components/sidebar.js | 1 + .../assets/components/tools/galaxy.css | 137 +++++++++++++++ .../assets/components/tools/galaxy.js | 166 ++++++++++++++++++ .../system/the_pond/templates/index.html | 1 + frogpilot/system/the_pond/the_pond.py | 39 ++++ 6 files changed, 346 insertions(+) create mode 100644 frogpilot/system/the_pond/assets/components/tools/galaxy.css create mode 100644 frogpilot/system/the_pond/assets/components/tools/galaxy.js diff --git a/frogpilot/system/the_pond/assets/components/router.js b/frogpilot/system/the_pond/assets/components/router.js index c17e80575..2cec5de07 100644 --- a/frogpilot/system/the_pond/assets/components/router.js +++ b/frogpilot/system/the_pond/assets/components/router.js @@ -4,6 +4,7 @@ import { hideSidebar } from "/assets/js/utils.js" import { DeviceSettings } from "/assets/components/tools/device_settings.js" import { DoorControl } from "/assets/components/tools/doors.js" import { ErrorLogs } from "/assets/components/tools/error_logs.js" +import { GalaxyPairing } from "/assets/components/tools/galaxy.js" import { Home } from "/assets/components/home/home.js" import { NavDestination } from "/assets/components/navigation/navigation_destination.js" import { NavKeys } from "/assets/components/navigation/navigation_keys.js" @@ -34,6 +35,7 @@ function Root() { createRoute("device_settings", "/device_settings", DeviceSettings), createRoute("doors", "/lock_or_unlock_doors", DoorControl), createRoute("errorLogs", "/manage_error_logs", ErrorLogs), + createRoute("galaxy", "/galaxy", GalaxyPairing), createRoute("navdestination", "/set_navigation_destination", NavDestination), createRoute("navkeys", "/manage_navigation_keys", NavKeys), createRoute("root", "/", Home), diff --git a/frogpilot/system/the_pond/assets/components/sidebar.js b/frogpilot/system/the_pond/assets/components/sidebar.js index 7dc930fb1..008a83dd6 100644 --- a/frogpilot/system/the_pond/assets/components/sidebar.js +++ b/frogpilot/system/the_pond/assets/components/sidebar.js @@ -18,6 +18,7 @@ const MenuItems = { { name: "Device Settings", link: "/device_settings", icon: "bi-sliders" }, { name: "Download Speed Limits", link: "/download_speed_limits", icon: "bi-download" }, { name: "Error Logs", link: "/manage_error_logs", icon: "bi-exclamation-triangle" }, + { name: "Galaxy", link: "/galaxy", icon: "bi-globe2" }, { name: "Lock/Unlock Doors", link: "/lock_or_unlock_doors", icon: "bi-door-closed" }, { name: "Model Manager", link: "/manage_models", icon: "bi-cpu" }, { name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill" }, diff --git a/frogpilot/system/the_pond/assets/components/tools/galaxy.css b/frogpilot/system/the_pond/assets/components/tools/galaxy.css new file mode 100644 index 000000000..2363d7d2c --- /dev/null +++ b/frogpilot/system/the_pond/assets/components/tools/galaxy.css @@ -0,0 +1,137 @@ +/* ――― Galaxy Pairing Widget ――― */ +.galaxy-wrapper { + max-width: var(--width-xxxl); + padding: var(--padding-base) var(--padding-lg) var(--padding-xxl); +} + +.galaxy-loading { + color: var(--text-muted); + padding: var(--padding-xl); + text-align: center; +} + +.galaxy-widget { + align-items: center; + background-color: var(--secondary-bg); + border-radius: var(--border-radius-lg); + box-shadow: var(--shadow-sm); + display: flex; + flex-direction: column; + gap: var(--gap-lg); + max-width: var(--width-lg); + padding: var(--padding-xl); + transition: box-shadow var(--transition-fast), transform var(--transition-fast); +} + +.galaxy-widget:hover { + box-shadow: var(--shadow-md); + transform: var(--hover-scale-sm); +} + +/* ――― Status Badge ――― */ +.galaxy-status-badge { + align-items: center; + border-radius: 2rem; + display: inline-flex; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-bold); + gap: var(--gap-xs); + padding: 0.3rem 1rem; +} + +.galaxy-paired { + background-color: rgba(94, 200, 200, 0.15); + color: var(--success-fg); +} + +.galaxy-unpaired { + background-color: rgba(224, 85, 119, 0.15); + color: var(--danger-fg); +} + +/* ――― Text ――― */ +.galaxy-text { + color: var(--text-color); + line-height: var(--line-height-base); + text-align: center; +} + +/* ――― URL Link ――― */ +.galaxy-url { + color: var(--main-fg); + font-size: var(--font-size-base); + font-weight: var(--font-weight-demi-bold); + text-decoration: underline; + text-underline-offset: 3px; + transition: color var(--transition-fast); + word-break: break-all; +} + +.galaxy-url:hover { + color: var(--accent-hover-bg); +} + +/* ――― Input Group ――― */ +.galaxy-input-group { + display: flex; + flex-direction: column; + gap: var(--gap-sm); + width: 100%; +} + +.galaxy-input { + background-color: var(--input-bg); + border: var(--border-style-input); + border-radius: var(--border-radius-lg); + box-sizing: border-box; + color: var(--text-color); + font-family: var(--font-body); + font-size: var(--font-size-base); + outline: none; + padding: var(--padding-sm) var(--padding-base); + transition: box-shadow var(--transition-fast); + width: 100%; +} + +.galaxy-input:focus { + box-shadow: 0 0 0 2px var(--main-fg); +} + +/* ――― Buttons ――― */ +.galaxy-button { + background-color: var(--input-bg); + border: none; + border-radius: var(--border-radius-lg); + color: var(--text-color); + font-size: var(--font-size-base); + font-weight: var(--font-weight-demi-bold); + padding: var(--padding-sm) var(--padding-base); + text-align: center; + transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast); + width: 100%; +} + +.galaxy-button:hover:not(:disabled) { + box-shadow: var(--shadow-md); + transform: var(--hover-scale-sm); +} + +.galaxy-button:disabled { + opacity: var(--disabled-opacity); +} + +.galaxy-button-danger { + background-color: rgba(224, 85, 119, 0.2); + color: var(--danger-fg); +} + +.galaxy-button-danger:hover:not(:disabled) { + background-color: rgba(224, 85, 119, 0.35); +} + +/* ――― Mobile ――― */ +@media only screen and (max-width: 768px) and (orientation: portrait) { + .galaxy-wrapper { + padding: var(--padding-sm) var(--padding-base) var(--padding-xl); + } +} diff --git a/frogpilot/system/the_pond/assets/components/tools/galaxy.js b/frogpilot/system/the_pond/assets/components/tools/galaxy.js new file mode 100644 index 000000000..0c37c5c94 --- /dev/null +++ b/frogpilot/system/the_pond/assets/components/tools/galaxy.js @@ -0,0 +1,166 @@ +import { html, reactive } from "https://esm.sh/@arrow-js/core" +import { isGalaxyTunnel } from "/assets/js/utils.js" +import { Modal } from "/assets/components/modal.js" + +const state = reactive({ + paired: false, + url: "", + password: "", + loading: true, + submitting: false, + showUnpairModal: false, + fetched: false, +}) + +async function fetchStatus() { + state.loading = true + try { + const res = await fetch("/api/galaxy/status") + const data = await res.json() + state.paired = data.paired + state.url = data.url + } catch (e) { + console.error("Failed to fetch Galaxy status:", e) + } + state.loading = false +} + +async function pair() { + if (state.submitting) return + const pw = state.password.trim() + if (pw.length < 6) { + showSnackbar("Password must be at least 6 characters.") + return + } + + state.submitting = true + try { + const res = await fetch("/api/galaxy/pair", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password: pw }), + }) + const data = await res.json() + if (res.ok) { + state.paired = true + state.url = data.url + state.password = "" + // Clear the DOM input value directly (Arrow.js doesn't two-way bind) + const input = document.querySelector(".galaxy-input") + if (input) input.value = "" + showSnackbar(data.message || "Paired!") + } else { + showSnackbar(data.error || "Pairing failed.") + } + } catch (e) { + showSnackbar("Network error — is the device reachable?") + } + state.submitting = false +} + +async function unpair() { + state.showUnpairModal = false + state.submitting = true + try { + const res = await fetch("/api/galaxy/unpair", { method: "POST" }) + const data = await res.json() + if (res.ok) { + state.paired = false + state.url = "" + showSnackbar(data.message || "Unpaired!") + } else { + showSnackbar(data.error || "Unpairing failed.") + } + } catch (e) { + showSnackbar("Network error — is the device reachable?") + } + state.submitting = false +} + +export function GalaxyPairing() { + if (isGalaxyTunnel()) { + return html` +
Galaxy pairing requires a direct connection.
Connect to your device's local network to use this feature.