mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
99c1a2d6c7
The address screen's element helper returns the plain element instead of Object.assign's intersection, which strict mode could not represent; the haptics test reads an optional argument as optional; three exports only their own file used are local now.
133 lines
4.5 KiB
TypeScript
133 lines
4.5 KiB
TypeScript
import { CapacitorHttp } from '@capacitor/core'
|
|
import { SplashScreen } from '@capacitor/splash-screen'
|
|
import { TrekShell } from '../src/native/trekShell'
|
|
import { probeServer, type ProbeFailure, type ProbeGet } from '../src/native/serverAddress'
|
|
import { pickLocale, translator } from './i18n'
|
|
import logoUrl from '../public/icons/icon-white.svg'
|
|
import './shell.css'
|
|
|
|
/**
|
|
* The only page the app ships itself. It asks for a server on first start and
|
|
* stands in when the remembered one cannot be reached; everything else is the
|
|
* UI that server delivers.
|
|
*/
|
|
|
|
const locale = pickLocale(navigator.languages?.length ? navigator.languages : [navigator.language])
|
|
const t = translator(locale)
|
|
document.documentElement.lang = locale
|
|
document.documentElement.dir = locale === 'ar' ? 'rtl' : 'ltr'
|
|
|
|
const root = document.getElementById('shell')!
|
|
|
|
const PROBE_TIMEOUT_MS = 8000
|
|
|
|
// Native HTTP rather than fetch: the probe goes to a server of the user's
|
|
// choosing, which has no reason to answer this page's origin with CORS.
|
|
const get: ProbeGet = async (url) => {
|
|
const response = await CapacitorHttp.get({ url, connectTimeout: PROBE_TIMEOUT_MS, readTimeout: PROBE_TIMEOUT_MS })
|
|
return { status: response.status, data: response.data, url: response.url }
|
|
}
|
|
|
|
function failureMessage(reason: ProbeFailure): string {
|
|
switch (reason) {
|
|
case 'invalid':
|
|
return t('native.connect.errorInvalid')
|
|
case 'unreachable':
|
|
return t('native.connect.errorUnreachable')
|
|
case 'notTrek':
|
|
return t('native.connect.errorNotTrek')
|
|
case 'tooOld':
|
|
return t('native.connect.errorTooOld')
|
|
}
|
|
}
|
|
|
|
function el<K extends keyof HTMLElementTagNameMap>(
|
|
tag: K,
|
|
props: Partial<HTMLElementTagNameMap[K]> = {},
|
|
...children: (Node | string)[]
|
|
): HTMLElementTagNameMap[K] {
|
|
// Typed as the plain element, not as Object.assign's intersection with the
|
|
// props, which strict mode cannot represent for an <input>.
|
|
const node: HTMLElementTagNameMap[K] = document.createElement(tag)
|
|
Object.assign(node, props)
|
|
node.append(...children)
|
|
return node
|
|
}
|
|
|
|
function header(title: string, hint: string) {
|
|
return [
|
|
el('img', { src: logoUrl, alt: '', className: 'shell-logo' }),
|
|
el('h1', { className: 'shell-title' }, title),
|
|
el('p', { className: 'shell-hint' }, hint),
|
|
]
|
|
}
|
|
|
|
function showConnect(prefill = '') {
|
|
const input = el('input', {
|
|
type: 'url',
|
|
className: 'shell-input',
|
|
placeholder: t('native.connect.placeholder'),
|
|
value: prefill,
|
|
autocapitalize: 'off',
|
|
spellcheck: false,
|
|
required: true,
|
|
})
|
|
input.setAttribute('inputmode', 'url')
|
|
input.setAttribute('autocomplete', 'url')
|
|
input.setAttribute('aria-label', t('native.connect.title'))
|
|
const error = el('p', { className: 'shell-error', role: 'alert' })
|
|
const submit = el('button', { type: 'submit', className: 'shell-button' }, t('native.connect.submit'))
|
|
const form = el('form', { className: 'shell-form', noValidate: true }, input, submit, error)
|
|
|
|
form.addEventListener('submit', async (event) => {
|
|
event.preventDefault()
|
|
error.textContent = ''
|
|
input.disabled = true
|
|
submit.disabled = true
|
|
submit.textContent = t('native.connect.checking')
|
|
const result = await probeServer(input.value, get)
|
|
if (result.ok) {
|
|
await TrekShell.setServer({ url: result.url })
|
|
return
|
|
}
|
|
error.textContent = failureMessage(result.reason)
|
|
input.disabled = false
|
|
submit.disabled = false
|
|
submit.textContent = t('native.connect.submit')
|
|
input.focus()
|
|
})
|
|
|
|
root.replaceChildren(...header(t('native.connect.title'), t('native.connect.hint')), form)
|
|
}
|
|
|
|
function showUnreachable(server: string) {
|
|
const retry = el('button', { type: 'button', className: 'shell-button' }, t('native.offline.retry'))
|
|
retry.addEventListener('click', () => {
|
|
retry.disabled = true
|
|
void TrekShell.reload()
|
|
})
|
|
const change = el('button', { type: 'button', className: 'shell-link' }, t('native.changeServer'))
|
|
change.addEventListener('click', () => { void TrekShell.resetServer() })
|
|
|
|
root.replaceChildren(
|
|
...header(t('native.offline.title'), t('native.offline.hint', { server: new URL(server).host })),
|
|
el('div', { className: 'shell-form' }, retry, change),
|
|
)
|
|
}
|
|
|
|
async function start() {
|
|
const { url } = await TrekShell.getServer()
|
|
if (!url) {
|
|
showConnect()
|
|
} else {
|
|
// Only reached when loading the server failed. On iOS the stored copy of
|
|
// its start page brings the offline-capable UI up anyway.
|
|
const { opened } = await TrekShell.openOfflineCopy()
|
|
if (opened) return
|
|
showUnreachable(url)
|
|
}
|
|
await SplashScreen.hide()
|
|
}
|
|
|
|
void start()
|