diff --git a/starpilot/system/the_galaxy/assets/mobile/css/material.css b/starpilot/system/the_galaxy/assets/mobile/css/material.css index 68d01f9c5e..da1022f6ad 100644 --- a/starpilot/system/the_galaxy/assets/mobile/css/material.css +++ b/starpilot/system/the_galaxy/assets/mobile/css/material.css @@ -1275,6 +1275,8 @@ button.gx-chip:hover { transition: transform var(--motion-slow); width: 84vw; z-index: calc(var(--z-drawer) + 1); + display: flex; + flex-direction: column; } [data-theme="light"] .gx-drawer { @@ -1724,6 +1726,46 @@ button.gx-chip:hover { .gx-nav-item i { font-size: 1.3rem; width: 24px; } +.gx-device-picker { + border-top: 1px solid var(--glass-border); + margin-top: auto; + padding-top: var(--sp-3); +} + +.gx-device-picker__heading { + align-items: center; + display: flex; + justify-content: space-between; + padding: 0 var(--sp-2) var(--sp-1); +} + +.gx-device-picker__heading .gx-nav-section__title { + padding: 0; +} + +.gx-device-picker__heading-icon { + color: var(--primary); + font-size: var(--fs-sm); + padding-right: var(--sp-2); +} + +.gx-device-picker__hint { + color: var(--text-muted); + font-size: var(--fs-xs); + padding: 0 var(--sp-2); +} + +.gx-device-picker__row { + align-items: center; + display: flex; + gap: var(--sp-1); +} + +.gx-device-picker__item { + flex: 1; + min-width: 0; +} + .gx-device-picker__name { min-width: 0; overflow: hidden; @@ -1742,6 +1784,67 @@ button.gx-chip:hover { white-space: nowrap; } +.gx-device-picker__rename { + color: var(--text-muted); + flex: 0 0 auto; + height: 36px; + width: 36px; +} + +.gx-device-picker__rename:hover { + color: var(--primary); +} + +.gx-device-picker__editor { + background: var(--surface-container); + border: 1px solid var(--glass-border); + border-radius: var(--radius-md); + margin: var(--sp-2) var(--sp-2) 0; + padding: var(--sp-3); +} + +.gx-device-picker__editor-label { + color: var(--text-muted); + display: block; + font-size: var(--fs-xs); + margin-bottom: var(--sp-2); +} + +.gx-device-picker__editor-row { + display: flex; + flex-wrap: wrap; + gap: var(--sp-2); +} + +.gx-device-picker__editor input { + background: var(--surface-container-high); + border: 1px solid var(--glass-border); + border-radius: var(--radius-sm); + color: var(--on-surface); + flex: 1 1 100%; + font: inherit; + min-width: 0; + padding: var(--sp-2); +} + +.gx-device-picker__editor button { + border: 1px solid var(--glass-border); + border-radius: var(--radius-sm); + cursor: pointer; + font: inherit; + padding: var(--sp-2) var(--sp-3); +} + +.gx-device-picker__save { + background: var(--primary); + color: var(--on-primary); +} + +.gx-device-picker__cancel { + background: transparent; + color: var(--on-surface); +} + .gx-scrim { align-items: center; background: rgba(0, 0, 0, 0.6); diff --git a/starpilot/system/the_galaxy/assets/mobile/js/components/AppShell.js b/starpilot/system/the_galaxy/assets/mobile/js/components/AppShell.js index 3ba1b9a50d..3978803f51 100644 --- a/starpilot/system/the_galaxy/assets/mobile/js/components/AppShell.js +++ b/starpilot/system/the_galaxy/assets/mobile/js/components/AppShell.js @@ -157,7 +157,6 @@ export const AppShell = { -
{{ tr("Main") }}
@@ -176,6 +175,7 @@ export const AppShell = { {{ tr(link.name, link.name) }}
+
diff --git a/starpilot/system/the_galaxy/assets/mobile/js/components/DevicePicker.js b/starpilot/system/the_galaxy/assets/mobile/js/components/DevicePicker.js index 16ecdf3e1d..57ff230bb7 100644 --- a/starpilot/system/the_galaxy/assets/mobile/js/components/DevicePicker.js +++ b/starpilot/system/the_galaxy/assets/mobile/js/components/DevicePicker.js @@ -1,4 +1,6 @@ const SLUG_RE = /^[A-Za-z0-9]{16}$/ +const DEVICE_NAMES_KEY = "galaxy-device-names" +const MAX_NAME_LENGTH = 40 export const DevicePicker = { name: "DevicePicker", @@ -6,14 +8,40 @@ export const DevicePicker = { return { devices: [], activeSlug: "", + customNames: {}, + draftName: "", + editingSlug: "", loading: true, } }, computed: { - hasMultipleDevices() { return this.devices.length > 1 }, + hasDevices() { return this.devices.length > 0 }, }, methods: { + loadCustomNames() { + try { + const saved = JSON.parse(localStorage.getItem(DEVICE_NAMES_KEY) || "{}") + if (saved && typeof saved === "object" && !Array.isArray(saved)) { + this.customNames = Object.fromEntries(Object.entries(saved).filter(([slug, name]) => ( + SLUG_RE.test(slug) && typeof name === "string" && name.trim() + ))) + } + } catch (error) { + this.customNames = {} + } + }, + saveCustomNames() { + try { + localStorage.setItem(DEVICE_NAMES_KEY, JSON.stringify(this.customNames)) + } catch (error) { + // Private browsing can disable localStorage; the current name still works. + } + }, + displayName(device, index) { + return this.customNames[device.slug] || device.name || `Comma ${index + 1}` + }, async loadDevices() { + this.loadCustomNames() try { const response = await fetch("/_gateway/devices", { cache: "no-store" }) if (!response.ok) return @@ -28,6 +56,30 @@ export const DevicePicker = { this.loading = false } }, + startRename(device, index) { + this.editingSlug = device.slug + this.draftName = this.displayName(device, index) + this.$nextTick(() => { + const input = this.$refs.deviceNameInput + ;(Array.isArray(input) ? input[0] : input)?.focus() + }) + }, + cancelRename() { + this.editingSlug = "" + this.draftName = "" + }, + saveRename(device) { + if (!device) return + const name = this.draftName.trim().slice(0, MAX_NAME_LENGTH) + if (name) this.customNames = { ...this.customNames, [device.slug]: name } + else { + const nextNames = { ...this.customNames } + delete nextNames[device.slug] + this.customNames = nextNames + } + this.saveCustomNames() + this.cancelRename() + }, selectDevice(device) { if (!device?.path || device.slug === this.activeSlug) return window.location.assign(device.path) @@ -37,16 +89,35 @@ export const DevicePicker = { this.loadDevices() }, template: ` -
-
Commas
- - - {{ device.name }} - Current - +
+
+
+
Commas
+
Switch device
+
+ +
+ +
+ +
+ + + +
+
`, } diff --git a/starpilot/system/the_galaxy/assets/mobile/js/views/Settings.js b/starpilot/system/the_galaxy/assets/mobile/js/views/Settings.js index 11af416db1..11f4131bc1 100644 --- a/starpilot/system/the_galaxy/assets/mobile/js/views/Settings.js +++ b/starpilot/system/the_galaxy/assets/mobile/js/views/Settings.js @@ -12,7 +12,7 @@ import { GalaxyToggleCard } from "../components/GalaxyToggleCard.js" import { GalaxySection } from "../components/GalaxySection.js" import { DevModeBanner } from "../components/DevModeBanner.js" import { LanguageSelector } from "../components/LanguageSelector.js" -import { setLanguage, t } from "../i18n.js" +import { languageState, setLanguage, t } from "../i18n.js" const LEGACY_PERSONALITY_KEYS = new Set([ "AccelerationProfile", "AggressiveFollow", "AggressiveFollowHigh", "CustomAccelProfile", @@ -22,6 +22,8 @@ const LEGACY_PERSONALITY_KEYS = new Set([ "StandardFollowHigh", "TrafficFollow", "TruckTuning", ]) +const LANGUAGE_SECTION_SLUG = "language" + export const Settings = { name: "Settings", components: { SettingTree, PersonalityProfiles, GalaxyToggleCard, GalaxySection, DevModeBanner, LongitudinalMode, LanguageSelector }, @@ -53,6 +55,7 @@ export const Settings = { }, hiddenAdvancedCount() { return countAdvancedHiddenByDeveloperMode(this.layout, this.values) }, searchActive() { return !!this.searchTerm }, + currentLanguage() { return languageState.code }, searchTerm: { get() { return store.search }, set(v) { store.search = v }, @@ -114,9 +117,16 @@ export const Settings = { }, applyRouteSection() { const route = store.route + if (route === "/settings") { + if (this.activeSectionSlug === LANGUAGE_SECTION_SLUG) { + const preferred = this.sections.find((s) => s.slug === this.defaultSectionSlug) + this.activeSectionSlug = (preferred || this.sections[0])?.slug || "" + } + return + } if (!route.startsWith("/settings/")) return const slug = route.replace(/^\/settings\/?/, "").split("?")[0].split("/")[0] - if (slug && this.sections.some((s) => s.slug === slug)) this.activeSectionSlug = slug + if (slug === LANGUAGE_SECTION_SLUG || this.sections.some((s) => s.slug === slug)) this.activeSectionSlug = slug if (store.params.open) this.expanded = { ...this.expanded, [store.params.open]: true } }, lockReason(param) { @@ -169,13 +179,20 @@ export const Settings = {
+
-
+ + +
{{ tr(activeSection.name, activeSection.name) }} @@ -189,8 +206,6 @@ export const Settings = {
{{ tr("No settings available.") }}
- -
`, } diff --git a/starpilot/system/the_galaxy/assets/mobile/js/views/SystemTools.js b/starpilot/system/the_galaxy/assets/mobile/js/views/SystemTools.js index 669cfcceee..59a4aa0522 100644 --- a/starpilot/system/the_galaxy/assets/mobile/js/views/SystemTools.js +++ b/starpilot/system/the_galaxy/assets/mobile/js/views/SystemTools.js @@ -17,6 +17,27 @@ function toPercent(value) { } const CORE_UPDATE_BRANCHES = ["StarPilot", "Dom"] +const REBOOT_PENDING_STORAGE_KEY = "galaxy-update-reboot-pending" + +function readRebootMarker() { + try { + const raw = localStorage.getItem(REBOOT_PENDING_STORAGE_KEY) + if (!raw) return null + const parsed = JSON.parse(raw) + const startedAt = Number(parsed?.startedAt) + return Number.isFinite(startedAt) && startedAt > 0 ? startedAt : null + } catch (e) { + return null + } +} + +function writeRebootMarker(startedAt) { + try { localStorage.setItem(REBOOT_PENDING_STORAGE_KEY, JSON.stringify({ startedAt })) } catch (e) {} +} + +function clearRebootMarker() { + try { localStorage.removeItem(REBOOT_PENDING_STORAGE_KEY) } catch (e) {} +} export const SystemTools = { name: "SystemTools", @@ -45,6 +66,11 @@ export const SystemTools = { isOnroad: false, fastStatus: null, + statusUnavailable: false, + rebootPending: !!readRebootMarker(), + rebootStartedAt: readRebootMarker() || 0, + rebootOfflineSeen: false, + reconnectedNotice: false, checkedForUpdates: false, busy: "", autoUpdateBusy: false, @@ -80,8 +106,8 @@ export const SystemTools = { return this.branchLoading || this.isOnroad || !!this.fastStatus?.isOnroad || this.updateInProgress || !!this.busy }, statusRebooting() { return String(this.fastStatus?.stage || "").trim().toLowerCase() === "rebooting" }, - updateInProgress() { return !!this.fastStatus?.running || this.statusRebooting }, - statusPollingNeeded() { return !this.fastStatus || this.updateInProgress }, + updateInProgress() { return !!this.fastStatus?.running || this.statusRebooting || this.rebootPending }, + statusPollingNeeded() { return !this.fastStatus || this.updateInProgress || this.rebootPending }, versionChoices() { return this.targetBranch === "StarPilot" ? releaseVersions(this.versionCommits) : this.versionCommits }, installVersionBlocked() { return this.branchSwitchBlocked || this.branchBusy || !this.branches.includes(this.targetBranch) || @@ -145,13 +171,42 @@ export const SystemTools = { try { const status = await api.getUpdateFastStatus() if (!status) throw new Error("Update status unavailable") + const stage = String(status.stage || "").trim().toLowerCase() + if (stage === "rebooting" && !this.rebootPending) { + this.rebootPending = true + this.rebootStartedAt = Date.now() + writeRebootMarker(this.rebootStartedAt) + } + const pendingAge = this.rebootStartedAt ? Date.now() - this.rebootStartedAt : 0 + const deviceReturned = this.rebootPending && !status.running && stage !== "rebooting" && + (this.statusUnavailable || pendingAge >= 30_000) + const updateFailed = this.rebootPending && stage === "error" this.fastStatus = status + this.statusUnavailable = false this.isOnroad = !!status.isOnroad + if (deviceReturned) this.clearRebootPending() + else if (updateFailed) this.clearRebootPending(false) } catch (e) { - this.fastStatus = null + this.statusUnavailable = true + if (this.rebootPending) this.rebootOfflineSeen = true + else this.fastStatus = null if (throwOnError) throw e } }, + markRebootPending() { + this.rebootPending = true + this.rebootStartedAt = Date.now() + this.rebootOfflineSeen = false + this.reconnectedNotice = false + writeRebootMarker(this.rebootStartedAt) + }, + clearRebootPending(showNotice = true) { + this.rebootPending = false + this.rebootStartedAt = 0 + this.rebootOfflineSeen = false + clearRebootMarker() + if (showNotice) this.reconnectedNotice = true + }, async backupToggles() { try { const blob = await api.backupToggles() @@ -228,6 +283,7 @@ export const SystemTools = { if (!(await GalaxyConfirm({ title: "Reset toggles to default?", message: "This resets all toggles to their default values and reboots.", confirmLabel: "Reset", danger: true }))) return try { await api.resetTogglesDefault() + this.markRebootPending() showSnackbar("Resetting toggles to default... rebooting.") } catch (e) { showSnackbar("Reset failed.", "error") @@ -347,6 +403,7 @@ export const SystemTools = { return } const result = await api.installUpdateVersion(branch, commit) + this.markRebootPending() showSnackbar(result?.message || `Installing ${version} on ${branch}...`) await this.loadFastStatus() } catch (e) { @@ -420,6 +477,7 @@ export const SystemTools = { } const fn = action === "fast" ? api.updateFast : action === "recover" ? api.updateRecover : api.updateRollback const payload = await fn() + this.markRebootPending() showSnackbar(payload?.message || "Update started.") await this.loadFastStatus() } catch (e) { @@ -432,6 +490,7 @@ export const SystemTools = { if (!(await GalaxyConfirm({ title: "Factory reset (SAVE ME)?", message: "This wipes params, backups, themes, models, maps, and route data, then reboots. This cannot be undone.", confirmLabel: "Factory Reset", danger: true }))) return try { await api.factoryReset() + this.markRebootPending() showSnackbar("SAVE ME initiated — factory resetting...") await this.loadFastStatus() } catch (e) { @@ -502,22 +561,26 @@ export const SystemTools = {
Loading update info...