- users.theme and users.theme_mode, added by migrate(); GET /api/me returns them and PATCH /api/me saves them, refusing anything but a plain name and light/dark/auto, since index() writes them into the page's <html> tag. - The page arrives with data-theme and data-choice already on <html> (and data-mode unless Auto), so it is drawn in the account's theme from the start. - A theme a browser kept in localStorage goes up to the account once, the first time an account with none loads the page. - Saves go one at a time, each with the choice as it stands: sent all at once, a quick run through the list could land out of order and keep a theme passed on the way. The browser test caught it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
59 lines
3.2 KiB
TypeScript
59 lines
3.2 KiB
TypeScript
/* ---------------- theme ---------------- */
|
|
// A theme, and for those that come in both, light, dark or Auto, chosen in Settings and kept on
|
|
// the account, so it follows you to another browser or computer. The server writes it onto the
|
|
// page's <html> tag (data-theme, data-choice) so the page is drawn in it from the start. The
|
|
// page gets data-mode, light or dark, which is all the CSS reads: Auto is worked out here, from
|
|
// the system, so no palette is written twice.
|
|
const THEMES: Record<string, {name: string, modes: boolean}> = {
|
|
modern: {name: 'Modern', modes: true},
|
|
classic: {name: 'Classic, the 2004 Mac app', modes: false},
|
|
dracula: {name: 'Dracula', modes: true},
|
|
material: {name: 'Material', modes: true},
|
|
adwaita: {name: 'Adwaita', modes: true},
|
|
flatremix: {name: 'Flat Remix', modes: true},
|
|
paper: {name: 'Paper', modes: false},
|
|
nordic: {name: 'Nordic', modes: true},
|
|
};
|
|
const MODES: Record<string, string> = {auto: 'Auto (matches your system)', light: 'Light', dark: 'Dark'};
|
|
// Before themes came in light and dark, ipx.theme in localStorage held one of these.
|
|
const OLD_THEMES: Record<string, [string, string]> = {dark: ['modern', 'dark'], light: ['modern', 'light'], auto: ['modern', 'auto']};
|
|
const systemDark = window.matchMedia?.('(prefers-color-scheme: dark)');
|
|
const theme = {name: 'modern', mode: 'dark'};
|
|
|
|
/// `save` for a choice made in Settings, which goes to the account; not for applying one.
|
|
function setTheme(name = theme.name, mode = theme.mode, save = false){
|
|
theme.name = THEMES[name] ? name : 'modern';
|
|
theme.mode = MODES[mode] ? mode : 'dark';
|
|
const both = THEMES[theme.name].modes;
|
|
const root = document.documentElement;
|
|
root.dataset.theme = theme.name;
|
|
// A theme with one palette has it whatever the mode; both of those are light.
|
|
root.dataset.mode = !both ? 'light'
|
|
: theme.mode === 'auto' ? (systemDark && !systemDark.matches ? 'light' : 'dark') : theme.mode;
|
|
const sel = $('#stheme'); if(sel) sel.value = theme.name;
|
|
const ms = $('#smode'); if(ms) ms.value = theme.mode;
|
|
const mf = $('#smodefield'); if(mf) mf.hidden = !both;
|
|
if(save) saveTheme();
|
|
}
|
|
|
|
/// One save at a time, each sending the choice as it stands when it goes. Sent as they came,
|
|
/// several at once, a quick run through the list could reach the server out of order and
|
|
/// leave the account on a theme passed on the way.
|
|
let themeSaving = Promise.resolve();
|
|
function saveTheme(){
|
|
themeSaving = themeSaving
|
|
.then(() => api('/api/me', {method: 'PATCH', body: JSON.stringify({theme: theme.name, mode: theme.mode})}))
|
|
.catch(e => toast(`Your theme was not saved: ${e.message}`, true));
|
|
}
|
|
systemDark?.addEventListener?.('change', () => { if(theme.mode === 'auto') setTheme(); });
|
|
(() => {
|
|
const root = document.documentElement;
|
|
if(root.dataset.choice) return setTheme(root.dataset.theme, root.dataset.choice);
|
|
// Nothing on the account yet. A theme this browser kept, from before themes were kept on the
|
|
// account, goes up to it once, so nobody has to choose again.
|
|
let name: string | null = null, mode: string | null = null;
|
|
try{ name = localStorage.getItem('ipx.theme'); mode = localStorage.getItem('ipx.mode'); }catch{}
|
|
if(OLD_THEMES[name]) [name, mode] = OLD_THEMES[name];
|
|
setTheme(name ?? undefined, mode ?? undefined, !!name);
|
|
})();
|