v0.2.10
RustBox CI / rustbox (push) Canceled after 0s

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-02 09:37:35 +01:00
1 parent 02d0b13ec1
commit db0d230d3b
12 files changed
+1146 -17

No files matched your search

+6 -1
View File
@@ -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 {
+12
View File
@@ -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;
+1 -1
View File
@@ -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",
+3 -3
View File
@@ -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
View File
@@ -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
+105 -7
View File
@@ -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);
+14
View File
@@ -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);