617 lines
32 KiB
JavaScript
617 lines
32 KiB
JavaScript
(function () {
|
||
"use strict";
|
||
|
||
const state = (window.PQueue && window.PQueue.state) || {};
|
||
const UI = (window.PQueue && window.PQueue.UI) || (window.PQueue = (window.PQueue || {}), window.PQueue.UI = {}, window.PQueue.UI);
|
||
const Format = (window.PQueue && window.PQueue.Format) || window.Format;
|
||
const Events = (window.PQueue && window.PQueue.Events) || window.Events;
|
||
|
||
UI.historySentinel = function historySentinel() {
|
||
const wrap = UI.el("div", { class: "pqueue-history-sentinel" });
|
||
const spinner = UI.icon("ti ti-loader-2", { size: "md", spin: true });
|
||
wrap.appendChild(spinner);
|
||
return wrap;
|
||
};
|
||
|
||
UI.historyCard = function historyCard(row) {
|
||
const attrs = { class: "pqueue-history-card", title: Format.tooltip(row) };
|
||
if (row && row.id != null) attrs["data-id"] = String(row.id);
|
||
const ts = row && (row.completed_at || row.created_at);
|
||
if (ts != null) attrs["data-ts"] = String(ts);
|
||
const [ms, idPart] = UI.historyKeyParts(row);
|
||
attrs["data-key"] = String(ms);
|
||
attrs["data-key2"] = String(idPart);
|
||
const card = UI.el("article", attrs);
|
||
const header = UI.el("div", { class: "pqueue-history-card__header" }, [UI.statusBadge(row.status || "success"), UI.el("span", { class: "pqueue-history-card__time", text: row.completed_at ? Format.relative(row.completed_at) : Format.relative(row.created_at) })]);
|
||
const fileLabel = UI.historyPrimaryFilename(row);
|
||
const meta = UI.el("div", { class: "pqueue-history-card__meta" }, [UI.el("span", { class: "pqueue-code", text: fileLabel }), UI.el("span", { class: "pqueue-history-card__duration", text: Format.duration(Number(row.duration_seconds)) || "—" })]);
|
||
const thumbs = UI.renderThumbs(row);
|
||
card.appendChild(header);
|
||
card.appendChild(meta);
|
||
card.appendChild(thumbs);
|
||
return card;
|
||
};
|
||
|
||
UI.renderThumbs = function renderThumbs(row) {
|
||
const container = UI.el("div", { class: "pqueue-thumb-row" });
|
||
const placeholderMeta = UI.thumbPlaceholder(row);
|
||
|
||
const ensurePlaceholder = () => {
|
||
const wrap = UI.el("div", { class: ["pqueue-thumb-wrap", "pqueue-thumb-wrap-placeholder", placeholderMeta.variant ? `pqueue-thumb-wrap-placeholder--${placeholderMeta.variant}` : null] });
|
||
wrap.appendChild(UI.thumbPlaceholderNode(placeholderMeta));
|
||
container.appendChild(wrap);
|
||
};
|
||
|
||
const attachFallback = (wrap, img, images) => {
|
||
img.addEventListener(
|
||
"error",
|
||
() => {
|
||
wrap.classList.add("pqueue-thumb-wrap-placeholder");
|
||
if (placeholderMeta.variant) wrap.classList.add(`pqueue-thumb-wrap-placeholder--${placeholderMeta.variant}`);
|
||
wrap.replaceChildren(UI.thumbPlaceholderNode(placeholderMeta));
|
||
if (!images.length) wrap.onclick = null;
|
||
},
|
||
{ once: true }
|
||
);
|
||
};
|
||
|
||
if (row.id) {
|
||
const galleryImages = UI.extractImages(row);
|
||
const url = new URL(`/api/pqueue/history/thumb/${row.id}`, window.location.origin);
|
||
url.searchParams.set("_", String(Date.now()));
|
||
const wrap = UI.el("div", { class: "pqueue-thumb-wrap" });
|
||
const img = UI.el("img", { class: "pqueue-thumb", src: url.href, title: `history-${row.id}`, loading: "lazy", decoding: "async", fetchpriority: "low" });
|
||
attachFallback(wrap, img, galleryImages);
|
||
wrap.appendChild(img);
|
||
const count = UI.countImages(row);
|
||
if (count > 1) wrap.appendChild(UI.el("div", { class: "pqueue-thumb-badge", text: `${count}` }));
|
||
if (galleryImages.length) wrap.onclick = () => UI.openGallery(galleryImages, 0, row.prompt_id);
|
||
container.appendChild(wrap);
|
||
}
|
||
|
||
if (!container.children.length) {
|
||
const images = UI.extractImages(row);
|
||
if (images.length) {
|
||
const first = images[0];
|
||
const url = UI.buildPreviewUrl(first, row.prompt_id);
|
||
const wrap = UI.el("div", { class: "pqueue-thumb-wrap" });
|
||
const img = UI.el("img", { class: "pqueue-thumb", src: url.href, title: first.filename, loading: "lazy", decoding: "async", fetchpriority: "low" });
|
||
attachFallback(wrap, img, images);
|
||
wrap.appendChild(img);
|
||
if (images.length > 1) wrap.appendChild(UI.el("div", { class: "pqueue-thumb-badge", text: `${images.length}` }));
|
||
wrap.onclick = () => UI.openGallery(images, 0, row.prompt_id);
|
||
container.appendChild(wrap);
|
||
}
|
||
}
|
||
|
||
if (!container.children.length) ensurePlaceholder();
|
||
return container;
|
||
};
|
||
|
||
UI.thumbPlaceholder = function thumbPlaceholder(row) {
|
||
const status = String(row?.status ?? "").toLowerCase();
|
||
if (["interrupted", "cancelled", "canceled", "stopped"].includes(status)) {
|
||
return { text: "Interrupted", icon: "ti ti-player-stop", variant: "interrupted" };
|
||
}
|
||
if (["error", "failed", "failure"].includes(status)) {
|
||
return { text: "Failed", icon: "ti ti-alert-triangle", variant: "failed" };
|
||
}
|
||
return { text: "Preview unavailable", icon: "ti ti-photo-off", variant: "empty" };
|
||
};
|
||
|
||
UI.thumbPlaceholderNode = function thumbPlaceholderNode(meta) {
|
||
return UI.el("div", { class: ["pqueue-thumb-placeholder", meta.variant ? `pqueue-thumb-placeholder--${meta.variant}` : null] }, [UI.icon(meta.icon), UI.el("span", { class: "pqueue-thumb-placeholder__text", text: meta.text })]);
|
||
};
|
||
|
||
UI.extractImages = function extractImages(row) {
|
||
try {
|
||
let outputs = row.outputs ?? {};
|
||
if (typeof outputs === "string") {
|
||
try {
|
||
outputs = JSON.parse(outputs);
|
||
} catch (err) {
|
||
outputs = {};
|
||
}
|
||
}
|
||
const images = [];
|
||
Object.values(outputs).forEach((value) => {
|
||
if (value && typeof value === "object") {
|
||
const list = value.images ?? value.ui?.images ?? [];
|
||
if (Array.isArray(list)) list.forEach((img) => pushImg(img));
|
||
} else if (Array.isArray(value)) {
|
||
value.forEach((img) => pushImg(img));
|
||
}
|
||
});
|
||
return images;
|
||
|
||
function pushImg(img) {
|
||
if (!img || typeof img !== "object") return;
|
||
const filename = img.filename ?? img.name;
|
||
if (!filename) return;
|
||
images.push({ filename, type: img.type ?? "output", subfolder: img.subfolder ?? "" });
|
||
}
|
||
} catch (err) {
|
||
return [];
|
||
}
|
||
};
|
||
|
||
UI.countImages = function countImages(row) {
|
||
try {
|
||
return UI.extractImages(row).length;
|
||
} catch (err) {
|
||
return 0;
|
||
}
|
||
};
|
||
|
||
UI.historyPrimaryFilename = function historyPrimaryFilename(row) {
|
||
try {
|
||
const images = UI.extractImages(row);
|
||
if (images.length && images[0]?.filename) {
|
||
return images[0].filename;
|
||
}
|
||
if (row.prompt_id) return String(row.prompt_id);
|
||
return "Unnamed output";
|
||
} catch (err) {
|
||
return row.prompt_id ? String(row.prompt_id) : "Unnamed output";
|
||
}
|
||
};
|
||
|
||
UI.renderHistory = function renderHistory() {
|
||
const grid = UI.el("div", { class: "pqueue-history" });
|
||
state.dom.historyGrid = grid;
|
||
if (!state.history.length) {
|
||
grid.appendChild(UI.emptyState({
|
||
icon: "ti ti-photo-off",
|
||
title: "History empty",
|
||
description: "Completed prompts will show their thumbnails here.",
|
||
}));
|
||
} else {
|
||
state.history.forEach((row) => grid.appendChild(UI.historyCard(row)));
|
||
}
|
||
const sentinel = UI.historySentinel();
|
||
state.dom.historySentinel = sentinel;
|
||
grid.appendChild(sentinel);
|
||
const count = (state.historyTotal != null) ? state.historyTotal : state.metrics.historyCount;
|
||
const base = (count != null) ? `${count} entries` : null;
|
||
const range = UI.currentHistoryRangeLabel();
|
||
const subtitle = [base, range].filter(Boolean).join(' • ');
|
||
|
||
// Collapsible card structure
|
||
const bodyInner = grid;
|
||
const body = UI.el("div", { class: "pqueue-card__body" }, [bodyInner]);
|
||
try {
|
||
body.style.overflow = 'hidden';
|
||
body.style.transition = 'height 120ms ease, opacity 120ms ease';
|
||
} catch (err) { /* noop */ }
|
||
|
||
const titleWrap = UI.el("div", { class: "pqueue-card__title-wrap" });
|
||
titleWrap.appendChild(UI.icon("ti ti-clock-bolt", { size: "md" }));
|
||
titleWrap.appendChild(UI.el("h3", { class: "pqueue-card__title", text: "History" }));
|
||
if (subtitle) titleWrap.appendChild(UI.el("span", { class: "pqueue-card__subtitle", text: subtitle }));
|
||
|
||
const toggle = UI.button({ icon: state.uiHistoryCollapsed ? "ti ti-chevron-down" : "ti ti-chevron-up", variant: "ghost", subtle: true, title: state.uiHistoryCollapsed ? "Expand" : "Collapse" });
|
||
toggle.addEventListener("click", () => {
|
||
try {
|
||
const isCollapsed = !!state.uiHistoryCollapsed;
|
||
const setIcon = (name) => { try { toggle.innerHTML = ''; toggle.appendChild(UI.icon(name)); } catch (err) { /* noop */ } };
|
||
if (!isCollapsed) {
|
||
const start = body.scrollHeight;
|
||
body.style.height = `${start}px`;
|
||
body.style.opacity = '1';
|
||
requestAnimationFrame(() => {
|
||
body.style.height = '0px';
|
||
body.style.opacity = '0';
|
||
});
|
||
const onEnd = () => {
|
||
body.removeEventListener('transitionend', onEnd);
|
||
setIcon('ti ti-chevron-down');
|
||
state.uiHistoryCollapsed = true;
|
||
try { wrap.setAttribute('data-collapsed', 'true'); } catch (err) { /* noop */ }
|
||
};
|
||
body.addEventListener('transitionend', onEnd);
|
||
} else {
|
||
const target = bodyInner.scrollHeight;
|
||
body.style.height = '0px';
|
||
body.style.opacity = '0';
|
||
requestAnimationFrame(() => {
|
||
const h = bodyInner.scrollHeight || target;
|
||
body.style.height = `${h}px`;
|
||
body.style.opacity = '1';
|
||
});
|
||
const onEnd = () => {
|
||
body.removeEventListener('transitionend', onEnd);
|
||
try { body.style.height = ''; } catch (err) { /* noop */ }
|
||
setIcon('ti ti-chevron-up');
|
||
state.uiHistoryCollapsed = false;
|
||
try { wrap.setAttribute('data-collapsed', 'false'); } catch (err) { /* noop */ }
|
||
};
|
||
body.addEventListener('transitionend', onEnd);
|
||
}
|
||
} catch (err) { /* noop */ }
|
||
});
|
||
|
||
const actions = UI.el("div", { class: "pqueue-card__actions" }, [...UI.historyFilters(), toggle]);
|
||
const header = UI.el("header", { class: "pqueue-card__header" }, [titleWrap, actions]);
|
||
const wrap = UI.el("section", { class: ["pqueue-card", "pqueue-card--history"] }, [header]);
|
||
try { wrap.setAttribute('data-collapsed', state.uiHistoryCollapsed ? 'true' : 'false'); } catch (err) { /* noop */ }
|
||
if (state.uiHistoryCollapsed) {
|
||
try { body.style.height = '0px'; body.style.opacity = '0'; } catch (err) { /* noop */ }
|
||
} else {
|
||
try { body.style.height = ''; body.style.opacity = '1'; } catch (err) { /* noop */ }
|
||
}
|
||
|
||
wrap.appendChild(body);
|
||
state.dom.historyCard = wrap;
|
||
state.dom.historySubtitle = wrap.querySelector('.pqueue-card__subtitle');
|
||
try { UI.ensureThumbObserver(); } catch (err) { /* noop */ }
|
||
return wrap;
|
||
};
|
||
|
||
UI.ensureHistoryObserver = function ensureHistoryObserver() {
|
||
try {
|
||
if (!('IntersectionObserver' in window)) return;
|
||
const grid = state.dom.historyGrid;
|
||
const sentinel = state.dom.historySentinel;
|
||
if (!grid || !sentinel) return;
|
||
|
||
if (state.historyObserver) {
|
||
state.historyObserver.disconnect();
|
||
state.historyObserver = null;
|
||
}
|
||
|
||
if (!state.historyPaging?.nextCursor && state.history?.length) {
|
||
const last = state.history[state.history.length - 1];
|
||
state.historyPaging.nextCursor = { id: last?.id, value: last?.id };
|
||
state.historyPaging.hasMore = true;
|
||
}
|
||
|
||
const scroller = UI.getScrollContainer();
|
||
const rootIsAncestor = (el, ancestor) => {
|
||
try {
|
||
if (!ancestor || !(ancestor instanceof Element)) return false;
|
||
return ancestor.contains(el);
|
||
} catch (err) { return false; }
|
||
};
|
||
const root = rootIsAncestor(sentinel, scroller) ? scroller : null;
|
||
const obs = new IntersectionObserver((entries) => {
|
||
entries.forEach((entry) => {
|
||
if (!entry.isIntersecting) return;
|
||
Events.loadMoreHistory();
|
||
});
|
||
}, { root: root, rootMargin: '360px 0px', threshold: 0 });
|
||
obs.observe(sentinel);
|
||
state.historyObserver = obs;
|
||
} catch (err) { /* noop */ }
|
||
};
|
||
|
||
UI.ensureThumbObserver = function ensureThumbObserver() {
|
||
try {
|
||
if (!('IntersectionObserver' in window)) return;
|
||
const grid = state.dom.historyGrid;
|
||
if (!grid) return;
|
||
|
||
if (state.thumbObserver) {
|
||
try { state.thumbObserver.disconnect(); } catch (e) {}
|
||
state.thumbObserver = null;
|
||
}
|
||
|
||
const scroller = UI.getScrollContainer();
|
||
const rootIsAncestor = (el, ancestor) => {
|
||
try { return ancestor instanceof Element ? ancestor.contains(el) : false; } catch (err) { return false; }
|
||
};
|
||
const root = rootIsAncestor(grid, scroller) ? scroller : null;
|
||
const ric = window.requestIdleCallback || ((cb) => setTimeout(cb, 0));
|
||
const obs = new IntersectionObserver((entries) => {
|
||
entries.forEach((entry) => {
|
||
const img = entry.target;
|
||
if (!(img instanceof HTMLImageElement)) return;
|
||
if (entry.isIntersecting) {
|
||
try { img.setAttribute('fetchpriority', 'high'); } catch (err) {}
|
||
try { img.decoding = 'async'; } catch (err) {}
|
||
try { ric(() => { try { img.decode().catch(() => {}); } catch (e) {} }); } catch (err) {}
|
||
} else {
|
||
try { img.setAttribute('fetchpriority', 'low'); } catch (err) {}
|
||
}
|
||
});
|
||
}, { root, rootMargin: '300px 0px', threshold: 0 });
|
||
|
||
const imgs = Array.from(grid.querySelectorAll('.pqueue-thumb'));
|
||
imgs.forEach((img) => { try { obs.observe(img); } catch (err) {} });
|
||
state.thumbObserver = obs;
|
||
} catch (err) { /* noop */ }
|
||
};
|
||
|
||
UI.mergeHistoryFromRefresh = function mergeHistoryFromRefresh(paged) {
|
||
try {
|
||
const grid = state.dom.historyGrid;
|
||
const sentinel = state.dom.historySentinel;
|
||
if (!grid || !sentinel) return;
|
||
|
||
let list = Array.isArray(paged?.history) ? paged.history : [];
|
||
try {
|
||
const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc';
|
||
list = list.slice().sort((a, b) => UI.compareHistoryRows(a, b, dir));
|
||
} catch (err) { /* noop */ }
|
||
if (!state.historyIds) state.historyIds = new Set();
|
||
UI.withStableAnchor(() => {
|
||
try { Array.from(grid.querySelectorAll('.pqueue-empty')).forEach((n) => n.remove()); } catch (err) { /* noop */ }
|
||
for (const row of list) {
|
||
const id = row?.id;
|
||
if (id == null || state.historyIds.has(id)) continue;
|
||
state.historyIds.add(id);
|
||
const card = UI.historyCard(row);
|
||
const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc';
|
||
const key = Number(card.getAttribute('data-key') || 0);
|
||
const key2 = Number(card.getAttribute('data-key2') || 0);
|
||
const existingCards = Array.from(grid.querySelectorAll('.pqueue-history-card'));
|
||
let inserted = false;
|
||
if (dir === 'desc') {
|
||
for (const existing of existingCards) {
|
||
const otherKey = Number(existing.getAttribute('data-key') || 0);
|
||
const otherKey2 = Number(existing.getAttribute('data-key2') || 0);
|
||
if (key > otherKey || (key === otherKey && key2 >= otherKey2)) { grid.insertBefore(card, existing); inserted = true; break; }
|
||
}
|
||
if (!inserted) grid.insertBefore(card, sentinel);
|
||
} else {
|
||
for (let i = existingCards.length - 1; i >= 0; i--) {
|
||
const existing = existingCards[i];
|
||
const otherKey = Number(existing.getAttribute('data-key') || 0);
|
||
const otherKey2 = Number(existing.getAttribute('data-key2') || 0);
|
||
if (key > otherKey || (key === otherKey && key2 >= otherKey2)) { grid.insertBefore(card, existing.nextSibling || sentinel); inserted = true; break; }
|
||
}
|
||
if (!inserted) grid.insertBefore(card, sentinel);
|
||
}
|
||
if (Array.isArray(state.history)) {
|
||
if (dir === 'desc') state.history.unshift(row);
|
||
else state.history.push(row);
|
||
}
|
||
}
|
||
});
|
||
if (typeof paged?.total === 'number') state.historyTotal = paged.total;
|
||
state.historyPaging = state.historyPaging || { isLoading: false, hasMore: true, nextCursor: null, params: { sort_by: 'id', sort_dir: 'desc', limit: 60 } };
|
||
state.historyPaging.nextCursor = paged?.next_cursor || state.historyPaging.nextCursor;
|
||
state.historyPaging.hasMore = !!paged?.has_more || state.historyPaging.hasMore;
|
||
if (!state.historyPaging.hasMore && sentinel) sentinel.style.display = 'none';
|
||
try { UI.ensureThumbObserver(); } catch (err) { /* noop */ }
|
||
} catch (err) { /* noop */ }
|
||
};
|
||
|
||
UI.reconcileHistoryFromState = function reconcileHistoryFromState() {
|
||
try {
|
||
const grid = state.dom.historyGrid;
|
||
const sentinel = state.dom.historySentinel;
|
||
if (!grid || !sentinel) return;
|
||
const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc';
|
||
UI.withStableAnchor(() => {
|
||
try { Array.from(grid.querySelectorAll('.pqueue-empty')).forEach((n) => n.remove()); } catch (err) { /* noop */ }
|
||
const existingIds = new Set(Array.from(grid.querySelectorAll('.pqueue-history-card')).map((n) => n.getAttribute('data-id')));
|
||
const ordered = Array.isArray(state.history) ? state.history.slice() : [];
|
||
try {
|
||
ordered.sort((a, b) => UI.compareHistoryRows(a, b, dir));
|
||
} catch (err) { /* noop */ }
|
||
|
||
for (const row of ordered) {
|
||
const id = row?.id != null ? String(row.id) : null;
|
||
if (!id || existingIds.has(id)) continue;
|
||
const card = UI.historyCard(row);
|
||
const key = Number(card.getAttribute('data-key') || 0);
|
||
const key2 = Number(card.getAttribute('data-key2') || 0);
|
||
const existingCards = Array.from(grid.querySelectorAll('.pqueue-history-card'));
|
||
let inserted = false;
|
||
if (dir === 'desc') {
|
||
for (const existing of existingCards) {
|
||
const otherKey = Number(existing.getAttribute('data-key') || 0);
|
||
const otherKey2 = Number(existing.getAttribute('data-key2') || 0);
|
||
if (key > otherKey || (key === otherKey && key2 >= otherKey2)) { grid.insertBefore(card, existing); inserted = true; break; }
|
||
}
|
||
if (!inserted) grid.insertBefore(card, sentinel);
|
||
} else {
|
||
for (let i = existingCards.length - 1; i >= 0; i--) {
|
||
const existing = existingCards[i];
|
||
const otherKey = Number(existing.getAttribute('data-key') || 0);
|
||
const otherKey2 = Number(existing.getAttribute('data-key2') || 0);
|
||
if (key > otherKey || (key === otherKey && key2 >= otherKey2)) { grid.insertBefore(card, existing.nextSibling || sentinel); inserted = true; break; }
|
||
}
|
||
if (!inserted) {
|
||
const firstCard = existingCards[0] || grid.querySelector('.pqueue-history-card');
|
||
grid.insertBefore(card, firstCard || sentinel);
|
||
}
|
||
}
|
||
existingIds.add(id);
|
||
}
|
||
|
||
try {
|
||
state.historyIds = new Set(Array.from(grid.querySelectorAll('.pqueue-history-card')).map((n) => Number(n.getAttribute('data-id'))).filter((v) => v != null));
|
||
} catch (err) { /* noop */ }
|
||
});
|
||
try { UI.ensureThumbObserver(); } catch (err) { /* noop */ }
|
||
} catch (err) { /* noop */ }
|
||
};
|
||
|
||
UI.currentHistoryRangeLabel = function currentHistoryRangeLabel() {
|
||
try {
|
||
const sDate = state.filters?.historySince || "";
|
||
const sTime = state.filters?.historySinceTime || "";
|
||
const uDate = state.filters?.historyUntil || "";
|
||
const uTime = state.filters?.historyUntilTime || "";
|
||
if (!sDate && !uDate) return "";
|
||
if (state.historyPreset === 'today') return 'Today';
|
||
if (state.historyPreset === '24h') return 'Last 24h';
|
||
if (state.historyPreset === '7d') return 'Last 7 days';
|
||
const compact = (d, t, end) => {
|
||
if (!d) return '';
|
||
const parts = d.split('-').map((x) => parseInt(x, 10));
|
||
let hh = end ? 23 : 0, mm = end ? 59 : 0;
|
||
if (t && /^\d{2}:\d{2}/.test(t)) {
|
||
const tt = t.split(':').map((x) => parseInt(x, 10));
|
||
hh = tt[0] ?? hh; mm = tt[1] ?? mm;
|
||
}
|
||
const pad = (n) => String(n).padStart(2, '0');
|
||
return `${parts[0]}-${pad(parts[1])}-${pad(parts[2])} ${pad(hh)}:${pad(mm)}`;
|
||
};
|
||
const left = compact(sDate, sTime, false);
|
||
const right = compact(uDate, uTime, true);
|
||
if (left && right) return `${left} – ${right}`;
|
||
if (left) return `Since ${left}`;
|
||
if (right) return `Until ${right}`;
|
||
return "";
|
||
} catch (err) {
|
||
return "";
|
||
}
|
||
};
|
||
|
||
UI.updateHistorySubtitle = function updateHistorySubtitle() {
|
||
try {
|
||
const el = state.dom.historySubtitle;
|
||
if (!el) return;
|
||
const count = (state.historyTotal != null) ? state.historyTotal : state.metrics?.historyCount;
|
||
const base = (count != null) ? `${count} entries` : null;
|
||
const range = UI.currentHistoryRangeLabel();
|
||
const text = [base, range].filter(Boolean).join(' • ');
|
||
el.textContent = text;
|
||
} catch (err) { /* noop */ }
|
||
};
|
||
|
||
UI.historyFilters = function historyFilters() {
|
||
const filtersBtn = UI.button({ icon: "ti ti-filter", variant: "ghost", subtle: true, title: "Filters" });
|
||
const sortToggle = UI.sortToggleButton();
|
||
|
||
filtersBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
Events.toggleFiltersPopover(e.currentTarget);
|
||
});
|
||
|
||
return [filtersBtn, sortToggle, UI.clearFiltersIcon()];
|
||
};
|
||
|
||
UI.clearFiltersIcon = function clearFiltersIcon() {
|
||
const btn = UI.button({ icon: "ti ti-x", variant: "ghost", subtle: true, title: "Clear filters" });
|
||
btn.addEventListener('click', () => Events.applyHistoryPreset('clear'));
|
||
return btn;
|
||
};
|
||
|
||
UI.sortToggleButton = function sortToggleButton() {
|
||
const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc';
|
||
const icon = dir === 'asc' ? 'ti ti-arrow-bar-to-up' : 'ti ti-arrow-bar-to-down';
|
||
const title = dir === 'asc' ? 'Sort ascending (click to toggle)' : 'Sort descending (click to toggle)';
|
||
const btn = UI.button({ icon, variant: 'ghost', subtle: true, title });
|
||
btn.addEventListener('click', () => {
|
||
const current = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc';
|
||
const next = current === 'asc' ? 'desc' : 'asc';
|
||
Events.setHistorySort(next);
|
||
UI.updateSortToggle();
|
||
});
|
||
state.dom.sortToggleBtn = btn;
|
||
return btn;
|
||
};
|
||
|
||
UI.updateSortToggle = function updateSortToggle() {
|
||
try {
|
||
const btn = state.dom.sortToggleBtn;
|
||
if (!btn) return;
|
||
const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc';
|
||
const icon = dir === 'asc' ? 'ti ti-arrow-bar-to-up' : 'ti ti-arrow-bar-to-down';
|
||
btn.innerHTML = '';
|
||
btn.appendChild(UI.icon(icon));
|
||
btn.title = dir === 'asc' ? 'Sort ascending (click to toggle)' : 'Sort descending (click to toggle)';
|
||
} catch (err) { /* noop */ }
|
||
};
|
||
|
||
UI.buildFiltersPopover = function buildFiltersPopover(anchor) {
|
||
const since = UI.el("input", { type: "date", class: "pqueue-input", value: state.filters?.historySince || "", id: "pqueue-since-date" });
|
||
const sinceTime = UI.el("input", { type: "time", class: "pqueue-input", value: state.filters?.historySinceTime || "", id: "pqueue-since-time", step: "1" });
|
||
const until = UI.el("input", { type: "date", class: "pqueue-input", value: state.filters?.historyUntil || "", id: "pqueue-until-date" });
|
||
const untilTime = UI.el("input", { type: "time", class: "pqueue-input", value: state.filters?.historyUntilTime || "", id: "pqueue-until-time", step: "1" });
|
||
const onChange = () => Events.updateHistoryDateFilters(since.value, until.value, sinceTime.value, untilTime.value);
|
||
[since, sinceTime, until, untilTime].forEach((el) => el.addEventListener("change", onChange));
|
||
|
||
state.dom.historySince = since;
|
||
state.dom.historySinceTime = sinceTime;
|
||
state.dom.historyUntil = until;
|
||
state.dom.historyUntilTime = untilTime;
|
||
|
||
const dates = UI.el("div", { class: "pqueue-popover__grid" }, [
|
||
UI.el("label", { class: "pqueue-field" }, [UI.el("span", { class: "pqueue-field__label", text: "Since date" }), since]),
|
||
UI.el("label", { class: "pqueue-field" }, [UI.el("span", { class: "pqueue-field__label", text: "Since time" }), sinceTime]),
|
||
UI.el("label", { class: "pqueue-field" }, [UI.el("span", { class: "pqueue-field__label", text: "Until date" }), until]),
|
||
UI.el("label", { class: "pqueue-field" }, [UI.el("span", { class: "pqueue-field__label", text: "Until time" }), untilTime]),
|
||
]);
|
||
|
||
const presets = UI.el("div", { class: "pqueue-popover__section pqueue-popover__presets" }, [
|
||
UI.button({ text: "Today", variant: "ghost", subtle: true, size: "sm", onClick: () => Events.applyHistoryPreset("today") }),
|
||
UI.button({ text: "Last 24h", variant: "ghost", subtle: true, size: "sm", onClick: () => Events.applyHistoryPreset("24h") }),
|
||
UI.button({ text: "Last 7d", variant: "ghost", subtle: true, size: "sm", onClick: () => Events.applyHistoryPreset("7d") }),
|
||
UI.button({ text: "Clear", variant: "ghost", subtle: true, size: "sm", onClick: () => Events.applyHistoryPreset("clear") }),
|
||
]);
|
||
|
||
const header = UI.el('div', { class: 'pqueue-popover__header pqueue-card__header' }, [
|
||
UI.el('div', { class: 'pqueue-popover__title', text: 'History filters' }),
|
||
UI.el('div', { class: 'pqueue-popover__hint', text: 'Local timezone applied' }),
|
||
UI.button({ icon: 'ti ti-x', variant: 'ghost', subtle: true, title: 'Close', onClick: () => { try { pop?.dispatchEvent(new CustomEvent('pqueue-close', { bubbles: true })); } catch (err) {} } })
|
||
]);
|
||
|
||
const footer = UI.el('div', { class: 'pqueue-popover__footer' }, [
|
||
UI.el('div', { class: 'pqueue-popover__footer-left' }, [UI.el('span', { class: 'pqueue-muted', text: UI.currentHistoryRangeLabel() })]),
|
||
UI.el('div', { class: 'pqueue-popover__footer-right' }, [
|
||
UI.button({ text: 'Clear', variant: 'ghost', subtle: true, size: 'sm', onClick: () => Events.applyHistoryPreset('clear') }),
|
||
UI.button({ text: 'Done', variant: 'secondary', size: 'sm', onClick: () => { try { pop?.dispatchEvent(new CustomEvent('pqueue-close', { bubbles: true })); } catch (err) {} } })
|
||
])
|
||
]);
|
||
|
||
const content = UI.el('div', { class: 'pqueue-card__body pqueue-popover__content' }, [
|
||
UI.el('div', { class: 'pqueue-popover__section' }, [UI.el('strong', { text: 'Date range' })]),
|
||
dates,
|
||
UI.el('div', { class: 'pqueue-popover__section' }, [UI.el('strong', { text: 'Presets' })]),
|
||
presets
|
||
]);
|
||
|
||
const pop = UI.el('div', { class: ['pqueue-popover', 'pqueue-card'], role: 'dialog', 'aria-label': 'History filters' }, [
|
||
UI.el('div', { class: 'pqueue-popover__arrow' }),
|
||
header,
|
||
content,
|
||
footer
|
||
]);
|
||
pop.setAttribute('data-open', 'false');
|
||
|
||
const keyHandler = (e) => {
|
||
if (e.key === 'Escape') {
|
||
e.preventDefault();
|
||
try { pop.dispatchEvent(new CustomEvent('pqueue-close', { bubbles: true })); } catch (err) {}
|
||
} else if (e.key === 'Tab') {
|
||
const focusables = pop.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||
const list = Array.from(focusables).filter((n) => !n.hasAttribute('disabled'));
|
||
if (!list.length) return;
|
||
const first = list[0];
|
||
const last = list[list.length - 1];
|
||
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
||
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
||
}
|
||
};
|
||
pop.addEventListener('keydown', keyHandler);
|
||
|
||
try {
|
||
const rect = anchor.getBoundingClientRect();
|
||
const maxLeft = window.innerWidth - 360;
|
||
const left = Math.min(rect.left + window.scrollX, maxLeft);
|
||
const top = rect.bottom + 8 + window.scrollY;
|
||
pop.style.top = `${top}px`;
|
||
pop.style.left = `${left}px`;
|
||
const arrow = pop.querySelector('.pqueue-popover__arrow');
|
||
if (arrow) {
|
||
const anchorCenter = rect.left + rect.width / 2 + window.scrollX;
|
||
const popLeft = left;
|
||
const offset = Math.max(14, Math.min((anchorCenter - popLeft), 346));
|
||
arrow.style.left = `${offset}px`;
|
||
}
|
||
} catch (err) {}
|
||
|
||
window.setTimeout(() => since.focus(), 0);
|
||
|
||
return pop;
|
||
};
|
||
})();
|
||
|
||
|
||
|
||
|