Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
d9300ba19This file is pinned to the selected release. Download the original to open it in your design tool.
import { i18nReady, t } from '../i18n.mjs'
import SimpleBLEClient from './ble-client.mjs'
function errorDetail(error) {
if (error instanceof Error) return error.message
if (error != null && typeof error === 'object' && 'message' in error) return String(error.message)
return String(error)
}
/** Initializes the preference page and permits an injected BLE client for runtime tests. */
export function initializePreferencePage({
document: pageDocument = globalThis.document,
confirm: confirmAction = globalThis.confirm,
client: suppliedClient,
translate: translateText = t,
} = {}) {
const requiredElement = (id) => {
const element = pageDocument.getElementById(id)
if (element == null) throw new Error(`Missing preference element: ${id}`)
return element
}
const form = requiredElement('form-preference')
const connectButton = requiredElement('ble-connect-button')
const disconnectButton = requiredElement('ble-disconnect-button')
const submitButton = requiredElement('preference-submit-button')
const clearWiFiButton = requiredElement('wifi-clear-button')
const connectionStatus = requiredElement('connection-status')
const saveStatus = requiredElement('save-status')
const controls = [...form.querySelectorAll('input, select, textarea, button')]
const preferenceProps = [
'wifi.ssid',
'wifi.password',
'driver.type',
'driver.offsetPan',
'driver.offsetTilt',
'ui.type',
'ui.language',
'tts.type',
'tts.host',
'tts.port',
'tts.token',
'tts.voice',
'tts.volume',
'ai.token',
'ai.context',
]
let submitting = false
const currentValues = new Map()
const dirtyProps = new Set()
const readOnlyProps = new Set()
const setStatus = (element, text, state = '') => {
element.textContent = text
element.dataset.state = state
}
const onCharacteristicValueChanged = ({ prop, value, readOnly = false }) => {
currentValues.set(prop, String(value))
const element = pageDocument.querySelector(`[name="${prop}"]`)
if (readOnly) {
readOnlyProps.add(prop)
dirtyProps.delete(prop)
} else {
readOnlyProps.delete(prop)
}
if (element != null) {
if (!dirtyProps.has(prop)) element.value = value
element.disabled = !client.isConnected() || readOnlyProps.has(prop)
}
}
const client =
suppliedClient ??
new SimpleBLEClient({
deviceName: 'STK',
onCharacteristicValueChanged,
})
const updateView = () => {
const connected = client.isConnected()
connectButton.hidden = connected
disconnectButton.hidden = !connected
controls.forEach((control) => {
control.disabled = !connected || readOnlyProps.has(control.name)
})
setStatus(connectionStatus, translateText(connected ? '接続済み' : '未接続'), connected ? 'success' : '')
}
const markDirty = (event) => {
const prop = event.target?.name
if (preferenceProps.includes(prop) && !readOnlyProps.has(prop)) dirtyProps.add(prop)
}
form.addEventListener('input', markDirty)
form.addEventListener('change', markDirty)
connectButton.addEventListener('click', async () => {
connectButton.disabled = true
setStatus(connectionStatus, translateText('接続中…'))
try {
await client.connect()
} catch (error) {
console.error(error)
setStatus(connectionStatus, translateText('接続できませんでした: {error}', { error: errorDetail(error) }), 'error')
} finally {
connectButton.disabled = false
if (client.isConnected()) updateView()
}
})
disconnectButton.addEventListener('click', async () => {
disconnectButton.disabled = true
try {
await client.disconnect()
} catch (error) {
console.error(error)
setStatus(connectionStatus, translateText('切断できませんでした: {error}', { error: errorDetail(error) }), 'error')
} finally {
disconnectButton.disabled = false
if (!client.isConnected()) updateView()
}
})
clearWiFiButton.addEventListener('click', async () => {
if (!client.isConnected() || submitting) return
if (
typeof confirmAction !== 'function' ||
!confirmAction(translateText('保存済みのSSIDとパスワードを消去しますか?次回はオフラインで起動します。'))
)
return
clearWiFiButton.disabled = true
submitButton.disabled = true
submitting = true
setStatus(saveStatus, translateText('Wi-Fi設定を消去中…'))
try {
const payload = { 'wifi.ssid': '', 'wifi.password': '' }
await client.send({ _batch: payload })
for (const [prop, value] of Object.entries(payload)) {
currentValues.set(prop, value)
dirtyProps.delete(prop)
const element = pageDocument.getElementById(prop)
if (element != null) element.value = value
}
setStatus(saveStatus, translateText('Wi-Fi設定を消去しました。再起動後はオフラインになります。'), 'success')
} catch (error) {
console.error(error)
setStatus(
saveStatus,
translateText('Wi-Fi設定を消去できませんでした: {error}', { error: errorDetail(error) }),
'error'
)
} finally {
submitting = false
clearWiFiButton.disabled = !client.isConnected()
submitButton.disabled = !client.isConnected()
}
})
form.addEventListener('submit', async (event) => {
event.preventDefault()
if (!client.isConnected() || submitting) return
const payload = {}
for (const prop of preferenceProps) {
const value = pageDocument.getElementById(prop)?.value
if (
!readOnlyProps.has(prop) &&
dirtyProps.has(prop) &&
value != null &&
currentValues.get(prop) !== value
) {
payload[prop] = value
}
}
if (Object.keys(payload).length === 0) {
setStatus(saveStatus, translateText('変更する項目がありません。'))
return
}
submitButton.disabled = true
setStatus(saveStatus, translateText('保存中…'))
submitting = true
try {
await client.send({ _batch: payload })
for (const [prop, value] of Object.entries(payload)) {
currentValues.set(prop, value)
dirtyProps.delete(prop)
}
setStatus(saveStatus, translateText('設定を送信しました。'), 'success')
} catch (error) {
console.error(error)
setStatus(saveStatus, translateText('保存できませんでした: {error}', { error: errorDetail(error) }), 'error')
} finally {
submitting = false
submitButton.disabled = !client.isConnected()
}
})
client.onDisconnected = () => {
submitting = false
currentValues.clear()
dirtyProps.clear()
readOnlyProps.clear()
setStatus(saveStatus, translateText('接続が切れました。保存されていない項目を確認してください。'), 'warning')
updateView()
}
updateView()
globalThis.lucide?.createIcons()
return { client, onCharacteristicValueChanged }
}
if (globalThis.document != null) {
globalThis.document.addEventListener('DOMContentLoaded', async () => {
await i18nReady
initializePreferencePage()
})
}