Settings: language and theme, kept in the app
Appearance in Settings chooses the language (the system's, or one of the nine the app has; the window opens again in it, its replies and shells let go first) and the theme (the account's, or Moria or Shire, drawn at once and the window painted in it). Kept in localStorage on this computer: /api/v2 has no PATCH /me for either yet, so the account's own stay as they are. i18n.js reads the choice and carries each language's own name from the server's catalogue. Tests E5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
9 files changed
+354
-18
No files matched your search
@@ -125,3 +125,10 @@ export async function openTerminal(target, cols, rows, { onOutput, onControl, on
|
||||
export function openLink(url) {
|
||||
return invoke("open_external", { url }).catch(() => {})
|
||||
}
|
||||
|
||||
/** Let go of every reply followed and every shell open (the page is about to load again). */
|
||||
export function letGoAll() {
|
||||
const ids = [...followers.keys()].map((id) => invoke("stream_close", { id }))
|
||||
const shells = [...terminals.keys()].map((id) => invoke("terminal_close", { id }))
|
||||
return Promise.allSettled([...ids, ...shells])
|
||||
}
|
||||
@@ -100,6 +100,7 @@ export const ICONS = {
|
||||
check: "M6.5 10.4l6-6 1.06 1.06L6.5 12.53 2.94 8.97 4 7.9z",
|
||||
bell: ["M8 1.5a4.5 4.5 0 014.5 4.5v3l1.5 2.5H2L3.5 9V6A4.5 4.5 0 018 1.5zM8 3a3 3 0 00-3 3v3.42l-.35.58h6.7L11 9.42V6a3 3 0 00-3-3z", "M6.25 12.5h3.5a1.75 1.75 0 01-3.5 0z"],
|
||||
archive: ["M1.5 2.5h13v3.5h-13zM3 4v.5h10V4z", "M2.5 6.5H4V12h8V6.5h1.5v7h-11z", "M6 7.5h4V9H6z"],
|
||||
contrast: ["M8 1.5a6.5 6.5 0 110 13 6.5 6.5 0 010-13zM8 3a5 5 0 100 10A5 5 0 008 3z", "M8 3a5 5 0 010 10z"],
|
||||
info: ["M8 1.5a6.5 6.5 0 110 13 6.5 6.5 0 010-13zM8 3a5 5 0 100 10A5 5 0 008 3z", "M7.25 7h1.5v4.5h-1.5zM7.25 4.5h1.5V6h-1.5z"],
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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 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.
|
||||
|
||||
import { invoke, 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)
|
||||
|
||||
function read(key) {
|
||||
try {
|
||||
return localStorage.getItem(key) || ""
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
function write(key, value) {
|
||||
try {
|
||||
if (value) localStorage.setItem(key, value)
|
||||
else localStorage.removeItem(key)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** The theme chosen in this app ("" for the account's). */
|
||||
export function chosenTheme() {
|
||||
const name = read("lembas.theme")
|
||||
return THEMES.includes(name) ? name : ""
|
||||
}
|
||||
|
||||
/** The account's theme, as the server says it. */
|
||||
export function accountTheme() {
|
||||
return account
|
||||
}
|
||||
|
||||
/** The look the window is drawn in: the one chosen here, else `name` (the account's, or Rust's last). */
|
||||
export function applyTheme(name) {
|
||||
name = chosenTheme() || name
|
||||
if (!THEMES.includes(name)) name = "moria"
|
||||
const root = document.documentElement
|
||||
root.dataset.theme = name
|
||||
root.dataset.base = name
|
||||
terminalThemeChanged()
|
||||
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) })
|
||||
}
|
||||
|
||||
/** 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) })
|
||||
}
|
||||
|
||||
/** The language chosen in this app ("" for the system's). */
|
||||
export function chosenLanguage() {
|
||||
return typeof LEMBAS_CHOSEN_LANG === "string" ? LEMBAS_CHOSEN_LANG : ""
|
||||
}
|
||||
|
||||
/** 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.
|
||||
await letGoAll()
|
||||
location.reload()
|
||||
}
|
||||
+3
-11
@@ -13,7 +13,8 @@ import { initViewer, onViewer, closeViewer } from "./viewer.js"
|
||||
import { initExplorer, showLocal, showFilesTab } from "./explorer.js"
|
||||
import { initList, refreshList, markCurrent } from "./list.js"
|
||||
import { initChat, openChat, newChat, closeChat, openChatId, chatMenu, statusChanged, draftKind, onThisComputer, modelsChanged, intoComposer } from "./chat.js"
|
||||
import { initTerminal, showTerminal, terminalOpen, terminalAvailable, toggleTerminal, terminalThemeChanged } from "./terminal.js"
|
||||
import { initTerminal, showTerminal, terminalOpen, terminalAvailable, toggleTerminal } from "./terminal.js"
|
||||
import { applyTheme, noteAccountTheme } from "./look.js"
|
||||
import { initAttach, allowAttach } from "./attach.js"
|
||||
import { initPalette, openPalette } from "./palette.js"
|
||||
import { initActivity, showPanel, togglePanel, panelOpen } from "./activity.js"
|
||||
@@ -64,14 +65,6 @@ function thisDevices() {
|
||||
return remembered.slice()
|
||||
}
|
||||
|
||||
function applyTheme(name) {
|
||||
if (name !== "moria" && name !== "shire") name = "moria"
|
||||
const root = document.documentElement
|
||||
root.dataset.theme = name
|
||||
root.dataset.base = name
|
||||
terminalThemeChanged()
|
||||
}
|
||||
|
||||
// --- Views ------------------------------------------------------------------------------------
|
||||
|
||||
function route() {
|
||||
@@ -99,8 +92,7 @@ async function startApp() {
|
||||
started = true
|
||||
try {
|
||||
me = await get("/me")
|
||||
applyTheme(me.theme)
|
||||
invoke("set_theme", { name: me.theme === "shire" ? "shire" : "moria" })
|
||||
noteAccountTheme(me.theme)
|
||||
} catch (err) {
|
||||
$("thread-note").textContent = why(err)
|
||||
$("thread-note").hidden = false
|
||||
|
||||
+30
-1
@@ -1,10 +1,12 @@
|
||||
// Settings: the app's, not the server's (no admin page here, ever -- an admin sees what everyone
|
||||
// sees). Grouped, with the groups listed beside them: the account (the server and the app's
|
||||
// sign-in), the agent and the folders it may work in, notifications, and this release.
|
||||
// sign-in), the agent and the folders it may work in, the language and theme, notifications, and
|
||||
// this release.
|
||||
|
||||
import { $, h, clear, icon, ICONS } from "./dom.js"
|
||||
import { invoke, listen } from "./bridge.js"
|
||||
import { agentBlock, onAgentChange, link } from "./agent.js"
|
||||
import { chosenTheme, chooseTheme, accountTheme, chosenLanguage, chooseLanguage } from "./look.js"
|
||||
|
||||
let folderError = ""
|
||||
let current = () => null
|
||||
@@ -13,6 +15,7 @@ const SECTIONS = [
|
||||
["account", ICONS.user, () => t("Account")],
|
||||
["agent", ICONS.terminal, () => t("Agent")],
|
||||
["folders", ICONS.folder, () => t("Project folders")],
|
||||
["look", ICONS.contrast, () => t("Appearance")],
|
||||
["notify", ICONS.bell, () => t("Notifications")],
|
||||
["about", ICONS.info, () => t("About")],
|
||||
]
|
||||
@@ -118,6 +121,8 @@ export function renderSettings(s) {
|
||||
)
|
||||
}
|
||||
|
||||
box.append(lookSection())
|
||||
|
||||
box.append(
|
||||
section(
|
||||
"notify",
|
||||
@@ -143,6 +148,30 @@ export function renderSettings(s) {
|
||||
)
|
||||
}
|
||||
|
||||
/** The language and the theme: this app's choice over the system's language and the account's theme. */
|
||||
function lookSection() {
|
||||
const named = (code) => LEMBAS_LANGS[code] || code
|
||||
const language = h(
|
||||
"select",
|
||||
{ id: "set-language", "aria-label": t("Language"), onchange: (e) => chooseLanguage(e.target.value) },
|
||||
[["", t("This computer's (%(language)s)", { language: named(LEMBAS_SYSTEM_LANG) })], ...Object.keys(LEMBAS_LANGS).map((code) => [code, named(code)])].map(([v, label]) =>
|
||||
h("option", { value: v, text: label, selected: chosenLanguage() === v, lang: v || undefined }),
|
||||
),
|
||||
)
|
||||
const label = (name) => `${name === "shire" ? "Shire" : "Moria"} — ${name === "shire" ? t("light") : t("dark")}`
|
||||
const theme = h(
|
||||
"select",
|
||||
{ id: "set-theme", "aria-label": t("Theme"), onchange: (e) => chooseTheme(e.target.value) },
|
||||
[["", t("The account's (%(theme)s)", { theme: label(accountTheme()) })], ["moria", label("moria")], ["shire", label("shire")]].map(([v, text]) => h("option", { value: v, text, selected: chosenTheme() === v })),
|
||||
)
|
||||
return section(
|
||||
"look",
|
||||
t("Appearance"),
|
||||
line(t("Language"), `${t("The interface only. Models answer in the language you write to them in, whatever this says.")} ${t("Kept in this app on this computer. The window opens again in the language chosen.")}`, language),
|
||||
line(t("Theme"), t("Kept in this app on this computer; the account's theme stays as it is."), theme),
|
||||
)
|
||||
}
|
||||
|
||||
export function initSettings(getStatus) {
|
||||
current = getStatus
|
||||
listen("remote-error", (e) => {
|
||||
|
||||
Reference in new issue
Block a user