import { rememberInstallPrompt } from "./install-app.js" import { toggleHaptic } from "./haptics.js" import { GxState } from "./state.js" import { BottomNav, NAV, primaryTab } from "./bottom-nav.js" import { UpdateRestart } from "./update-restart.js" import { DeviceRestartWatch } from "./device-restart.js" import { bootScreenMode, dismissBootScreen, showBootScreen, showRestartScreen } from "./boot-screen.js" import { GxNotice } from "./notice.js" import { createApp, reactive, defineAsyncComponent } from "../vendor/vue/vue.esm-browser.js" import { route, navigate, navigateBack, ensureBackEntry, setRouteLeaveGuard, startRouter } from "./router.js" import { loadCatalog } from "./startup.js" import { Tools } from "./tools.js" window.addEventListener("beforeinstallprompt", rememberInstallPrompt) const Logs = page(() => import("./logs.js").then((module) => module.Logs)) const SoftwarePage = page(() => import("./software-status.js").then((module) => module.SoftwarePage)) const NavigationPage = page(() => import("./navigation.js").then((module) => module.NavigationPage)) const ModelsPage = page(() => import("./models.js").then((module) => module.ModelsPage)) const LaboratoryPage = page(() => import("./model-laboratory.js").then((module) => module.LaboratoryPage)) import { SettingsPage } from "./settings.js" import { SettingsRecoveryPage } from "./settings-recovery.js" import { ToggleSearch } from "./toggle-search.js" import { SaveFeedback } from "./save-feedback.js" const PlotsPage = page(() => import("./plots.js").then((module) => module.PlotsPage)) const FlmPage = page(() => import("./flm.js").then((module) => module.FlmPage)) import { LocalAuth } from "./auth-client.js" import { Home } from "./home.js" import { LocalAccess } from "./local-access.js" import { MenuTile } from "./menu-tile.js" const LocalRecordingsPage = page(() => import("./record-history.js").then((module) => module.LocalRecordingsPage)) const CamerasPage = page(() => import("./cameras.js").then((module) => module.CamerasPage)) const SentryEventsPage = page(() => import("./sentry.js").then((module) => module.SentryEventsPage)) const VasmPage = page(() => import("./vasm.js").then((module) => module.VasmPage)) const PipPage = page(() => import("./pip.js").then((module) => module.PipPage)) import { DrivingPage, drivingPage } from "./driving.js" const LongitudinalCurvesPage = page(() => import("./longitudinal-curves.js").then((module) => module.LongitudinalCurvesPage)) import { DevicePreferencesPage, devicePage } from "./device-preferences.js" const BluetoothPage = page(() => import("./bluetooth.js").then((module) => module.BluetoothPage)) const StarPortPage = page(() => import("./starport.js").then((module) => module.StarPortPage)) const SpotifyPage = page(() => import("./spotify.js").then((module) => module.SpotifyPage)) const VehicleControlsPage = page(() => import("./vehicle-controls.js").then((module) => module.VehicleControlsPage)) const OnroadLayoutPage = page(() => import("./onroad-layout.js").then((module) => module.OnroadLayoutPage)) const GalaxyPage = page(() => import("./galaxy.js").then((module) => module.GalaxyPage)) import { DevicePicker } from "./device-picker.js" import { DeviceState } from "./device-state.js" import { spawnAmbientStars, pauseStarsOnSlowFirefoxScroll } from "./ambient-stars.js" function page(loader) { return defineAsyncComponent({ loader, delay: 150, timeout: 10000, loadingComponent: { components: { GxState }, template: 'Opening page…' }, errorComponent: { components: { GxState }, template: 'This page could not open.', methods: { reload() { location.reload() } } }, onError(_error, retry, fail, attempts) { if (attempts < 2) setTimeout(retry, 500); else fail() }, }) } const THEME_KEY = "galaxy-preview-theme" const PIN_KEY = "galaxy-preview-nav-pinned" const stored = (key) => { try { return localStorage.getItem(key) } catch { return null } } const save = (key, value) => { try { localStorage.setItem(key, value) } catch {} } const state = reactive({ tools: [], loading: true, updateRestarting: false, error: "", pageError: "", monitorMode: "sample", drawerOpen: false, navPinned: stored(PIN_KEY) === "true", theme: stored(THEME_KEY) === "light" ? "light" : "dark", searchPage: "", }) const updateRestart = new UpdateRestart({ publish: waiting => { state.updateRestarting = waiting if (waiting) showRestartScreen() else if (bootScreenMode() === "restart" && !state.loading) dismissBootScreen() } }) const deviceRestart = new DeviceRestartWatch({ paused: () => state.updateRestarting }) const authState = reactive({ status: "checking", error: "", localAccess: false, gatewayAccess: false }) const authForm = reactive({ password: "" }) const auth = new LocalAuth({ publish: (update) => { Object.assign(authState, update) if (update.status === "unavailable") scheduleReconnect() } }) createApp({ components: { BottomNav, Home, LocalAccess, Tools, Logs, SoftwarePage, NavigationPage, ModelsPage, LaboratoryPage, SettingsPage, SettingsRecoveryPage, ToggleSearch, SaveFeedback, PlotsPage, FlmPage, LocalRecordingsPage, CamerasPage, SentryEventsPage, VasmPage, PipPage, DrivingPage, DevicePreferencesPage, BluetoothPage, StarPortPage, SpotifyPage, LongitudinalCurvesPage, VehicleControlsPage, OnroadLayoutPage, GalaxyPage, DevicePicker, DeviceState, MenuTile }, data: () => ({ state, authState, authForm, route, NAV, routeDirection: 1, updateRestart, deviceRestart, nextLayoutLeavePresentation: "inline" }), computed: { visibleTools() { return state.tools.filter(tool => tool.visibility !== "authenticated" || (state.monitorMode === "local" && authState.status === "authenticated")).sort((a, b) => Number(a.path === "/galaxy") - Number(b.path === "/galaxy")) }, currentTool() { return state.tools.find((tool) => route.path === tool.path || route.path.startsWith(tool.path + "/")) }, drivingSettingsPage() { return drivingPage(route.path) }, deviceSettingsPage() { return devicePage(route.path) }, pageName() { if (this.currentTool) return this.currentTool.name if (route.path === "/device-preferences") return "Force Drive State" return NAV.find((item) => item.path === route.path)?.name || "This page" }, isLight() { return state.theme === "light" }, connectionLabel() { if (state.monitorMode !== "local") return "Preview" if (authState.status === "authenticated") return authState.localAccess ? "Local connection" : "Remote connection" return authState.status === "checking" ? "Connecting…" : "Not connected" }, }, errorCaptured(error, _instance, info) { console.error("Galaxy page error", info, error) if (updateRestart.prepared) updateRestart.start(updateRestart.prepared, { provisional: true }) else state.pageError = "This page could not finish loading." return false }, watch: { 'route.path'(path, previous) { state.pageError = "" if (path !== "/settings") state.searchPage = "" else if (route.view.searchPage !== undefined) state.searchPage = route.view.searchPage this.routeDirection = route.direction < 0 || primaryTab(path) < primaryTab(previous) ? -1 : 1 } }, methods: { toggleHaptic, setPageActive(element, active) { element.inert = !active; element.setAttribute("aria-hidden", String(!active)) }, reloadPage() { location.reload() }, go(path) { navigate(path, () => { state.drawerOpen = false; state.searchPage = "" }) }, goFromMenu(path) { state.drawerOpen = false if (path === route.path) return this.nextLayoutLeavePresentation = "modal" this.go(path) }, openSearchHit(hit) { navigate(hit.path || "/settings", () => { state.drawerOpen = false; state.searchPage = hit.page || "" }) }, returnFromSearch() { state.searchPage = "" }, returnToDriving() { this.go("/driving") }, returnToDevice() { this.go("/device-preferences") }, ensureBackEntry, routeBack() { state.drawerOpen = false; navigateBack() }, toggleTheme() { state.theme = this.isLight ? "dark" : "light" document.documentElement.dataset.theme = state.theme save(THEME_KEY, state.theme) }, togglePin() { state.navPinned = !state.navPinned state.drawerOpen = state.navPinned save(PIN_KEY, String(state.navPinned)) }, isActive(path) { return route.path === path || route.path.startsWith(path + "/") }, async signIn() { const password = authForm.password authForm.password = "" await auth.login(password) }, async signOut() { await auth.logout() }, sessionExpired() { if (initializing) return; auth.expired(); initialize() }, }, mounted() { document.documentElement.dataset.theme = state.theme this.visibility = () => { if (!document.hidden) deviceRestart.wake() clearTimeout(reconnectTimer) reconnectTimer = null if (!document.hidden && (state.error || authState.status === "unavailable") && !initializing) initialize() } document.addEventListener("visibilitychange", this.visibility) window.addEventListener("pageshow", this.visibility) setRouteLeaveGuard((proceed, { back } = {}) => { if (back && this.$refs.activeSettings?.navigateBack()) return if (back && state.searchPage) { this.returnFromSearch(); return } const presentation = this.nextLayoutLeavePresentation this.nextLayoutLeavePresentation = "inline" if (this.$refs.activeLayout) this.$refs.activeLayout.requestLeave(proceed, presentation) else if (this.$refs.activeSettings) this.$refs.activeSettings.requestRouteLeave(proceed) else proceed() }) }, beforeUnmount() { reconnectEnabled = false clearTimeout(reconnectTimer) startupGeneration++ document.removeEventListener("visibilitychange", this.visibility) window.removeEventListener("pageshow", this.visibility) deviceRestart.stop() }, template: `
{{ connectionLabel }}
{{ state.error }}

Galaxy access

Checking Galaxy access…

Remote Galaxy access is not configured.

Galaxy access is unavailable.

Reconnect to Galaxy

{{ authState.error }}
{{ state.pageError }}
Routes are unavailable through the Galaxy tunnel. Open Galaxy locally to access Routes.
This capability is unavailable in this build. No operation was attempted.
`, }).component("GxState", GxState).component("GxNotice", GxNotice).mount("#galaxy-app") const galaxyBackground = document.getElementById("galaxy-bg") spawnAmbientStars(galaxyBackground) pauseStarsOnSlowFirefoxScroll(galaxyBackground) startRouter() let startupGeneration = 0 let reconnectTimer = null let initializing = false let reconnectEnabled = true function scheduleReconnect() { if (!reconnectEnabled || document.hidden || initializing || reconnectTimer !== null) return reconnectTimer = setTimeout(() => { reconnectTimer = null if (reconnectEnabled && !document.hidden && !initializing && (state.error || authState.status === "unavailable")) initialize() }, 3000) } async function initialize() { const generation = ++startupGeneration initializing = true clearTimeout(reconnectTimer) reconnectTimer = null state.loading = !state.tools.length if (state.loading && !state.updateRestarting) showBootScreen("connecting", "Connecting to your device…") try { const catalog = await loadCatalog() if (generation !== startupGeneration) return state.monitorMode = catalog.mode deviceRestart.track(catalog.instance) state.tools = catalog.tools state.error = "" if (state.monitorMode === "local") await auth.check() else authState.status = "preview" } catch { if (generation === startupGeneration) state.error = "Galaxy could not connect. Reconnecting automatically…" } finally { if (generation === startupGeneration) { state.loading = false if (bootScreenMode() === "connecting") dismissBootScreen() initializing = false if (state.error || authState.status === "unavailable") scheduleReconnect() } } } initialize()