Language and theme follow the account: PATCH /me for the theme, /me's language by default
ci / ui (push) Waiting to run
ci / check (push) Failing after 2s
ci / start (push) Failing after 2s

The server's /api/v2 now has PATCH /me {language, theme} and /me lists its languages and
themes. The theme is the account's and choosing one saves it there (a refusal is said);
a server whose /me lists no themes keeps the earlier choice in the app. The language is the
account's unless Settings chose this computer's or another (kept in the app, as the phone
does); a change of the account's opens the window again in it, once.

Fixtures re-pinned to the server's dev (f7f2ff2), with the new /api/v2 parity ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-10 13:19:44 +00:00
1 parent 151fc5fcca
commit 04736db057
23 files changed
+783 -84

No files matched your search

+76 -32
View File
@@ -1,16 +1,18 @@
// The app's language and theme, as chosen in Settings: kept in this app (localStorage), over the
// system's language and the account's theme. The account's own language and theme are the
// server's to change (`PATCH /me` has neither yet), so a choice here stays on this computer.
// The app's language and theme, as Settings chooses them.
//
// The language is read once, as the page loads (ui/i18n.js, `lembas.language`): every sentence is
// drawn through `t()` as it is needed, so a new one opens the window again. The theme is applied
// at once.
// The language is the account's (`/me`'s `language`) unless Settings chose this computer's or
// another one; that choice is kept in this app (localStorage `lembas.language`), as the phone
// keeps its own. It is read once, as the page loads (ui/i18n.js): every sentence is drawn through
// `t()` as it is needed, so a new language opens the window again.
//
// The theme is the account's, changed with `PATCH /me` so the web interface and the phone follow
// it. A server whose `/me` lists no `themes` cannot be told: there the choice is kept in this app.
import { invoke, letGoAll } from "./bridge.js"
import { invoke, patch, letGoAll } from "./bridge.js"
import { terminalThemeChanged } from "./terminal.js"
const THEMES = ["moria", "shire"]
let account = "moria" // the account's theme, as /me says (a custom one counts as Moria)
const LOOKS = ["moria", "shire"] // the two looks tokens.css draws; any other theme draws as Moria
let me = null // /me, as last read
function read(key) {
try {
@@ -27,21 +29,35 @@ function write(key, value) {
} catch {}
}
/** The theme chosen in this app ("" for the account's). */
export function chosenTheme() {
/** The app's code for a server's language (`pt_BR` -> `pt`), or "" for one the app lacks. */
function known(code) {
const c = String(code || "")
.trim()
.toLowerCase()
.split(/[-_]/)[0]
return Object.prototype.hasOwnProperty.call(LEMBAS_LANGS, c) ? c : ""
}
/** Whether the server takes the theme on `PATCH /me`: its `/me` lists the themes it offers. */
export function accountThemes() {
return Array.isArray(me?.themes) && me.themes.length ? me.themes : null
}
/** The theme chosen in this app, where the server cannot be told ("" for the account's). */
export function localTheme() {
const name = read("lembas.theme")
return THEMES.includes(name) ? name : ""
return !accountThemes() && LOOKS.includes(name) ? name : ""
}
/** The account's theme, as the server says it. */
export function accountTheme() {
return account
return me?.theme || "moria"
}
/** The look the window is drawn in: the one chosen here, else `name` (the account's, or Rust's last). */
/** The look the window is drawn in: `name` (the account's, or Rust's last), or the one kept here. */
export function applyTheme(name) {
name = chosenTheme() || name
if (!THEMES.includes(name)) name = "moria"
name = localTheme() || name
if (!LOOKS.includes(name)) name = "moria"
const root = document.documentElement
root.dataset.theme = name
root.dataset.base = name
@@ -49,29 +65,57 @@ export function applyTheme(name) {
return name
}
/** The account's theme is known (from /me): drawn in it, unless one is chosen here. */
export function noteAccountTheme(name) {
account = THEMES.includes(name) ? name : "moria"
invoke("set_theme", { name: applyTheme(account) })
function paint() {
invoke("set_theme", { name: applyTheme(accountTheme()) })
}
/** Choose a theme here ("" goes back to the account's): drawn at once, and the window painted in it. */
export function chooseTheme(name) {
write("lembas.theme", THEMES.includes(name) ? name : "")
invoke("set_theme", { name: applyTheme(account) })
/**
* `/me` was read: drawn in the account's theme, and, where the language follows the account and
* it is now another, the window opens again in it.
*/
export function noteMe(next) {
me = next || null
paint()
if (!me || !me.language) return
const account = known(me.language)
const before = read("lembas.account-language")
write("lembas.account-language", account)
// Only once it is kept: where it cannot be, opening again would find it unknown again, and again.
if (read("lembas.account-language") !== account) return
if (LEMBAS_CHOICE === "" && account !== before && (account || LEMBAS_SYSTEM_LANG) !== LEMBAS_LANG) reopen()
}
/** The language chosen in this app ("" for the system's). */
/**
* Choose a theme: the account's, through `PATCH /me` (rejects with the server's words); where the
* server cannot be told, kept here ("" goes back to the account's).
*/
export async function chooseTheme(name) {
if (accountThemes()) {
const r = await patch("/me", { theme: name })
me = r && r.theme ? r : { ...me, theme: name }
} else write("lembas.theme", LOOKS.includes(name) ? name : "")
paint()
}
/** Settings' language: "" the account's, "system" this computer's, or a code the app has. */
export function chosenLanguage() {
return typeof LEMBAS_CHOSEN_LANG === "string" ? LEMBAS_CHOSEN_LANG : ""
return LEMBAS_CHOICE === "system" || known(LEMBAS_CHOICE) ? LEMBAS_CHOICE : ""
}
/** Choose a language here ("" goes back to the system's): the window opens again in it. */
export async function chooseLanguage(code) {
if (code === chosenLanguage()) return
write("lembas.language", code && Object.prototype.hasOwnProperty.call(LEMBAS_LANGS, code) ? code : "")
// The replies followed and the shells open are let go first: the page that opens again has
// no handle on them.
/** The account's language, as /me last said it ("" if the app lacks it or the server says none). */
export function accountLanguage() {
return me?.language ? known(me.language) : ""
}
/** Choose the language; the window opens again in it. */
export function chooseLanguage(choice) {
if (choice === chosenLanguage()) return
write("lembas.language", choice === "system" || known(choice) ? choice : "")
return reopen()
}
/** Open the window again: the replies followed and the shells open let go first. */
async function reopen() {
await letGoAll()
location.reload()
}