855 lines
53 KiB
JavaScript
855 lines
53 KiB
JavaScript
|
||
function resolveTheme(theme) {
|
||
if (theme !== "system") return theme;
|
||
return matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
||
}
|
||
|
||
const preferenceMap = {
|
||
theme: "theme", interfaceSkin: "interface_skin", presetScaleUnlocked: "preset_scale_unlocked", density: "density", preview: "preview_pane", scale: "ui_scale", language: "language",
|
||
colorPreset: "color_preset", accentColor: "accent_color", backgroundColor: "background_color", panelColor: "panel_color", textColor: "text_color", fontFamily: "font_family", fontSize: "font_size",
|
||
rowHeight: "row_height", folderPaneWidth: "folder_pane_width", messagePaneWidth: "message_pane_width",
|
||
borderRadius: "border_radius", contrast: "contrast", showAvatars: "show_avatars",
|
||
showAttachmentIcons: "show_attachment_icons", showFlags: "show_flags", reduceMotion: "reduce_motion", composeFormat: "compose_format",
|
||
composeAutosaveMs: "compose_autosave_ms", defaultPriority: "default_priority", requestMdnDefault: "request_mdn_default", attachmentReminder: "attachment_reminder", composeFontFamily: "compose_font_family", replyPosition: "reply_position", quoteStyle: "quote_style", forwardMode: "forward_mode", spellcheckEnabled: "spellcheck_enabled",
|
||
dateFormat: "date_format", timeFormat: "time_format", iconSize: "icon_size", toolbarDensity: "toolbar_density", sidebarItemHeight: "sidebar_item_height",
|
||
readerMaxWidth: "reader_max_width", composerWidth: "composer_width", selectionColor: "selection_color", unreadFontWeight: "unread_font_weight", unreadBadgeStyle: "unread_badge_style",
|
||
compactHeaders: "compact_headers", showBorders: "show_borders", scrollbarDensity: "scrollbar_density", monochrome: "monochrome", folderBackground: "folder_background", messageBackground: "message_background", readerBackground: "reader_background",
|
||
};
|
||
const preferenceTimers = new Map();
|
||
let presetPromotionTimer = null;
|
||
let presetPromotionInFlight = false;
|
||
let presetPromotionNeedsAnotherSave = false;
|
||
let presetPromotionActive = false;
|
||
|
||
const interfaceStyleProfiles = {
|
||
"roundcube-classic": {
|
||
label: "Roundcube Classic",
|
||
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 12, rowHeight: 44, folderPaneWidth: 224, messagePaneWidth: 410, borderRadius: 0, contrast: "normal", iconSize: 16, toolbarDensity: "compact", sidebarItemHeight: 24, readerMaxWidth: 0, composerWidth: 900, selectionColor: "#cc3333", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: true, showBorders: true, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
"roundcube-larry": {
|
||
label: "Roundcube Larry",
|
||
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 48, folderPaneWidth: 230, messagePaneWidth: 420, borderRadius: 3, contrast: "normal", iconSize: 17, toolbarDensity: "normal", sidebarItemHeight: 28, readerMaxWidth: 0, composerWidth: 920, selectionColor: "#c7e3ef", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: true, showBorders: true, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
"roundcube-elastic": {
|
||
label: "Roundcube Elastic",
|
||
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 14, rowHeight: 52, folderPaneWidth: 238, messagePaneWidth: 440, borderRadius: 4, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 35, readerMaxWidth: 0, composerWidth: 940, selectionColor: "#ebf8ff", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
clean: {
|
||
label: "Clean",
|
||
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 58, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 8, contrast: "soft", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 32, readerMaxWidth: 980, composerWidth: 940, selectionColor: "#dce9ff", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
gmail: {
|
||
label: "Gmail inspired",
|
||
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 248, messagePaneWidth: 440, borderRadius: 10, contrast: "soft", iconSize: 19, toolbarDensity: "normal", sidebarItemHeight: 34, readerMaxWidth: 980, composerWidth: 960, selectionColor: "#d3e3fd", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
proton: {
|
||
label: "Proton inspired",
|
||
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 242, messagePaneWidth: 430, borderRadius: 9, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 33, readerMaxWidth: 980, composerWidth: 940, selectionColor: "#e7e1ff", unreadFontWeight: 720, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
zoho: {
|
||
label: "Zoho inspired",
|
||
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 52, folderPaneWidth: 236, messagePaneWidth: 420, borderRadius: 6, contrast: "normal", iconSize: 18, toolbarDensity: "compact", sidebarItemHeight: 30, readerMaxWidth: 0, composerWidth: 920, selectionColor: "#e1ebff", unreadFontWeight: 710, unreadBadgeStyle: "dot", compactHeaders: true, showBorders: true, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
outlook: {
|
||
label: "Outlook inspired",
|
||
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 50, folderPaneWidth: 232, messagePaneWidth: 420, borderRadius: 4, contrast: "normal", iconSize: 18, toolbarDensity: "compact", sidebarItemHeight: 30, readerMaxWidth: 0, composerWidth: 940, selectionColor: "#deecf9", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: true, showBorders: true, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
minimal: {
|
||
label: "Minimal",
|
||
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 14, rowHeight: 60, folderPaneWidth: 230, messagePaneWidth: 430, borderRadius: 12, contrast: "soft", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 34, readerMaxWidth: 860, composerWidth: 900, selectionColor: "#ececec", unreadFontWeight: 680, unreadBadgeStyle: "outline", compactHeaders: false, showBorders: false, scrollbarDensity: "hidden", monochrome: true },
|
||
},
|
||
graphite: {
|
||
label: "Graphite",
|
||
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 50, folderPaneWidth: 220, messagePaneWidth: 400, borderRadius: 2, contrast: "normal", iconSize: 17, toolbarDensity: "compact", sidebarItemHeight: 27, readerMaxWidth: 0, composerWidth: 880, selectionColor: "#32373c", unreadFontWeight: 720, unreadBadgeStyle: "solid", compactHeaders: true, showBorders: true, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
fastmail: {
|
||
label: "Fastmail inspired",
|
||
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 240, messagePaneWidth: 430, borderRadius: 7, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 32, readerMaxWidth: 980, composerWidth: 940, selectionColor: "#d9e8ff", unreadFontWeight: 710, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
"apple-mail": {
|
||
label: "Apple Mail inspired",
|
||
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 54, folderPaneWidth: 238, messagePaneWidth: 430, borderRadius: 9, contrast: "soft", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 32, readerMaxWidth: 920, composerWidth: 920, selectionColor: "#dcebff", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
yahoo: {
|
||
label: "Yahoo Mail inspired",
|
||
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 244, messagePaneWidth: 435, borderRadius: 10, contrast: "normal", iconSize: 19, toolbarDensity: "normal", sidebarItemHeight: 33, readerMaxWidth: 960, composerWidth: 950, selectionColor: "#ede3ff", unreadFontWeight: 720, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
hey: {
|
||
label: "HEY inspired",
|
||
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "humanist", fontSize: 14, rowHeight: 62, folderPaneWidth: 250, messagePaneWidth: 450, borderRadius: 12, contrast: "normal", iconSize: 19, toolbarDensity: "spacious", sidebarItemHeight: 36, readerMaxWidth: 900, composerWidth: 930, selectionColor: "#fff0a8", unreadFontWeight: 760, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
nord: {
|
||
label: "Nord",
|
||
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 52, folderPaneWidth: 232, messagePaneWidth: 420, borderRadius: 6, contrast: "soft", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 30, readerMaxWidth: 940, composerWidth: 920, selectionColor: "#d8dee9", unreadFontWeight: 720, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
"high-contrast": {
|
||
label: "High contrast",
|
||
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 14, rowHeight: 60, folderPaneWidth: 240, messagePaneWidth: 440, borderRadius: 2, contrast: "high", iconSize: 20, toolbarDensity: "normal", sidebarItemHeight: 34, readerMaxWidth: 980, composerWidth: 960, selectionColor: "#ffe500", unreadFontWeight: 800, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: true, scrollbarDensity: "normal", monochrome: false },
|
||
},
|
||
github: {
|
||
label: "GitHub inspired",
|
||
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 52, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 6, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 30, readerMaxWidth: 980, composerWidth: 940, selectionColor: "#ddf4ff", unreadFontWeight: 720, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
gitlab: {
|
||
label: "GitLab inspired",
|
||
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 52, folderPaneWidth: 238, messagePaneWidth: 430, borderRadius: 7, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 30, readerMaxWidth: 980, composerWidth: 940, selectionColor: "#eee7ff", unreadFontWeight: 720, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
discord: {
|
||
label: "Discord inspired",
|
||
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 246, messagePaneWidth: 440, borderRadius: 8, contrast: "normal", iconSize: 19, toolbarDensity: "normal", sidebarItemHeight: 34, readerMaxWidth: 980, composerWidth: 950, selectionColor: "#e0e3ff", unreadFontWeight: 720, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
slack: {
|
||
label: "Slack inspired",
|
||
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 244, messagePaneWidth: 435, borderRadius: 8, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 33, readerMaxWidth: 960, composerWidth: 950, selectionColor: "#f3e6f5", unreadFontWeight: 720, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
reddit: {
|
||
label: "Reddit inspired",
|
||
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 52, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 9, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 31, readerMaxWidth: 980, composerWidth: 940, selectionColor: "#fff0e8", unreadFontWeight: 720, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
notion: {
|
||
label: "Notion inspired",
|
||
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 14, rowHeight: 58, folderPaneWidth: 238, messagePaneWidth: 430, borderRadius: 5, contrast: "soft", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 32, readerMaxWidth: 900, composerWidth: 920, selectionColor: "#efefef", unreadFontWeight: 700, unreadBadgeStyle: "outline", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
|
||
},
|
||
};
|
||
|
||
function resolvedInterfaceSkin(prefs = state.prefs) {
|
||
return prefs.interfaceSkin === "default"
|
||
? (root.dataset.defaultInterfaceSkin || "rustbox")
|
||
: (prefs.interfaceSkin || root.dataset.defaultInterfaceSkin || "rustbox");
|
||
}
|
||
|
||
function activeInterfaceStyle(prefs = state.prefs) {
|
||
return interfaceStyleProfiles[resolvedInterfaceSkin(prefs)] || null;
|
||
}
|
||
|
||
const localizedTextNodes = new Map();
|
||
const localizedAttributes = new Map();
|
||
|
||
function translateScopedUiText(locale) {
|
||
for (const [node, original] of localizedTextNodes.entries()) {
|
||
if (node.isConnected) node.nodeValue = original;
|
||
else localizedTextNodes.delete(node);
|
||
}
|
||
for (const [node, originals] of localizedAttributes.entries()) {
|
||
if (!node.isConnected) { localizedAttributes.delete(node); continue; }
|
||
for (const [attribute, original] of Object.entries(originals)) node.setAttribute(attribute, original);
|
||
}
|
||
if (locale !== "pl") return;
|
||
const map = state.translations._text || {};
|
||
if (!map || typeof map !== "object") return;
|
||
$$('[data-i18n-scope]').forEach((scope) => {
|
||
const walker = document.createTreeWalker(scope, NodeFilter.SHOW_TEXT);
|
||
let node;
|
||
while ((node = walker.nextNode())) {
|
||
const parent = node.parentElement;
|
||
if (!parent || parent.closest("script,style,pre,code,[data-i18n-skip]")) continue;
|
||
if (!localizedTextNodes.has(node)) localizedTextNodes.set(node, node.nodeValue);
|
||
const original = localizedTextNodes.get(node) || "";
|
||
const normalized = original.trim().replace(/\s+/g, " ");
|
||
const translated = map[normalized];
|
||
if (!translated) continue;
|
||
const leading = original.match(/^\s*/)?.[0] || "";
|
||
const trailing = original.match(/\s*$/)?.[0] || "";
|
||
node.nodeValue = `${leading}${translated}${trailing}`;
|
||
}
|
||
const nodes = [scope, ...scope.querySelectorAll("[title],[aria-label],[placeholder],[data-placeholder]")];
|
||
for (const node of nodes) {
|
||
const originals = localizedAttributes.get(node) || {};
|
||
for (const attribute of ["title", "aria-label", "placeholder", "data-placeholder"]) {
|
||
if (!node.hasAttribute(attribute)) continue;
|
||
if (!(attribute in originals)) originals[attribute] = node.getAttribute(attribute);
|
||
const translated = map[originals[attribute]];
|
||
if (translated) node.setAttribute(attribute, translated);
|
||
}
|
||
localizedAttributes.set(node, originals);
|
||
}
|
||
});
|
||
}
|
||
|
||
async function loadTranslations(language) {
|
||
const locale = ["en", "pl"].includes(language) ? language : "en";
|
||
try {
|
||
const response = await fetch(`${BASE}/assets/i18n/${locale}.json`, { credentials: "same-origin" });
|
||
state.translations = response.ok ? await response.json() : {};
|
||
} catch (_) { state.translations = {}; }
|
||
root.lang = locale;
|
||
$$('[data-i18n]').forEach((node) => { const key = node.dataset.i18n; if (state.translations[key]) node.textContent = state.translations[key]; });
|
||
$$('[data-i18n-placeholder]').forEach((node) => { const key = node.dataset.i18nPlaceholder; if (state.translations[key]) node.placeholder = state.translations[key]; });
|
||
$$('[data-i18n-title]').forEach((node) => { const key = node.dataset.i18nTitle; if (state.translations[key]) node.title = state.translations[key]; });
|
||
$$('[data-i18n-aria-label]').forEach((node) => { const key = node.dataset.i18nAriaLabel; if (state.translations[key]) node.setAttribute('aria-label', state.translations[key]); });
|
||
translateScopedUiText(locale);
|
||
}
|
||
|
||
function tr(key, fallback = key) {
|
||
return state.translations[key] || fallback;
|
||
}
|
||
|
||
function trText(text) {
|
||
return state.translations?._text?.[text] || text;
|
||
}
|
||
|
||
function applyPreferences() {
|
||
const p = state.prefs;
|
||
const fixedStyle = activeInterfaceStyle(p);
|
||
const visual = fixedStyle
|
||
? { ...p, ...fixedStyle.preferences }
|
||
: p;
|
||
root.dataset.theme = resolveTheme(p.theme);
|
||
root.dataset.interfaceSkin = resolvedInterfaceSkin(p);
|
||
root.dataset.density = visual.density;
|
||
root.dataset.preview = visual.preview;
|
||
root.dataset.scale = visual.scale;
|
||
root.dataset.colorPreset = p.colorPreset;
|
||
root.dataset.fontFamily = visual.fontFamily;
|
||
root.dataset.contrast = visual.contrast;
|
||
root.dataset.hideAvatars = String(!p.showAvatars);
|
||
root.dataset.hideAttachments = String(!p.showAttachmentIcons);
|
||
root.dataset.hideFlags = String(!p.showFlags);
|
||
root.dataset.reduceMotion = String(p.reduceMotion);
|
||
root.dataset.toolbarDensity = visual.toolbarDensity;
|
||
root.dataset.unreadBadge = visual.unreadBadgeStyle;
|
||
root.dataset.compactHeaders = String(visual.compactHeaders);
|
||
root.dataset.showBorders = String(visual.showBorders);
|
||
root.dataset.scrollbarDensity = visual.scrollbarDensity;
|
||
root.dataset.monochrome = String(visual.monochrome);
|
||
root.style.setProperty("--user-accent", p.accentColor);
|
||
root.style.setProperty("--user-bg", p.backgroundColor);
|
||
root.style.setProperty("--user-panel", p.panelColor);
|
||
root.style.setProperty("--user-text", p.textColor);
|
||
const scaleFactor = ({ small: 0.90, normal: 1, large: 1.12 })[visual.scale] || 1;
|
||
const densityFactor = visual.density === "comfortable" ? 1.13 : 0.93;
|
||
root.style.setProperty("--ui-scale-factor", String(scaleFactor));
|
||
root.style.setProperty("--density-factor", String(densityFactor));
|
||
root.style.setProperty("--ui-size", `${Math.round(Number(visual.fontSize || 13) * scaleFactor * 100) / 100}px`);
|
||
root.style.setProperty("--row-height", `${Math.max(42, Math.round(Number(visual.rowHeight || 58) * scaleFactor))}px`);
|
||
root.style.setProperty("--folder-pane-width", `${Math.max(170, Math.round(Number(visual.folderPaneWidth || 228) * scaleFactor))}px`);
|
||
root.style.setProperty("--message-pane-width", `${Math.max(300, Math.round(Number(visual.messagePaneWidth || 420) * scaleFactor))}px`);
|
||
root.style.setProperty("--control-height", `${Math.max(26, Math.round(30 * scaleFactor * densityFactor))}px`);
|
||
root.style.setProperty("--appbar-height", `${Math.max(38, Math.round(44 * scaleFactor * densityFactor))}px`);
|
||
root.style.setProperty("--dialog-header-height", `${Math.max(38, Math.round(43 * scaleFactor * densityFactor))}px`);
|
||
root.style.setProperty("--icon-size", `${Math.max(14, Math.round(Number(visual.iconSize || 18) * scaleFactor))}px`);
|
||
const toolbarFactor = ({ compact: 0.88, normal: 1, spacious: 1.14 })[visual.toolbarDensity] || 1;
|
||
root.style.setProperty("--toolbar-density-factor", String(toolbarFactor));
|
||
root.style.setProperty("--sidebar-item-height", `${Math.max(24, Math.round(Number(visual.sidebarItemHeight || 29) * scaleFactor))}px`);
|
||
root.style.setProperty("--reader-max-width", Number(visual.readerMaxWidth) > 0 ? `${Number(visual.readerMaxWidth)}px` : "none");
|
||
root.style.setProperty("--composer-width", `${Math.max(640, Number(visual.composerWidth || 900))}px`);
|
||
root.style.setProperty("--compose-font-family", composeFontCss(p.composeFontFamily));
|
||
const lockedRoundcubeSkin = Boolean(fixedStyle);
|
||
if (lockedRoundcubeSkin) {
|
||
["--selected", "--folder-bg", "--message-bg", "--reader-bg"].forEach((name) => root.style.removeProperty(name));
|
||
} else {
|
||
root.style.setProperty("--selected", visual.selectionColor || "#28352e");
|
||
root.style.setProperty("--folder-bg", p.folderBackground || "var(--sidebar)");
|
||
root.style.setProperty("--message-bg", p.messageBackground || "var(--panel)");
|
||
root.style.setProperty("--reader-bg", p.readerBackground || "var(--panel)");
|
||
}
|
||
root.style.setProperty("--unread-font-weight", String(Number(visual.unreadFontWeight || 730)));
|
||
root.style.setProperty("--button-pad-x", `${Math.max(8, Math.round(10 * scaleFactor))}px`);
|
||
root.style.setProperty("--button-pad-y", `${Math.max(3, Math.round(5 * scaleFactor * densityFactor))}px`);
|
||
root.style.setProperty("--field-pad-x", `${Math.max(7, Math.round(8 * scaleFactor))}px`);
|
||
root.style.setProperty("--field-pad-y", `${Math.max(3, Math.round(5 * scaleFactor * densityFactor))}px`);
|
||
root.style.setProperty("--radius-control", `${visual.borderRadius}px`);
|
||
root.style.setProperty("--radius-panel", `${Math.min(visual.borderRadius + 1, 13)}px`);
|
||
|
||
const presetCss = fixedStyle ? getComputedStyle(root) : null;
|
||
const presetColor = (name, fallback) => presetCss ? presetCssHex(presetCss, name, fallback) : fallback;
|
||
const displayedAccent = presetColor("--accent", p.accentColor);
|
||
const displayedBackground = presetColor("--bg", p.backgroundColor);
|
||
const displayedPanel = presetColor("--panel", p.panelColor);
|
||
const displayedText = presetColor("--text", p.textColor);
|
||
const displayedFolderBackground = presetColor("--folder-bg", p.folderBackground || "#101310");
|
||
const displayedMessageBackground = presetColor("--message-bg", p.messageBackground || "#181c19");
|
||
const displayedReaderBackground = presetColor("--reader-bg", p.readerBackground || "#181c19");
|
||
const values = {
|
||
"#pref-theme": p.theme, "#pref-interface-skin": p.interfaceSkin, "#pref-density": visual.density, "#pref-preview": visual.preview, "#pref-scale": visual.scale,
|
||
"#pref-language": p.language, "#pref-color-preset": fixedStyle ? "custom" : p.colorPreset, "#pref-accent": displayedAccent,
|
||
"#pref-background": displayedBackground, "#pref-panel": displayedPanel, "#pref-text-color": displayedText,
|
||
"#pref-font-family": visual.fontFamily, "#pref-font-size": visual.fontSize, "#pref-row-height": visual.rowHeight,
|
||
"#pref-folder-width": visual.folderPaneWidth, "#pref-message-width": visual.messagePaneWidth, "#pref-radius": visual.borderRadius,
|
||
"#pref-contrast": visual.contrast, "#pref-date-format": p.dateFormat, "#pref-time-format": p.timeFormat,
|
||
"#compose-pref-format": p.composeFormat, "#compose-pref-autosave": String(p.composeAutosaveMs), "#compose-pref-priority": p.defaultPriority, "#compose-pref-font": p.composeFontFamily, "#compose-pref-reply-position": p.replyPosition, "#compose-pref-quote-style": p.quoteStyle, "#compose-pref-forward-mode": p.forwardMode,
|
||
"#pref-icon-size": visual.iconSize, "#pref-toolbar-density": visual.toolbarDensity, "#pref-sidebar-height": visual.sidebarItemHeight, "#pref-reader-width": String(visual.readerMaxWidth), "#pref-composer-width": visual.composerWidth,
|
||
"#pref-selection-color": visual.selectionColor, "#pref-unread-weight": visual.unreadFontWeight, "#pref-unread-badge": visual.unreadBadgeStyle, "#pref-scrollbar-density": visual.scrollbarDensity,
|
||
};
|
||
Object.entries(values).forEach(([selector, value]) => { const node = $(selector); if (node) node.value = value; });
|
||
const checks = { "#pref-preset-scale-unlocked": p.presetScaleUnlocked, "#pref-avatars": p.showAvatars, "#pref-attachment-icons": p.showAttachmentIcons, "#pref-flags": p.showFlags, "#pref-reduce-motion": p.reduceMotion, "#compose-pref-mdn": p.requestMdnDefault, "#compose-pref-attachment-reminder": p.attachmentReminder, "#compose-pref-spellcheck": p.spellcheckEnabled, "#pref-compact-headers": visual.compactHeaders, "#pref-show-borders": visual.showBorders, "#pref-monochrome": visual.monochrome, "#pref-folder-bg-enabled": Boolean(p.folderBackground), "#pref-message-bg-enabled": Boolean(p.messageBackground), "#pref-reader-bg-enabled": Boolean(p.readerBackground) };
|
||
Object.entries(checks).forEach(([selector, value]) => { const node = $(selector); if (node) node.checked = Boolean(value); });
|
||
const outputs = { "#pref-accent-value": displayedAccent, "#pref-background-value": displayedBackground, "#pref-panel-value": displayedPanel, "#pref-text-color-value": displayedText, "#pref-font-size-value": `${visual.fontSize} px`, "#pref-row-height-value": `${visual.rowHeight} px`, "#pref-folder-width-value": `${visual.folderPaneWidth} px`, "#pref-message-width-value": `${visual.messagePaneWidth} px`, "#pref-radius-value": `${visual.borderRadius} px`, "#pref-icon-size-value": `${visual.iconSize} px`, "#pref-sidebar-height-value": `${visual.sidebarItemHeight} px`, "#pref-composer-width-value": `${visual.composerWidth} px`, "#pref-selection-color-value": visual.selectionColor, "#pref-unread-weight-value": String(visual.unreadFontWeight) };
|
||
Object.entries(outputs).forEach(([selector, value]) => { const node = $(selector); if (node) node.textContent = value; });
|
||
[["#pref-folder-bg", displayedFolderBackground], ["#pref-message-bg", displayedMessageBackground], ["#pref-reader-bg", displayedReaderBackground]].forEach(([selector, value]) => { const node = $(selector); if (node) node.value = value; });
|
||
const customizationLocked = Boolean(fixedStyle);
|
||
[
|
||
"#pref-color-preset", "#pref-accent", "#pref-background", "#pref-panel", "#pref-text-color",
|
||
"#pref-density", "#pref-preview", "#pref-font-family", "#pref-font-size",
|
||
"#pref-row-height", "#pref-folder-width", "#pref-message-width", "#pref-radius", "#pref-contrast",
|
||
"#pref-icon-size", "#pref-toolbar-density", "#pref-sidebar-height", "#pref-reader-width", "#pref-composer-width",
|
||
"#pref-selection-color", "#pref-unread-weight", "#pref-unread-badge", "#pref-scrollbar-density",
|
||
"#pref-compact-headers", "#pref-show-borders", "#pref-monochrome",
|
||
"#pref-folder-bg-enabled", "#pref-folder-bg", "#pref-message-bg-enabled", "#pref-message-bg", "#pref-reader-bg-enabled", "#pref-reader-bg",
|
||
].forEach((selector) => { const node = $(selector); if (node) node.disabled = customizationLocked && !p.presetScaleUnlocked; });
|
||
const scaleControl = $("#pref-scale");
|
||
if (scaleControl) scaleControl.disabled = customizationLocked && !p.presetScaleUnlocked;
|
||
const scaleUnlock = $("#pref-preset-scale-unlocked");
|
||
if (scaleUnlock) scaleUnlock.disabled = !fixedStyle;
|
||
const scaleUnlockRow = $("#pref-preset-scale-unlocked-row");
|
||
if (scaleUnlockRow) scaleUnlockRow.classList.toggle("hidden", !fixedStyle);
|
||
const skinNote = $("#pref-interface-skin-note");
|
||
if (skinNote) {
|
||
const styleLabel = state.prefs.language === "pl" ? (state.translations._text?.[fixedStyle?.label] || fixedStyle?.label) : fixedStyle?.label;
|
||
skinNote.textContent = fixedStyle
|
||
? `${styleLabel} — ${tr("fixed_interface_style_note", "predefined interface style. Enable preset editing below to create a customizable copy on the first change.")}`
|
||
: tr("custom_interface_style_note", "RustBox is fully customizable: colors, spacing, panes, typography and advanced interface controls are available below.");
|
||
}
|
||
}
|
||
|
||
function normalizeServerPreferences(data) {
|
||
if (!data) return;
|
||
state.prefs = {
|
||
...state.prefs,
|
||
theme: data.theme || root.dataset.defaultTheme || "system", interfaceSkin: data.interface_skin || "default", presetScaleUnlocked: Boolean(data.preset_scale_unlocked), density: data.density || "compact", preview: data.preview_pane || "right", composeFormat: data.compose_format || "html",
|
||
composeAutosaveMs: Number(data.compose_autosave_ms || 3000), defaultPriority: data.default_priority || "normal", requestMdnDefault: Boolean(data.request_mdn_default), attachmentReminder: data.attachment_reminder !== false, composeFontFamily: data.compose_font_family || "system", replyPosition: data.reply_position || "above", quoteStyle: data.quote_style || "standard", forwardMode: data.forward_mode || "inline", spellcheckEnabled: data.spellcheck_enabled !== false,
|
||
scale: data.ui_scale || "normal", language: data.language || "en", colorPreset: data.color_preset || "sage",
|
||
accentColor: data.accent_color || "#6f9c86", backgroundColor: data.background_color || "#121513", panelColor: data.panel_color || "#181c19", textColor: data.text_color || "#edf0ec", fontFamily: data.font_family || "system", fontSize: data.font_size || 13,
|
||
rowHeight: data.row_height || 58, folderPaneWidth: data.folder_pane_width || 228, messagePaneWidth: data.message_pane_width || 420,
|
||
borderRadius: data.border_radius ?? 4, contrast: data.contrast || "normal", showAvatars: data.show_avatars !== false,
|
||
showAttachmentIcons: data.show_attachment_icons !== false, showFlags: data.show_flags !== false, reduceMotion: Boolean(data.reduce_motion), dateFormat: data.date_format || "locale", timeFormat: data.time_format || "locale",
|
||
iconSize: Number(data.icon_size || 18), toolbarDensity: data.toolbar_density || "normal", sidebarItemHeight: Number(data.sidebar_item_height || 29), readerMaxWidth: Number(data.reader_max_width || 0), composerWidth: Number(data.composer_width || 900), selectionColor: data.selection_color || "#28352e", unreadFontWeight: Number(data.unread_font_weight || 730), unreadBadgeStyle: data.unread_badge_style || "dot", compactHeaders: Boolean(data.compact_headers), showBorders: data.show_borders !== false, scrollbarDensity: data.scrollbar_density || "thin", monochrome: Boolean(data.monochrome), folderBackground: data.folder_background || "", messageBackground: data.message_background || "", readerBackground: data.reader_background || "",
|
||
};
|
||
}
|
||
|
||
async function loadPreferences() {
|
||
try { normalizeServerPreferences(await request("/settings/display", { method: "GET" })); }
|
||
catch (error) { console.warn("Could not load display preferences", error); }
|
||
applyPreferences();
|
||
await loadTranslations(state.prefs.language);
|
||
}
|
||
|
||
async function loadMailPreferences() {
|
||
try {
|
||
const prefs = await request("/settings/mailbox", { method: "GET" });
|
||
state.mailPrefs = { ...state.mailPrefs, ...prefs };
|
||
state.perPage = Number(state.mailPrefs.messages_per_page || 300);
|
||
const values = {
|
||
"#mail-pref-per-page": String(state.perPage),
|
||
"#mail-pref-read-delay": String(state.mailPrefs.mark_read_delay_ms || 0),
|
||
"#mail-pref-remote-images": state.mailPrefs.remote_images || "ask",
|
||
"#mail-pref-preferred-body": state.mailPrefs.preferred_body || "auto",
|
||
"#mail-pref-thread-mode": state.mailPrefs.thread_mode || "threaded",
|
||
"#mail-pref-sort": state.mailPrefs.default_sort || "date_desc",
|
||
};
|
||
Object.entries(values).forEach(([selector, value]) => { const node = $(selector); if (node) node.value = value; });
|
||
const messageSort = $("#message-sort");
|
||
if (messageSort) messageSort.value = state.mailPrefs.default_sort || "date_desc";
|
||
const collapse = $("#mail-pref-collapse-quotes");
|
||
if (collapse) collapse.checked = state.mailPrefs.collapse_quotes !== false;
|
||
const snippets = $("#mail-pref-show-snippets");
|
||
if (snippets) snippets.checked = state.mailPrefs.show_snippets !== false;
|
||
const flaggedTop = $("#mail-pref-flagged-top");
|
||
if (flaggedTop) flaggedTop.checked = Boolean(state.mailPrefs.flagged_on_top);
|
||
const syncKeywords = $("#mail-pref-sync-keywords");
|
||
if (syncKeywords) syncKeywords.checked = Boolean(state.mailPrefs.sync_imap_keywords);
|
||
} catch (error) { console.warn("Could not load mailbox preferences", error); }
|
||
}
|
||
|
||
async function saveMailPreferences() {
|
||
const payload = {
|
||
messages_per_page: Number($("#mail-pref-per-page").value),
|
||
mark_read_delay_ms: Number($("#mail-pref-read-delay").value),
|
||
remote_images: $("#mail-pref-remote-images").value,
|
||
preferred_body: $("#mail-pref-preferred-body").value,
|
||
thread_mode: $("#mail-pref-thread-mode").value,
|
||
collapse_quotes: $("#mail-pref-collapse-quotes").checked,
|
||
show_snippets: $("#mail-pref-show-snippets").checked,
|
||
default_sort: $("#mail-pref-sort").value,
|
||
flagged_on_top: $("#mail-pref-flagged-top").checked,
|
||
sync_imap_keywords: $("#mail-pref-sync-keywords").checked,
|
||
};
|
||
try {
|
||
state.mailPrefs = await request("/settings/mailbox", { method: "PUT", body: JSON.stringify(payload) });
|
||
state.perPage = Number(state.mailPrefs.messages_per_page || 300);
|
||
toast("Mailbox settings saved");
|
||
if (state.currentDetail) renderMessageDetail(state.currentDetail);
|
||
await loadMessages(true);
|
||
} catch (error) { toast(error.message, "error"); }
|
||
}
|
||
|
||
async function reconcileImapLabels() {
|
||
const button = $("#mail-pref-reconcile-labels");
|
||
const status = $("#mail-pref-reconcile-status");
|
||
if (!$("#mail-pref-sync-keywords")?.checked) {
|
||
toast("Enable IMAP keyword synchronization first", "warning");
|
||
return;
|
||
}
|
||
if (button) button.disabled = true;
|
||
if (status) status.textContent = "Reconciling…";
|
||
try {
|
||
const result = await request("/tags/reconcile", { method: "POST", body: "{}" });
|
||
const added = Number(result.labels_added || 0);
|
||
const removed = Number(result.labels_removed || 0);
|
||
const folders = Number(result.folders_checked || 0);
|
||
const skipped = Number(result.skipped_folders || 0);
|
||
if (status) status.textContent = `${folders} folder(s) checked · +${added} / −${removed}${skipped ? ` · ${skipped} skipped` : ""}`;
|
||
toast(`Labels reconciled: +${added}, −${removed}`, "success");
|
||
await loadTags();
|
||
await loadMessages(true);
|
||
} catch (error) {
|
||
if (status) status.textContent = error.message;
|
||
toast(error.message, "error");
|
||
} finally {
|
||
if (button) button.disabled = false;
|
||
}
|
||
}
|
||
|
||
const presetEditablePreferenceKeys = new Set([
|
||
"colorPreset", "accentColor", "backgroundColor", "panelColor", "textColor",
|
||
"density", "preview", "scale", "fontFamily", "fontSize", "rowHeight",
|
||
"folderPaneWidth", "messagePaneWidth", "borderRadius", "contrast",
|
||
"iconSize", "toolbarDensity", "sidebarItemHeight", "readerMaxWidth", "composerWidth",
|
||
"selectionColor", "unreadFontWeight", "unreadBadgeStyle", "scrollbarDensity",
|
||
"compactHeaders", "showBorders", "monochrome",
|
||
"folderBackground", "messageBackground", "readerBackground",
|
||
]);
|
||
|
||
function presetCssHex(styles, name, fallback) {
|
||
const value = styles.getPropertyValue(name).trim();
|
||
if (/^#[0-9a-f]{6}$/i.test(value)) return value;
|
||
const shortHex = value.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/i);
|
||
if (shortHex) return `#${shortHex.slice(1).map((part) => part + part).join("")}`;
|
||
return fallback;
|
||
}
|
||
|
||
function customAppearancePayload(prefs) {
|
||
const keys = [
|
||
"interfaceSkin", "presetScaleUnlocked", "colorPreset", "accentColor", "backgroundColor", "panelColor", "textColor",
|
||
"density", "preview", "scale", "fontFamily", "fontSize", "rowHeight", "folderPaneWidth", "messagePaneWidth",
|
||
"borderRadius", "contrast", "iconSize", "toolbarDensity", "sidebarItemHeight", "readerMaxWidth", "composerWidth",
|
||
"selectionColor", "unreadFontWeight", "unreadBadgeStyle", "scrollbarDensity", "compactHeaders", "showBorders", "monochrome",
|
||
"folderBackground", "messageBackground", "readerBackground",
|
||
];
|
||
return Object.fromEntries(keys.map((key) => [preferenceMap[key], prefs[key]]).filter(([field]) => field));
|
||
}
|
||
|
||
function schedulePromotedAppearanceSave() {
|
||
if (presetPromotionTimer) clearTimeout(presetPromotionTimer);
|
||
if (presetPromotionInFlight) {
|
||
presetPromotionNeedsAnotherSave = true;
|
||
return;
|
||
}
|
||
presetPromotionTimer = setTimeout(async () => {
|
||
presetPromotionTimer = null;
|
||
presetPromotionInFlight = true;
|
||
presetPromotionNeedsAnotherSave = false;
|
||
try {
|
||
await request("/settings/display", { method: "PUT", body: JSON.stringify(customAppearancePayload(state.prefs)) });
|
||
} catch (error) {
|
||
toast(error.message, "error");
|
||
} finally {
|
||
presetPromotionInFlight = false;
|
||
if (presetPromotionNeedsAnotherSave) schedulePromotedAppearanceSave();
|
||
else presetPromotionActive = false;
|
||
}
|
||
}, 180);
|
||
}
|
||
|
||
function promotePresetToCustom(key, value) {
|
||
const fixedStyle = activeInterfaceStyle(state.prefs);
|
||
if (!fixedStyle || !state.prefs.presetScaleUnlocked || !presetEditablePreferenceKeys.has(key)) return false;
|
||
|
||
const styles = getComputedStyle(root);
|
||
state.prefs = {
|
||
...state.prefs,
|
||
...fixedStyle.preferences,
|
||
interfaceSkin: "rustbox",
|
||
presetScaleUnlocked: false,
|
||
colorPreset: "custom",
|
||
accentColor: presetCssHex(styles, "--accent", state.prefs.accentColor),
|
||
backgroundColor: presetCssHex(styles, "--bg", state.prefs.backgroundColor),
|
||
panelColor: presetCssHex(styles, "--panel", state.prefs.panelColor),
|
||
textColor: presetCssHex(styles, "--text", state.prefs.textColor),
|
||
selectionColor: presetCssHex(styles, "--selected", fixedStyle.preferences.selectionColor || state.prefs.selectionColor),
|
||
folderBackground: presetCssHex(styles, "--folder-bg", state.prefs.folderBackground || "#101310"),
|
||
messageBackground: presetCssHex(styles, "--message-bg", state.prefs.messageBackground || "#181c19"),
|
||
readerBackground: presetCssHex(styles, "--reader-bg", state.prefs.readerBackground || "#181c19"),
|
||
[key]: value,
|
||
};
|
||
presetPromotionActive = true;
|
||
applyPreferences();
|
||
schedulePromotedAppearanceSave();
|
||
return true;
|
||
}
|
||
|
||
function savePreference(key, value) {
|
||
if (promotePresetToCustom(key, value)) return;
|
||
state.prefs[key] = value;
|
||
if (presetPromotionActive && presetEditablePreferenceKeys.has(key)) {
|
||
if (["accentColor", "backgroundColor", "panelColor", "textColor"].includes(key)) state.prefs.colorPreset = "custom";
|
||
applyPreferences();
|
||
schedulePromotedAppearanceSave();
|
||
return;
|
||
}
|
||
if (["accentColor", "backgroundColor", "panelColor", "textColor"].includes(key)) state.prefs.colorPreset = "custom";
|
||
applyPreferences();
|
||
if (key === "language") loadTranslations(value);
|
||
const previousTimer = preferenceTimers.get(key);
|
||
if (previousTimer) clearTimeout(previousTimer);
|
||
const timer = setTimeout(async () => {
|
||
preferenceTimers.delete(key);
|
||
const field = preferenceMap[key];
|
||
if (!field) return;
|
||
const payload = { [field]: state.prefs[key] };
|
||
if (["accentColor", "backgroundColor", "panelColor", "textColor"].includes(key)) payload.color_preset = "custom";
|
||
try { await request("/settings/display", { method: "PUT", body: JSON.stringify(payload) }); }
|
||
catch (error) { toast(error.message, "error"); }
|
||
}, 180);
|
||
preferenceTimers.set(key, timer);
|
||
}
|
||
|
||
async function resetAppearance() {
|
||
const payload = { theme: root.dataset.defaultTheme || "system", interface_skin: "default", preset_scale_unlocked: false, density: "compact", preview_pane: "right", ui_scale: "normal", color_preset: "sage", accent_color: "#6f9c86", background_color: "#121513", panel_color: "#181c19", text_color: "#edf0ec", font_family: "system", font_size: 13, row_height: 58, folder_pane_width: 228, message_pane_width: 420, border_radius: 4, contrast: "normal", show_avatars: true, show_attachment_icons: true, show_flags: true, reduce_motion: false, date_format: "locale", time_format: "locale", icon_size: 18, toolbar_density: "normal", sidebar_item_height: 29, reader_max_width: 0, composer_width: 900, selection_color: "#28352e", unread_font_weight: 730, unread_badge_style: "dot", compact_headers: false, show_borders: true, scrollbar_density: "thin", monochrome: false, folder_background: "", message_background: "", reader_background: "" };
|
||
try { normalizeServerPreferences(await request("/settings/display", { method: "PUT", body: JSON.stringify(payload) })); applyPreferences(); toast("Appearance reset"); }
|
||
catch (error) { toast(error.message, "error"); }
|
||
}
|
||
|
||
|
||
function formatDate(raw, long = false) {
|
||
const date = new Date(raw);
|
||
if (Number.isNaN(date.getTime())) return raw || "";
|
||
const timeOptions = state.prefs.timeFormat === "24h" ? { hour: "2-digit", minute: "2-digit", hour12: false } : state.prefs.timeFormat === "12h" ? { hour: "numeric", minute: "2-digit", hour12: true } : { hour: "2-digit", minute: "2-digit" };
|
||
if (long) {
|
||
if (state.prefs.dateFormat === "iso") return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${date.toLocaleTimeString([], timeOptions)}`;
|
||
const locale = state.prefs.dateFormat === "eu" ? "en-GB" : state.prefs.dateFormat === "us" ? "en-US" : undefined;
|
||
return `${date.toLocaleDateString(locale)} ${date.toLocaleTimeString([], timeOptions)}`;
|
||
}
|
||
const now = new Date();
|
||
if (date.toDateString() === now.toDateString()) return date.toLocaleTimeString([], timeOptions);
|
||
if (state.prefs.dateFormat === "iso") return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
||
if (state.prefs.dateFormat === "eu") return date.toLocaleDateString("en-GB");
|
||
if (state.prefs.dateFormat === "us") return date.toLocaleDateString("en-US");
|
||
if (date.getFullYear() === now.getFullYear()) return date.toLocaleDateString([], { month: "short", day: "numeric" });
|
||
return date.toLocaleDateString([], { year: "numeric", month: "short", day: "numeric" });
|
||
}
|
||
|
||
function formatBytes(value) {
|
||
const bytes = Number(value || 0);
|
||
if (!Number.isFinite(bytes) || bytes < 1) return "0 B";
|
||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||
const index = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
|
||
return `${(bytes / (1024 ** index)).toFixed(index ? 1 : 0)} ${units[index]}`;
|
||
}
|
||
|
||
function initial(value) {
|
||
return (value || "?").trim().charAt(0).toUpperCase() || "?";
|
||
}
|
||
|
||
function showLogin() {
|
||
root.dataset.theme = resolveTheme(root.dataset.defaultTheme || "system");
|
||
root.dataset.interfaceSkin = root.dataset.defaultInterfaceSkin || "rustbox";
|
||
root.dataset.colorPreset = "sage";
|
||
$("#boot").classList.add("hidden");
|
||
$("#app-view").classList.add("hidden");
|
||
$("#login-view").classList.remove("hidden");
|
||
}
|
||
|
||
function showApp() {
|
||
$("#boot").classList.add("hidden");
|
||
$("#login-view").classList.add("hidden");
|
||
$("#app-view").classList.remove("hidden");
|
||
}
|
||
|
||
function finishAuthenticatedStartup({ showLoginToast = false } = {}) {
|
||
state.startupToastsMuted = false;
|
||
armRealtimeNotifications();
|
||
if (showLoginToast) toast(tr("login_success", "Successfully logged in"), "success");
|
||
}
|
||
|
||
function prepareAuthenticatedStartup() {
|
||
state.startupToastsMuted = true;
|
||
resetRealtimeNotificationGate();
|
||
}
|
||
|
||
function settingsTabAvailable(name) {
|
||
if (name === "openpgp") return state.authPolicy.pgpEnabled !== false;
|
||
if (name === "sieve") return state.authPolicy.sieveEnabled !== false;
|
||
if (name === "diagnostics") return state.authPolicy.diagnosticsEnabled !== false;
|
||
if (name === "about") return state.authPolicy.aboutEnabled !== false;
|
||
return true;
|
||
}
|
||
|
||
function applyFeatureAvailability() {
|
||
for (const [name, enabled] of [
|
||
["sieve", state.authPolicy.sieveEnabled !== false],
|
||
["diagnostics", state.authPolicy.diagnosticsEnabled !== false],
|
||
["about", state.authPolicy.aboutEnabled !== false],
|
||
]) {
|
||
$(`.settings-tab[data-tab="${name}"]`)?.classList.toggle("hidden", !enabled);
|
||
$(`.settings-panel[data-panel="${name}"]`)?.toggleAttribute("data-feature-disabled", !enabled);
|
||
}
|
||
$("#diagnostic-sieve-card")?.classList.toggle("hidden", state.authPolicy.sieveEnabled === false);
|
||
$("#quick-filter-btn")?.classList.toggle("hidden", state.authPolicy.sieveEnabled === false);
|
||
}
|
||
|
||
async function loadAuthPolicy() {
|
||
try {
|
||
const policy = await request("/auth/policy", { method: "GET" });
|
||
state.authPolicy = { ...state.authPolicy, ...policy };
|
||
} catch (_) {
|
||
state.authPolicy = { ...state.authPolicy, rememberMeEnabled: true };
|
||
}
|
||
const enabled = state.authPolicy.rememberMeEnabled !== false;
|
||
["#login-remember-row", "#add-account-remember-row"].forEach((selector) => $(selector)?.classList.toggle("hidden", !enabled));
|
||
["#login-remember", "#add-account-remember"].forEach((selector) => {
|
||
const input = $(selector);
|
||
if (!input) return;
|
||
input.disabled = !enabled;
|
||
if (!enabled) input.checked = false;
|
||
});
|
||
["#login-remember-label", "#add-account-remember-label"].forEach((selector) => {
|
||
const label = $(selector);
|
||
if (label) label.textContent = "Keep me signed in";
|
||
});
|
||
}
|
||
|
||
async function loadSessionPolicy() {
|
||
try {
|
||
const policy = await request("/auth/session-policy", { method: "GET" });
|
||
state.authPolicy = { ...state.authPolicy, ...policy };
|
||
} catch (_) {
|
||
state.authPolicy = {
|
||
...state.authPolicy,
|
||
pgpEnabled: false,
|
||
sieveEnabled: false,
|
||
diagnosticsEnabled: false,
|
||
aboutEnabled: false,
|
||
};
|
||
}
|
||
setOpenPgpAvailability(state.authPolicy.pgpEnabled !== false);
|
||
applyFeatureAvailability();
|
||
}
|
||
|
||
async function loadAccounts() {
|
||
try {
|
||
const data = await request("/auth/accounts", { method: "GET" });
|
||
state.accounts = data.accounts || [];
|
||
if (!state.accounts.length) {
|
||
state.accountId = null;
|
||
localStorage.removeItem("rustbox.activeAccount");
|
||
state.folders = [];
|
||
updateUnreadAppBadge();
|
||
showLogin();
|
||
return false;
|
||
}
|
||
if (!state.accountId || !state.accounts.some((account) => account.id === state.accountId)) {
|
||
state.accountId = state.accounts[0].id;
|
||
localStorage.setItem("rustbox.activeAccount", state.accountId);
|
||
}
|
||
const account = state.accounts.find((item) => item.id === state.accountId) || state.accounts[0];
|
||
$("#account-email").textContent = account.email;
|
||
showApp();
|
||
return true;
|
||
} catch (error) {
|
||
console.error(error);
|
||
showLogin();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function renderAccountSettings() {
|
||
const list = $("#account-settings-list");
|
||
if (!list) return;
|
||
list.replaceChildren();
|
||
if (!state.accounts.length) {
|
||
list.append(el("div", "empty-list", "No active accounts."));
|
||
return;
|
||
}
|
||
for (const account of state.accounts) {
|
||
const row = el("div", `account-settings-row ${account.id === state.accountId ? "active" : ""}`);
|
||
const email = el("span", "account-email", account.email || "Account");
|
||
const use = el("button", "button quiet", account.id === state.accountId ? "Current" : "Switch");
|
||
use.type = "button";
|
||
use.disabled = account.id === state.accountId;
|
||
use.addEventListener("click", () => switchAccount(account.id));
|
||
const remove = el("button", "button quiet danger", "Remove");
|
||
remove.type = "button";
|
||
remove.addEventListener("click", () => removeBrowserAccount(account.id));
|
||
row.append(email, use, remove);
|
||
list.append(row);
|
||
}
|
||
}
|
||
|
||
async function loadActiveSessions() {
|
||
const list = $("#account-session-list");
|
||
if (!list) return;
|
||
list.replaceChildren(el("div", "empty-list", "Loading sessions…"));
|
||
try {
|
||
const data = await request("/auth/sessions", { method: "GET" });
|
||
const sessions = data.sessions || [];
|
||
list.replaceChildren();
|
||
if (!sessions.length) {
|
||
list.append(el("div", "empty-list", "No active sessions."));
|
||
return;
|
||
}
|
||
for (const session of sessions) {
|
||
const row = el("div", `account-settings-row ${session.current ? "active" : ""}`);
|
||
const details = el("div", "account-session-details");
|
||
const expiry = Number(session.expiresAt || 0);
|
||
const expiryText = expiry ? ` · expires ${new Date(expiry * 1000).toLocaleString()}` : "";
|
||
const rememberedText = session.remembered ? " · remembered" : "";
|
||
details.append(
|
||
el("strong", "account-email", session.current ? `${session.email} · current` : session.email),
|
||
el("span", "muted", `Created ${new Date(session.createdAt * 1000).toLocaleString()} · active ${new Date(session.lastActive * 1000).toLocaleString()}${expiryText}${rememberedText}`),
|
||
);
|
||
const client = el("span", "muted", session.clientName || "Unknown device");
|
||
if (session.userAgent) client.title = session.userAgent;
|
||
details.append(client);
|
||
const action = el("button", "button quiet danger", session.current ? "Current" : "Revoke");
|
||
action.type = "button";
|
||
action.disabled = Boolean(session.current);
|
||
action.addEventListener("click", () => revokeActiveSession(session.id));
|
||
row.append(details, action);
|
||
list.append(row);
|
||
}
|
||
} catch (error) {
|
||
list.replaceChildren(el("div", "empty-list", error.message));
|
||
}
|
||
}
|
||
|
||
async function revokeActiveSession(id) {
|
||
if (!await appConfirm("Revoke this RustBox session?", { title: "Revoke session", confirmLabel: "Revoke", danger: true })) return;
|
||
try {
|
||
await request(`/auth/sessions/${encodeURIComponent(id)}`, { method: "DELETE" });
|
||
toast("Session revoked");
|
||
await loadActiveSessions();
|
||
} catch (error) { toast(error.message, "error"); }
|
||
}
|
||
|
||
async function revokeOtherSessions() {
|
||
if (!await appConfirm("Revoke every other active RustBox session for this account?", { title: "Revoke other sessions", confirmLabel: "Revoke sessions", danger: true })) return;
|
||
try {
|
||
const result = await request("/auth/sessions/revoke-others", { method: "POST", body: "{}" });
|
||
toast(`Revoked ${result.count || 0} session(s)`);
|
||
await loadActiveSessions();
|
||
} catch (error) { toast(error.message, "error"); }
|
||
}
|
||
|
||
async function reloadActiveAccount() {
|
||
setFolderDrawerOpen(false);
|
||
state.folder = "INBOX";
|
||
state.selected = null;
|
||
state.currentDetail = null;
|
||
state.selectedMessages.clear();
|
||
state.selectionMode = false;
|
||
closeReader();
|
||
await loadSessionPolicy();
|
||
await loadPreferences();
|
||
await loadMailPreferences();
|
||
if (state.authPolicy.pgpEnabled !== false) await loadOpenPgp();
|
||
else setOpenPgpAvailability(false);
|
||
await loadIdentities();
|
||
await loadFolders(true);
|
||
await loadSpecialFolders();
|
||
await loadQuota();
|
||
await loadTags();
|
||
await loadNotificationPreferences();
|
||
await loadTrustedSenders();
|
||
startCalendarReminderWatcher();
|
||
renderAccountSettings();
|
||
}
|
||
|
||
async function switchAccount(accountId) {
|
||
if (!state.accounts.some((account) => account.id === accountId) || accountId === state.accountId) return;
|
||
state.accountId = accountId;
|
||
localStorage.setItem("rustbox.activeAccount", accountId);
|
||
const account = state.accounts.find((item) => item.id === accountId);
|
||
if (account) $("#account-email").textContent = account.email;
|
||
try { await reloadActiveAccount(); } catch (error) { toast(error.message, "error"); }
|
||
}
|
||
|
||
async function removeBrowserAccount(accountId) {
|
||
const account = state.accounts.find((item) => item.id === accountId);
|
||
if (!account || !await appConfirm(`Remove ${account.email} from this browser session?`, { title: "Remove account", confirmLabel: "Remove", danger: true })) return;
|
||
try {
|
||
await request(`/auth/accounts/${encodeURIComponent(accountId)}`, { method: "DELETE" });
|
||
const removingCurrent = accountId === state.accountId;
|
||
await loadAccounts();
|
||
if (!state.accounts.length) return;
|
||
if (removingCurrent) await reloadActiveAccount();
|
||
else renderAccountSettings();
|
||
} catch (error) { toast(error.message, "error"); }
|
||
}
|
||
|
||
async function addBrowserAccount(event) {
|
||
event.preventDefault();
|
||
const errorBox = $("#add-account-error");
|
||
const submit = $("#add-account-form button[type=submit]");
|
||
errorBox.classList.add("hidden");
|
||
setBusy(submit, true);
|
||
try {
|
||
const data = await request("/auth/login", { method: "POST", body: JSON.stringify({ email: $("#add-account-email").value.trim(), password: $("#add-account-password").value, remember: $("#add-account-remember").checked }) });
|
||
state.accountId = data.account.id;
|
||
localStorage.setItem("rustbox.activeAccount", state.accountId);
|
||
$("#add-account-dialog").close();
|
||
$("#add-account-form").reset();
|
||
await loadAccounts();
|
||
await reloadActiveAccount();
|
||
disconnectRealtime();
|
||
connectRealtime();
|
||
toast("Account added");
|
||
} catch (error) {
|
||
errorBox.textContent = error.message;
|
||
errorBox.classList.remove("hidden");
|
||
} finally { setBusy(submit, false); }
|
||
}
|
||
|
||
async function login(event) {
|
||
event.preventDefault();
|
||
const submit = $("#login-form button[type=submit]");
|
||
const errorBox = $("#login-error");
|
||
errorBox.classList.add("hidden");
|
||
setBusy(submit, true);
|
||
prepareAuthenticatedStartup();
|
||
try {
|
||
const data = await request("/auth/login", {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
email: $("#login-email").value.trim(),
|
||
password: $("#login-password").value,
|
||
remember: $("#login-remember").checked,
|
||
}),
|
||
});
|
||
state.accountId = data.account.id;
|
||
localStorage.setItem("rustbox.activeAccount", state.accountId);
|
||
await loadAccounts();
|
||
await loadSessionPolicy();
|
||
await loadPreferences();
|
||
await loadMailPreferences();
|
||
if (state.authPolicy.pgpEnabled !== false) await loadOpenPgp();
|
||
else setOpenPgpAvailability(false);
|
||
await loadIdentities();
|
||
await loadFolders(true);
|
||
await loadSpecialFolders();
|
||
await loadQuota();
|
||
await loadTags();
|
||
await loadNotificationPreferences();
|
||
await loadTrustedSenders();
|
||
startCalendarReminderWatcher();
|
||
connectRealtime();
|
||
finishAuthenticatedStartup({ showLoginToast: true });
|
||
} catch (error) {
|
||
errorBox.textContent = error.message;
|
||
errorBox.classList.remove("hidden");
|
||
} finally {
|
||
setBusy(submit, false);
|
||
}
|
||
}
|
||
|
||
async function logout() {
|
||
disconnectRealtime();
|
||
prepareAuthenticatedStartup();
|
||
if (state.calendarReminderTimer) { clearInterval(state.calendarReminderTimer); state.calendarReminderTimer = null; }
|
||
try { await request("/auth/logout", { method: "POST", body: "{}" }); } catch (_) { }
|
||
state.accountId = null;
|
||
state.accounts = [];
|
||
localStorage.removeItem("rustbox.activeAccount");
|
||
showLogin();
|
||
}
|