1 parent
a238ff5e23
commit
93762fe9ef
65 files changed
+5420
-2792
No files matched your search
@@ -68,7 +68,7 @@
|
||||
imageEditorBlob: null,
|
||||
draftSaveTimer: null,
|
||||
prefs: {
|
||||
theme: "system", interfaceSkin: "rustbox", density: "compact", preview: "right", scale: "normal", language: "en", composeFormat: "html",
|
||||
theme: root.dataset.defaultTheme || "system", interfaceSkin: "default", presetScaleUnlocked: false, density: "compact", preview: "right", scale: "normal", language: "en", composeFormat: "html",
|
||||
composeAutosaveMs: 3000, defaultPriority: "normal", requestMdnDefault: false, attachmentReminder: true, composeFontFamily: "system", replyPosition: "above", quoteStyle: "standard", forwardMode: "inline", spellcheckEnabled: true,
|
||||
colorPreset: "sage", accentColor: "#6f9c86", backgroundColor: "#121513", panelColor: "#181c19", textColor: "#edf0ec", fontFamily: "system", fontSize: 13,
|
||||
rowHeight: 58, folderPaneWidth: 228, messagePaneWidth: 420, borderRadius: 4,
|
||||
|
||||
+371
-304
@@ -1,333 +1,400 @@
|
||||
/* RustBox UI module: 10-core.js */
|
||||
function setConnectivityState(online, detail = "") {
|
||||
state.networkOnline = Boolean(online);
|
||||
document.body.classList.toggle("network-offline", !state.networkOnline);
|
||||
const banner = $("#offline-banner");
|
||||
if (banner) {
|
||||
banner.classList.toggle("hidden", state.networkOnline);
|
||||
banner.textContent = detail || (navigator.onLine ? "RustBox cannot reach the server. Read-only cached content may still be visible." : "You are offline. RustBox will reconnect when the network returns.");
|
||||
}
|
||||
const label = $("#connection-label");
|
||||
if (!label) return;
|
||||
if (!state.networkOnline) label.textContent = navigator.onLine ? "Reconnecting" : "Offline";
|
||||
else if (["Offline", "Reconnecting", "Connection error"].includes(label.textContent)) label.textContent = "Connected";
|
||||
function setConnectivityState(online, detail = "") {
|
||||
state.networkOnline = Boolean(online);
|
||||
document.body.classList.toggle("network-offline", !state.networkOnline);
|
||||
const banner = $("#offline-banner");
|
||||
if (banner) {
|
||||
banner.classList.toggle("hidden", state.networkOnline);
|
||||
banner.textContent = detail || (navigator.onLine ? "RustBox cannot reach the server. Read-only cached content may still be visible." : "You are offline. RustBox will reconnect when the network returns.");
|
||||
}
|
||||
const label = $("#connection-label");
|
||||
if (!label) return;
|
||||
if (!state.networkOnline) label.textContent = navigator.onLine ? "Reconnecting" : "Offline";
|
||||
else if (["Offline", "Reconnecting", "Connection error"].includes(label.textContent)) label.textContent = "Connected";
|
||||
}
|
||||
|
||||
function listStateStorageKey(folder = state.folder) {
|
||||
return `rustbox.listState.${state.accountId || "anonymous"}.${encodeURIComponent(folder || "INBOX")}`;
|
||||
function listStateStorageKey(folder = state.folder) {
|
||||
return `rustbox.listState.${state.accountId || "anonymous"}.${encodeURIComponent(folder || "INBOX")}`;
|
||||
}
|
||||
|
||||
function lastFolderStorageKey() {
|
||||
return `rustbox.lastFolder.${state.accountId || "anonymous"}`;
|
||||
}
|
||||
|
||||
function activeSearchStorageKey() {
|
||||
return `rustbox.activeSearch.${state.accountId || "anonymous"}`;
|
||||
}
|
||||
|
||||
function saveActiveSearchContext() {
|
||||
if (!state.searchContext) return sessionStorage.removeItem(activeSearchStorageKey());
|
||||
sessionStorage.setItem(activeSearchStorageKey(), JSON.stringify(state.searchContext));
|
||||
}
|
||||
|
||||
function readActiveSearchContext() {
|
||||
try {
|
||||
const parsed = JSON.parse(sessionStorage.getItem(activeSearchStorageKey()) || "null");
|
||||
return parsed && typeof parsed.params === "string" ? parsed : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function lastFolderStorageKey() {
|
||||
return `rustbox.lastFolder.${state.accountId || "anonymous"}`;
|
||||
function clearActiveSearchContext() {
|
||||
state.searchContext = null;
|
||||
state.searchAllResults = null;
|
||||
sessionStorage.removeItem(activeSearchStorageKey());
|
||||
}
|
||||
|
||||
function readMessageListState(folder = state.folder) {
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem(listStateStorageKey(folder)) || "null");
|
||||
if (!parsed || typeof parsed !== "object") return null;
|
||||
return {
|
||||
scrollTop: Math.max(0, Number(parsed.scrollTop) || 0),
|
||||
page: Math.max(0, Number(parsed.page) || 0),
|
||||
};
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function activeSearchStorageKey() {
|
||||
return `rustbox.activeSearch.${state.accountId || "anonymous"}`;
|
||||
function saveMessageListState() {
|
||||
if (!state.accountId) return;
|
||||
const list = $("#message-list");
|
||||
if (!list) return;
|
||||
if (state.searchContext) {
|
||||
state.searchContext.scrollTop = list.scrollTop;
|
||||
saveActiveSearchContext();
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(listStateStorageKey(), JSON.stringify({ scrollTop: list.scrollTop, page: state.page }));
|
||||
localStorage.setItem(lastFolderStorageKey(), state.folder);
|
||||
}
|
||||
|
||||
function saveActiveSearchContext() {
|
||||
if (!state.searchContext) return sessionStorage.removeItem(activeSearchStorageKey());
|
||||
sessionStorage.setItem(activeSearchStorageKey(), JSON.stringify(state.searchContext));
|
||||
}
|
||||
function scheduleMessageListStateSave() {
|
||||
clearTimeout(state.listStateTimer);
|
||||
state.listStateTimer = setTimeout(saveMessageListState, 120);
|
||||
}
|
||||
|
||||
function readActiveSearchContext() {
|
||||
async function request(path, options = {}) {
|
||||
const headers = new Headers(options.headers || {});
|
||||
if (state.accountId) headers.set("X-Active-Account", state.accountId);
|
||||
if (options.body && !(options.body instanceof FormData)) headers.set("Content-Type", "application/json");
|
||||
if (options.method && options.method !== "GET") headers.set("X-Requested-With", "XMLHttpRequest");
|
||||
|
||||
const method = String(options.method || "GET").toUpperCase();
|
||||
const maxAttempts = method === "GET" || method === "HEAD" ? 3 : 1;
|
||||
let lastNetworkError = null;
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
||||
try {
|
||||
const parsed = JSON.parse(sessionStorage.getItem(activeSearchStorageKey()) || "null");
|
||||
return parsed && typeof parsed.params === "string" ? parsed : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearActiveSearchContext() {
|
||||
state.searchContext = null;
|
||||
state.searchAllResults = null;
|
||||
sessionStorage.removeItem(activeSearchStorageKey());
|
||||
}
|
||||
|
||||
function readMessageListState(folder = state.folder) {
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem(listStateStorageKey(folder)) || "null");
|
||||
if (!parsed || typeof parsed !== "object") return null;
|
||||
return {
|
||||
scrollTop: Math.max(0, Number(parsed.scrollTop) || 0),
|
||||
page: Math.max(0, Number(parsed.page) || 0),
|
||||
};
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveMessageListState() {
|
||||
if (!state.accountId) return;
|
||||
const list = $("#message-list");
|
||||
if (!list) return;
|
||||
if (state.searchContext) {
|
||||
state.searchContext.scrollTop = list.scrollTop;
|
||||
saveActiveSearchContext();
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(listStateStorageKey(), JSON.stringify({ scrollTop: list.scrollTop, page: state.page }));
|
||||
localStorage.setItem(lastFolderStorageKey(), state.folder);
|
||||
}
|
||||
|
||||
function scheduleMessageListStateSave() {
|
||||
clearTimeout(state.listStateTimer);
|
||||
state.listStateTimer = setTimeout(saveMessageListState, 120);
|
||||
}
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const headers = new Headers(options.headers || {});
|
||||
if (state.accountId) headers.set("X-Active-Account", state.accountId);
|
||||
if (options.body && !(options.body instanceof FormData)) headers.set("Content-Type", "application/json");
|
||||
if (options.method && options.method !== "GET") headers.set("X-Requested-With", "XMLHttpRequest");
|
||||
|
||||
const method = String(options.method || "GET").toUpperCase();
|
||||
const maxAttempts = method === "GET" || method === "HEAD" ? 3 : 1;
|
||||
let lastNetworkError = null;
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(apiPath(path), {
|
||||
credentials: "include",
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
if ([502, 503, 504].includes(response.status) && attempt + 1 < maxAttempts) {
|
||||
await sleep(250 * (attempt + 1));
|
||||
continue;
|
||||
}
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
const payload = contentType.includes("application/json")
|
||||
? await response.json().catch(() => ({}))
|
||||
: await response.text().catch(() => "");
|
||||
if (!response.ok) {
|
||||
const message = payload?.error?.message || (typeof payload === "string" && payload) || `Request failed (${response.status})`;
|
||||
const error = new Error(message);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
setConnectivityState(true);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.status) throw error;
|
||||
lastNetworkError = error;
|
||||
setConnectivityState(false);
|
||||
if (attempt + 1 < maxAttempts) {
|
||||
await sleep(350 * (attempt + 1));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
const error = new Error(navigator.onLine ? "RustBox server is temporarily unreachable" : "You are offline");
|
||||
error.cause = lastNetworkError;
|
||||
error.network = true;
|
||||
throw error;
|
||||
}
|
||||
|
||||
function uploadRequest(path, formData, onProgress = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", apiPath(path), true);
|
||||
xhr.withCredentials = true;
|
||||
if (state.accountId) xhr.setRequestHeader("X-Active-Account", state.accountId);
|
||||
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
||||
if (typeof onProgress === "function") {
|
||||
xhr.upload.addEventListener("progress", (event) => {
|
||||
if (event.lengthComputable && event.total > 0) onProgress(Math.round((event.loaded / event.total) * 100));
|
||||
});
|
||||
}
|
||||
xhr.addEventListener("error", () => reject(new Error("Upload failed: network error")));
|
||||
xhr.addEventListener("abort", () => reject(new Error("Upload cancelled")));
|
||||
xhr.addEventListener("load", () => {
|
||||
const contentType = xhr.getResponseHeader("content-type") || "";
|
||||
let payload = xhr.responseText || "";
|
||||
if (contentType.includes("application/json")) {
|
||||
try { payload = payload ? JSON.parse(payload) : {}; } catch (_) { payload = {}; }
|
||||
}
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
const message = payload?.error?.message || (typeof payload === "string" && payload) || `Upload failed (${xhr.status})`;
|
||||
const error = new Error(message);
|
||||
error.status = xhr.status;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(payload);
|
||||
const response = await fetch(apiPath(path), {
|
||||
credentials: "include",
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
function toast(message, kind = "info", duration = null, action = null) {
|
||||
const normalized = ["success", "error", "warning", "info"].includes(kind) ? kind : "info";
|
||||
const lifetime = Number(duration) > 0 ? Number(duration) : normalized === "error" ? 6500 : normalized === "warning" ? 5200 : 3800;
|
||||
const node = el("div", `toast toast-${normalized}`);
|
||||
node.setAttribute("role", normalized === "error" ? "alert" : "status");
|
||||
const iconName = normalized === "success" ? "check-circle" : normalized === "error" || normalized === "warning" ? "circle-alert" : "info";
|
||||
const icon = el("span", "toast-icon");
|
||||
icon.innerHTML = svgIcon(iconName);
|
||||
const text = el("span", "toast-message", message);
|
||||
let actionButton = null;
|
||||
if (action?.label && typeof action.onClick === "function") {
|
||||
actionButton = el("button", "toast-action", action.label);
|
||||
actionButton.type = "button";
|
||||
}
|
||||
const countdown = el("span", "toast-countdown");
|
||||
countdown.setAttribute("aria-hidden", "true");
|
||||
const close = el("button", "toast-close");
|
||||
close.type = "button";
|
||||
close.setAttribute("aria-label", "Dismiss notification");
|
||||
close.innerHTML = svgIcon("x");
|
||||
const controls = el("span", "toast-controls");
|
||||
if (actionButton) controls.append(actionButton);
|
||||
controls.append(close);
|
||||
node.append(icon, text, countdown, controls);
|
||||
const openDialogs = $$("dialog[open]");
|
||||
let stack = $("#toast-stack");
|
||||
if (openDialogs.length) {
|
||||
const dialog = openDialogs[openDialogs.length - 1];
|
||||
stack = dialog.querySelector(":scope > .modal-toast-stack");
|
||||
if (!stack) {
|
||||
stack = el("div", "toast-stack modal-toast-stack");
|
||||
stack.setAttribute("aria-live", "polite");
|
||||
dialog.append(stack);
|
||||
if ([502, 503, 504].includes(response.status) && attempt + 1 < maxAttempts) {
|
||||
await sleep(250 * (attempt + 1));
|
||||
continue;
|
||||
}
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
const payload = contentType.includes("application/json")
|
||||
? await response.json().catch(() => ({}))
|
||||
: await response.text().catch(() => "");
|
||||
if (!response.ok) {
|
||||
const message = payload?.error?.message || (typeof payload === "string" && payload) || `Request failed (${response.status})`;
|
||||
const error = new Error(message);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
setConnectivityState(true);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.status) throw error;
|
||||
lastNetworkError = error;
|
||||
setConnectivityState(false);
|
||||
if (attempt + 1 < maxAttempts) {
|
||||
await sleep(350 * (attempt + 1));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
stack.append(node);
|
||||
requestAnimationFrame(() => node.classList.add("visible"));
|
||||
}
|
||||
const error = new Error(navigator.onLine ? "RustBox server is temporarily unreachable" : "You are offline");
|
||||
error.cause = lastNetworkError;
|
||||
error.network = true;
|
||||
throw error;
|
||||
}
|
||||
|
||||
let remaining = lifetime;
|
||||
let startedAt = 0;
|
||||
let timeout = null;
|
||||
let ticker = null;
|
||||
let dismissed = false;
|
||||
const renderCountdown = () => { countdown.textContent = `${Math.max(1, Math.ceil(remaining / 1000))}s`; };
|
||||
const stopTimers = () => { clearTimeout(timeout); clearInterval(ticker); timeout = null; ticker = null; };
|
||||
const dismiss = (immediate = false) => {
|
||||
if (dismissed) return;
|
||||
dismissed = true;
|
||||
stopTimers();
|
||||
if (immediate) return node.remove();
|
||||
node.classList.remove("visible");
|
||||
setTimeout(() => node.remove(), 160);
|
||||
};
|
||||
const pause = () => {
|
||||
if (dismissed || !startedAt) return;
|
||||
remaining = Math.max(0, remaining - (performance.now() - startedAt));
|
||||
startedAt = 0;
|
||||
stopTimers();
|
||||
renderCountdown();
|
||||
};
|
||||
const resume = () => {
|
||||
if (dismissed || remaining <= 0) return dismiss();
|
||||
stopTimers();
|
||||
startedAt = performance.now();
|
||||
renderCountdown();
|
||||
timeout = setTimeout(() => dismiss(), remaining);
|
||||
ticker = setInterval(() => {
|
||||
if (!startedAt) return;
|
||||
const elapsed = performance.now() - startedAt;
|
||||
const visibleRemaining = Math.max(0, remaining - elapsed);
|
||||
countdown.textContent = `${Math.max(1, Math.ceil(visibleRemaining / 1000))}s`;
|
||||
}, 200);
|
||||
};
|
||||
close.addEventListener("click", (event) => {
|
||||
function uploadRequest(path, formData, onProgress = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", apiPath(path), true);
|
||||
xhr.withCredentials = true;
|
||||
if (state.accountId) xhr.setRequestHeader("X-Active-Account", state.accountId);
|
||||
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
||||
if (typeof onProgress === "function") {
|
||||
xhr.upload.addEventListener("progress", (event) => {
|
||||
if (event.lengthComputable && event.total > 0) onProgress(Math.round((event.loaded / event.total) * 100));
|
||||
});
|
||||
}
|
||||
xhr.addEventListener("error", () => reject(new Error("Upload failed: network error")));
|
||||
xhr.addEventListener("abort", () => reject(new Error("Upload cancelled")));
|
||||
xhr.addEventListener("load", () => {
|
||||
const contentType = xhr.getResponseHeader("content-type") || "";
|
||||
let payload = xhr.responseText || "";
|
||||
if (contentType.includes("application/json")) {
|
||||
try { payload = payload ? JSON.parse(payload) : {}; } catch (_) { payload = {}; }
|
||||
}
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
const message = payload?.error?.message || (typeof payload === "string" && payload) || `Upload failed (${xhr.status})`;
|
||||
const error = new Error(message);
|
||||
error.status = xhr.status;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(payload);
|
||||
});
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
function toast(message, kind = "info", duration = null, action = null) {
|
||||
const normalized = ["success", "error", "warning", "info"].includes(kind) ? kind : "info";
|
||||
const lifetime = Number(duration) > 0 ? Number(duration) : normalized === "error" ? 6500 : normalized === "warning" ? 5200 : 3800;
|
||||
const node = el("div", `toast toast-${normalized}`);
|
||||
node.setAttribute("role", normalized === "error" ? "alert" : "status");
|
||||
const iconName = normalized === "success" ? "check-circle" : normalized === "error" || normalized === "warning" ? "circle-alert" : "info";
|
||||
const icon = el("span", "toast-icon");
|
||||
icon.innerHTML = svgIcon(iconName);
|
||||
const text = el("span", "toast-message", message);
|
||||
let actionButton = null;
|
||||
if (action?.label && typeof action.onClick === "function") {
|
||||
actionButton = el("button", "toast-action", action.label);
|
||||
actionButton.type = "button";
|
||||
}
|
||||
const countdown = el("span", "toast-countdown");
|
||||
countdown.setAttribute("aria-hidden", "true");
|
||||
const close = el("button", "toast-close");
|
||||
close.type = "button";
|
||||
close.setAttribute("aria-label", "Dismiss notification");
|
||||
close.innerHTML = svgIcon("x");
|
||||
const controls = el("span", "toast-controls");
|
||||
if (actionButton) controls.append(actionButton);
|
||||
controls.append(close);
|
||||
node.append(icon, text, countdown, controls);
|
||||
const openDialogs = $$("dialog[open]");
|
||||
let stack = $("#toast-stack");
|
||||
if (openDialogs.length) {
|
||||
const dialog = openDialogs[openDialogs.length - 1];
|
||||
stack = dialog.querySelector(":scope > .modal-toast-stack");
|
||||
if (!stack) {
|
||||
stack = el("div", "toast-stack modal-toast-stack");
|
||||
stack.setAttribute("aria-live", "polite");
|
||||
dialog.append(stack);
|
||||
}
|
||||
}
|
||||
stack.append(node);
|
||||
requestAnimationFrame(() => node.classList.add("visible"));
|
||||
|
||||
let remaining = lifetime;
|
||||
let startedAt = 0;
|
||||
let timeout = null;
|
||||
let ticker = null;
|
||||
let dismissed = false;
|
||||
const renderCountdown = () => { countdown.textContent = `${Math.max(1, Math.ceil(remaining / 1000))}s`; };
|
||||
const stopTimers = () => { clearTimeout(timeout); clearInterval(ticker); timeout = null; ticker = null; };
|
||||
const dismiss = (immediate = false) => {
|
||||
if (dismissed) return;
|
||||
dismissed = true;
|
||||
stopTimers();
|
||||
if (immediate) return node.remove();
|
||||
node.classList.remove("visible");
|
||||
setTimeout(() => node.remove(), 160);
|
||||
};
|
||||
const pause = () => {
|
||||
if (dismissed || !startedAt) return;
|
||||
remaining = Math.max(0, remaining - (performance.now() - startedAt));
|
||||
startedAt = 0;
|
||||
stopTimers();
|
||||
renderCountdown();
|
||||
};
|
||||
const resume = () => {
|
||||
if (dismissed || remaining <= 0) return dismiss();
|
||||
stopTimers();
|
||||
startedAt = performance.now();
|
||||
renderCountdown();
|
||||
timeout = setTimeout(() => dismiss(), remaining);
|
||||
ticker = setInterval(() => {
|
||||
if (!startedAt) return;
|
||||
const elapsed = performance.now() - startedAt;
|
||||
const visibleRemaining = Math.max(0, remaining - elapsed);
|
||||
countdown.textContent = `${Math.max(1, Math.ceil(visibleRemaining / 1000))}s`;
|
||||
}, 200);
|
||||
};
|
||||
close.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
dismiss(true);
|
||||
});
|
||||
if (actionButton) {
|
||||
actionButton.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
dismiss(true);
|
||||
try { await action.onClick(); }
|
||||
catch (error) { toast(error?.message || "Action failed", "error"); }
|
||||
});
|
||||
if (actionButton) {
|
||||
actionButton.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
dismiss(true);
|
||||
try { await action.onClick(); }
|
||||
catch (error) { toast(error?.message || "Action failed", "error"); }
|
||||
});
|
||||
}
|
||||
node.addEventListener("mouseenter", pause);
|
||||
node.addEventListener("mouseleave", resume);
|
||||
node.addEventListener("focusin", pause);
|
||||
node.addEventListener("focusout", (event) => { if (!node.contains(event.relatedTarget)) resume(); });
|
||||
resume();
|
||||
}
|
||||
|
||||
function progressToast(message) {
|
||||
const node = el("div", "toast toast-info toast-progress");
|
||||
node.setAttribute("role", "status");
|
||||
const icon = el("span", "toast-icon");
|
||||
icon.innerHTML = svgIcon("paperclip");
|
||||
const content = el("span", "toast-progress-content");
|
||||
const text = el("span", "toast-message", message);
|
||||
const track = el("span", "toast-progress-track");
|
||||
const bar = el("span", "toast-progress-bar");
|
||||
track.append(bar);
|
||||
content.append(text, track);
|
||||
const value = el("span", "toast-progress-value", "0%");
|
||||
const close = el("button", "toast-close");
|
||||
close.type = "button";
|
||||
close.setAttribute("aria-label", "Dismiss notification");
|
||||
close.innerHTML = svgIcon("x");
|
||||
let stack = $("#toast-stack");
|
||||
const openDialogs = $$("dialog[open]");
|
||||
if (openDialogs.length) {
|
||||
const dialog = openDialogs[openDialogs.length - 1];
|
||||
stack = dialog.querySelector(":scope > .modal-toast-stack");
|
||||
if (!stack) {
|
||||
stack = el("div", "toast-stack modal-toast-stack");
|
||||
stack.setAttribute("aria-live", "polite");
|
||||
dialog.append(stack);
|
||||
}
|
||||
node.addEventListener("mouseenter", pause);
|
||||
node.addEventListener("mouseleave", resume);
|
||||
node.addEventListener("focusin", pause);
|
||||
node.addEventListener("focusout", (event) => { if (!node.contains(event.relatedTarget)) resume(); });
|
||||
resume();
|
||||
}
|
||||
node.append(icon, content, value, close);
|
||||
stack.append(node);
|
||||
requestAnimationFrame(() => node.classList.add("visible"));
|
||||
let dismissed = false;
|
||||
const dismiss = (delay = 0) => {
|
||||
if (dismissed) return;
|
||||
dismissed = true;
|
||||
setTimeout(() => {
|
||||
node.classList.remove("visible");
|
||||
setTimeout(() => node.remove(), 160);
|
||||
}, Math.max(0, delay));
|
||||
};
|
||||
close.addEventListener("click", () => dismiss());
|
||||
return {
|
||||
update(percent, label = null) {
|
||||
const normalized = Math.max(0, Math.min(100, Math.round(Number(percent) || 0)));
|
||||
bar.style.width = `${normalized}%`;
|
||||
value.textContent = `${normalized}%`;
|
||||
if (label) text.textContent = label;
|
||||
},
|
||||
complete(label = trText("Upload complete")) {
|
||||
bar.style.width = "100%";
|
||||
value.textContent = "100%";
|
||||
text.textContent = label;
|
||||
node.classList.remove("toast-info");
|
||||
node.classList.add("toast-success");
|
||||
icon.innerHTML = svgIcon("check-circle");
|
||||
dismiss(1200);
|
||||
},
|
||||
fail(label) {
|
||||
text.textContent = label || trText("Upload failed");
|
||||
node.classList.remove("toast-info");
|
||||
node.classList.add("toast-error");
|
||||
icon.innerHTML = svgIcon("circle-alert");
|
||||
dismiss(4800);
|
||||
},
|
||||
dismiss,
|
||||
};
|
||||
}
|
||||
|
||||
function appDialog({ title = "RustBox", message = "", input = null, selectOptions = null, confirmLabel = "OK", cancelLabel = "Cancel", danger = false } = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const dialog = el("dialog", "dialog app-prompt-dialog");
|
||||
const form = el("form", "dialog-card app-prompt-card");
|
||||
form.method = "dialog";
|
||||
const header = el("header", "dialog-header");
|
||||
header.append(el("strong", "", title));
|
||||
const close = el("button", "icon-button");
|
||||
close.type = "button";
|
||||
close.setAttribute("aria-label", cancelLabel);
|
||||
close.innerHTML = svgIcon("x");
|
||||
header.append(close);
|
||||
const body = el("div", "app-prompt-body");
|
||||
if (message) body.append(el("p", "app-prompt-message", message));
|
||||
let control = null;
|
||||
if (Array.isArray(selectOptions)) {
|
||||
control = document.createElement("select");
|
||||
control.className = "app-prompt-control";
|
||||
for (const option of selectOptions) control.add(new Option(option.label ?? option.value, option.value));
|
||||
if (input?.value !== undefined) control.value = String(input.value);
|
||||
body.append(control);
|
||||
} else if (input) {
|
||||
control = input.multiline ? document.createElement("textarea") : document.createElement("input");
|
||||
control.className = "app-prompt-control";
|
||||
if (!input.multiline) control.type = input.type || "text";
|
||||
control.value = input.value ?? "";
|
||||
if (input.placeholder) control.placeholder = input.placeholder;
|
||||
if (input.required) control.required = true;
|
||||
body.append(control);
|
||||
}
|
||||
const actions = el("footer", "dialog-actions app-prompt-actions");
|
||||
const cancel = el("button", "button quiet", cancelLabel);
|
||||
cancel.type = "button";
|
||||
const confirm = el("button", danger ? "button danger" : "button primary", confirmLabel);
|
||||
confirm.type = "submit";
|
||||
actions.append(cancel, confirm);
|
||||
form.append(header, body, actions);
|
||||
dialog.append(form);
|
||||
document.body.append(dialog);
|
||||
function appDialog({ title = "RustBox", message = "", input = null, selectOptions = null, confirmLabel = "OK", cancelLabel = "Cancel", danger = false } = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const dialog = el("dialog", "dialog app-prompt-dialog");
|
||||
const form = el("form", "dialog-card app-prompt-card");
|
||||
form.method = "dialog";
|
||||
const header = el("header", "dialog-header");
|
||||
header.append(el("strong", "", title));
|
||||
const close = el("button", "icon-button");
|
||||
close.type = "button";
|
||||
close.setAttribute("aria-label", cancelLabel);
|
||||
close.innerHTML = svgIcon("x");
|
||||
header.append(close);
|
||||
const body = el("div", "app-prompt-body");
|
||||
if (message) body.append(el("p", "app-prompt-message", message));
|
||||
let control = null;
|
||||
if (Array.isArray(selectOptions)) {
|
||||
control = document.createElement("select");
|
||||
control.className = "app-prompt-control";
|
||||
for (const option of selectOptions) control.add(new Option(option.label ?? option.value, option.value));
|
||||
if (input?.value !== undefined) control.value = String(input.value);
|
||||
body.append(control);
|
||||
} else if (input) {
|
||||
control = input.multiline ? document.createElement("textarea") : document.createElement("input");
|
||||
control.className = "app-prompt-control";
|
||||
if (!input.multiline) control.type = input.type || "text";
|
||||
control.value = input.value ?? "";
|
||||
if (input.placeholder) control.placeholder = input.placeholder;
|
||||
if (input.required) control.required = true;
|
||||
body.append(control);
|
||||
}
|
||||
const actions = el("footer", "dialog-actions app-prompt-actions");
|
||||
const cancel = el("button", "button quiet", cancelLabel);
|
||||
cancel.type = "button";
|
||||
const confirm = el("button", danger ? "button danger" : "button primary", confirmLabel);
|
||||
confirm.type = "submit";
|
||||
actions.append(cancel, confirm);
|
||||
form.append(header, body, actions);
|
||||
dialog.append(form);
|
||||
document.body.append(dialog);
|
||||
|
||||
let settled = false;
|
||||
const finish = (value) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(value);
|
||||
if (dialog.open) dialog.close();
|
||||
else dialog.remove();
|
||||
};
|
||||
close.addEventListener("click", () => finish(null));
|
||||
cancel.addEventListener("click", () => finish(null));
|
||||
dialog.addEventListener("cancel", (event) => { event.preventDefault(); finish(null); });
|
||||
dialog.addEventListener("close", () => { if (!settled) { settled = true; resolve(null); } dialog.remove(); }, { once: true });
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
if (control && !control.reportValidity()) return;
|
||||
finish(control ? control.value : true);
|
||||
});
|
||||
dialog.showModal();
|
||||
setTimeout(() => (control || confirm).focus(), 20);
|
||||
let settled = false;
|
||||
const finish = (value) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(value);
|
||||
if (dialog.open) dialog.close();
|
||||
else dialog.remove();
|
||||
};
|
||||
close.addEventListener("click", () => finish(null));
|
||||
cancel.addEventListener("click", () => finish(null));
|
||||
dialog.addEventListener("cancel", (event) => { event.preventDefault(); finish(null); });
|
||||
dialog.addEventListener("close", () => { if (!settled) { settled = true; resolve(null); } dialog.remove(); }, { once: true });
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
if (control && !control.reportValidity()) return;
|
||||
finish(control ? control.value : true);
|
||||
});
|
||||
}
|
||||
dialog.showModal();
|
||||
setTimeout(() => (control || confirm).focus(), 20);
|
||||
});
|
||||
}
|
||||
|
||||
async function appConfirm(message, { title = "Confirm action", confirmLabel = "Continue", danger = false } = {}) {
|
||||
return (await appDialog({ title, message, confirmLabel, danger })) === true;
|
||||
}
|
||||
async function appConfirm(message, { title = "Confirm action", confirmLabel = "Continue", danger = false } = {}) {
|
||||
return (await appDialog({ title, message, confirmLabel, danger })) === true;
|
||||
}
|
||||
|
||||
function appPrompt(title, { message = "", value = "", placeholder = "", type = "text", confirmLabel = "OK", required = false } = {}) {
|
||||
return appDialog({ title, message, input: { value, placeholder, type, required }, confirmLabel });
|
||||
}
|
||||
function appPrompt(title, { message = "", value = "", placeholder = "", type = "text", confirmLabel = "OK", required = false } = {}) {
|
||||
return appDialog({ title, message, input: { value, placeholder, type, required }, confirmLabel });
|
||||
}
|
||||
|
||||
function appSelect(title, options, { message = "", value = "", confirmLabel = "Choose" } = {}) {
|
||||
return appDialog({ title, message, input: { value }, selectOptions: options, confirmLabel });
|
||||
}
|
||||
function appSelect(title, options, { message = "", value = "", confirmLabel = "Choose" } = {}) {
|
||||
return appDialog({ title, message, input: { value }, selectOptions: options, confirmLabel });
|
||||
}
|
||||
|
||||
function setBusy(button, busy) {
|
||||
if (!button) return;
|
||||
button.disabled = busy;
|
||||
button.setAttribute("aria-busy", String(busy));
|
||||
}
|
||||
function setBusy(button, busy) {
|
||||
if (!button) return;
|
||||
button.disabled = busy;
|
||||
button.setAttribute("aria-busy", String(busy));
|
||||
}
|
||||
|
||||
+622
-511
File diff suppressed because it is too large.
Load diff
+694
-679
File diff suppressed because it is too large.
Load diff
+1153
-1072
File diff suppressed because it is too large.
Load diff
@@ -455,7 +455,7 @@ function showContextMenu(x, y, items) {
|
||||
menu.replaceChildren();
|
||||
for (const item of items) {
|
||||
if (item === "separator") { menu.append(el("div", "separator")); continue; }
|
||||
const button = el("button", item.danger ? "danger" : "", item.label);
|
||||
const button = el("button", item.danger ? "danger" : "", trText(item.label));
|
||||
button.type = "button";
|
||||
button.addEventListener("click", async () => { hideContextMenu(); await item.action(); });
|
||||
menu.append(button);
|
||||
@@ -476,7 +476,7 @@ async function ensureMessageOpen(message) {
|
||||
async function promptFolder(label, exclude = null) {
|
||||
const options = orderedFolders().map((folder) => folder.name).filter((name) => name !== exclude);
|
||||
if (!options.length) return null;
|
||||
return appSelect(label, options.map((name) => ({ value: name, label: name })), { value: options[0], confirmLabel: "Choose folder" });
|
||||
return appSelect(trText(label), options.map((name) => ({ value: name, label: name })), { value: options[0], confirmLabel: trText("Choose folder") });
|
||||
}
|
||||
|
||||
function openMessageContextMenu(x, y, message) {
|
||||
@@ -735,6 +735,7 @@ async function showMessageSource() {
|
||||
const source = await blob.text();
|
||||
const dialog = document.createElement("dialog");
|
||||
dialog.className = "dialog";
|
||||
dialog.dataset.i18nScope = "";
|
||||
const card = el("div", "dialog-card source-card");
|
||||
const header = el("header", "dialog-header");
|
||||
header.append(el("strong", "", "Message source"));
|
||||
@@ -747,6 +748,7 @@ async function showMessageSource() {
|
||||
const download = el("button", "button primary", "Download .eml"); download.type = "button"; download.addEventListener("click", downloadEml);
|
||||
actions.append(copy, download);
|
||||
card.append(header, pre, actions); dialog.append(card); document.body.append(dialog);
|
||||
translateScopedUiText(state.prefs.language);
|
||||
dialog.addEventListener("close", () => dialog.remove(), { once: true });
|
||||
dialog.showModal();
|
||||
} catch (error) { toast(error.message, "error"); }
|
||||
@@ -813,7 +815,7 @@ async function forwardAsAttachment() {
|
||||
try {
|
||||
const blob = await fetchEmlBlob(detail);
|
||||
openCompose("new");
|
||||
$("#compose-title").textContent = "Forward as attachment";
|
||||
$("#compose-title").textContent = trText("Forward as attachment");
|
||||
$("#compose-subject").value = /^fwd:/i.test(detail.subject || "") ? detail.subject : `Fwd: ${detail.subject || ""}`;
|
||||
const safe = String(detail.subject || `message-${detail.uid}`).replace(/[\\/:*?"<>|]+/g, "_").slice(0, 120) || `message-${detail.uid}`;
|
||||
const file = new File([blob], `${safe}.eml`, { type: "message/rfc822" });
|
||||
|
||||
@@ -2,29 +2,46 @@
|
||||
async function loadQuota() {
|
||||
const summary = $("#quota-summary span");
|
||||
const bar = $("#quota-bar");
|
||||
if (!summary || !bar) return;
|
||||
summary.textContent = "Loading quota…";
|
||||
bar.style.width = "0%";
|
||||
const sidebar = $("#folder-quota");
|
||||
const sidebarText = $("#folder-quota-text");
|
||||
const sidebarBar = $("#folder-quota-bar");
|
||||
if (!summary && !sidebarText) return;
|
||||
if (summary) summary.textContent = tr("loading_quota", "Loading quota…");
|
||||
if (bar) bar.style.width = "0%";
|
||||
if (sidebarText) sidebarText.textContent = tr("loading", "Loading…");
|
||||
if (sidebarBar) sidebarBar.style.width = "0%";
|
||||
const setLevel = (level) => {
|
||||
for (const box of [$("#quota-summary"), sidebar]) {
|
||||
box?.classList.toggle("quota-warning", level === "warning");
|
||||
box?.classList.toggle("quota-danger", level === "danger");
|
||||
}
|
||||
};
|
||||
try {
|
||||
const quota = await request("/quota", { method: "GET" });
|
||||
const used = Number(quota.usage_bytes || 0);
|
||||
const limit = Number(quota.limit_bytes || 0);
|
||||
if (limit > 0) {
|
||||
const percent = Math.max(0, Math.min(100, (used / limit) * 100));
|
||||
summary.textContent = `${formatBytes(used)} of ${formatBytes(limit)} (${percent.toFixed(1)}%)`;
|
||||
bar.style.width = `${percent}%`;
|
||||
const quotaBox = $("#quota-summary");
|
||||
if (summary) summary.textContent = `${formatBytes(used)} ${tr("of", "of")} ${formatBytes(limit)} (${percent.toFixed(1)}%)`;
|
||||
if (bar) bar.style.width = `${percent}%`;
|
||||
if (sidebarText) sidebarText.textContent = `${percent.toFixed(0)}% · ${formatBytes(used)} / ${formatBytes(limit)}`;
|
||||
if (sidebarBar) sidebarBar.style.width = `${percent}%`;
|
||||
const level = percent >= 95 ? "danger" : percent >= 80 ? "warning" : "normal";
|
||||
quotaBox?.classList.toggle("quota-warning", level === "warning");
|
||||
quotaBox?.classList.toggle("quota-danger", level === "danger");
|
||||
setLevel(level);
|
||||
if (level !== "normal" && state.quotaWarningLevel !== `${state.accountId}:${level}`) {
|
||||
state.quotaWarningLevel = `${state.accountId}:${level}`;
|
||||
toast(level === "danger" ? `Mailbox storage is ${percent.toFixed(0)}% full` : `Mailbox storage is ${percent.toFixed(0)}% used`, level === "danger" ? "error" : "warning");
|
||||
}
|
||||
} else {
|
||||
summary.textContent = `${formatBytes(used)} used · server limit unavailable`;
|
||||
if (summary) summary.textContent = `${formatBytes(used)} ${tr("used", "used")} · ${tr("server_limit_unavailable", "server limit unavailable")}`;
|
||||
if (sidebarText) sidebarText.textContent = `${formatBytes(used)} ${tr("used", "used")}`;
|
||||
setLevel("normal");
|
||||
}
|
||||
} catch (error) { summary.textContent = `Quota unavailable: ${error.message}`; }
|
||||
} catch (error) {
|
||||
if (summary) summary.textContent = `${tr("quota_unavailable", "Quota unavailable")}: ${error.message}`;
|
||||
if (sidebarText) sidebarText.textContent = tr("unavailable", "Unavailable");
|
||||
setLevel("normal");
|
||||
}
|
||||
}
|
||||
|
||||
function openSettings(tab = "appearance") {
|
||||
|
||||
@@ -61,7 +61,41 @@ function bindPaneResizer(selector, preferenceKey, minPx, maxPx) {
|
||||
});
|
||||
}
|
||||
|
||||
function positionToolbarMenu(menu) {
|
||||
if (!menu?.open) return;
|
||||
const popover = menu.querySelector(".toolbar-menu-popover");
|
||||
const summary = menu.querySelector("summary");
|
||||
if (!popover || !summary) return;
|
||||
if (matchMedia("(max-width: 560px)").matches) {
|
||||
Object.assign(popover.style, { left: "8px", right: "8px", top: "auto", bottom: "8px" });
|
||||
return;
|
||||
}
|
||||
Object.assign(popover.style, { right: "auto", bottom: "auto" });
|
||||
const rect = summary.getBoundingClientRect();
|
||||
const width = Math.max(164, popover.offsetWidth || 164);
|
||||
const height = popover.offsetHeight || 80;
|
||||
const margin = 8;
|
||||
let left = Math.min(rect.left, window.innerWidth - width - margin);
|
||||
left = Math.max(margin, left);
|
||||
let top = rect.bottom + 5;
|
||||
if (top + height > window.innerHeight - margin) top = Math.max(margin, rect.top - height - 5);
|
||||
popover.style.left = `${Math.round(left)}px`;
|
||||
popover.style.top = `${Math.round(top)}px`;
|
||||
}
|
||||
|
||||
function bindToolbarMenus() {
|
||||
const menus = $$(".toolbar-menu");
|
||||
menus.forEach((menu) => menu.addEventListener("toggle", () => {
|
||||
if (!menu.open) return;
|
||||
menus.forEach((other) => { if (other !== menu) other.open = false; });
|
||||
requestAnimationFrame(() => positionToolbarMenu(menu));
|
||||
}));
|
||||
window.addEventListener("resize", () => menus.forEach(positionToolbarMenu), { passive: true });
|
||||
document.addEventListener("scroll", () => menus.forEach(positionToolbarMenu), true);
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
bindToolbarMenus();
|
||||
bindPaneResizer("#folder-resizer", "folderPaneWidth", 170, 420);
|
||||
bindPaneResizer("#message-resizer", "messagePaneWidth", 300, 760);
|
||||
bindRecipientAutocomplete("#compose-to");
|
||||
@@ -95,20 +129,23 @@ function bindEvents() {
|
||||
setComposeMode(next, true);
|
||||
insertIdentitySignature(editor);
|
||||
scheduleDraftSave();
|
||||
updateComposeMessageSize();
|
||||
});
|
||||
$("#compose-from").addEventListener("change", () => { insertIdentitySignature($("#compose-editor")); updateComposePgpUi(true); });
|
||||
$("#compose-from").addEventListener("change", () => { insertIdentitySignature($("#compose-editor")); updateComposePgpUi(true); updateComposeMessageSize(); });
|
||||
["#compose-to", "#compose-cc", "#compose-bcc"].forEach((selector) => $(selector).addEventListener("input", () => {
|
||||
state.pgpRecipientStatus = [];
|
||||
updateComposePgpUi();
|
||||
scheduleDraftSave();
|
||||
updateComposeMessageSize();
|
||||
}));
|
||||
$("#compose-subject").addEventListener("input", scheduleDraftSave);
|
||||
$("#compose-subject").addEventListener("input", () => { scheduleDraftSave(); updateComposeMessageSize(); });
|
||||
["#compose-priority", "#compose-mdn", "#compose-pgp-mode", "#compose-pgp-inline", "#compose-pgp-attach-key", "#compose-pgp-attach-recipient-keys"].forEach((selector) => $(selector).addEventListener("change", async () => {
|
||||
if (selector === "#compose-pgp-attach-recipient-keys" && $(selector).checked) await checkComposePgpRecipients(false, composePgpVisibleRecipients());
|
||||
updateComposePgpUi();
|
||||
scheduleDraftSave();
|
||||
updateComposeMessageSize();
|
||||
}));
|
||||
$("#compose-editor").addEventListener("input", () => { if (state.composeSelectedImage && !state.composeSelectedImage.isConnected) setSelectedInlineImage(null); scheduleDraftSave(); scheduleUnusedInlineAttachmentCleanup(); });
|
||||
$("#compose-editor").addEventListener("input", () => { if (state.composeSelectedImage && !state.composeSelectedImage.isConnected) setSelectedInlineImage(null); scheduleDraftSave(); scheduleUnusedInlineAttachmentCleanup(); updateComposeMessageSize(); });
|
||||
$("#compose-editor").addEventListener("click", (event) => {
|
||||
const image = event.target.closest?.("img[data-rustbox-cid]");
|
||||
setSelectedInlineImage(image || null);
|
||||
@@ -145,8 +182,16 @@ function bindEvents() {
|
||||
$("#flag-btn").addEventListener("click", toggleFlagged);
|
||||
$("#archive-btn").addEventListener("click", archiveCurrent);
|
||||
$("#spam-btn").addEventListener("click", junkCurrent);
|
||||
$("#move-select").addEventListener("change", async (event) => { const target = event.target.value; event.target.value = ""; if (target) await moveCurrentTo(target); });
|
||||
$("#copy-select").addEventListener("change", async (event) => { const target = event.target.value; event.target.value = ""; if (target) await copyCurrentTo(target); });
|
||||
$("#reader-move-action").addEventListener("click", async () => {
|
||||
$("#message-transfer-menu").open = false;
|
||||
const target = await promptFolder("Move to folder", state.currentDetail?.folder || state.folder);
|
||||
if (target) await moveCurrentTo(target);
|
||||
});
|
||||
$("#reader-copy-action").addEventListener("click", async () => {
|
||||
$("#message-transfer-menu").open = false;
|
||||
const target = await promptFolder("Copy to folder", state.currentDetail?.folder || state.folder);
|
||||
if (target) await copyCurrentTo(target);
|
||||
});
|
||||
$("#bulk-select-all").addEventListener("change", (event) => { const source = state.searchAllResults || state.messages; for (const message of source) { const key = messageKey(message); event.target.checked ? state.selectedMessages.add(key) : state.selectedMessages.delete(key); } renderMessages(); });
|
||||
$("#bulk-read").addEventListener("click", () => performBulk("read"));
|
||||
$("#bulk-unread").addEventListener("click", () => performBulk("unread"));
|
||||
@@ -221,11 +266,13 @@ function bindEvents() {
|
||||
$("#rename-folder-btn").addEventListener("click", renameFolder);
|
||||
$("#delete-folder-btn").addEventListener("click", deleteFolder);
|
||||
$("#subscribe-folder-btn").addEventListener("click", toggleCurrentFolderSubscription);
|
||||
$("#headers-btn").addEventListener("click", showHeaders);
|
||||
$("#source-btn").addEventListener("click", showMessageSource);
|
||||
$("#headers-btn").addEventListener("click", () => { $("#message-details-menu").open = false; showHeaders(); });
|
||||
$("#source-btn").addEventListener("click", async () => { $("#message-details-menu").open = false; await showMessageSource(); });
|
||||
$("#quick-filter-btn").addEventListener("click", () => createFilterFromCurrentMessage("sender"));
|
||||
$("#reader-from").addEventListener("click", (event) => { const rect = event.currentTarget.getBoundingClientRect(); openCurrentSenderMenu(rect.left, rect.bottom + 4); });
|
||||
$("#reader-from").addEventListener("keydown", (event) => { if (["Enter", " "].includes(event.key)) { event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); openCurrentSenderMenu(rect.left, rect.bottom + 4); } });
|
||||
$("#reader-to").addEventListener("click", toggleReaderAddressExpansion);
|
||||
$("#reader-to").addEventListener("keydown", (event) => { if (["Enter", " "].includes(event.key)) { event.preventDefault(); toggleReaderAddressExpansion(); } });
|
||||
$("#print-btn").addEventListener("click", printCurrentMessage);
|
||||
$("#pgp-reader-process").addEventListener("click", processCurrentPgpMessage);
|
||||
$("#pgp-reader-discover").addEventListener("click", discoverCurrentPgpSigner);
|
||||
@@ -250,6 +297,7 @@ function bindEvents() {
|
||||
$$(".settings-tab").forEach((button) => button.addEventListener("click", () => { selectSettingsTab(button.dataset.tab); if (button.dataset.tab === "sieve") loadSieve(); if (button.dataset.tab === "identities") { loadIdentities(); loadOpenPgp(); } if (button.dataset.tab === "openpgp") loadOpenPgp(); if (button.dataset.tab === "notifications") loadNotificationPreferences(); if (button.dataset.tab === "mailbox") loadMailPreferences(); if (button.dataset.tab === "composing") loadResponseTemplates(); if (button.dataset.tab === "diagnostics") loadDiagnostics(); if (button.dataset.tab === "account") { renderAccountSettings(); loadActiveSessions(); } if (button.dataset.tab === "folders") { loadSpecialFolders(); loadQuota(); } }));
|
||||
$("#pref-theme").addEventListener("change", (event) => savePreference("theme", event.target.value));
|
||||
$("#pref-interface-skin").addEventListener("change", (event) => savePreference("interfaceSkin", event.target.value));
|
||||
$("#pref-preset-scale-unlocked").addEventListener("change", (event) => savePreference("presetScaleUnlocked", event.target.checked));
|
||||
$("#pref-density").addEventListener("change", (event) => savePreference("density", event.target.value));
|
||||
$("#pref-preview").addEventListener("change", (event) => savePreference("preview", event.target.value));
|
||||
$("#pref-scale").addEventListener("change", (event) => savePreference("scale", event.target.value));
|
||||
@@ -396,7 +444,10 @@ function bindEvents() {
|
||||
$("#sieve-deactivate").addEventListener("click", deactivateSieve);
|
||||
$("#sieve-delete").addEventListener("click", deleteSieve);
|
||||
bindEditor();
|
||||
document.addEventListener("click", (event) => { if (!event.target.closest("#context-menu")) hideContextMenu(); });
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!event.target.closest("#context-menu")) hideContextMenu();
|
||||
if (!event.target.closest(".toolbar-menu")) $$(".toolbar-menu[open]").forEach((menu) => { menu.open = false; });
|
||||
});
|
||||
window.addEventListener("blur", hideContextMenu);
|
||||
document.addEventListener("scroll", hideContextMenu, true);
|
||||
document.addEventListener("keydown", (event) => {
|
||||
@@ -438,7 +489,10 @@ function bindEvents() {
|
||||
else closeReader();
|
||||
}
|
||||
});
|
||||
matchMedia("(prefers-color-scheme: light)").addEventListener("change", () => { if (state.prefs.theme === "system") applyPreferences(); });
|
||||
matchMedia("(prefers-color-scheme: light)").addEventListener("change", () => {
|
||||
if (!$("#login-view").classList.contains("hidden")) showLogin();
|
||||
else if (state.prefs.theme === "system") applyPreferences();
|
||||
});
|
||||
window.addEventListener("resize", () => { if (innerWidth > 820) setFolderDrawerOpen(false); });
|
||||
}
|
||||
|
||||
@@ -447,7 +501,7 @@ async function boot() {
|
||||
bindEvents();
|
||||
await loadAuthPolicy();
|
||||
const authenticated = await loadAccounts();
|
||||
if (authenticated) { await loadPreferences(); await loadMailPreferences(); await loadOpenPgp(); await loadIdentities(); await loadResponseTemplates(); await loadFolders(true); await loadSpecialFolders(); await loadTags(); await loadNotificationPreferences(); await loadTrustedSenders(); startCalendarReminderWatcher(); connectRealtime(); } else { await loadTranslations(state.prefs.language); }
|
||||
if (authenticated) { await loadPreferences(); await loadMailPreferences(); await loadOpenPgp(); await loadIdentities(); await loadResponseTemplates(); await loadFolders(true); await loadSpecialFolders(); await loadQuota(); await loadTags(); await loadNotificationPreferences(); await loadTrustedSenders(); startCalendarReminderWatcher(); connectRealtime(); } else { await loadTranslations(state.prefs.language); }
|
||||
}
|
||||
|
||||
boot();
|
||||
|
||||
Reference in new issue
Block a user