feat(upload): warn on large uploads over slow/metered connections
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m54s
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m54s
Detects if the user is on a slow (2G/3G) or metered (saveData) connection and prompts them with a confirmation dialog if they attempt to upload files totaling 200MB or more. This prevents accidental high data usage and warns users about potential long upload times. Also includes the dialogs JS and CSS in the base layout to support the confirmation modal.
This commit is contained in:
263
backend/static/css/04-dialogs.css
Normal file
263
backend/static/css/04-dialogs.css
Normal file
@@ -0,0 +1,263 @@
|
||||
.warpbox-dialog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 130;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 1rem;
|
||||
background: color-mix(in srgb, var(--background) 60%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
opacity: 0;
|
||||
transition: opacity 160ms ease;
|
||||
}
|
||||
|
||||
.warpbox-dialog-overlay.is-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.warpbox-dialog {
|
||||
position: relative;
|
||||
width: min(28rem, 100%);
|
||||
max-height: min(34rem, 90vh);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--card);
|
||||
color: var(--card-foreground);
|
||||
box-shadow: var(--shadow);
|
||||
opacity: 0;
|
||||
transform: translateY(0.6rem) scale(0.98);
|
||||
transition: opacity 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
.warpbox-dialog:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.warpbox-dialog-overlay.is-visible .warpbox-dialog {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
.warpbox-dialog-head {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 0.85rem;
|
||||
align-items: center;
|
||||
padding: 1.1rem 3.25rem 0 1.1rem;
|
||||
}
|
||||
|
||||
.warpbox-dialog-icon {
|
||||
width: 1.9rem;
|
||||
height: 1.9rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--primary) 20%, transparent);
|
||||
color: var(--primary);
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.warpbox-dialog-warning .warpbox-dialog-icon {
|
||||
background: color-mix(in srgb, var(--primary) 26%, transparent);
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.warpbox-dialog-error .warpbox-dialog-icon {
|
||||
background: color-mix(in srgb, var(--danger) 18%, transparent);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.warpbox-dialog-title {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.warpbox-dialog-close {
|
||||
position: absolute;
|
||||
top: 1.1rem;
|
||||
right: 1.1rem;
|
||||
z-index: 2;
|
||||
min-height: 1.9rem;
|
||||
height: 1.9rem;
|
||||
width: 1.9rem;
|
||||
padding: 0;
|
||||
border-color: var(--border);
|
||||
color: var(--muted-foreground);
|
||||
background: var(--surface-1);
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.warpbox-dialog-close:hover {
|
||||
color: var(--foreground);
|
||||
background: var(--surface-1-hover);
|
||||
}
|
||||
|
||||
.warpbox-dialog-body {
|
||||
padding: 0.85rem 1.1rem 1.1rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.warpbox-dialog-message {
|
||||
margin: 0 0 0.75rem;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.warpbox-dialog-message:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.warpbox-dialog-field {
|
||||
width: 100%;
|
||||
border: 1px solid var(--input);
|
||||
border-radius: calc(var(--radius) - 0.35rem);
|
||||
background: var(--surface-1);
|
||||
color: var(--foreground);
|
||||
padding: 0.55rem 0.7rem;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.warpbox-dialog-field:focus {
|
||||
outline: 2px solid var(--ring);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.warpbox-dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.55rem;
|
||||
padding: 0 1.1rem 1.1rem;
|
||||
}
|
||||
|
||||
html.warpbox-dialog-open,
|
||||
html.warpbox-dialog-open body {
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.dialog-file-list {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
max-height: 14rem;
|
||||
overflow: auto;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.dialog-file-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.5rem 0.65rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: calc(var(--radius) - 0.35rem);
|
||||
background: var(--surface-1);
|
||||
}
|
||||
|
||||
.dialog-file-icon {
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.dialog-file-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.dialog-file-size {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .warpbox-dialog {
|
||||
border: 1px solid #000000;
|
||||
border-radius: 0;
|
||||
background: #c0c0c0;
|
||||
color: #000000;
|
||||
box-shadow: inset -1px -1px 0 #404040, inset 1px 1px 0 #ffffff, inset -2px -2px 0 #808080, inset 2px 2px 0 #dfdfdf, 4px 4px 0 rgba(0, 0, 0, 0.45);
|
||||
font-family: "PixeloidSans", "PixelOperator", "Microsoft Sans Serif", Tahoma, sans-serif;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .warpbox-dialog-head {
|
||||
padding-top: 0.2rem;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .warpbox-dialog::before {
|
||||
content: "Warpbox";
|
||||
display: block;
|
||||
margin: 0.18rem 0.18rem 0;
|
||||
padding: 0.22rem 0.35rem;
|
||||
background: linear-gradient(to right, #000078, 80%, #0f80cd);
|
||||
color: #ffffff;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .warpbox-dialog-error::before {
|
||||
content: "Warpbox - Error";
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .warpbox-dialog-warning::before {
|
||||
content: "Warpbox - Warning";
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .warpbox-dialog-info::before {
|
||||
content: "Warpbox - Info";
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .warpbox-dialog-icon {
|
||||
border: 1px solid #000000;
|
||||
background: #ffffff;
|
||||
color: #000078;
|
||||
box-shadow: inset 1px 1px 0 #808080, inset -1px -1px 0 #ffffff;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .warpbox-dialog-warning .warpbox-dialog-icon {
|
||||
color: #9a5b00;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .warpbox-dialog-error .warpbox-dialog-icon {
|
||||
color: #c00000;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .warpbox-dialog-message {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .warpbox-dialog-close {
|
||||
top: 0.36rem;
|
||||
right: 0.3rem;
|
||||
width: 1.1rem;
|
||||
height: 0.95rem;
|
||||
min-height: 0.95rem;
|
||||
background: #c0c0c0;
|
||||
color: #000000;
|
||||
border: 1px solid #000000;
|
||||
box-shadow: inset -1px -1px 0 #404040, inset 1px 1px 0 #ffffff, inset -2px -2px 0 #808080, inset 2px 2px 0 #dfdfdf;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.warpbox-dialog-overlay {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.warpbox-dialog {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
299
backend/static/js/04-dialogs.js
Normal file
299
backend/static/js/04-dialogs.js
Normal file
@@ -0,0 +1,299 @@
|
||||
(function () {
|
||||
const VARIANTS = ["info", "warning", "error"];
|
||||
const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
window.Warpbox = window.Warpbox || {};
|
||||
let dialogIdCounter = 0;
|
||||
|
||||
function defaultTitle(variant) {
|
||||
if (variant === "error") {
|
||||
return "Error";
|
||||
}
|
||||
if (variant === "warning") {
|
||||
return "Warning";
|
||||
}
|
||||
return "Info";
|
||||
}
|
||||
|
||||
function normalizeOptions(options, message) {
|
||||
if (typeof options === "string") {
|
||||
options = { message: options };
|
||||
} else {
|
||||
options = options || {};
|
||||
}
|
||||
if (message) {
|
||||
options.message = message;
|
||||
}
|
||||
const variant = VARIANTS.includes(options.variant) ? options.variant : "info";
|
||||
return {
|
||||
variant,
|
||||
title: options.title || defaultTitle(variant),
|
||||
message: options.message || "",
|
||||
body: options.body || null,
|
||||
actions: Array.isArray(options.actions) ? options.actions : [],
|
||||
dismissible: options.dismissible !== false,
|
||||
closable: options.closable !== false,
|
||||
onClose: typeof options.onClose === "function" ? options.onClose : null,
|
||||
};
|
||||
}
|
||||
|
||||
function focusableElements(container) {
|
||||
return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => el.offsetParent !== null);
|
||||
}
|
||||
|
||||
function dialog(options, message) {
|
||||
const config = normalizeOptions(options, message);
|
||||
const previouslyFocused = document.activeElement;
|
||||
dialogIdCounter += 1;
|
||||
const titleId = "warpbox-dialog-title-" + dialogIdCounter;
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "warpbox-dialog-overlay";
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "warpbox-dialog warpbox-dialog-" + config.variant;
|
||||
card.setAttribute("role", config.variant === "error" ? "alertdialog" : "dialog");
|
||||
card.setAttribute("aria-modal", "true");
|
||||
card.setAttribute("aria-labelledby", titleId);
|
||||
card.setAttribute("tabindex", "-1");
|
||||
|
||||
const head = document.createElement("div");
|
||||
head.className = "warpbox-dialog-head";
|
||||
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "warpbox-dialog-icon";
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
icon.textContent = config.variant === "error" ? "!" : config.variant === "warning" ? "?" : "i";
|
||||
|
||||
const title = document.createElement("h2");
|
||||
title.id = titleId;
|
||||
title.className = "warpbox-dialog-title";
|
||||
title.textContent = config.title;
|
||||
|
||||
head.append(icon, title);
|
||||
|
||||
if (config.closable) {
|
||||
const close = document.createElement("button");
|
||||
close.type = "button";
|
||||
close.className = "warpbox-dialog-close";
|
||||
close.setAttribute("aria-label", "Close dialog");
|
||||
close.textContent = "x";
|
||||
close.addEventListener("click", () => closeDialog());
|
||||
head.append(close);
|
||||
}
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.className = "warpbox-dialog-body";
|
||||
|
||||
if (config.message) {
|
||||
const text = document.createElement("p");
|
||||
text.className = "warpbox-dialog-message";
|
||||
text.textContent = config.message;
|
||||
body.append(text);
|
||||
}
|
||||
|
||||
if (config.body) {
|
||||
const nodes = Array.isArray(config.body) ? config.body : [config.body];
|
||||
nodes.forEach((node) => {
|
||||
if (node instanceof Node) {
|
||||
body.append(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
card.append(head, body);
|
||||
|
||||
let autofocusTarget = null;
|
||||
if (config.actions.length > 0) {
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "warpbox-dialog-actions";
|
||||
config.actions.forEach((action) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "button " + (action.kind === "primary" ? "button-primary" : action.kind === "ghost" ? "button-ghost" : "button-outline");
|
||||
button.textContent = action.label || "OK";
|
||||
button.addEventListener("click", () => {
|
||||
if (typeof action.onClick === "function") {
|
||||
action.onClick();
|
||||
}
|
||||
if (action.dismiss !== false) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
if (action.autofocus) {
|
||||
autofocusTarget = button;
|
||||
}
|
||||
actions.append(button);
|
||||
});
|
||||
card.append(actions);
|
||||
}
|
||||
|
||||
overlay.append(card);
|
||||
document.body.append(overlay);
|
||||
document.documentElement.classList.add("warpbox-dialog-open");
|
||||
window.requestAnimationFrame(() => {
|
||||
overlay.classList.add("is-visible");
|
||||
(autofocusTarget || card).focus();
|
||||
});
|
||||
|
||||
function handleKeydown(event) {
|
||||
if (event.key === "Escape") {
|
||||
if (config.dismissible) {
|
||||
event.preventDefault();
|
||||
closeDialog();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key !== "Tab") {
|
||||
return;
|
||||
}
|
||||
const focusable = focusableElements(card);
|
||||
if (focusable.length === 0) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
if (event.shiftKey && document.activeElement === first) {
|
||||
event.preventDefault();
|
||||
last.focus();
|
||||
} else if (!event.shiftKey && document.activeElement === last) {
|
||||
event.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function handleOverlayClick(event) {
|
||||
if (config.dismissible && event.target === overlay) {
|
||||
closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", handleKeydown, true);
|
||||
overlay.addEventListener("click", handleOverlayClick);
|
||||
|
||||
let closed = false;
|
||||
function closeDialog() {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
document.removeEventListener("keydown", handleKeydown, true);
|
||||
overlay.removeEventListener("click", handleOverlayClick);
|
||||
overlay.classList.remove("is-visible");
|
||||
document.documentElement.classList.remove("warpbox-dialog-open");
|
||||
window.setTimeout(() => overlay.remove(), 180);
|
||||
if (previouslyFocused && typeof previouslyFocused.focus === "function") {
|
||||
previouslyFocused.focus();
|
||||
}
|
||||
if (config.onClose) {
|
||||
config.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
element: overlay,
|
||||
close: closeDialog,
|
||||
};
|
||||
}
|
||||
|
||||
window.Warpbox.dialog = dialog;
|
||||
|
||||
window.Warpbox.alertDialog = function alertDialog(message, options) {
|
||||
const config = (typeof options === "object" && options) || {};
|
||||
return new Promise((resolve) => {
|
||||
dialog({
|
||||
...config,
|
||||
message: typeof message === "string" ? message : config.message,
|
||||
actions: [{ label: config.okLabel || "OK", kind: "primary", autofocus: true }],
|
||||
onClose: () => {
|
||||
if (typeof config.onClose === "function") {
|
||||
config.onClose();
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.Warpbox.confirmDialog = function confirmDialog(message, options) {
|
||||
const config = (typeof options === "object" && options) || {};
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
function settle(value) {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
resolve(value);
|
||||
}
|
||||
dialog({
|
||||
...config,
|
||||
message: typeof message === "string" ? message : config.message,
|
||||
actions: [
|
||||
{ label: config.cancelLabel || "Cancel", kind: "outline", autofocus: true, onClick: () => settle(false) },
|
||||
{ label: config.confirmLabel || "Confirm", kind: "primary", onClick: () => settle(true) },
|
||||
],
|
||||
onClose: () => {
|
||||
if (typeof config.onClose === "function") {
|
||||
config.onClose();
|
||||
}
|
||||
settle(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.Warpbox.promptDialog = function promptDialog(message, options) {
|
||||
const config = (typeof options === "object" && options) || {};
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
function settle(value) {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
resolve(value);
|
||||
}
|
||||
|
||||
const field = document.createElement("input");
|
||||
field.type = config.inputType || "text";
|
||||
field.className = "warpbox-dialog-field";
|
||||
if (config.placeholder) {
|
||||
field.placeholder = config.placeholder;
|
||||
}
|
||||
if (typeof config.value === "string") {
|
||||
field.value = config.value;
|
||||
}
|
||||
|
||||
let controller = null;
|
||||
field.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
settle(field.value);
|
||||
if (controller) {
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
controller = dialog({
|
||||
...config,
|
||||
message: typeof message === "string" ? message : config.message,
|
||||
body: field,
|
||||
actions: [
|
||||
{ label: config.cancelLabel || "Cancel", kind: "outline", onClick: () => settle(null) },
|
||||
{ label: config.okLabel || "OK", kind: "primary", onClick: () => settle(field.value) },
|
||||
],
|
||||
onClose: () => {
|
||||
if (typeof config.onClose === "function") {
|
||||
config.onClose();
|
||||
}
|
||||
settle(null);
|
||||
},
|
||||
});
|
||||
|
||||
window.requestAnimationFrame(() => field.focus());
|
||||
});
|
||||
};
|
||||
})();
|
||||
@@ -17,6 +17,7 @@
|
||||
const RESUMABLE_SESSIONS_KEY = "warpbox-resumable-sessions";
|
||||
const SHARE_CACHE = "warpbox-share-target-v1";
|
||||
const SHARE_LATEST_KEY = "/__warpbox_share_target__/latest";
|
||||
const CELLULAR_WARNING_THRESHOLD_BYTES = 200 * 1024 * 1024;
|
||||
|
||||
if (!form || !dropZone || !fileInput) {
|
||||
return;
|
||||
@@ -106,6 +107,12 @@
|
||||
if (!validateSelectedFilesWithinLimit(selectedFiles)) {
|
||||
return;
|
||||
}
|
||||
if (isSlowOrMeteredConnection() && totalSelectedBytes(selectedFiles) >= CELLULAR_WARNING_THRESHOLD_BYTES) {
|
||||
const proceed = await confirmCellularUpload(selectedFiles);
|
||||
if (!proceed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const submit = form.querySelector("button[type='submit']");
|
||||
const formData = uploadFormData();
|
||||
@@ -228,6 +235,56 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isSlowOrMeteredConnection() {
|
||||
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
||||
if (!connection) {
|
||||
return false;
|
||||
}
|
||||
if (connection.saveData === true) {
|
||||
return true;
|
||||
}
|
||||
return ["slow-2g", "2g", "3g"].includes(connection.effectiveType);
|
||||
}
|
||||
|
||||
function totalSelectedBytes(files) {
|
||||
return files.reduce((sum, file) => sum + file.size, 0);
|
||||
}
|
||||
|
||||
function confirmCellularUpload(files) {
|
||||
const list = document.createElement("div");
|
||||
list.className = "dialog-file-list";
|
||||
files.forEach((file) => {
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "svg-icon svg-icon-document dialog-file-icon";
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
|
||||
const name = document.createElement("span");
|
||||
name.className = "dialog-file-name";
|
||||
name.textContent = file.name;
|
||||
name.title = file.name;
|
||||
|
||||
const size = document.createElement("span");
|
||||
size.className = "dialog-file-size";
|
||||
size.textContent = window.Warpbox.formatBytes(file.size);
|
||||
|
||||
const row = document.createElement("div");
|
||||
row.className = "dialog-file-row";
|
||||
row.append(icon, name, size);
|
||||
list.append(row);
|
||||
});
|
||||
|
||||
const totalLabel = window.Warpbox.formatBytes(totalSelectedBytes(files));
|
||||
const message = `You're on a slow or metered connection. You're about to upload ${files.length} file${files.length === 1 ? "" : "s"} (${totalLabel} total) — this could take a while or use up your data plan.`;
|
||||
|
||||
return window.Warpbox.confirmDialog(message, {
|
||||
title: "Slow connection detected",
|
||||
variant: "warning",
|
||||
body: list,
|
||||
confirmLabel: "Upload anyway",
|
||||
cancelLabel: "Cancel",
|
||||
});
|
||||
}
|
||||
|
||||
function isShareTargetLaunch() {
|
||||
const params = new URLSearchParams(window.location.search || "");
|
||||
return params.has("share-target");
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
|
||||
<script src="/static/js/05-theme.js?version={{.AppVersion}}"></script>
|
||||
<link rel="stylesheet" href="/static/css/00-base.css?version={{.AppVersion}}">
|
||||
<link rel="stylesheet" href="/static/css/04-dialogs.css?version={{.AppVersion}}">
|
||||
<link rel="stylesheet" href="/static/css/10-layout.css?version={{.AppVersion}}">
|
||||
<link rel="stylesheet" href="/static/css/15-revamp.css?version={{.AppVersion}}">
|
||||
<link rel="stylesheet" href="/static/css/16-retro.css?version={{.AppVersion}}">
|
||||
@@ -70,6 +71,7 @@
|
||||
<script defer src="/static/js/00-utils.js?version={{.AppVersion}}"></script>
|
||||
<script defer src="/static/js/02-pwa.js?version={{.AppVersion}}"></script>
|
||||
<script defer src="/static/js/03-popups.js?version={{.AppVersion}}"></script>
|
||||
<script defer src="/static/js/04-dialogs.js?version={{.AppVersion}}"></script>
|
||||
<script defer src="/static/js/10-file-browser.js?version={{.AppVersion}}"></script>
|
||||
<script defer src="/static/js/12-reactions.js?version={{.AppVersion}}"></script>
|
||||
<script defer src="/static/js/13-share.js?version={{.AppVersion}}"></script>
|
||||
|
||||
Reference in New Issue
Block a user