diff --git a/frogpilot/common/frogpilot_variables.py b/frogpilot/common/frogpilot_variables.py
index 2355b651f..4f1fef9d5 100644
--- a/frogpilot/common/frogpilot_variables.py
+++ b/frogpilot/common/frogpilot_variables.py
@@ -456,7 +456,7 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("WarningSoftVolume", "101", 2, "101"),
("WheelIcon", "frog", 0, "stock"),
("WheelSpeed", "0", 2, "0"),
- ("StopDistance", "6", 3, "6"),
+ ("StopDistance", "6.0", 3, "6.0"),
("RecoveryPower", "1.0", 2, "1.0")
]
diff --git a/frogpilot/system/galaxy/galaxy.py b/frogpilot/system/galaxy/galaxy.py
new file mode 100644
index 000000000..13cedb81e
--- /dev/null
+++ b/frogpilot/system/galaxy/galaxy.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python3
+import platform
+import shutil
+import signal
+import subprocess
+import tarfile
+import time
+import threading
+import urllib.request
+from http.server import HTTPServer, BaseHTTPRequestHandler
+from pathlib import Path
+
+from openpilot.common.params import Params
+
+GALAXY_DIR = Path("/data/galaxy")
+FRPC_VERSION = "0.67.0"
+FRPC_LOG = GALAXY_DIR / "frpc.log"
+AUTH_PORT = 8083
+
+process = None
+auth_server = None
+
+
+class AuthHandler(BaseHTTPRequestHandler):
+ """Serves only GET /glxyauth — returns the PIN hash file contents."""
+ def do_GET(self):
+ if self.path == "/glxyauth":
+ auth_file = GALAXY_DIR / "glxyauth"
+ if auth_file.exists():
+ data = auth_file.read_bytes()
+ self.send_response(200)
+ self.send_header("Content-Type", "text/plain")
+ self.send_header("Content-Length", str(len(data)))
+ self.end_headers()
+ self.wfile.write(data)
+ return
+ self.send_response(404)
+ self.end_headers()
+
+ def log_message(self, format, *args):
+ pass # suppress request logs
+
+
+def start_auth_server():
+ global auth_server
+ if auth_server is not None:
+ return
+ auth_server = HTTPServer(("127.0.0.1", AUTH_PORT), AuthHandler)
+ thread = threading.Thread(target=auth_server.serve_forever, daemon=True)
+ thread.start()
+ print(f"Galaxy: Auth server listening on 127.0.0.1:{AUTH_PORT}")
+
+
+def cleanup_frpc(*_):
+ global process
+ if process is not None and process.poll() is None:
+ process.terminate()
+ try:
+ process.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ process.kill()
+ process = None
+
+
+def get_arch_url():
+ arch = platform.machine()
+ if arch in ("aarch64", "arm64"):
+ return f"https://github.com/fatedier/frp/releases/download/v{FRPC_VERSION}/frp_{FRPC_VERSION}_linux_arm64.tar.gz", f"frp_{FRPC_VERSION}_linux_arm64"
+ elif arch in ("x86_64", "amd64"):
+ return f"https://github.com/fatedier/frp/releases/download/v{FRPC_VERSION}/frp_{FRPC_VERSION}_linux_amd64.tar.gz", f"frp_{FRPC_VERSION}_linux_amd64"
+ return None, None
+
+
+def setup_frpc():
+ GALAXY_DIR.mkdir(parents=True, exist_ok=True)
+ frpc_bin = GALAXY_DIR / "frpc"
+
+ if not frpc_bin.exists():
+ print("Galaxy: Downloading frpc...")
+ url, folder_name = get_arch_url()
+ if not url:
+ print("Galaxy: Unsupported architecture")
+ return False
+
+ tar_path = GALAXY_DIR / "frp.tar.gz"
+ try:
+ urllib.request.urlretrieve(url, tar_path)
+ with tarfile.open(tar_path, "r:gz") as tar:
+ tar.extractall(path=GALAXY_DIR, filter='data')
+
+ # Move binary
+ extracted_bin = GALAXY_DIR / folder_name / "frpc"
+ extracted_bin.rename(frpc_bin)
+ frpc_bin.chmod(0o755)
+
+ # Cleanup
+ tar_path.unlink()
+ shutil.rmtree(GALAXY_DIR / folder_name)
+ print("Galaxy: frpc downloaded and installed.")
+ except Exception as e:
+ print(f"Galaxy: Failed to install frpc: {e}")
+ return False
+
+ return True
+
+
+def main():
+ global process
+ params = Params()
+
+ signal.signal(signal.SIGTERM, cleanup_frpc)
+ signal.signal(signal.SIGINT, cleanup_frpc)
+
+ # Wait for DongleId to be set (usually set on boot/pairing)
+ dongle_id = params.get("DongleId", encoding='utf8')
+ while not dongle_id:
+ print("Galaxy: Waiting for DongleId...")
+ time.sleep(5)
+ dongle_id = params.get("DongleId", encoding='utf8')
+
+ print(f"Galaxy: DongleId: {dongle_id}")
+ print("Galaxy: Starting manager loop...")
+
+ while True:
+ glxyauth_file = GALAXY_DIR / "glxyauth"
+ galaxy_pin = glxyauth_file.read_text().strip() if glxyauth_file.exists() else None
+ is_paired = galaxy_pin and len(galaxy_pin) == 64
+
+ if is_paired:
+ if process is None or process.poll() is not None:
+ if process is not None:
+ print(f"Galaxy: frpc exited with code {process.returncode}. Restarting...")
+
+ print("Galaxy: PIN set. Preparing frpc tunnel...")
+ if not setup_frpc():
+ print("Galaxy: FRPC setup failed. Retrying later...")
+ time.sleep(10)
+ continue
+
+ # Start the tiny auth HTTP server (serves /glxyauth on localhost)
+ start_auth_server()
+
+ frpc_toml = GALAXY_DIR / "frpc.toml"
+ config = f"""\
+serverAddr = "galaxy.firestar.link"
+serverPort = 7000
+
+[transport]
+tls.enable = true
+poolCount = 2
+
+[[proxies]]
+name = "{dongle_id}_pond"
+type = "http"
+localIP = "127.0.0.1"
+localPort = 8082
+customDomains = ["{dongle_id}.devices.local"]
+transport.useCompression = true
+
+[[proxies]]
+name = "{dongle_id}_auth"
+type = "http"
+localIP = "127.0.0.1"
+localPort = {AUTH_PORT}
+customDomains = ["auth-{dongle_id}.devices.local"]
+"""
+ frpc_toml.write_text(config)
+
+ print("Galaxy: Starting frpc tunnel...")
+ log_file = open(FRPC_LOG, 'a')
+ process = subprocess.Popen(
+ [str(GALAXY_DIR / "frpc"), "-c", str(frpc_toml)],
+ stdout=log_file,
+ stderr=log_file
+ )
+ else:
+ if process is not None and process.poll() is None:
+ print("Galaxy: PIN cleared. Stopping frpc tunnel...")
+ cleanup_frpc()
+
+ time.sleep(3)
+
+
+if __name__ == "__main__":
+ main()
+
diff --git a/frogpilot/system/the_pond/assets/components/home/home.css b/frogpilot/system/the_pond/assets/components/home/home.css
index 08e8e8a9f..cc80e874c 100644
--- a/frogpilot/system/the_pond/assets/components/home/home.css
+++ b/frogpilot/system/the_pond/assets/components/home/home.css
@@ -6,7 +6,7 @@
}
.disk .progress {
- background: linear-gradient(to right, green 0%, yellow 80%, orange 90%, red 100%);
+ background: linear-gradient(to right, #5ec8c8 0%, #8b6cc5 60%, #e05577 85%, #c04466 100%);
border-radius: var(--border-radius-md);
height: var(--padding-base);
overflow: hidden;
@@ -78,4 +78,4 @@
gap: 0.5em 1em;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/assets/components/home/home.js b/frogpilot/system/the_pond/assets/components/home/home.js
index 55312b4e2..7a700cb81 100644
--- a/frogpilot/system/the_pond/assets/components/home/home.js
+++ b/frogpilot/system/the_pond/assets/components/home/home.js
@@ -102,18 +102,18 @@ export function Home() {
return html`
${() => {
- if (state.isLoading) {
- return html`
Loading...
`;
- }
+ if (state.isLoading) {
+ return html`
Loading...
`;
+ }
- if (state.error) {
- return html`
Failed to load data: ${state.error}
`;
- }
+ if (state.error) {
+ return html`
Failed to load data: ${state.error}
`;
+ }
- if (state.data) {
- const { driveStats, firehoseStats, softwareInfo } = state.data;
- return html`
-
The Pond
+ if (state.data) {
+ const { driveStats, firehoseStats, softwareInfo } = state.data;
+ return html`
+
Galaxy
${DriveStat("All Time", driveStats?.all, state.unit)}
@@ -139,10 +139,10 @@ export function Home() {
${renderSoftwareInfo(softwareInfo)}
`;
- }
+ }
- return html`
No data available.
`;
- }}
+ return html`
No data available.
`;
+ }}
`;
}
diff --git a/frogpilot/system/the_pond/assets/components/main.css b/frogpilot/system/the_pond/assets/components/main.css
index 89bdfe320..7a360fa49 100644
--- a/frogpilot/system/the_pond/assets/components/main.css
+++ b/frogpilot/system/the_pond/assets/components/main.css
@@ -20,47 +20,49 @@
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
- /* Colors */
- --accent-bg: #673ab7;
- --accent-hover-bg: #512da8;
- --card-bg: #234423;
+ /* Colors — Galaxy palette (cosmic purple · teal · rose · amber) */
+ --accent-bg: #8b6cc5;
+ --accent-hover-bg: #7558b0;
+ --card-bg: #121224;
--color-black: #000000;
- --color-confirm: #1a73e8;
- --color-confirm-hover: #0758ad;
- --color-gray-100: #f5f5f5;
- --color-gray-200: #e0e0e0;
- --color-gray-300: #c2c2c2;
- --color-gray-400: #a4a4a4;
- --color-gray-500: #8f8f8f;
- --color-gray-600: #737373;
- --color-gray-700: #595959;
- --color-gray-800: #333333;
- --color-gray-900: #1a1a1a;
+ --color-confirm: #8b6cc5;
+ --color-confirm-hover: #7558b0;
+ --color-gray-100: #f0f0f8;
+ --color-gray-200: #d8d8e4;
+ --color-gray-300: #b8b8cc;
+ --color-gray-400: #9898b0;
+ --color-gray-500: #7e7e98;
+ --color-gray-600: #636380;
+ --color-gray-700: #4a4a64;
+ --color-gray-800: #2e2e44;
+ --color-gray-900: #1a1a30;
--color-white: #ffffff;
- --danger-bg: #b71c1c;
- --danger-fg: #ef1313;
- --danger-hover-bg: #d32f2f;
- --input-bg: #2f5432;
- --main-bg: #0b1b0b;
- --main-fg: #178643;
- --secondary-bg: #264026;
- --selected-camera-bg: #1f2f1f;
- --sidebar-active-bg: #64c87826;
- --sidebar-bg: #264026;
- --sidebar-border-color: #1e2e1e;
+ --danger-bg: #e05577;
+ --danger-fg: #e05577;
+ --danger-hover-bg: #c04466;
+ --glow-primary: 0 0 0 2px var(--main-fg), 0 0 10px rgba(139, 108, 197, 0.35);
+ --input-bg: #161630;
+ --main-bg: #06060f;
+ --main-fg: #8b6cc5;
+ --secondary-bg: #0e0e1a;
+ --selected-camera-bg: #0b0b18;
+ --sidebar-active-bg: rgba(139, 108, 197, 0.15);
+ --sidebar-bg: #0a0a16;
+ --sidebar-border-color: #1e1e3e;
--sidebar-fg: #ffffff;
- --sidebar-title-fg: #193446;
- --success-bg: #039226;
- --success-fg: #00a100;
- --success-hover-bg: #01be2d;
- --text-color: #ffffff;
- --text-muted: #a0a0a0;
+ --sidebar-title-fg: #8b6cc5;
+ --success-bg: #5ec8c8;
+ --success-fg: #5ec8c8;
+ --success-hover-bg: #4ab3b3;
+ --switch-inactive-bg: #1e1e3e;
+ --text-color: #e8e8f0;
+ --text-muted: #8080a8;
--text-on-primary: var(--sidebar-fg);
--text-on-surface: var(--text-color);
- --thumb-color: #178643;
- --track-color: #1a3a1a;
- --warning-bg: #ff9800;
- --warning-hover-bg: #f57c00;
+ --thumb-color: #8b6cc5;
+ --track-color: #14142e;
+ --warning-bg: #d4a060;
+ --warning-hover-bg: #b8884a;
/* Effects */
--disabled-opacity: 0.5;
@@ -71,7 +73,7 @@
--hover-scale-lg: scale(1.1);
/* Fonts */
- --font-body: "Open Sans", sans-serif;
+ --font-body: "Inter", "Open Sans", sans-serif;
--font-mono: "Courier New", Courier, monospace;
--font-size-xs: 0.75rem;
--font-size-sm: 0.85rem;
@@ -96,9 +98,9 @@
--width-xxxxl: 1200px;
/* Shadows */
- --shadow-xs: 0 1px 2px rgba(0,0,0,0.05);
- --shadow-sm: 0 2px 4px rgba(0,0,0,0.1);
- --shadow-md: 0 4px 12px rgba(0,0,0,0.2);
+ --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
+ --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.2);
/* Spacing */
--gap-xxs: 0.125rem;
@@ -172,12 +174,14 @@ input[type="checkbox"],
input[type="radio"],
.manage-keys-link,
.route_card,
-.sidebar .menu_section > li > ul > li,
+.sidebar .menu_section>li>ul>li,
.clickable {
cursor: pointer;
}
-body { padding-bottom: 0 !important; }
+body {
+ padding-bottom: 0 !important;
+}
/* ――― Layout containers ――― */
.content {
@@ -199,9 +203,17 @@ h3 {
}
/* ――― Helpers & states ――― */
-.hidden { display: none; }
-html { cursor: default; }
-.no_scroll { overflow: hidden; }
+.hidden {
+ display: none;
+}
+
+html {
+ cursor: default;
+}
+
+.no_scroll {
+ overflow: hidden;
+}
.not_implemented {
cursor: not-allowed;
@@ -251,7 +263,9 @@ textarea {
cursor: text;
}
-label[for] { cursor: default; }
+label[for] {
+ cursor: default;
+}
/* ――― Snackbar component ――― */
.snackbar {
@@ -297,6 +311,7 @@ a {
bottom: 0;
opacity: 0;
}
+
to {
bottom: var(--snackbar-offset, 30px);
opacity: 1;
@@ -304,13 +319,20 @@ a {
}
@keyframes fadeout {
- from { opacity: 1; }
- to { opacity: 0; }
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ }
}
/* ――― Breakpoint overrides ――― */
@media only screen and (max-width: var(--breakpoint-md)) {
- .content { margin-left: 0; }
+ .content {
+ margin-left: 0;
+ }
}
@media only screen and (max-width: 768px) and (orientation: portrait) {
@@ -320,6 +342,7 @@ a {
padding-left: 1rem;
padding-right: 1rem;
}
+
#snackbar_wrapper {
left: 50%;
transform: translateX(-50%);
@@ -333,6 +356,7 @@ a {
bottom: 0;
opacity: 0;
}
+
to {
bottom: var(--snackbar-offset, 30px);
opacity: 1;
@@ -340,18 +364,62 @@ a {
}
@-webkit-keyframes fadeout {
- from { opacity: 1; }
- to { opacity: 0; }
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ }
+}
+
+/* ——— Tunnel notice banner ——— */
+.tunnel-notice {
+ align-items: center;
+ background: linear-gradient(135deg, var(--card-bg), var(--secondary-bg));
+ border: var(--border-width-thin) var(--border-style-base) var(--sidebar-border-color);
+ border-left: 3px solid var(--main-fg);
+ border-radius: var(--border-radius-lg);
+ box-shadow: var(--shadow-md);
+ color: var(--text-color);
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-md);
+ margin: var(--margin-xl) auto;
+ max-width: var(--width-xl);
+ padding: var(--padding-xl) var(--padding-xxl);
+ text-align: center;
+}
+
+.tunnel-notice-icon {
+ font-size: 2.5rem;
+ opacity: 0.8;
+}
+
+.tunnel-notice-title {
+ color: var(--text-color);
+ font-size: var(--font-size-lg);
+ font-weight: var(--font-weight-bold);
+ margin: 0;
+}
+
+.tunnel-notice-body {
+ color: var(--text-muted);
+ font-size: var(--font-size-base);
+ line-height: var(--line-height-base);
+ margin: 0;
}
::-webkit-scrollbar {
height: 8px;
width: 8px;
}
+
::-webkit-scrollbar-track {
background: var(--track-color);
}
+
::-webkit-scrollbar-thumb {
background-color: var(--thumb-color);
border-radius: var(--border-radius-sm);
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/assets/components/navigation/navigation_destination.css b/frogpilot/system/the_pond/assets/components/navigation/navigation_destination.css
index b535df486..ff816fc15 100644
--- a/frogpilot/system/the_pond/assets/components/navigation/navigation_destination.css
+++ b/frogpilot/system/the_pond/assets/components/navigation/navigation_destination.css
@@ -109,7 +109,7 @@
.favorites-toggle-button:hover {
background-color: var(--main-fg);
- box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
+ box-shadow: var(--glow-primary);
color: var(--text-color);
font-weight: var(--font-weight-bold);
transform: var(--hover-scale-sm);
@@ -352,7 +352,7 @@
.navigation-summary-widget button.directions:hover {
background-color: var(--success-hover-bg);
- box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
+ box-shadow: var(--glow-primary);
color: var(--text-color);
transform: var(--hover-scale-sm);
}
@@ -466,12 +466,12 @@
}
.search-provider-toggle button.active {
- box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
+ box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
.search-provider-toggle button:hover {
- box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
+ box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
@@ -533,4 +533,4 @@
min-width: 0;
width: calc(100% - var(--padding-xl));
}
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/assets/components/navigation/navigation_keys.css b/frogpilot/system/the_pond/assets/components/navigation/navigation_keys.css
index 91ee91dfa..641400395 100644
--- a/frogpilot/system/the_pond/assets/components/navigation/navigation_keys.css
+++ b/frogpilot/system/the_pond/assets/components/navigation/navigation_keys.css
@@ -47,7 +47,7 @@
transition: opacity 0.5s ease;
}
-.navkeys-group > .navkeys-row:last-of-type {
+.navkeys-group>.navkeys-row:last-of-type {
margin-bottom: 0;
}
@@ -79,7 +79,7 @@
.navkeys-input:hover,
.navkeys-input:focus {
border-color: var(--thumb-color);
- box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
+ box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
@@ -153,4 +153,4 @@
.navkeys-input {
font-size: var(--font-size-sm);
}
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/assets/components/recordings/dashcam_routes.js b/frogpilot/system/the_pond/assets/components/recordings/dashcam_routes.js
index 74e0677a7..84ea0c608 100644
--- a/frogpilot/system/the_pond/assets/components/recordings/dashcam_routes.js
+++ b/frogpilot/system/the_pond/assets/components/recordings/dashcam_routes.js
@@ -1,4 +1,5 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
+import { isGalaxyTunnel } from "/assets/js/utils.js"
import { getOrdinalSuffix } from "/assets/components/navigation/navigation_utilities.js"
import { Modal } from "/assets/components/modal.js";
@@ -124,27 +125,27 @@ async function deleteRoute(route) {
}
async function resetRouteName(route, dlg) {
- const res = await fetch(`/api/routes/reset_name`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ name: route.name })
- });
- if (res.ok) {
- const { timestamp } = await res.json();
- closeDialog(dlg);
- const routeInList = state.routes.find(r => r.name === route.name);
- if (routeInList) {
- routeInList.timestamp = formatRouteDate(timestamp);
- }
- route.timestamp = formatRouteDate(timestamp);
- const overlayTitleSpan = overlay.querySelector(".media-player-title span");
- if (overlayTitleSpan) {
- overlayTitleSpan.textContent = formatRouteDate(timestamp);
- }
- showSnackbar("Route name reset!");
- } else {
- showSnackbar("Resetting name failed...", "error");
+ const res = await fetch(`/api/routes/reset_name`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ name: route.name })
+ });
+ if (res.ok) {
+ const { timestamp } = await res.json();
+ closeDialog(dlg);
+ const routeInList = state.routes.find(r => r.name === route.name);
+ if (routeInList) {
+ routeInList.timestamp = formatRouteDate(timestamp);
}
+ route.timestamp = formatRouteDate(timestamp);
+ const overlayTitleSpan = overlay.querySelector(".media-player-title span");
+ if (overlayTitleSpan) {
+ overlayTitleSpan.textContent = formatRouteDate(timestamp);
+ }
+ showSnackbar("Route name reset!");
+ } else {
+ showSnackbar("Resetting name failed...", "error");
+ }
}
async function renameRoute(route) {
@@ -317,6 +318,16 @@ async function deleteAllRoutes() {
}
export function RouteRecordings() {
+ if (isGalaxyTunnel()) {
+ return html`
+
+
🛰️
+
Dashcam Routes Unavailable via Galaxy
+
Loading dashcam routes requires a direct connection. Connect to your device's local network to use this feature.
+
+ `;
+ }
+
if (state.selectedRoute && !overlay) openOverlay(state.selectedRoute);
return html`
@@ -332,75 +343,75 @@ export function RouteRecordings() {
${() => {
- const routesToShow = state.routes.filter(r => !state.showPreservedOnly || r.is_preserved);
+ const routesToShow = state.routes.filter(r => !state.showPreservedOnly || r.is_preserved);
- if (routesToShow.length === 0) {
- if (state.loading && state.total > 0) {
- return html`Processing Routes: ${state.progress} of ${state.total}
`;
- }
- if (state.loading && !state.isDeletingAll) {
- return html`Loading...
`;
- }
- if (state.isDeletingAll) {
- return html`Deleting routes...
`;
- }
- if (state.showPreservedOnly) {
- return html`No preserved routes...
`;
- }
- if (state.error) {
- return html`${state.error}
`;
- }
- return html`No routes found...
`;
- }
+ if (routesToShow.length === 0) {
+ if (state.loading && state.total > 0) {
+ return html`Processing Routes: ${state.progress} of ${state.total}
`;
+ }
+ if (state.loading && !state.isDeletingAll) {
+ return html`Loading...
`;
+ }
+ if (state.isDeletingAll) {
+ return html`Deleting routes...
`;
+ }
+ if (state.showPreservedOnly) {
+ return html`No preserved routes...
`;
+ }
+ if (state.error) {
+ return html`${state.error}
`;
+ }
+ return html`No routes found...
`;
+ }
- return html`
+ return html`
${routesToShow.map(
- route => html`
+ route => html`
{
- if (state.selectedRoute) return;
+ if (state.selectedRoute) return;
- const card = e.currentTarget;
- const gif = card.querySelector(".recording-preview-gif");
- const png = card.querySelector(".recording-preview-png");
+ const card = e.currentTarget;
+ const gif = card.querySelector(".recording-preview-gif");
+ const png = card.querySelector(".recording-preview-png");
- if (card.dataset.gifLoaded) {
- png.style.display = "none";
- gif.style.display = "block";
- return;
- }
+ if (card.dataset.gifLoaded) {
+ png.style.display = "none";
+ gif.style.display = "block";
+ return;
+ }
- card.dataset.loadingGif = "true";
- const preloader = new Image();
- preloader.onload = () => {
- if (card.dataset.loadingGif === "true") {
- gif.src = preloader.src;
- png.style.display = "none";
- gif.style.display = "block";
- card.dataset.gifLoaded = true;
- }
- delete card.dataset.loadingGif;
- };
- preloader.onerror = () => {
- console.error("Failed to load preview GIF:", preloader.src);
- delete card.dataset.loadingGif;
- };
+ card.dataset.loadingGif = "true";
+ const preloader = new Image();
+ preloader.onload = () => {
+ if (card.dataset.loadingGif === "true") {
+ gif.src = preloader.src;
+ png.style.display = "none";
+ gif.style.display = "block";
+ card.dataset.gifLoaded = true;
+ }
+ delete card.dataset.loadingGif;
+ };
+ preloader.onerror = () => {
+ console.error("Failed to load preview GIF:", preloader.src);
+ delete card.dataset.loadingGif;
+ };
- preloader.src = gif.dataset.src;
- }}"
+ preloader.src = gif.dataset.src;
+ }}"
@mouseleave="${e => {
- const card = e.currentTarget;
- card.querySelector(".recording-preview-png").style.display = "block";
- card.querySelector(".recording-preview-gif").style.display = "none";
- if (card.dataset.loadingGif === "true") {
- delete card.dataset.loadingGif;
- }
- }}"
+ const card = e.currentTarget;
+ card.querySelector(".recording-preview-png").style.display = "block";
+ card.querySelector(".recording-preview-gif").style.display = "none";
+ if (card.dataset.loadingGif === "true") {
+ delete card.dataset.loadingGif;
+ }
+ }}"
@click="${() => {
- state.selectedRoute = route;
- }}"
+ state.selectedRoute = route;
+ }}"
>
togglePreserved(route, e)}">
${() => html`
`}
@@ -410,6 +421,7 @@ export function RouteRecordings() {
src="${route.png}"
class="recording-preview recording-preview-png"
style="display:block;"
+ loading="lazy"
>
${route.timestamp}
`
- )}
+ )}
`;
- }}
+ }}
${() => {
- if (state.routes.length > 0) {
- return html`
+ if (state.routes.length > 0) {
+ return html`
(state.showDeleteAllModal = true)}"
@@ -435,17 +447,17 @@ export function RouteRecordings() {
${() => (state.isDeletingAll ? "Deleting..." : "Delete All Routes")}
`;
- }
- return "";
- }}
+ }
+ return "";
+ }}
${() => state.showDeleteAllModal ? Modal({
- title: "Confirm Delete All",
- message: "Are you sure you want to delete all routes? This action cannot be undone...",
- onConfirm: deleteAllRoutes,
- onCancel: () => { state.showDeleteAllModal = false; },
- confirmText: "Delete All"
- }) : ""}
+ title: "Confirm Delete All",
+ message: "Are you sure you want to delete all routes? This action cannot be undone...",
+ onConfirm: deleteAllRoutes,
+ onCancel: () => { state.showDeleteAllModal = false; },
+ confirmText: "Delete All"
+ }) : ""}
`;
}
diff --git a/frogpilot/system/the_pond/assets/components/recordings/screen_recordings.js b/frogpilot/system/the_pond/assets/components/recordings/screen_recordings.js
index 0a4406e43..a2015a546 100644
--- a/frogpilot/system/the_pond/assets/components/recordings/screen_recordings.js
+++ b/frogpilot/system/the_pond/assets/components/recordings/screen_recordings.js
@@ -1,4 +1,5 @@
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({
@@ -142,8 +143,8 @@ async function renameFile(rec) {
}
function confirmDeleteFile(rec) {
- state.recordingToDelete = rec;
- state.showDeleteModal = true;
+ state.recordingToDelete = rec;
+ state.showDeleteModal = true;
}
async function deleteFile() {
@@ -152,11 +153,11 @@ async function deleteFile() {
const res = await fetch(`/api/screen_recordings/delete/${encodeURIComponent(rec.filename)}`, { method: "DELETE" })
if (res.ok) {
- closeOverlay();
- refresh();
- showSnackbar("Recording deleted!");
+ closeOverlay();
+ refresh();
+ showSnackbar("Recording deleted!");
} else {
- showSnackbar("Delete failed...", "error");
+ showSnackbar("Delete failed...", "error");
}
state.showDeleteModal = false;
@@ -221,6 +222,16 @@ async function deleteAllRecordings() {
}
export function ScreenRecordings() {
+ if (isGalaxyTunnel()) {
+ return html`
+
+
🛰️
+
Screen Recordings Unavailable via Galaxy
+
Loading screen recordings requires a direct connection. Connect to your device's local network to use this feature.
+
+ `;
+ }
+
if (state.selectedRecording && !overlay) openOverlay(state.selectedRecording)
return html`
@@ -229,77 +240,77 @@ export function ScreenRecordings() {
Screen Recordings
${() => {
- if (state.loading && state.recordings.length === 0) return html`Loading...
`
- if (state.error) return html`${state.error}
`
- if (state.progress > 0 && state.progress < state.total) {
- return html`Processing Recordings: ${state.progress} of ${state.total}
`
- }
- if (state.recordings.length === 0 && !state.loading) {
- return html`No screen recordings found...
`
- }
- return ""
- }}
+ if (state.loading && state.recordings.length === 0) return html`Loading...
`
+ if (state.error) return html`${state.error}
`
+ if (state.progress > 0 && state.progress < state.total) {
+ return html`Processing Recordings: ${state.progress} of ${state.total}
`
+ }
+ if (state.recordings.length === 0 && !state.loading) {
+ return html`No screen recordings found...
`
+ }
+ return ""
+ }}
${() => state.recordings.map(rec => {
- const displayName = rec.is_custom_name ? rec.filename.replace(/\.mp4$/i, "").replace(/_/g, " ") : formatScreenRecordingDate(rec.timestamp)
- return html`
+ const displayName = rec.is_custom_name ? rec.filename.replace(/\.mp4$/i, "").replace(/_/g, " ") : formatScreenRecordingDate(rec.timestamp)
+ return html`
{
- if (state.selectedRecording) return;
+ if (state.selectedRecording) return;
- const card = e.currentTarget;
- const gif = card.querySelector(".recording-preview-gif");
- const png = card.querySelector(".recording-preview-png");
+ const card = e.currentTarget;
+ const gif = card.querySelector(".recording-preview-gif");
+ const png = card.querySelector(".recording-preview-png");
- if (card.dataset.gifLoaded) {
- png.style.display = "none";
- gif.style.display = "block";
- return;
- }
+ if (card.dataset.gifLoaded) {
+ png.style.display = "none";
+ gif.style.display = "block";
+ return;
+ }
- card.dataset.loadingGif = "true";
- const preloader = new Image();
- preloader.onload = () => {
- if (card.dataset.loadingGif === "true") {
- gif.src = preloader.src;
- png.style.display = "none";
- gif.style.display = "block";
- card.dataset.gifLoaded = true;
- }
- delete card.dataset.loadingGif;
- };
- preloader.onerror = () => {
- console.error("Failed to load preview GIF:", preloader.src);
- delete card.dataset.loadingGif;
- };
+ card.dataset.loadingGif = "true";
+ const preloader = new Image();
+ preloader.onload = () => {
+ if (card.dataset.loadingGif === "true") {
+ gif.src = preloader.src;
+ png.style.display = "none";
+ gif.style.display = "block";
+ card.dataset.gifLoaded = true;
+ }
+ delete card.dataset.loadingGif;
+ };
+ preloader.onerror = () => {
+ console.error("Failed to load preview GIF:", preloader.src);
+ delete card.dataset.loadingGif;
+ };
- preloader.src = gif.dataset.src;
- }}"
+ preloader.src = gif.dataset.src;
+ }}"
@mouseleave="${e => {
- const card = e.currentTarget;
- card.querySelector(".recording-preview-png").style.display = "block";
- card.querySelector(".recording-preview-gif").style.display = "none";
- if (card.dataset.loadingGif === "true") {
- delete card.dataset.loadingGif;
- }
- }}"
+ const card = e.currentTarget;
+ card.querySelector(".recording-preview-png").style.display = "block";
+ card.querySelector(".recording-preview-gif").style.display = "none";
+ if (card.dataset.loadingGif === "true") {
+ delete card.dataset.loadingGif;
+ }
+ }}"
@click="${() => { state.selectedRecording = rec }}"
>
${displayName}
`
- })}
+ })}
${() => {
- if (state.recordings.length > 0) {
- return html`
+ if (state.recordings.length > 0) {
+ return html`
(state.showDeleteAllModal = true)}"
@@ -307,24 +318,24 @@ export function ScreenRecordings() {
Delete All Recordings
`
- }
- return ""
- }}
+ }
+ return ""
+ }}
${() => state.showDeleteModal ? Modal({
- title: "Confirm Delete",
- message: `Are you sure you want to delete ${state.recordingToDelete.filename} ?`,
- onConfirm: deleteFile,
- onCancel: () => { state.showDeleteModal = false; state.recordingToDelete = null; },
- confirmText: "Delete"
- }) : ""}
+ title: "Confirm Delete",
+ message: `Are you sure you want to delete ${state.recordingToDelete.filename} ?`,
+ onConfirm: deleteFile,
+ onCancel: () => { state.showDeleteModal = false; state.recordingToDelete = null; },
+ confirmText: "Delete"
+ }) : ""}
${() => state.showDeleteAllModal ? Modal({
- title: "Confirm Delete All",
- message: "Are you sure you want to delete all screen recordings? This action cannot be undone...",
- onConfirm: deleteAllRecordings,
- onCancel: () => { state.showDeleteAllModal = false; },
- confirmText: "Delete All"
- }) : ""}
+ title: "Confirm Delete All",
+ message: "Are you sure you want to delete all screen recordings? This action cannot be undone...",
+ onConfirm: deleteAllRecordings,
+ onCancel: () => { state.showDeleteAllModal = false; },
+ confirmText: "Delete All"
+ }) : ""}
`
}
diff --git a/frogpilot/system/the_pond/assets/components/router.js b/frogpilot/system/the_pond/assets/components/router.js
index 361496da4..657c2e5a9 100644
--- a/frogpilot/system/the_pond/assets/components/router.js
+++ b/frogpilot/system/the_pond/assets/components/router.js
@@ -1,6 +1,7 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { createBrowserHistory, createRouter } from "https://esm.sh/@remix-run/router@1.3.1"
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 { Home } from "/assets/components/home/home.js"
@@ -11,7 +12,6 @@ import { SettingsView } from "/assets/components/settings.js"
import { ScreenRecordings } from "/assets/components/recordings/screen_recordings.js"
import { Sidebar } from "/assets/components/sidebar.js"
import { SpeedLimits } from "/assets/components/tools/speed_limits.js"
-import { TailscaleControl } from "/assets/components/tailscale/tailscale.js"
import { ThemeMaker } from "/assets/components/tools/theme_maker.js"
import { TmuxLog } from "/assets/components/tools/tmux.js"
import { ToggleControl } from "/assets/components/tools/toggles.js"
@@ -23,13 +23,14 @@ function createRoute(id, path, component) {
return {
id,
path,
- loader: () => {},
+ loader: () => { },
element: component,
}
}
function Root() {
let routes = [
+ createRoute("device_settings", "/device_settings", DeviceSettings),
createRoute("doors", "/lock_or_unlock_doors", DoorControl),
createRoute("errorLogs", "/manage_error_logs", ErrorLogs),
createRoute("navdestination", "/set_navigation_destination", NavDestination),
@@ -39,7 +40,6 @@ function Root() {
createRoute("screen_recordings", "/screen_recordings", ScreenRecordings),
createRoute("settings", "/settings/:section/:subsection?", SettingsView),
createRoute("speed_limits", "/download_speed_limits", SpeedLimits),
- createRoute("tailscale", "/manage_tailscale", TailscaleControl),
createRoute("thememaker", "/theme_maker", ThemeMaker),
createRoute("tmux", "/manage_tmux", TmuxLog),
createRoute("toggles", "/manage_toggles", ToggleControl),
@@ -76,17 +76,17 @@ function Root() {
${() => Sidebar(routerState.activePathFull)}
${() => {
- if (!routerState.initialized || routerState.navigation.state === "loading") {
- return html`
Loading...
`
- }
+ if (!routerState.initialized || routerState.navigation.state === "loading") {
+ return html`
Loading...
`
+ }
- if (routerState.errors?.root?.status === 404) {
- return html`
Not Found `
- }
+ if (routerState.errors?.root?.status === 404) {
+ return html`
Not Found `
+ }
- const match = routes.find(r => r.path === routerState.activePath)
- return match.element({ params: routerState.params })
- }}
+ const match = routes.find(r => r.path === routerState.activePath)
+ return match.element({ params: routerState.params })
+ }}
`
}
diff --git a/frogpilot/system/the_pond/assets/components/settings.css b/frogpilot/system/the_pond/assets/components/settings.css
index 1bf43a43b..2ccc37f00 100644
--- a/frogpilot/system/the_pond/assets/components/settings.css
+++ b/frogpilot/system/the_pond/assets/components/settings.css
@@ -30,7 +30,7 @@
position: relative;
}
-.dropdown > select {
+.dropdown>select {
appearance: none;
background-color: var(--sidebar-bg);
border: var(--border-width-thin) solid var(--sidebar-border-color);
@@ -74,19 +74,19 @@ input.searchfield:focus {
outline: none;
}
-input:checked + .slider {
+input:checked+.slider {
background-color: var(--success-bg);
}
-input:checked + .slider:before {
+input:checked+.slider:before {
transform: translateX(26px);
}
-input:checked + .slider.loading:before {
+input:checked+.slider.loading:before {
animation: rotationChecked 1s linear infinite;
}
-input:focus + .slider {
+input:focus+.slider {
box-shadow: 0 0 var(--border-width-thin) var(--success-bg);
}
@@ -133,11 +133,11 @@ input:focus + .slider {
z-index: 1;
}
-.options > input {
+.options>input {
display: none;
}
-.options > input:checked + label {
+.options>input:checked+label {
background-color: var(--success-bg);
color: var(--text-color);
font-weight: bold;
@@ -220,14 +220,13 @@ input:focus + .slider {
grid-column: span 2;
}
-.setting.subsetting_link {
-}
+.setting.subsetting_link {}
.setting.subtoggle {
margin: 0 0 0 var(--padding-xl);
}
-.setting.subtoggle + .setting {
+.setting.subtoggle+.setting {
margin-top: var(--border-radius-xl);
}
@@ -300,7 +299,7 @@ input.searchfield {
animation: rotation 1s linear infinite;
background-color: none !important;
border: var(--border-width-thick) solid var(--sidebar-fg);
- border-bottom-color: #46439b;
+ border-bottom-color: var(--main-fg);
box-sizing: border-box;
}
@@ -337,6 +336,7 @@ i.switch {
0% {
transform: rotate(0deg);
}
+
100% {
transform: rotate(360deg);
}
@@ -346,7 +346,8 @@ i.switch {
0% {
transform: translateX(26px) rotate(0deg);
}
+
100% {
transform: translateX(26px) rotate(360deg);
}
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/assets/components/sidebar.css b/frogpilot/system/the_pond/assets/components/sidebar.css
index c2395e8a8..9f627227d 100644
--- a/frogpilot/system/the_pond/assets/components/sidebar.css
+++ b/frogpilot/system/the_pond/assets/components/sidebar.css
@@ -60,7 +60,10 @@
}
.sidebar {
- background-color: var(--sidebar-bg);
+ background:
+ radial-gradient(ellipse at 30% 80%, rgba(139, 108, 197, 0.06) 0%, transparent 60%),
+ radial-gradient(ellipse at 70% 20%, rgba(94, 200, 200, 0.04) 0%, transparent 50%),
+ var(--sidebar-bg);
border-right: var(--border-width-thin) solid var(--sidebar-border-color);
display: flex;
flex-direction: column;
@@ -102,7 +105,7 @@
padding: 0;
}
-.sidebar .menu_section > li > a span {
+.sidebar .menu_section>li>a span {
color: var(--text-color);
display: inline-block;
font-size: var(--font-size-lg);
@@ -111,13 +114,13 @@
padding-left: var(--padding-sm);
}
-.sidebar .menu_section > li > ul {
+.sidebar .menu_section>li>ul {
list-style: none;
margin: 0;
padding: 0;
}
-.sidebar .menu_section > li > ul > li {
+.sidebar .menu_section>li>ul>li {
border-radius: var(--border-radius-sm);
margin-bottom: var(--padding-sm);
overflow: hidden;
@@ -125,23 +128,23 @@
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
}
-.sidebar .menu_section > li > ul > li > a.menu-item-link {
+.sidebar .menu_section>li>ul>li>a.menu-item-link {
border-radius: inherit;
}
-.sidebar .menu_section > li > ul > li.active,
-.sidebar .menu_section > li > ul > li:hover {
+.sidebar .menu_section>li>ul>li.active,
+.sidebar .menu_section>li>ul>li:hover {
background-color: var(--sidebar-active-bg);
- box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
+ box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
-.sidebar .menu_section > li > ul > li.active > a {
+.sidebar .menu_section>li>ul>li.active>a {
color: var(--color-white);
font-weight: var(--font-weight-bold);
}
-.sidebar .menu_section > li > ul > li:hover > a {
+.sidebar .menu_section>li>ul>li:hover>a {
color: var(--color-white);
font-weight: var(--font-weight-demi-bold);
}
@@ -203,9 +206,13 @@
}
.sidebar_header p {
- color: var(--success-hover-bg);
+ background: linear-gradient(135deg, #8b6cc5 0%, #5ec8c8 55%, #d4789c 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
+ letter-spacing: 0.05em;
}
.sidebar_widget {
@@ -234,7 +241,7 @@
transition: var(--transition-fast);
}
- .sidebar .menu_section > li > a {
+ .sidebar .menu_section>li>a {
font-size: var(--padding-sm);
}
@@ -262,7 +269,7 @@
transition: var(--transition-fast);
}
- .sidebar .menu_section > li > a {
+ .sidebar .menu_section>li>a {
font-size: var(--padding-sm);
}
@@ -277,4 +284,4 @@
#menu_button {
display: none;
}
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/assets/components/sidebar.js b/frogpilot/system/the_pond/assets/components/sidebar.js
index bd23d0519..5fbcca1e0 100644
--- a/frogpilot/system/the_pond/assets/components/sidebar.js
+++ b/frogpilot/system/the_pond/assets/components/sidebar.js
@@ -14,10 +14,8 @@ const MenuItems = {
{ name: "Dashcam Routes", link: "/dashcam_routes", icon: "bi-camera-reels" },
{ name: "Screen Recordings", link: "/screen_recordings", icon: "bi-record-circle" },
],
- tailscale: [
- { name: "Tailscale", link: "/manage_tailscale", icon: "bi-wifi" },
- ],
tools: [
+ { 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: "Lock/Unlock Doors", link: "/lock_or_unlock_doors", icon: "bi-door-closed" },
@@ -88,10 +86,9 @@ export function Sidebar() {
${() => state.selectorAction
- ? TmuxLogSelector({
- action: state.selectorAction,
- closeFn: () => (state.selectorAction = null)
- })
- : ""
- }
+ ? TmuxLogSelector({
+ action: state.selectorAction,
+ closeFn: () => (state.selectorAction = null)
+ })
+ : ""
+ }
${() => logSelectorState.showDeleteAllModal ? Modal({
- title: "Delete All Logs",
- message: "Are you sure you want to delete all of your session logs?",
- onConfirm: deleteAllSessions,
- onCancel: () => { logSelectorState.showDeleteAllModal = false },
- confirmText: "Delete All"
- }) : ""}
+ title: "Delete All Logs",
+ message: "Are you sure you want to delete all of your session logs?",
+ onConfirm: deleteAllSessions,
+ onCancel: () => { logSelectorState.showDeleteAllModal = false },
+ confirmText: "Delete All"
+ }) : ""}
`;
}
diff --git a/frogpilot/system/the_pond/assets/components/tools/toggles.js b/frogpilot/system/the_pond/assets/components/tools/toggles.js
index e3f8c8dfb..db685061a 100644
--- a/frogpilot/system/the_pond/assets/components/tools/toggles.js
+++ b/frogpilot/system/the_pond/assets/components/tools/toggles.js
@@ -1,7 +1,8 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { Modal } from "/assets/components/modal.js"
+import { TailscaleControl } from "/assets/components/tailscale/tailscale.js"
-export function ToggleControl () {
+export function ToggleControl() {
const state = reactive({
showResetDefaultModal: false,
showResetStockModal: false,
@@ -14,7 +15,7 @@ export function ToggleControl () {
fileInput.addEventListener("change", restoreToggles)
document.body.appendChild(fileInput)
- async function backupToggles () {
+ async function backupToggles() {
const response = await fetch("/api/toggles/backup", { method: "POST" })
const blob = await response.blob()
@@ -26,7 +27,7 @@ export function ToggleControl () {
URL.revokeObjectURL(downloadUrl)
}
- async function restoreToggles (event) {
+ async function restoreToggles(event) {
const uploadedFile = event.target.files[0]
if (uploadedFile) {
const fileContents = await uploadedFile.text()
@@ -45,11 +46,11 @@ export function ToggleControl () {
}
}
- function confirmResetDefault () {
+ function confirmResetDefault() {
state.showResetDefaultModal = true;
}
- async function resetTogglesToDefault () {
+ async function resetTogglesToDefault() {
state.showResetDefaultModal = false;
showSnackbar("Resetting toggles to their default values...");
await new Promise(resolve => setTimeout(resolve, 3000));
@@ -58,11 +59,11 @@ export function ToggleControl () {
await fetch("/api/toggles/reset_default", { method: "POST" });
}
- function confirmResetStock () {
+ function confirmResetStock() {
state.showResetStockModal = true;
}
- async function resetTogglesToStock () {
+ async function resetTogglesToStock() {
state.showResetStockModal = false;
showSnackbar("Resetting toggles to stock openpilot values...");
await new Promise(resolve => setTimeout(resolve, 3000));
@@ -71,7 +72,7 @@ export function ToggleControl () {
await fetch("/api/toggles/reset_stock", { method: "POST" });
}
- function triggerRestorePrompt () {
+ function triggerRestorePrompt() {
fileInput.click()
}
@@ -98,20 +99,22 @@ export function ToggleControl () {
Reset Toggles to Stock
+
+ ${TailscaleControl()}
${() => state.showResetDefaultModal ? Modal({
- title: "Reset Toggles",
- message: "Are you sure you want to reset all toggles to their default FrogPilot values?",
- onConfirm: resetTogglesToDefault,
- onCancel: () => { state.showResetDefaultModal = false; },
- confirmText: "Reset to Default"
- }) : ""}
+ title: "Reset Toggles",
+ message: "Are you sure you want to reset all toggles to their default FrogPilot values?",
+ onConfirm: resetTogglesToDefault,
+ onCancel: () => { state.showResetDefaultModal = false; },
+ confirmText: "Reset to Default"
+ }) : ""}
${() => state.showResetStockModal ? Modal({
- title: "Reset Toggles",
- message: "Are you sure you want to reset all toggles to stock openpilot values?",
- onConfirm: resetTogglesToStock,
- onCancel: () => { state.showResetStockModal = false; },
- confirmText: "Reset to Stock"
- }) : ""}
+ title: "Reset Toggles",
+ message: "Are you sure you want to reset all toggles to stock openpilot values?",
+ onConfirm: resetTogglesToStock,
+ onCancel: () => { state.showResetStockModal = false; },
+ confirmText: "Reset to Stock"
+ }) : ""}
`
}
diff --git a/frogpilot/system/the_pond/assets/components/tools/tsk_manager.css b/frogpilot/system/the_pond/assets/components/tools/tsk_manager.css
index 5a2f51743..da08a009f 100644
--- a/frogpilot/system/the_pond/assets/components/tools/tsk_manager.css
+++ b/frogpilot/system/the_pond/assets/components/tools/tsk_manager.css
@@ -73,7 +73,7 @@
transition: opacity var(--transition-slow);
}
-.tskkeys-group > .tskkeys-row:last-of-type {
+.tskkeys-group>.tskkeys-row:last-of-type {
margin-bottom: 0;
}
@@ -105,7 +105,7 @@
.tskkeys-input:hover,
.tskkeys-input:focus {
border-color: var(--thumb-color);
- box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
+ box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
@@ -211,4 +211,4 @@
.tskkeys-input {
font-size: var(--font-size-sm);
}
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/assets/images/android-chrome-192x192.png b/frogpilot/system/the_pond/assets/images/android-chrome-192x192.png
index 4ad74d653..f0b018fc7 100644
Binary files a/frogpilot/system/the_pond/assets/images/android-chrome-192x192.png and b/frogpilot/system/the_pond/assets/images/android-chrome-192x192.png differ
diff --git a/frogpilot/system/the_pond/assets/images/android-chrome-512x512.png b/frogpilot/system/the_pond/assets/images/android-chrome-512x512.png
index 820616880..09f3d92d9 100644
Binary files a/frogpilot/system/the_pond/assets/images/android-chrome-512x512.png and b/frogpilot/system/the_pond/assets/images/android-chrome-512x512.png differ
diff --git a/frogpilot/system/the_pond/assets/images/favicon-16x16.png b/frogpilot/system/the_pond/assets/images/favicon-16x16.png
index a1d047862..7c4dc7ce1 100644
Binary files a/frogpilot/system/the_pond/assets/images/favicon-16x16.png and b/frogpilot/system/the_pond/assets/images/favicon-16x16.png differ
diff --git a/frogpilot/system/the_pond/assets/images/favicon-32x32.png b/frogpilot/system/the_pond/assets/images/favicon-32x32.png
index 130785c99..07d0e0f98 100644
Binary files a/frogpilot/system/the_pond/assets/images/favicon-32x32.png and b/frogpilot/system/the_pond/assets/images/favicon-32x32.png differ
diff --git a/frogpilot/system/the_pond/assets/images/favicon.ico b/frogpilot/system/the_pond/assets/images/favicon.ico
index 03380a673..5dda417f8 100644
Binary files a/frogpilot/system/the_pond/assets/images/favicon.ico and b/frogpilot/system/the_pond/assets/images/favicon.ico differ
diff --git a/frogpilot/system/the_pond/assets/images/main_logo.png b/frogpilot/system/the_pond/assets/images/main_logo.png
index f450d8c56..1cc55ea81 100644
Binary files a/frogpilot/system/the_pond/assets/images/main_logo.png and b/frogpilot/system/the_pond/assets/images/main_logo.png differ
diff --git a/frogpilot/system/the_pond/assets/js/utils.js b/frogpilot/system/the_pond/assets/js/utils.js
index 43d089153..4584f57f3 100644
--- a/frogpilot/system/the_pond/assets/js/utils.js
+++ b/frogpilot/system/the_pond/assets/js/utils.js
@@ -65,3 +65,11 @@ export function hideSidebar() {
document.getElementById("sidebarUnderlay")?.classList.add("hidden")
html.classList.remove("no_scroll")
}
+
+/**
+ * Returns true when the page is being accessed through the Galaxy tunnel
+ * (the public-facing domain is galaxy.firestar.link)
+ */
+export function isGalaxyTunnel() {
+ return window.location.hostname === 'galaxy.firestar.link';
+}
diff --git a/frogpilot/system/the_pond/assets/manifest.json b/frogpilot/system/the_pond/assets/manifest.json
index d9e4031da..7c146c636 100644
--- a/frogpilot/system/the_pond/assets/manifest.json
+++ b/frogpilot/system/the_pond/assets/manifest.json
@@ -1,5 +1,5 @@
{
- "name": "The Pond",
+ "name": "Galaxy",
"short_name": "",
"icons": [
{
@@ -17,4 +17,4 @@
"background_color": "#151414",
"theme_color": "#151414",
"display": "standalone"
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/templates/index.html b/frogpilot/system/the_pond/templates/index.html
index 6321bd41d..d4c5f9035 100644
--- a/frogpilot/system/the_pond/templates/index.html
+++ b/frogpilot/system/the_pond/templates/index.html
@@ -1,59 +1,62 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
-
-
+
- The Pond
-
+
+
+
-
-
-
-
-
-
-
+ Galaxy
+
-
+
+
+
+
+
+
+
+
+