1 parent
02d0b13ec1
commit
db0d230d3b
12 files changed
+1146
-17
No files matched your search
@@ -185,11 +185,16 @@ label>span,
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.checkbox-row input {
|
||||
.checkbox-row input[type="checkbox"] {
|
||||
width: 15px;
|
||||
min-width: 15px;
|
||||
max-width: 15px;
|
||||
height: 15px;
|
||||
min-height: 0;
|
||||
flex: 0 0 15px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.checkbox-row span {
|
||||
|
||||
@@ -2304,6 +2304,18 @@ kbd {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pane-background-control input[type="checkbox"] {
|
||||
width: 15px;
|
||||
min-width: 15px;
|
||||
max-width: 15px;
|
||||
height: 15px;
|
||||
min-height: 0;
|
||||
flex: 0 0 15px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
@media (max-width:820px) {
|
||||
.pane-background-grid {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"unavailable": "Unavailable",
|
||||
"show_full_addresses": "Show full addresses",
|
||||
"collapse_addresses": "Collapse addresses",
|
||||
"fixed_interface_style_note": "fixed interface style. Theme still switches light/dark; choose RustBox (custom) to edit colors, spacing and geometry.",
|
||||
"fixed_interface_style_note": "predefined interface style. Enable preset editing below to create a customizable copy on the first change.",
|
||||
"custom_interface_style_note": "RustBox is fully customizable: colors, spacing, panes, typography and advanced interface controls are available below.",
|
||||
"reply": "Reply",
|
||||
"reply_all": "Reply all",
|
||||
|
||||
@@ -601,8 +601,8 @@
|
||||
"Draft saved": "Szkic zapisany",
|
||||
"Uploading files…": "Wysyłanie plików…",
|
||||
"Upload complete": "Wysyłanie zakończone",
|
||||
"Allow UI scaling for predefined style": "Zezwól na skalowanie interfejsu dla gotowego stylu",
|
||||
"When enabled, the UI size control can override the scale defined by the selected preset.": "Po włączeniu ustawienie rozmiaru interfejsu może nadpisać skalę wybranego stylu.",
|
||||
"Allow editing predefined style": "Zezwól na edycję gotowego stylu",
|
||||
"When enabled, the first appearance change creates and saves a RustBox custom copy of the selected preset.": "Po włączeniu pierwsza zmiana wyglądu utworzy i zapisze edytowalną kopię wybranego stylu jako RustBox custom.",
|
||||
"Maximum attachment size": "Maksymalny rozmiar załącznika",
|
||||
"Server-wide limit set with": "Globalny limit serwera ustawiany przez",
|
||||
"in rustbox.toml.": "w rustbox.toml.",
|
||||
@@ -631,7 +631,7 @@
|
||||
"Reddit inspired": "Inspirowany Reddit",
|
||||
"Notion inspired": "Inspirowany Notion"
|
||||
},
|
||||
"fixed_interface_style_note": "stały styl interfejsu. Motyw nadal przełącza jasny/ciemny; wybierz RustBox (niestandardowy), aby edytować kolory, odstępy i geometrię.",
|
||||
"fixed_interface_style_note": "gotowy styl interfejsu. Włącz edycję presetu poniżej, aby przy pierwszej zmianie utworzyć jego edytowalną kopię.",
|
||||
"custom_interface_style_note": "RustBox można w pełni dostosować: kolory, odstępy, panele, typografię i zaawansowane ustawienia interfejsu.",
|
||||
"reply": "Odpowiedz",
|
||||
"reply_all": "Odpowiedz wszystkim",
|
||||
|
||||
+7
-5
@@ -611,9 +611,9 @@
|
||||
</select><small id="pref-interface-skin-note" class="muted">RustBox is fully customizable: colors,
|
||||
spacing, panes and typography.</small></label>
|
||||
<label id="pref-preset-scale-unlocked-row" class="checkbox-row hidden"><input
|
||||
id="pref-preset-scale-unlocked" type="checkbox"><span>Allow UI scaling for predefined
|
||||
style</span><small class="muted">When enabled, the UI size control can override the scale defined by
|
||||
the selected preset.</small></label>
|
||||
id="pref-preset-scale-unlocked" type="checkbox"><span>Allow editing predefined style</span><small
|
||||
class="muted">When enabled, the first appearance change creates and saves a RustBox custom copy of the
|
||||
selected preset.</small></label>
|
||||
<label><span data-i18n="color_preset">Color preset</span><select id="pref-color-preset">
|
||||
<option value="sage">Sage</option>
|
||||
<option value="blue">Blue</option>
|
||||
@@ -886,8 +886,10 @@
|
||||
<label class="checkbox-row"><input id="compose-pref-spellcheck" type="checkbox"><span>Use browser
|
||||
spellcheck while composing</span></label>
|
||||
<div class="compose-limit-summary" aria-label="Compose size limits">
|
||||
<span>Maximum attachment size: <strong id="compose-pref-max-attachment">__RUSTBOX_MAX_ATTACHMENT_MB__ MiB</strong></span>
|
||||
<span>Maximum message size: <strong id="compose-pref-max-message">__RUSTBOX_MAX_MESSAGE_MB__ MiB</strong></span>
|
||||
<span>Maximum attachment size: <strong id="compose-pref-max-attachment">__RUSTBOX_MAX_ATTACHMENT_MB__
|
||||
MiB</strong></span>
|
||||
<span>Maximum message size: <strong id="compose-pref-max-message">__RUSTBOX_MAX_MESSAGE_MB__
|
||||
MiB</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted">Markdown is stored as source text and sent as a safe rendered HTML alternative with a
|
||||
|
||||
@@ -16,6 +16,10 @@ const preferenceMap = {
|
||||
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": {
|
||||
@@ -188,7 +192,7 @@ function applyPreferences() {
|
||||
const p = state.prefs;
|
||||
const fixedStyle = activeInterfaceStyle(p);
|
||||
const visual = fixedStyle
|
||||
? { ...p, ...fixedStyle.preferences, scale: p.presetScaleUnlocked ? p.scale : fixedStyle.preferences.scale }
|
||||
? { ...p, ...fixedStyle.preferences }
|
||||
: p;
|
||||
root.dataset.theme = resolveTheme(p.theme);
|
||||
root.dataset.interfaceSkin = resolvedInterfaceSkin(p);
|
||||
@@ -247,10 +251,19 @@ function applyPreferences() {
|
||||
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": p.colorPreset, "#pref-accent": p.accentColor,
|
||||
"#pref-background": p.backgroundColor, "#pref-panel": p.panelColor, "#pref-text-color": p.textColor,
|
||||
"#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,
|
||||
@@ -261,9 +274,9 @@ function applyPreferences() {
|
||||
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": p.accentColor, "#pref-background-value": p.backgroundColor, "#pref-panel-value": p.panelColor, "#pref-text-color-value": p.textColor, "#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) };
|
||||
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", p.folderBackground || "#101310"], ["#pref-message-bg", p.messageBackground || "#181c19"], ["#pref-reader-bg", p.readerBackground || "#181c19"]].forEach(([selector, value]) => { const node = $(selector); if (node) node.value = 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",
|
||||
@@ -273,7 +286,7 @@ function applyPreferences() {
|
||||
"#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; });
|
||||
].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");
|
||||
@@ -284,7 +297,7 @@ function applyPreferences() {
|
||||
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", "fixed interface style. Theme still switches light/dark; choose RustBox (custom) to edit colors, spacing and geometry.")}`
|
||||
? `${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.");
|
||||
}
|
||||
}
|
||||
@@ -387,8 +400,93 @@ async function reconcileImapLabels() {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -728,6 +728,13 @@ function attachmentContentUrl(folder, uid, attachment) {
|
||||
return `${path}${path.includes("?") ? "&" : "?"}account_id=${encodeURIComponent(state.accountId)}`;
|
||||
}
|
||||
|
||||
function needsStandaloneIosPdfPreview() {
|
||||
const ua = navigator.userAgent || "";
|
||||
const iosDevice = /iPad|iPhone|iPod/i.test(ua);
|
||||
const ipadDesktopMode = navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
|
||||
return iosDevice || ipadDesktopMode;
|
||||
}
|
||||
|
||||
function attachmentPreviewLimitBytes(attachment) {
|
||||
const mime = attachmentMime(attachment);
|
||||
const filename = String(attachment?.filename || "").toLowerCase();
|
||||
@@ -768,6 +775,13 @@ async function openAttachment(folder, uid, attachment) {
|
||||
const declaredMime = attachmentMime(attachment);
|
||||
const filename = String(attachment?.filename || "").toLowerCase();
|
||||
const isPdf = declaredMime === "application/pdf" || filename.endsWith(".pdf");
|
||||
if (isPdf && needsStandaloneIosPdfPreview()) {
|
||||
const previewUrl = attachmentContentUrl(folder, uid, attachment);
|
||||
const previewWindow = window.open(previewUrl, "_blank");
|
||||
if (previewWindow) previewWindow.opener = null;
|
||||
else window.location.assign(previewUrl);
|
||||
return;
|
||||
}
|
||||
const blob = isPdf ? null : await fetchAttachmentBlob(folder, uid, attachment);
|
||||
const mime = isPdf ? "application/pdf" : attachmentMime(attachment, blob);
|
||||
const isHtml = mime.includes("html") || /\.(html?|xhtml)$/i.test(filename);
|
||||
|
||||
Reference in new issue
Block a user