mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-09 22:13:43 +08:00
117 lines
7.9 KiB
JavaScript
117 lines
7.9 KiB
JavaScript
import { requestJson } from "./startup.js"
|
||
import { GxNotice } from "./notice.js"
|
||
import { GxState } from "./state.js"
|
||
import { GalaxySettingRow, switchRow } from "./galaxy-setting-row.js"
|
||
import { beginSave, finishSave } from "./save-feedback.js"
|
||
|
||
const validPassword = password => typeof password === 'string' && (password === '' || /^[\x21-\x7e]{12,63}$/.test(password))
|
||
|
||
export function validHotspot(value) {
|
||
return value?.version === 1 && /^[a-f0-9]{64}$/.test(value.revision) &&
|
||
typeof value.editable === 'boolean' && typeof value.active === 'boolean' &&
|
||
['active', 'starting', 'waiting', 'disabled', 'error', 'unavailable'].includes(value.state) &&
|
||
typeof value.reason === 'string' && value.reason.length <= 256 &&
|
||
value.url === 'http://172.31.254.1:8082/' && typeof value.config?.enabled === 'boolean' &&
|
||
/^TheGalaxy-[a-f0-9]{4}$/.test(value.config.ssid) &&
|
||
validPassword(value.config.password)
|
||
}
|
||
|
||
export const GalaxyHotspot = {
|
||
components: { GxNotice, GxState, GalaxySettingRow },
|
||
props: { mode: String, unauthorized: Function },
|
||
data: () => ({ snapshot: null, draft: null, revision: '', busy: false, error: '', saveError: '', notice: '',
|
||
showPassword: false, timer: null, controller: null, generation: 0, active: false }),
|
||
computed: {
|
||
dirty() { return JSON.stringify(this.draft) !== JSON.stringify(this.snapshot?.config) },
|
||
passwordValid() { return validPassword(this.draft?.password) },
|
||
},
|
||
mounted() { this.start() },
|
||
beforeUnmount() { this.stop() },
|
||
watch: { mode() { this.stop(); this.start() } },
|
||
methods: {
|
||
switchRow,
|
||
start() { if (this.mode === 'local') { this.active = true; this.refresh() } },
|
||
stop() {
|
||
this.active = false; this.generation++; clearTimeout(this.timer); this.controller?.abort()
|
||
this.snapshot = this.draft = null; this.revision = ''; this.showPassword = false; this.busy = false; this.saveError = ''
|
||
},
|
||
adopt(value, reset = false) {
|
||
if (!validHotspot(value)) throw new Error('Invalid hotspot status')
|
||
const preserve = this.draft && this.dirty && !reset
|
||
this.snapshot = value
|
||
if (!preserve) { this.draft = { ...value.config }; this.revision = value.revision }
|
||
},
|
||
async refresh() {
|
||
if (!this.active || this.busy) return
|
||
clearTimeout(this.timer); this.controller?.abort()
|
||
const generation = ++this.generation
|
||
this.controller = new AbortController()
|
||
try {
|
||
const value = await requestJson('./api/galaxy/hotspot', { request: { signal: this.controller.signal } })
|
||
if (!this.active || generation !== this.generation) return
|
||
this.adopt(value); this.error = ''
|
||
} catch (error) {
|
||
if (!this.active || generation !== this.generation) return
|
||
if (error.status === 401) { this.stop(); this.unauthorized(); return }
|
||
this.error = error.message
|
||
} finally {
|
||
if (this.active && generation === this.generation) this.timer = setTimeout(() => this.refresh(), 5000)
|
||
}
|
||
},
|
||
async save() {
|
||
if (!this.active || this.busy || !this.snapshot?.editable || !this.passwordValid) return
|
||
const feedback = beginSave()
|
||
clearTimeout(this.timer); this.controller?.abort()
|
||
const generation = ++this.generation
|
||
this.controller = new AbortController(); this.busy = true; this.error = ''; this.saveError = ''; this.notice = ''
|
||
try {
|
||
const value = await requestJson('./api/galaxy/hotspot', { request: { signal: this.controller.signal,
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ revision: this.revision, config: this.draft }),
|
||
} })
|
||
if (!this.active || generation !== this.generation) return
|
||
this.adopt(value, true)
|
||
this.notice = value.config.enabled ? 'Settings saved. Join the network once its status is active.' : 'Hotspot disabled.'
|
||
finishSave(feedback, true)
|
||
} catch (error) {
|
||
finishSave(feedback, false)
|
||
if (!this.active || generation !== this.generation) return
|
||
if (error.status === 401) { this.stop(); this.unauthorized(); return }
|
||
this.saveError = `${error.message} If Wi-Fi disconnected, reconnect and reload to check whether settings saved.`
|
||
} finally {
|
||
if (this.active && generation === this.generation) { this.busy = false; this.timer = setTimeout(() => this.refresh(), 1000) }
|
||
}
|
||
},
|
||
},
|
||
template: `<section class="gx-card gx-panel gx-stack"><div class="gx-section__header"><i class="bi bi-wifi" aria-hidden="true"></i><span class="gx-section__title">Galaxy Wi-Fi Hotspot</span></div>
|
||
<p>Local Galaxy access for iPhone and Android. No Android Auto, Bluetooth browser support, or cellular connection needed.</p>
|
||
<GxState v-if="mode !== 'local'">Connect to your comma to configure the hotspot.</GxState>
|
||
<GxState v-else-if="!snapshot && !error" loading>Loading hotspot settings…</GxState>
|
||
<template v-if="snapshot && draft">
|
||
<GxNotice :busy="snapshot.state === 'starting'" :tone="['error', 'unavailable'].includes(snapshot.state) ? 'warn' : 'info'">Status: {{ snapshot.state }} {{ snapshot.reason }}</GxNotice>
|
||
<GalaxySettingRow feedback="draft" :row="switchRow('Broadcast hotspot automatically', draft.enabled)" :index="0"
|
||
:busy="busy" :disabled="!snapshot.editable" :save-value="(index, value) => { draft.enabled = value === 'On' }" />
|
||
<div class="gx-stack">
|
||
<p>Network name: <strong>{{ snapshot.config.ssid }}</strong></p>
|
||
<p class="gx-note">Fixed to TheGalaxy- plus the last four characters of your comma dongle ID. Once saved, the switch and password survive restarts.</p>
|
||
<label class="gx-field-group">Password <input class="gx-field" :type="showPassword ? 'text' : 'password'" v-model="draft.password" :disabled="busy || !snapshot.editable" maxlength="63" autocomplete="new-password" placeholder="Blank generates a password" @input="saveError = ''" /></label>
|
||
<div class="gx-row gx-row--borderless"><div class="gx-row__info"><span class="gx-row__label">Show password</span></div>
|
||
<label class="gx-switch"><input type="checkbox" role="switch" aria-label="Show password" v-model="showPassword" :disabled="busy || !snapshot.editable"><span class="gx-switch__track"></span><span class="gx-switch__thumb"></span></label></div>
|
||
<p v-if="!passwordValid" class="gx-note">Use a 12–63 character password without spaces, or leave it blank to generate one.</p>
|
||
<div class="gx-settings__controls gx-actions"><button class="gx-btn" type="button" :disabled="!dirty || busy || !snapshot.editable || !passwordValid" @click="save">{{ busy ? 'Saving…' : 'Save hotspot' }}</button></div>
|
||
</div>
|
||
<GxNotice v-if="!snapshot.editable">Park before changing hotspot settings.</GxNotice>
|
||
<p>Join the network and stay connected if warned of no Internet. Open the address below, then bookmark it.</p>
|
||
<div class="gx-actions"><a class="gx-btn gx-btn--tonal" :href="snapshot.url">Open {{ snapshot.url }}</a></div>
|
||
</template>
|
||
<ul class="gx-install-benefits">
|
||
<li><i class="bi bi-power" aria-hidden="true"></i><span>Starts automatically when enabled. Needs the comma on and Wi-Fi available.</span></li>
|
||
<li><i class="bi bi-wifi" aria-hidden="true"></i><span>Works standalone, or shares the channel of connected Wi-Fi or Android Auto.</span></li>
|
||
<li><i class="bi bi-phone" aria-hidden="true"></i><span>Local access without Internet; no Internet sharing.</span></li>
|
||
<li><i class="bi bi-arrow-repeat" aria-hidden="true"></i><span>Network or channel changes may interrupt access. Changing the password or disabling the hotspot disconnects phones.</span></li>
|
||
<li><i class="bi bi-globe" aria-hidden="true"></i><span>The public galaxy.firestar.link address still needs Internet and its tunnel.</span></li>
|
||
</ul>
|
||
<GxNotice v-if="error || saveError" tone="danger">{{ saveError || error }}</GxNotice><GxNotice v-if="notice" tone="success">{{ notice }}</GxNotice>
|
||
</section>`,
|
||
}
|