mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-09 22:13:43 +08:00
d921cf6559
Render tool menus from the catalog and update navigation, guides, and tests
76 lines
4.9 KiB
JavaScript
76 lines
4.9 KiB
JavaScript
import { GxNotice } from "./notice.js"
|
|
import { beginSave, finishSave } from "./save-feedback.js"
|
|
export const validDriveState = (value) => !!value && ["auto", "offroad", "onroad"].includes(value.mode) &&
|
|
typeof value.available === "boolean" && typeof value.overrideAllowed === "boolean" &&
|
|
(value.revision === null || typeof value.revision === "string" && /^[0-9a-f]{32}$/.test(value.revision)) &&
|
|
[null, "offroad", "onroad"].includes(value.effective)
|
|
|
|
export const DriveStatePanel = {
|
|
components: { GxNotice },
|
|
props: { offroadOnly: { type: Boolean, default: false } },
|
|
data: () => ({ state: null, error: "", busy: false }),
|
|
computed: {
|
|
pending() { return this.state?.mode !== "auto" && this.state?.effective !== this.state?.mode },
|
|
},
|
|
mounted() {
|
|
this.active = true
|
|
this.generation = 0
|
|
this.load()
|
|
this.timer = setInterval(() => { if (!document.hidden) this.load() }, 1000)
|
|
},
|
|
beforeUnmount() { this.active = false; this.generation++; clearInterval(this.timer); this.controller?.abort() },
|
|
methods: {
|
|
async load() {
|
|
if (this.busy || this.loading || !this.active) return
|
|
this.loading = true
|
|
const generation = ++this.generation
|
|
this.controller = new AbortController()
|
|
const controller = this.controller
|
|
const deadline = setTimeout(() => controller.abort(), 3000)
|
|
try {
|
|
const response = await fetch("./api/drive-state/status", { credentials: "same-origin", cache: "no-store", signal: this.controller.signal })
|
|
const value = await response.json()
|
|
if (!response.ok || !validDriveState(value)) throw new Error(value.error || "Drive state unavailable")
|
|
if (this.active && generation === this.generation) { this.state = value; this.error = "" }
|
|
} catch (error) { if (this.active && generation === this.generation) { this.state = null; this.error = "Drive state unavailable" } }
|
|
finally { clearTimeout(deadline); if (generation === this.generation) this.loading = false }
|
|
},
|
|
async change(mode) {
|
|
if (this.busy || !this.state?.available) return
|
|
if (!["onroad", "offroad", "auto"].includes(mode)) return
|
|
const feedback = beginSave()
|
|
const revision = this.state.revision
|
|
this.busy = true
|
|
this.loading = false
|
|
const generation = ++this.generation
|
|
this.controller?.abort()
|
|
this.controller = new AbortController()
|
|
const controller = this.controller
|
|
const deadline = setTimeout(() => controller.abort(), 3000)
|
|
try {
|
|
const response = await fetch("./api/drive-state/action", { method: "POST", credentials: "same-origin", cache: "no-store",
|
|
signal: this.controller.signal, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mode, revision }) })
|
|
const value = await response.json()
|
|
if (!response.ok || !validDriveState(value)) throw new Error(value.error || "Drive state unavailable")
|
|
if (this.active && generation === this.generation) { this.state = value; this.error = ""; finishSave(feedback, true) }
|
|
} catch (error) { if (this.active && generation === this.generation) this.error = error.message; finishSave(feedback, false) }
|
|
finally { clearTimeout(deadline); this.busy = false; if (this.active && generation === this.generation) this.load() }
|
|
},
|
|
},
|
|
template: `<section class="gx-card gx-force-drive" aria-label="Force Drive State" :inert="busy" :aria-busy="busy || undefined"><div class="gx-section__header"><i class="bi bi-car-front" aria-hidden="true"></i><span class="gx-section__title">Force Drive State</span></div><div class="gx-panel">
|
|
<GxNotice tone="danger" v-if="error">{{ error }}</GxNotice>
|
|
<GxNotice tone="danger" v-if="state && !state.available">The drive-state manager is unavailable. Reconnect to check again.</GxNotice>
|
|
<GxNotice v-if="pending" tone="info">Waiting for the device to switch {{ state.mode }}…</GxNotice>
|
|
<p>Device: {{ state?.effective === "onroad" ? "Onroad" : state?.effective === "offroad" ? "Offroad" : "Checking…" }}</p>
|
|
<p v-if="!offroadOnly">Auto follows the car. Force Onroad starts driving services at any time. Return to Auto restores normal operation.</p>
|
|
<label v-if="offroadOnly" class="gx-row"><span>Force Offroad</span><input type="checkbox" role="switch"
|
|
:checked="state?.mode === 'offroad'" :disabled="!state?.available"
|
|
@change="change($event.target.checked ? 'offroad' : 'auto')" /></label>
|
|
<p v-if="offroadOnly" class="gx-note">Park the vehicle and disengage controls to enable. Turn off to restore automatic drive state.</p>
|
|
<div v-if="!offroadOnly" class="gx-force-drive__actions gx-actions"><button v-for="mode in ['onroad', 'auto']" :key="mode" type="button"
|
|
class="gx-btn" :class="state?.mode === mode ? '' : 'gx-btn--tonal'"
|
|
:disabled="!state?.available"
|
|
:aria-pressed="state?.mode === mode" @click="change(mode)">{{ mode === 'auto' ? 'Return to Auto' : 'Onroad' }}</button></div>
|
|
</div></section>`,
|
|
}
|