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: `
{{ state.error }}
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()