fix turbo on dashboard + optimize a few more things.
This commit is contained in:
@@ -2,7 +2,6 @@ import { createBuildProfileManager } from "./build-profile-manager.js";
|
||||
import {
|
||||
createBuildHistoryManager,
|
||||
formatFileSize,
|
||||
updateExpirationTimer,
|
||||
} from "./build-history-manager.js";
|
||||
|
||||
const form = document.getElementById("build-form");
|
||||
@@ -2259,7 +2258,8 @@ function showBuildFiles(files, buildId, expiresAt) {
|
||||
const timer = document.createElement("span");
|
||||
timer.id = "expiration-timer";
|
||||
timer.className = "text-yellow-400 font-medium";
|
||||
timer.dataset.expires = String(expiresAt);
|
||||
timer.dataset.controller = "countdown";
|
||||
timer.dataset.countdownExpiresAtValue = String(expiresAt);
|
||||
timer.textContent = "Calculating...";
|
||||
right.appendChild(clockIcon);
|
||||
right.appendChild(expiresLabel);
|
||||
@@ -2270,9 +2270,6 @@ function showBuildFiles(files, buildId, expiresAt) {
|
||||
buildInfoDiv.appendChild(infoRow);
|
||||
buildFilesDiv.appendChild(buildInfoDiv);
|
||||
|
||||
updateExpirationTimer(timer, expiresAt);
|
||||
setInterval(() => updateExpirationTimer(timer, expiresAt), 60000);
|
||||
|
||||
files.forEach((file) => {
|
||||
const fileDiv = document.createElement("div");
|
||||
fileDiv.className =
|
||||
|
||||
@@ -108,6 +108,7 @@ function moveClientCardImmediately(msg) {
|
||||
|
||||
export function registerRenderer(fn) {
|
||||
render = fn;
|
||||
state.lastDigest = "";
|
||||
}
|
||||
|
||||
function clientQueryParams(page = state.page) {
|
||||
|
||||
@@ -23,12 +23,6 @@ uploadInfinite?.addEventListener("change", () => {
|
||||
});
|
||||
if (uploadMaxDownloads) uploadMaxDownloads.disabled = true;
|
||||
|
||||
const passwordInfoBtn = document.getElementById("password-info-btn");
|
||||
const passwordInfoPanel = document.getElementById("password-info-panel");
|
||||
passwordInfoBtn?.addEventListener("click", () => {
|
||||
passwordInfoPanel?.classList.toggle("hidden");
|
||||
});
|
||||
|
||||
async function loadCurrentUser() {
|
||||
try {
|
||||
const res = await fetch("/api/auth/me");
|
||||
@@ -113,12 +107,14 @@ async function loadFiles() {
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button class="action-btn px-2.5 py-1.5 text-sm bg-slate-800 hover:bg-slate-700 text-slate-200 rounded border border-slate-700 transition-colors"
|
||||
data-action="copy-link" data-file-id="${f.id}" title="Copy download link">
|
||||
data-controller="clipboard" data-action="clipboard#copy"
|
||||
data-clipboard-text-value="${getDownloadUrl(f.id)}"
|
||||
data-clipboard-success-message-value="Download link copied to clipboard!" title="Copy download link">
|
||||
<i class="fa-solid fa-link"></i>
|
||||
</button>
|
||||
${canManage ? `
|
||||
<button class="action-btn px-2.5 py-1.5 text-sm bg-slate-800 hover:bg-slate-700 text-slate-200 rounded border border-slate-700 transition-colors"
|
||||
data-action="edit" data-file-id="${f.id}"
|
||||
data-file-action="edit" data-file-id="${f.id}"
|
||||
data-has-password="${f.hasPassword ? 1 : 0}"
|
||||
data-max-downloads="${f.maxDownloads !== null ? f.maxDownloads : ''}"
|
||||
data-expires-at="${f.expiresAt || ''}"
|
||||
@@ -127,7 +123,9 @@ async function loadFiles() {
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button class="action-btn px-2.5 py-1.5 text-sm bg-red-900/40 hover:bg-red-800/60 text-red-200 rounded border border-red-700/60 transition-colors"
|
||||
data-action="delete" data-file-id="${f.id}" data-filename="${escapeHtml(f.filename)}" title="Delete file">
|
||||
data-controller="confirm" data-action="confirm#confirm"
|
||||
data-confirm-message-value="Delete this file? This cannot be undone."
|
||||
data-file-action="delete" data-file-id="${f.id}" title="Delete file">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
` : ""}
|
||||
@@ -137,57 +135,21 @@ async function loadFiles() {
|
||||
})
|
||||
.join("");
|
||||
|
||||
attachActionListeners();
|
||||
} catch (err) {
|
||||
console.error("Load files error:", err);
|
||||
fileList.innerHTML = `<tr><td colspan="7" class="px-6 py-12 text-center text-red-400">Failed to load files</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function attachActionListeners() {
|
||||
fileList.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest(".action-btn");
|
||||
if (!btn) return;
|
||||
fileList?.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("[data-file-action]");
|
||||
if (!btn) return;
|
||||
|
||||
const action = btn.dataset.action;
|
||||
const fileId = btn.dataset.fileId;
|
||||
|
||||
switch (action) {
|
||||
case "copy-link":
|
||||
copyDownloadLink(fileId);
|
||||
break;
|
||||
case "edit":
|
||||
openEditModal(btn);
|
||||
break;
|
||||
case "delete":
|
||||
deleteFile(fileId, btn.dataset.filename);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function copyDownloadLink(fileId) {
|
||||
const url = getDownloadUrl(fileId);
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
if (window.showToast) {
|
||||
window.showToast("Download link copied to clipboard!", "success");
|
||||
}
|
||||
}).catch(() => {
|
||||
const input = document.createElement("input");
|
||||
input.value = url;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(input);
|
||||
if (window.showToast) {
|
||||
window.showToast("Download link copied!", "success");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteFile(fileId, filename) {
|
||||
if (!confirm(`Delete "${filename}"? This cannot be undone.`)) return;
|
||||
if (btn.dataset.fileAction === "edit") openEditModal(btn);
|
||||
if (btn.dataset.fileAction === "delete") deleteFile(btn.dataset.fileId);
|
||||
});
|
||||
|
||||
async function deleteFile(fileId) {
|
||||
try {
|
||||
const res = await fetch(`/api/file-share/${fileId}`, { method: "DELETE" });
|
||||
if (res.ok) {
|
||||
|
||||
@@ -1141,6 +1141,7 @@ function initializeRenderer() {
|
||||
}
|
||||
});
|
||||
initDashboardStats();
|
||||
renderCachedClients({ reorder: true, force: true });
|
||||
setupDashboardThumbnailLoader();
|
||||
refreshGroupFilter();
|
||||
loadWithOptions();
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import { Application } from "/vendor/hotwired/stimulus.js";
|
||||
import SessionsController from "./controllers/sessions-controller.js";
|
||||
import ClipboardController from "./controllers/clipboard-controller.js";
|
||||
import ConfirmController from "./controllers/confirm-controller.js";
|
||||
import CountdownController from "./controllers/countdown-controller.js";
|
||||
import RevealController from "./controllers/reveal-controller.js";
|
||||
import ToggleController from "./controllers/toggle-controller.js";
|
||||
|
||||
const application = Application.start();
|
||||
application.debug = false;
|
||||
application.register("sessions", SessionsController);
|
||||
application.register("clipboard", ClipboardController);
|
||||
application.register("confirm", ConfirmController);
|
||||
application.register("countdown", CountdownController);
|
||||
application.register("reveal", RevealController);
|
||||
application.register("toggle", ToggleController);
|
||||
|
||||
window.OverlordStimulus = application;
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Controller } from "/vendor/hotwired/stimulus.js";
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
text: String,
|
||||
successMessage: { type: String, default: "Copied to clipboard" },
|
||||
};
|
||||
|
||||
async copy(event) {
|
||||
event.preventDefault();
|
||||
if (!this.hasTextValue) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.textValue);
|
||||
} catch {
|
||||
const input = document.createElement("textarea");
|
||||
input.value = this.textValue;
|
||||
input.setAttribute("readonly", "");
|
||||
input.style.position = "fixed";
|
||||
input.style.opacity = "0";
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
input.remove();
|
||||
}
|
||||
|
||||
window.showToast?.(this.successMessageValue, "success");
|
||||
this.element.dispatchEvent(new CustomEvent("clipboard:copied", {
|
||||
bubbles: true,
|
||||
detail: { text: this.textValue },
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Controller } from "/vendor/hotwired/stimulus.js";
|
||||
|
||||
export default class extends Controller {
|
||||
static values = { message: String };
|
||||
|
||||
confirm(event) {
|
||||
if (!this.hasMessageValue || window.confirm(this.messageValue)) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Controller } from "/vendor/hotwired/stimulus.js";
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
expiresAt: Number,
|
||||
interval: { type: Number, default: 60_000 },
|
||||
};
|
||||
|
||||
connect() {
|
||||
this.update();
|
||||
if (this.expiresAtValue > Date.now()) {
|
||||
this.clock = window.setInterval(() => this.update(), this.intervalValue);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
window.clearInterval(this.clock);
|
||||
}
|
||||
|
||||
update() {
|
||||
const remaining = this.expiresAtValue - Date.now();
|
||||
if (remaining <= 0) {
|
||||
this.element.textContent = "Expired";
|
||||
this.setTone("text-red-400");
|
||||
window.clearInterval(this.clock);
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(remaining / 86_400_000);
|
||||
const hours = Math.floor((remaining % 86_400_000) / 3_600_000);
|
||||
const minutes = Math.floor((remaining % 3_600_000) / 60_000);
|
||||
this.element.textContent = days > 0
|
||||
? `${days}d ${hours}h`
|
||||
: hours > 0
|
||||
? `${hours}h ${minutes}m`
|
||||
: `${minutes}m`;
|
||||
this.setTone(days >= 3
|
||||
? "text-green-400"
|
||||
: days >= 1
|
||||
? "text-yellow-400"
|
||||
: "text-orange-400");
|
||||
}
|
||||
|
||||
setTone(tone) {
|
||||
this.element.classList.remove(
|
||||
"text-red-400",
|
||||
"text-green-400",
|
||||
"text-yellow-400",
|
||||
"text-orange-400",
|
||||
);
|
||||
this.element.classList.add(tone, "font-medium");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Controller } from "/vendor/hotwired/stimulus.js";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["input", "icon"];
|
||||
|
||||
toggle(event) {
|
||||
event.preventDefault();
|
||||
const revealing = this.inputTarget.type === "password";
|
||||
this.inputTarget.type = revealing ? "text" : "password";
|
||||
event.currentTarget.setAttribute("aria-pressed", String(revealing));
|
||||
event.currentTarget.setAttribute("aria-label", revealing ? "Hide value" : "Show value");
|
||||
if (this.hasIconTarget) {
|
||||
this.iconTarget.classList.toggle("fa-eye", !revealing);
|
||||
this.iconTarget.classList.toggle("fa-eye-slash", revealing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,13 +12,6 @@ export default class extends Controller {
|
||||
window.clearInterval(this.clock);
|
||||
}
|
||||
|
||||
confirm(event) {
|
||||
const message = event.currentTarget.dataset.confirmMessage;
|
||||
if (message && !window.confirm(message)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
updateTimes() {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
for (const element of this.timeTargets) {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Controller } from "/vendor/hotwired/stimulus.js";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["content"];
|
||||
|
||||
toggle(event) {
|
||||
event.preventDefault();
|
||||
const hidden = this.contentTarget.classList.toggle("hidden");
|
||||
event.currentTarget.setAttribute("aria-expanded", String(!hidden));
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ const persistentIds = [
|
||||
"chat-bubble",
|
||||
"chat-panel",
|
||||
"cert-trust-banner",
|
||||
"command-menu",
|
||||
];
|
||||
|
||||
function normalizeUrl(value, base = window.location.href) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { runWithoutPageTracking } from "./turbo-navigation.js";
|
||||
|
||||
// ── Context menu data ─────────────────────────────────────────────────────────
|
||||
const MENU_GROUPS = [
|
||||
{
|
||||
@@ -254,7 +256,6 @@ menuStyle.textContent = `
|
||||
#ctx-main { border-radius: 8px 8px 0 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(menuStyle);
|
||||
|
||||
// ── Build DOM ─────────────────────────────────────────────────────────────────
|
||||
function buildItemHTML(item) {
|
||||
@@ -282,9 +283,11 @@ const subPanelsHTML =
|
||||
|
||||
const menu = document.createElement("div");
|
||||
menu.id = "command-menu";
|
||||
menu.setAttribute("data-turbo-permanent", "");
|
||||
menu.setAttribute("role", "menu");
|
||||
menu.setAttribute("aria-hidden", "true");
|
||||
menu.innerHTML = `<div id="ctx-main">${mainRowsHTML}</div><div id="ctx-sub">${subPanelsHTML}</div>`;
|
||||
menu.prepend(menuStyle);
|
||||
document.body.appendChild(menu);
|
||||
|
||||
const ctxMain = menu.querySelector("#ctx-main");
|
||||
@@ -363,32 +366,34 @@ function refitMobileMenu() {
|
||||
}
|
||||
|
||||
ctxMain.querySelectorAll(".ctx-row").forEach(rowEl => {
|
||||
rowEl.addEventListener("mouseenter", () => {
|
||||
runWithoutPageTracking(() => rowEl.addEventListener("mouseenter", () => {
|
||||
if (isMobileMenu()) return;
|
||||
if (rowEl.disabled || rowEl.getAttribute("aria-disabled") === "true") return;
|
||||
const groupId = rowEl.dataset.groupToggle;
|
||||
if (groupId) showSubmenu(groupId, rowEl);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
// Debounced hide — prevents the 5px gap between panels from closing the submenu
|
||||
function scheduleHide() { if (!isMobileMenu()) _hideTimer = setTimeout(hideSubmenu, 150); }
|
||||
function cancelHide() { clearTimeout(_hideTimer); _hideTimer = null; }
|
||||
|
||||
ctxMain.addEventListener("mouseleave", scheduleHide);
|
||||
ctxMain.addEventListener("mouseenter", cancelHide);
|
||||
ctxSub.addEventListener("mouseenter", cancelHide);
|
||||
ctxSub.addEventListener("mouseleave", scheduleHide);
|
||||
runWithoutPageTracking(() => {
|
||||
ctxMain.addEventListener("mouseleave", scheduleHide);
|
||||
ctxMain.addEventListener("mouseenter", cancelHide);
|
||||
ctxSub.addEventListener("mouseenter", cancelHide);
|
||||
ctxSub.addEventListener("mouseleave", scheduleHide);
|
||||
});
|
||||
|
||||
// Touch / mobile: tap to toggle group
|
||||
ctxMain.querySelectorAll(".ctx-row").forEach(rowEl => {
|
||||
rowEl.addEventListener("click", (e) => {
|
||||
runWithoutPageTracking(() => rowEl.addEventListener("click", (e) => {
|
||||
const groupId = rowEl.dataset.groupToggle;
|
||||
if (!groupId) return;
|
||||
if (rowEl.disabled || rowEl.getAttribute("aria-disabled") === "true") return;
|
||||
e.stopPropagation();
|
||||
if (activeGroupId === groupId) { hideSubmenu(); } else { showSubmenu(groupId, rowEl); }
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
const modal = document.createElement("div");
|
||||
|
||||
Reference in New Issue
Block a user