278 lines
10 KiB
TypeScript
278 lines
10 KiB
TypeScript
import { get } from "../../api/client.js";
|
|
import { buildStackMembersURL } from "../../api/endpoints.js";
|
|
import { EVENTS } from "../../app/events.js";
|
|
|
|
const STACK_GROUP_FETCH_LIMIT = 501;
|
|
|
|
export function bindCardOverlayButton(button: any): any {
|
|
if (!button || button.dataset?.mjrOverlayButtonBound === "1") return button;
|
|
|
|
try {
|
|
button.dataset.mjrOverlayButtonBound = "1";
|
|
} catch (e: any) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
button.draggable = false;
|
|
} catch (e: any) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
const stopOnly = (event: any) => {
|
|
event.stopPropagation();
|
|
};
|
|
const stopAndPrevent = (event: any) => {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
};
|
|
|
|
button.addEventListener("pointerdown", stopOnly);
|
|
button.addEventListener("mousedown", stopAndPrevent);
|
|
button.addEventListener("touchstart", stopOnly, { passive: true });
|
|
button.addEventListener("dblclick", stopOnly);
|
|
button.addEventListener("keydown", stopOnly);
|
|
button.addEventListener("dragstart", stopAndPrevent);
|
|
|
|
return button;
|
|
}
|
|
|
|
function _setIconCount(button: any, iconClass: any, count: number | null = null, countClass = "mjr-stack-group-button-count") {
|
|
button.textContent = "";
|
|
const icon = document.createElement("span");
|
|
icon.className = iconClass;
|
|
button.appendChild(icon);
|
|
if (count != null) {
|
|
const countEl = document.createElement("span");
|
|
countEl.className = countClass;
|
|
countEl.textContent = String(count);
|
|
button.appendChild(countEl);
|
|
}
|
|
}
|
|
|
|
function _isGroupEnabled(gridContainer: any) {
|
|
return String(gridContainer?.dataset?.mjrGroupStacks || "") === "1";
|
|
}
|
|
|
|
function _getGroupKey(asset: any) {
|
|
const stackId = String(asset?.stack_id || "").trim();
|
|
if (stackId) return `stack:${stackId}`;
|
|
return "";
|
|
}
|
|
|
|
async function _fetchGroupMembers(asset: any) {
|
|
const stackId = String(asset?.stack_id || "").trim();
|
|
if (stackId) {
|
|
const res = await get(buildStackMembersURL(stackId, { limit: STACK_GROUP_FETCH_LIMIT }), { timeoutMs: 30_000 });
|
|
if (res?.ok && Array.isArray(res?.data)) return res.data;
|
|
throw new Error(String(res?.error || "Failed to load stack members"));
|
|
}
|
|
return [asset].filter(Boolean);
|
|
}
|
|
|
|
function _sortMembers(items: any) {
|
|
return (Array.isArray(items) ? items : []).slice().sort((a: any, b: any) => {
|
|
const am = Number(a?.mtime || a?.created_at || 0) || 0;
|
|
const bm = Number(b?.mtime || b?.created_at || 0) || 0;
|
|
return bm - am;
|
|
});
|
|
}
|
|
|
|
export function getStackAwareAssetKey(gridContainer: any, asset: any, fallbackKey: string): string {
|
|
if (!_isGroupEnabled(gridContainer)) return fallbackKey;
|
|
const groupKey = _getGroupKey(asset);
|
|
return groupKey || fallbackKey;
|
|
}
|
|
|
|
export function ensureStackGroupCard(gridContainer: any, card: any, asset: any): void {
|
|
if (!card || !asset || !_isGroupEnabled(gridContainer)) {
|
|
card?.querySelector?.(".mjr-stack-group-button")?.remove?.();
|
|
if (card?.dataset) {
|
|
delete card.dataset.mjrStacked;
|
|
delete card.dataset.mjrStackCount;
|
|
delete card.dataset.mjrStackGroupKey;
|
|
}
|
|
return;
|
|
}
|
|
// Vue cards handle their own stack buttons reactively via AssetCardInner.vue
|
|
if (card._mjrIsVue) return;
|
|
const groupKey = _getGroupKey(asset);
|
|
if (!groupKey) {
|
|
card.querySelector?.(".mjr-stack-group-button")?.remove?.();
|
|
delete card.dataset.mjrStacked;
|
|
delete card.dataset.mjrStackCount;
|
|
delete card.dataset.mjrStackGroupKey;
|
|
return;
|
|
}
|
|
card.dataset.mjrStackGroupKey = groupKey;
|
|
|
|
const cachedMembers = gridContainer?._mjrStackMembersCache?.get?.(groupKey);
|
|
const knownCount = Array.isArray(cachedMembers)
|
|
? cachedMembers.length
|
|
: Number(asset?.stack_asset_count || asset?._mjrFeedGroupCount || 0) || 0;
|
|
if (knownCount > 1) {
|
|
card.dataset.mjrStacked = "true";
|
|
card.dataset.mjrStackCount = String(knownCount);
|
|
} else {
|
|
delete card.dataset.mjrStacked;
|
|
delete card.dataset.mjrStackCount;
|
|
}
|
|
|
|
let button = card.querySelector(".mjr-stack-group-button");
|
|
if (!button) {
|
|
button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "mjr-stack-group-button";
|
|
button.setAttribute("aria-label", "Open generation group in grid");
|
|
bindCardOverlayButton(button);
|
|
button.addEventListener("click", async (event: any) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (button.dataset?.mjrStackOpening === "1") return;
|
|
|
|
try {
|
|
button.dataset.mjrStackOpening = "1";
|
|
button.disabled = true;
|
|
const activeGrid = button?._mjrGridContainer;
|
|
const activeCard = button?._mjrCard;
|
|
const activeAsset = button?._mjrAsset;
|
|
const activeGroupKey = String(button?._mjrGroupKey || "").trim();
|
|
if (!activeGrid || !activeCard || !activeAsset || !activeGroupKey) return;
|
|
if (!activeGrid._mjrStackMembersCache) {
|
|
activeGrid._mjrStackMembersCache = new Map();
|
|
}
|
|
let members = activeGrid._mjrStackMembersCache.get(activeGroupKey);
|
|
if (!Array.isArray(members)) {
|
|
members = await _fetchGroupMembers(activeAsset);
|
|
activeGrid._mjrStackMembersCache.set(activeGroupKey, members);
|
|
}
|
|
const ordered = _sortMembers(members);
|
|
const count = ordered.length;
|
|
if (count > 1) {
|
|
activeCard.dataset.mjrStacked = "true";
|
|
activeCard.dataset.mjrStackCount = String(count);
|
|
_setIconCount(button, "pi pi-clone", count);
|
|
}
|
|
activeGrid.dispatchEvent(
|
|
new CustomEvent(EVENTS.OPEN_STACK_GROUP, {
|
|
bubbles: true,
|
|
detail: {
|
|
asset: activeAsset,
|
|
members: ordered,
|
|
title: `Generation group (${count} assets)`,
|
|
stackId: String(activeAsset?.stack_id || "").trim(),
|
|
},
|
|
}),
|
|
);
|
|
} catch (err: any) {
|
|
console.debug?.(err);
|
|
} finally {
|
|
try {
|
|
delete button.dataset.mjrStackOpening;
|
|
button.disabled = false;
|
|
} catch (e: any) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
});
|
|
card.appendChild(button);
|
|
}
|
|
|
|
button._mjrGridContainer = gridContainer;
|
|
button._mjrCard = card;
|
|
button._mjrAsset = asset;
|
|
button._mjrGroupKey = groupKey;
|
|
button.title =
|
|
knownCount > 1
|
|
? `Open generation group in grid (${knownCount} assets)`
|
|
: "Open generation group in grid";
|
|
_setIconCount(button, "pi pi-clone", knownCount > 1 ? knownCount : null);
|
|
}
|
|
|
|
export function disposeStackGroupCards(gridContainer: any): void {
|
|
try {
|
|
gridContainer._mjrStackMembersCache?.clear?.();
|
|
} catch (e: any) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attach or update a "duplicate stack" button on a card.
|
|
* Called when a filename collision is detected (same filename, different paths).
|
|
* The button opens all copies in the similar/stack-group panel.
|
|
*/
|
|
export function ensureDupStackCard(gridContainer: any, card: any, asset: any): void {
|
|
if (!card || !asset) return;
|
|
// Vue cards handle their own dup-stack buttons reactively via AssetCardInner.vue
|
|
if (card._mjrIsVue) return;
|
|
const isDupStack = !!asset._mjrDupStack;
|
|
const count = Number(asset._mjrDupCount || 0) || 0;
|
|
|
|
// Remove the button if no longer a dup stack
|
|
if (!isDupStack || count < 2) {
|
|
const existing = card.querySelector(".mjr-dup-stack-button");
|
|
if (existing) existing.remove();
|
|
delete card.dataset.mjrDupStacked;
|
|
delete card.dataset.mjrDupCount;
|
|
return;
|
|
}
|
|
|
|
// Mark the card
|
|
card.dataset.mjrDupStacked = "true";
|
|
card.dataset.mjrDupCount = String(count);
|
|
|
|
// Update existing button or create it
|
|
let button = card.querySelector(".mjr-dup-stack-button");
|
|
if (!button) {
|
|
button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "mjr-dup-stack-button";
|
|
button.setAttribute("aria-label", "Show all duplicates");
|
|
bindCardOverlayButton(button);
|
|
|
|
button.addEventListener("click", (event: any) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (button.dataset?.mjrStackOpening === "1") return;
|
|
try {
|
|
button.dataset.mjrStackOpening = "1";
|
|
button.disabled = true;
|
|
const activeGrid = button?._mjrGridContainer;
|
|
const activeAsset = button?._mjrAsset;
|
|
if (!activeGrid || !activeAsset) return;
|
|
const members = Array.isArray(activeAsset._mjrDupMembers)
|
|
? activeAsset._mjrDupMembers
|
|
: [activeAsset];
|
|
const n = members.length;
|
|
activeGrid.dispatchEvent(
|
|
new CustomEvent(EVENTS.OPEN_STACK_GROUP, {
|
|
bubbles: true,
|
|
detail: {
|
|
asset: activeAsset,
|
|
members,
|
|
title: `Duplicates - ${n} copies`,
|
|
isDupGroup: true,
|
|
},
|
|
}),
|
|
);
|
|
} catch (err: any) {
|
|
console.debug?.(err);
|
|
} finally {
|
|
try {
|
|
delete button.dataset.mjrStackOpening;
|
|
button.disabled = false;
|
|
} catch (e: any) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
});
|
|
card.appendChild(button);
|
|
}
|
|
|
|
button._mjrGridContainer = gridContainer;
|
|
button._mjrAsset = asset;
|
|
const label = `${count} duplicate${count > 1 ? "s" : ""}`;
|
|
button.title = `${label} - click to compare all copies`;
|
|
_setIconCount(button, "pi pi-copy", count, "mjr-dup-stack-count");
|
|
}
|