mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-02 14:13:44 +08:00
actually refresh
This commit is contained in:
@@ -2,7 +2,7 @@ import { html, reactive } from "/assets/vendor/arrow-core.js"
|
||||
import { createBrowserHistory, createRouter } from "/assets/vendor/remix-router-1.3.1.js"
|
||||
import { hideSidebar } from "/assets/js/utils.js"
|
||||
import { DeviceSettings } from "/assets/components/tools/device_settings.js?v=favorite-c4-hint-1"
|
||||
import { Bluetooth } from "/assets/components/tools/bluetooth.js?v=bluetooth-13"
|
||||
import { Bluetooth } from "/assets/components/tools/bluetooth.js?v=bluetooth-live-15"
|
||||
import { WheelControls } from "/assets/components/tools/wheel_controls.js?v=controllers-2"
|
||||
import { ErrorLogs } from "/assets/components/tools/error_logs.js"
|
||||
import { VehicleFeatures } from "/assets/components/tools/vehicle_features.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { html, reactive } from "/assets/vendor/arrow-core.js"
|
||||
import { galaxyPath } from "/assets/js/utils.js"
|
||||
import { escapeHtml, galaxyPath } from "/assets/js/utils.js"
|
||||
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
@@ -14,6 +14,9 @@ const state = reactive({
|
||||
pairingAddress: "",
|
||||
devices: [],
|
||||
revision: 0,
|
||||
deviceSignature: "",
|
||||
refreshing: false,
|
||||
lastUpdated: 0,
|
||||
prompt: null,
|
||||
audioTestAddress: "",
|
||||
audioTestLabel: "",
|
||||
@@ -25,6 +28,8 @@ let audioTestTimer = null
|
||||
let pollTimer = null
|
||||
let refreshRequested = false
|
||||
let refreshPromise = null
|
||||
const POLL_INTERVAL_MS = 750
|
||||
const ACTIVE_POLL_INTERVAL_MS = 250
|
||||
|
||||
function bluetoothPageActive() {
|
||||
const currentPath = window.location.pathname.replace(/\/+$/, "")
|
||||
@@ -32,11 +37,22 @@ function bluetoothPageActive() {
|
||||
return document.querySelector(".bluetoothPage") !== null || currentPath === bluetoothPath
|
||||
}
|
||||
|
||||
function schedulePoll() {
|
||||
if (pollTimer !== null) return
|
||||
pollTimer = setInterval(() => {
|
||||
if (bluetoothPageActive() && state.busy !== "power") refresh()
|
||||
}, 750)
|
||||
function pollDelay() {
|
||||
return state.busy || state.discovering || state.pairingAddress ? ACTIVE_POLL_INTERVAL_MS : POLL_INTERVAL_MS
|
||||
}
|
||||
|
||||
function schedulePoll(delay = pollDelay()) {
|
||||
if (pollTimer !== null) clearTimeout(pollTimer)
|
||||
pollTimer = setTimeout(async () => {
|
||||
pollTimer = null
|
||||
try {
|
||||
if (bluetoothPageActive() && document.visibilityState !== "hidden" && state.busy !== "power") {
|
||||
await refresh()
|
||||
}
|
||||
} finally {
|
||||
schedulePoll()
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
|
||||
function startAudioTestCountdown(address, delayMs, requestStartedAt) {
|
||||
@@ -101,8 +117,20 @@ async function refreshOnce() {
|
||||
state.offroad = !!payload.offroad
|
||||
state.selectedAudio = String(payload.selected_audio || "")
|
||||
state.pairingAddress = String(payload.pairing_address || "")
|
||||
state.devices = Array.isArray(payload.devices) ? payload.devices : []
|
||||
state.revision++
|
||||
const devices = Array.isArray(payload.devices) ? payload.devices : []
|
||||
const deviceSignature = JSON.stringify({
|
||||
enabled: state.enabled,
|
||||
discovering: state.discovering,
|
||||
selectedAudio: state.selectedAudio,
|
||||
pairingAddress: state.pairingAddress,
|
||||
devices,
|
||||
})
|
||||
state.devices = devices
|
||||
if (state.deviceSignature !== deviceSignature) {
|
||||
state.deviceSignature = deviceSignature
|
||||
state.revision++
|
||||
}
|
||||
state.lastUpdated = Date.now()
|
||||
state.prompt = payload.prompt || null
|
||||
state.error = payload.error || (response.ok ? "" : "Bluetooth service unavailable")
|
||||
}
|
||||
@@ -166,6 +194,7 @@ async function refresh() {
|
||||
refreshRequested = true
|
||||
if (refreshPromise !== null) return refreshPromise
|
||||
|
||||
state.refreshing = true
|
||||
refreshPromise = (async () => {
|
||||
while (refreshRequested) {
|
||||
refreshRequested = false
|
||||
@@ -184,6 +213,7 @@ async function refresh() {
|
||||
await refreshPromise
|
||||
} finally {
|
||||
refreshPromise = null
|
||||
state.refreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,11 +221,12 @@ function initialize() {
|
||||
if (initialized) return
|
||||
initialized = true
|
||||
window.addEventListener("focus", refresh)
|
||||
window.addEventListener("pageshow", refresh)
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden && bluetoothPageActive()) refresh()
|
||||
if (document.visibilityState !== "hidden" && bluetoothPageActive()) refresh()
|
||||
})
|
||||
refresh()
|
||||
schedulePoll()
|
||||
schedulePoll(0)
|
||||
}
|
||||
|
||||
function normalizedAddress(device) {
|
||||
@@ -291,9 +322,10 @@ function deviceRow(device) {
|
||||
`
|
||||
}
|
||||
|
||||
function deviceSection(title, icon, devices, emptyText = "") {
|
||||
function deviceSection(title, icon, devices, emptyText = "", revision = null) {
|
||||
const revisionAttribute = revision === null ? "" : ` data-revision="${revision}"`
|
||||
return html`
|
||||
<section class="bluetoothSection">
|
||||
<section class="bluetoothSection"${revisionAttribute}>
|
||||
<div class="bluetoothSectionHeader">
|
||||
<div><i class="bi ${icon}" aria-hidden="true"></i><h3>${title}</h3></div>
|
||||
<span>${devices.length}</span>
|
||||
@@ -307,6 +339,77 @@ function deviceSection(title, icon, devices, emptyText = "") {
|
||||
`
|
||||
}
|
||||
|
||||
function renderDisabledAttribute(disabled) {
|
||||
return disabled ? " disabled" : ""
|
||||
}
|
||||
|
||||
function renderDeviceActions(device) {
|
||||
const selected = state.selectedAudio.toUpperCase() === device.address.toUpperCase()
|
||||
const pairing = isPairing(device)
|
||||
const address = escapeHtml(device.address)
|
||||
const name = escapeHtml(device.name)
|
||||
const actions = []
|
||||
if (!device.paired) {
|
||||
actions.push("<button data-bluetooth-operation=\"pair\" data-address=\"" + address + "\"" +
|
||||
renderDisabledAttribute(!state.offroad || !!state.busy || pairing) + ">" + (pairing ? "Pairing…" : "Pair") + "</button>")
|
||||
}
|
||||
if (device.paired || device.connected) {
|
||||
const operation = device.connected ? "disconnect" : "connect"
|
||||
actions.push("<button data-bluetooth-operation=\"" + operation + "\" data-address=\"" + address + "\"" +
|
||||
renderDisabledAttribute(!!state.busy) + ">" + (device.connected ? "Disconnect" : "Connect") + "</button>")
|
||||
if (device.audio) {
|
||||
actions.push("<button class=\"" + (selected ? "selected" : "") + "\" data-bluetooth-operation=\"select_audio\" data-address=\"" +
|
||||
(selected ? "" : address) + "\"" + renderDisabledAttribute(!!state.busy) + ">" +
|
||||
(selected ? "Stop Using for Audio" : "Use for Audio") + "</button>")
|
||||
if (device.connected) {
|
||||
const testing = state.audioTestAddress.toUpperCase() === device.address.toUpperCase() && !!state.audioTestLabel
|
||||
actions.push("<button data-bluetooth-operation=\"test_audio\" data-address=\"" + address + "\"" +
|
||||
renderDisabledAttribute(!state.offroad || !!state.busy || !!state.audioTestLabel) + ">" +
|
||||
(testing ? "Test Audio: " + escapeHtml(state.audioTestLabel) : "Test Audio") + "</button>")
|
||||
}
|
||||
}
|
||||
if (device.paired) {
|
||||
actions.push("<button class=\"bluetoothIconButton bluetoothForgetButton\" data-bluetooth-operation=\"forget\" data-address=\"" +
|
||||
address + "\" data-device-name=\"" + name + "\" title=\"Forget device\" aria-label=\"Forget " + name + "\"" +
|
||||
renderDisabledAttribute(!state.offroad || !!state.busy) + "><i class=\"bi bi-trash3\" aria-hidden=\"true\"></i></button>")
|
||||
}
|
||||
}
|
||||
return "<div class=\"bluetoothActions\">" + actions.join("") + "</div>"
|
||||
}
|
||||
|
||||
function renderDeviceRow(device) {
|
||||
const name = escapeHtml(device.name)
|
||||
return "<div class=\"bluetoothDeviceRow " + (device.connected ? "connected" : "") + "\">" +
|
||||
"<div class=\"bluetoothDeviceIcon\"><i class=\"bi " + deviceIcon(device) + "\" aria-hidden=\"true\"></i></div>" +
|
||||
"<div class=\"bluetoothDeviceDetails\"><div class=\"bluetoothDeviceName\"><h3>" + name +
|
||||
"</h3>" + (device.connected ? "<span class=\"bluetoothConnectedDot\" title=\"Connected\"></span>" : "") +
|
||||
"</div><p>" + deviceCapabilities(device) + "</p><span class=\"bluetoothDeviceStatus\">" +
|
||||
escapeHtml(deviceStatus(device)) + "</span></div>" + renderDeviceActions(device) + "</div>"
|
||||
}
|
||||
|
||||
function renderDeviceSection(title, icon, devices, emptyText = "", revision = null) {
|
||||
const revisionAttribute = revision === null ? "" : " data-revision=\"" + revision + "\""
|
||||
const rows = devices.length
|
||||
? devices.map(renderDeviceRow).join("")
|
||||
: "<div class=\"bluetoothEmptyState\">" + escapeHtml(emptyText) + "</div>"
|
||||
return html(["<section class=\"bluetoothSection\"" + revisionAttribute + ">" +
|
||||
"<div class=\"bluetoothSectionHeader\"><div><i class=\"bi " + icon + "\" aria-hidden=\"true\"></i><h3>" + title +
|
||||
"</h3></div><span>" + devices.length + "</span></div><div class=\"bluetoothSectionBody\">" + rows +
|
||||
"</div></section>"])
|
||||
}
|
||||
|
||||
function handleDeviceListClick(event) {
|
||||
const target = event.target
|
||||
const button = target && typeof target.closest === "function"
|
||||
? target.closest("button[data-bluetooth-operation]")
|
||||
: null
|
||||
if (!button) return
|
||||
const operation = button.dataset.bluetoothOperation
|
||||
const address = button.dataset.address || ""
|
||||
if (operation === "forget" && !window.confirm("Forget " + (button.dataset.deviceName || "this device") + "?")) return
|
||||
request(operation, { address })
|
||||
}
|
||||
|
||||
export function Bluetooth() {
|
||||
initialize()
|
||||
return html`
|
||||
@@ -348,10 +451,11 @@ export function Bluetooth() {
|
||||
<button class="bluetoothSecondaryButton" disabled="${() => !!state.busy}" @click="${refresh}">
|
||||
<i class="bi bi-arrow-clockwise" aria-hidden="true"></i> Refresh
|
||||
</button>
|
||||
<span class="bluetoothLiveStatus" aria-live="polite">${() => state.busy ? "Updating…" : state.lastUpdated ? "Live" : ""}</span>
|
||||
<span class="bluetoothScanHint">Put a device in pairing mode before searching.</span>
|
||||
</div>
|
||||
|
||||
<div class="bluetoothDeviceList">
|
||||
<div class="bluetoothDeviceList" @click="${handleDeviceListClick}">
|
||||
${() => state.loading ? html`<div class="bluetoothLoading"><span></span><span></span><span></span></div>` : ""}
|
||||
${() => !state.loading && !state.enabled ? html`
|
||||
<div class="bluetoothEmptyPage">
|
||||
@@ -360,14 +464,12 @@ export function Bluetooth() {
|
||||
<p>Turn it on to reconnect saved devices or find something new.</p>
|
||||
</div>
|
||||
` : ""}
|
||||
${() => {
|
||||
if (state.loading || !state.enabled) return ""
|
||||
void state.revision
|
||||
return html`
|
||||
${deviceSection("My Devices", "bi-check2-circle", knownDevices(), "No saved devices yet.")}
|
||||
${deviceSection("Available Devices", "bi-radar", availableDevices(), state.discovering ? "Searching for nearby devices…" : "No nearby devices found. Start a search to try again.")}
|
||||
`
|
||||
}}
|
||||
${() => !state.loading && state.enabled
|
||||
? renderDeviceSection("My Devices", "bi-check2-circle", knownDevices(), "No saved devices yet.", state.revision)
|
||||
: ""}
|
||||
${() => !state.loading && state.enabled
|
||||
? renderDeviceSection("Available Devices", "bi-radar", availableDevices(), state.discovering ? "Searching for nearby devices…" : "No nearby devices found. Start a search to try again.")
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
|
||||
|
||||
<script type="module">
|
||||
import("/assets/components/router.js?v=router-cycle-fix-5").catch((err) => {
|
||||
import("/assets/components/router.js?v=router-cycle-fix-6").catch((err) => {
|
||||
console.error("[the_galaxy] bootstrap failed", err);
|
||||
const target = document.getElementById("app") || document.body;
|
||||
const pre = document.createElement("pre");
|
||||
|
||||
@@ -22,7 +22,7 @@ def test_router_and_settings_cache_bust_is_consistent():
|
||||
index = INDEX_PATH.read_text(encoding="utf-8")
|
||||
|
||||
assert "/assets/components/settings.js?v=router-cycle-fix-5" in router
|
||||
assert "/assets/components/router.js?v=router-cycle-fix-5" in index
|
||||
assert "/assets/components/router.js?v=router-cycle-fix-6" in index
|
||||
|
||||
|
||||
def test_bluetooth_actions_use_reactive_disabled_bindings():
|
||||
@@ -37,8 +37,8 @@ def test_bluetooth_actions_use_reactive_disabled_bindings():
|
||||
assert 'request("test_audio", { address: device.address })' in source
|
||||
assert "startAudioTestCountdown" in source
|
||||
assert "The test sound is sent at NOW" in source
|
||||
assert 'deviceSection("My Devices"' in source
|
||||
assert 'deviceSection("Available Devices"' in source
|
||||
assert 'renderDeviceSection("My Devices"' in source
|
||||
assert 'renderDeviceSection("Available Devices"' in source
|
||||
assert "bluetoothForgetButton" in source
|
||||
assert "bi-trash3" in source
|
||||
assert "state.pairingAddress" in source
|
||||
@@ -50,6 +50,13 @@ def test_bluetooth_actions_use_reactive_disabled_bindings():
|
||||
assert "schedulePoll(250)" in source
|
||||
assert "while (refreshRequested)" in source
|
||||
assert 'cache: "no-store"' in source
|
||||
assert "const ACTIVE_POLL_INTERVAL_MS = 250" in source
|
||||
assert "document.visibilityState !== \"hidden\"" in source
|
||||
assert "window.addEventListener(\"pageshow\", refresh)" in source
|
||||
assert 'state.revision)' in source
|
||||
assert "const revisionAttribute" in source
|
||||
assert 'data-revision="${revision}"' in source
|
||||
assert 'bluetooth-live-15' in ROUTER_PATH.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_controller_test_mode_has_explicit_start_and_stop():
|
||||
|
||||
Reference in New Issue
Block a user