Files
Andrés Zsögön 0bed3c80a2 Release 2.3.0 with responsive mobile editing
- Add responsive touch and pen workflows across the editor, Gallery, Merger, and About views.
- Improve Gallery density, batching, and thumbnail memory behavior.
- Localize the updated interface and installation documentation across all supported languages.
2026-08-14 19:28:17 -03:00

2232 lines
90 KiB
JavaScript

import { t } from "./i18n.js";
import {
drawBoneWithOutline,
getPersistedSetting,
isValidKeypoint,
setPersistedSetting,
showToast,
toRgba
} from "../utils.js";
import { getFormatForPose } from "../formats/index.js";
import { registerModule } from "./index.js";
import { UiIcons } from "../ui-icons.js";
const GALLERY_VIEW_MODE_KEY = "openpose_editor.gallery.viewMode";
const GALLERY_VIEW_MODES = new Set(["medium", "large", "small", "tiles"]);
const COMPACT_GALLERY_BATCH_SIZE = 120;
const HAND_EDGES = [
[0, 1], [1, 2], [2, 3], [3, 4],
[0, 5], [5, 6], [6, 7], [7, 8],
[0, 9], [9, 10], [10, 11], [11, 12],
[0, 13], [13, 14], [14, 15], [15, 16],
[0, 17], [17, 18], [18, 19], [19, 20]
];
const HAND_KEYPOINT_COLORS = [
[100, 100, 100],
[100, 0, 0], [150, 0, 0], [200, 0, 0], [255, 0, 0],
[100, 100, 0], [150, 150, 0], [200, 200, 0], [255, 255, 0],
[0, 100, 50], [0, 150, 75], [0, 200, 100], [0, 255, 125],
[0, 50, 100], [0, 75, 150], [0, 100, 200], [0, 125, 255],
[100, 0, 100], [150, 0, 150], [200, 0, 200], [255, 0, 255]
];
function normalizeGallerySearch(value) {
return String(value || "")
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/\\/g, "/")
.toLowerCase()
.trim();
}
function matchesGallerySearch(values, query) {
const normalizedQuery = normalizeGallerySearch(query);
if (!normalizedQuery) {
return true;
}
const haystack = normalizeGallerySearch(values.filter(Boolean).join(" "));
return normalizedQuery.split(/\s+/).every((token) => haystack.includes(token));
}
function isValidGalleryViewMode(mode) {
return GALLERY_VIEW_MODES.has(mode);
}
function loadGalleryViewMode() {
const stored = getPersistedSetting(GALLERY_VIEW_MODE_KEY, null);
if (isValidGalleryViewMode(stored)) {
return stored;
}
return null;
}
function storeGalleryViewMode(mode) {
if (!isValidGalleryViewMode(mode)) {
return false;
}
return setPersistedSetting(GALLERY_VIEW_MODE_KEY, mode);
}
// Utility function to count keypoints in face/hand groups
function countExtraKeypoints(groups) {
if (!Array.isArray(groups)) return 0;
let count = 0;
for (const group of groups) {
if (Array.isArray(group)) {
for (const kp of group) {
if (Array.isArray(kp)) count++;
}
}
}
return count;
}
function getGalleryPresetDetails(preset) {
const keypoints = Array.isArray(preset?.keypoints) ? preset.keypoints : [];
const detectedFormat = getFormatForPose(keypoints);
const keypointsPerPerson = detectedFormat?.keypoints?.length || 18;
const personCount = Math.max(1, Math.floor(keypoints.length / keypointsPerPerson));
return {
detectedFormat,
personCount,
bodyCount: keypoints.filter(isValidKeypoint).length,
faceCount: countExtraKeypoints(preset?.faceKeypoints),
leftHandCount: countExtraKeypoints(preset?.handLeftKeypoints),
rightHandCount: countExtraKeypoints(preset?.handRightKeypoints)
};
}
function getGalleryFilename(preset) {
const source = String(preset?.displayFilename || preset?.sourceFile || "");
return source.replace(/\\/g, "/").split("/").pop() || "\u2014";
}
function formatCanvasMetaSize(width, height) {
const canvasWidth = Number(width);
const canvasHeight = Number(height);
if (
Number.isFinite(canvasWidth) && canvasWidth > 0 &&
Number.isFinite(canvasHeight) && canvasHeight > 0
) {
return t("gallery.canvas.size", {
width: Math.round(canvasWidth),
height: Math.round(canvasHeight)
});
}
return t("gallery.canvas.unknown");
}
function formatGalleryStats(poseCount, fileCount, libraryCount) {
return t("gallery.stats.summary", {
poses: t("gallery.count.poses", { count: poseCount }),
files: t("gallery.count.files", { count: fileCount }),
libraries: t("gallery.count.libraries", { count: libraryCount })
});
}
function isStandardOpenPosePoseObject(payload) {
if (!payload || typeof payload !== "object") {
return false;
}
const canvasWidth = Number(payload.canvas_width);
const canvasHeight = Number(payload.canvas_height);
if (!Number.isFinite(canvasWidth) || canvasWidth <= 0 || !Number.isFinite(canvasHeight) || canvasHeight <= 0) {
return false;
}
if (!Array.isArray(payload.people) || payload.people.length === 0) {
return false;
}
for (const person of payload.people) {
if (!person || typeof person !== "object") {
return false;
}
if (!Array.isArray(person.pose_keypoints_2d) || person.pose_keypoints_2d.length === 0) {
return false;
}
if (person.pose_keypoints_2d.length % 3 !== 0) {
return false;
}
}
return true;
}
function isStandardOpenPoseCollectionPayload(payload) {
if (!payload || typeof payload !== "object") {
return false;
}
const keys = Object.keys(payload);
if (keys.length <= 1) {
return false;
}
for (const key of keys) {
if (!isStandardOpenPosePoseObject(payload[key])) {
return false;
}
}
return true;
}
/**
* GalleryManager handles all Gallery tab logic.
* It receives a reference to the OpenPose instance for accessing shared data.
*/
class GalleryManager {
constructor(container, openposeInstance) {
this.container = container;
this.openpose = openposeInstance;
this.galleryContainer = container.querySelector(".openpose-gallery-content");
const storedViewMode = loadGalleryViewMode();
this.viewMode = storedViewMode || "medium";
if (!storedViewMode) {
storeGalleryViewMode(this.viewMode);
}
this.collectionFiles = new Set();
this.emptyPoseFiles = [];
this.searchQuery = "";
this.selectedPresetId = null;
this.focusedHandSide = null;
this.compactRenderLimit = COMPACT_GALLERY_BATCH_SIZE;
this.lastCompact = null;
this.needsRefresh = true;
this.thumbnailObserver = null;
this.thumbnailRenderers = new WeakMap();
this.setViewMode(this.viewMode);
this.clearSelection();
}
markDirty() {
this.needsRefresh = true;
}
stopThumbnailObserver(release = false) {
this.thumbnailObserver?.disconnect();
this.thumbnailObserver = null;
if (!release) {
return;
}
this.galleryContainer?.querySelectorAll(".openpose-gallery-thumbnail").forEach((surface) => {
const thumbnail = this.thumbnailRenderers.get(surface);
if (!thumbnail?.rendered) {
return;
}
if (surface.tagName === "IMG") {
surface.removeAttribute("src");
} else {
surface.width = 1;
surface.height = 1;
}
thumbnail.rendered = false;
});
}
startThumbnailObserver() {
this.stopThumbnailObserver();
const surfaces = Array.from(this.galleryContainer?.querySelectorAll(".openpose-gallery-thumbnail") || []);
if (surfaces.length === 0) {
return;
}
if (typeof IntersectionObserver !== "function") {
surfaces.forEach((surface) => this.renderThumbnail(surface));
return;
}
this.thumbnailObserver = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
this.renderThumbnail(entry.target);
continue;
}
const thumbnail = this.thumbnailRenderers.get(entry.target);
if (!thumbnail?.rendered) {
continue;
}
if (entry.target.tagName === "IMG") {
continue;
}
entry.target.width = 1;
entry.target.height = 1;
thumbnail.rendered = false;
}
}, {
root: this.galleryContainer,
rootMargin: "320px 160px"
});
surfaces.forEach((surface) => this.thumbnailObserver.observe(surface));
const rootRect = this.galleryContainer.getBoundingClientRect();
const minY = rootRect.top - 320;
const maxY = rootRect.bottom + 320;
for (const surface of surfaces) {
const rect = surface.getBoundingClientRect();
if (rect.bottom < minY) {
continue;
}
if (rect.top > maxY) {
break;
}
this.renderThumbnail(surface);
}
}
queueThumbnail(surface, size, render) {
surface.classList.add("openpose-gallery-thumbnail");
if (surface.tagName === "IMG") {
surface.alt = "";
surface.draggable = false;
} else {
surface.width = 1;
surface.height = 1;
}
this.thumbnailRenderers.set(surface, {
size,
render,
rendered: false
});
}
renderThumbnail(surface) {
const thumbnail = this.thumbnailRenderers.get(surface);
if (!thumbnail || thumbnail.rendered) {
return;
}
const canvas = surface.tagName === "IMG" ? document.createElement("canvas") : surface;
canvas.width = thumbnail.size;
canvas.height = thumbnail.size;
thumbnail.render(canvas);
if (surface.tagName === "IMG") {
surface.src = canvas.toDataURL("image/png");
}
thumbnail.rendered = true;
}
clearSelection() {
this.selectedPresetId = null;
this.focusedHandSide = null;
this.container.querySelectorAll(".openpose-gallery-hand-row.is-active").forEach((row) => {
row.classList.remove("is-active");
});
this.container.querySelectorAll(".openpose-gallery-item.is-selected").forEach((item) => {
item.classList.remove("is-selected");
item.style.background = "transparent";
item.style.boxShadow = "none";
});
const button = this.container.querySelector('[data-action="gallery-insert-pose"]');
if (button) {
button.disabled = true;
button.style.opacity = "0.5";
button.style.cursor = "not-allowed";
}
this.updateSelectedDetails(null);
const canvas = this.container.querySelector(".openpose-gallery-selected-preview");
const ctx = canvas?.getContext("2d");
if (!canvas || !ctx) {
return;
}
const frame = canvas.closest(".openpose-preset-preview-frame");
canvas.width = Math.max(1, Math.round(frame?.clientWidth || 220));
canvas.height = Math.max(1, Math.round(frame?.clientHeight || 220));
ctx.clearRect(0, 0, canvas.width, canvas.height);
const previewSurface = this.openpose.getPreviewSurfaceFill();
if (previewSurface) {
ctx.fillStyle = previewSurface;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
}
renderLoading() {
if (!this.galleryContainer) {
return;
}
this.stopThumbnailObserver();
this.clearSelection();
this.galleryContainer.innerHTML = "";
const loading = document.createElement("div");
loading.className = "openpose-alert openpose-alert-warning alert alert-warning openpose-gallery-loading";
loading.setAttribute("role", "status");
const icon = document.createElement("span");
icon.className = "openpose-alert-icon";
icon.textContent = "\u231B";
const body = document.createElement("div");
body.className = "openpose-alert-body";
const message = document.createElement("strong");
message.textContent = t("gallery.state.loading");
body.appendChild(message);
loading.append(icon, body);
this.galleryContainer.appendChild(loading);
galleryOverlay.applyStyles(this.container);
const statsBadge = this.container.querySelector(".openpose-gallery-stats-badge");
if (statsBadge) {
statsBadge.textContent = t("gallery.state.loading");
}
const canvas = this.container.querySelector(".openpose-gallery-selected-preview");
const ctx = canvas?.getContext("2d");
if (!canvas || !ctx) {
return;
}
const frame = canvas.closest(".openpose-preset-preview-frame");
canvas.width = Math.max(1, Math.round(frame?.clientWidth || 220));
canvas.height = Math.max(1, Math.round(frame?.clientHeight || 220));
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = this.openpose.getPreviewSurfaceFill();
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#999";
ctx.font = "bold 80px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("\u231B", canvas.width / 2, canvas.height / 2);
}
selectPreset(preset, selectedItem = null) {
if (!preset?.id) {
this.clearSelection();
return;
}
this.selectedPresetId = preset.id;
this.focusedHandSide = null;
this.container.querySelectorAll(".openpose-gallery-hand-row.is-active").forEach((row) => {
row.classList.remove("is-active");
});
this.container.querySelectorAll(".openpose-gallery-item").forEach((item) => {
const isSelected = item === selectedItem || item._galleryPresetId === preset.id;
item.classList.toggle("is-selected", isSelected);
item.style.background = isSelected
? "var(--openpose-gallery-selection-bg)"
: "transparent";
item.style.boxShadow = "none";
});
const button = this.container.querySelector('[data-action="gallery-insert-pose"]');
if (button) {
button.disabled = false;
button.style.opacity = "";
button.style.cursor = "pointer";
}
this.renderSelectedPreview(preset);
this.updateSelectedDetails(preset);
}
updateSelectedDetails(preset) {
const empty = this.container.querySelector(".openpose-gallery-details-empty");
const content = this.container.querySelector(".openpose-gallery-details-content");
if (!empty || !content) {
return;
}
empty.hidden = !!preset;
content.hidden = !preset;
if (!preset) {
return;
}
const details = getGalleryPresetDetails(preset);
const canvasWidth = Number(preset.canvas_width ?? preset.width);
const canvasHeight = Number(preset.canvas_height ?? preset.height);
const location = preset.galleryGroupTitle || preset.sourceFile || "\u2014";
const values = {
name: this.openpose.normalizePoseName(preset.label || preset.id || t("gallery.fallback.pose")),
file: getGalleryFilename(preset),
location,
canvas: Number.isFinite(canvasWidth) && canvasWidth > 0 && Number.isFinite(canvasHeight) && canvasHeight > 0
? `${Math.round(canvasWidth)} \u00D7 ${Math.round(canvasHeight)} px`
: "\u2014",
people: String(details.personCount),
body: String(details.bodyCount),
face: String(details.faceCount),
leftHand: String(details.leftHandCount),
rightHand: String(details.rightHandCount)
};
for (const [key, value] of Object.entries(values)) {
const element = content.querySelector(`[data-gallery-detail="${key}"]`);
if (element) {
element.textContent = value;
if (key === "file" || key === "location") {
element.title = value;
}
}
}
const handCounts = {
left: details.leftHandCount,
right: details.rightHandCount
};
this.container.querySelectorAll(".openpose-gallery-hand-row").forEach((row) => {
const available = handCounts[row.dataset.galleryHand] > 0;
row.classList.toggle("is-available", available);
row.tabIndex = available ? 0 : -1;
row.setAttribute("aria-disabled", available ? "false" : "true");
row.title = available
? t("gallery.hand.preview", {
hand: t(`gallery.hand.${row.dataset.galleryHand}`)
})
: "";
const icon = row.querySelector(".openpose-gallery-hand-zoom");
if (icon) {
icon.hidden = !available;
}
});
}
getSelectedPreset() {
return this.openpose.presets.find((preset) => preset.id === this.selectedPresetId) || null;
}
focusSelectedHand(side) {
const preset = this.getSelectedPreset();
const groups = side === "right" ? preset?.handRightKeypoints : preset?.handLeftKeypoints;
if (!preset || countExtraKeypoints(groups) === 0) {
return;
}
this.focusedHandSide = side;
this.container.querySelectorAll(".openpose-gallery-hand-row").forEach((row) => {
row.classList.toggle("is-active", row.dataset.galleryHand === side);
});
this.renderSelectedHandPreview(preset, side);
}
clearFocusedHand() {
if (!this.focusedHandSide) {
return;
}
this.focusedHandSide = null;
this.container.querySelectorAll(".openpose-gallery-hand-row.is-active").forEach((row) => {
row.classList.remove("is-active");
});
const preset = this.getSelectedPreset();
if (preset) {
this.renderSelectedPreview(preset);
}
}
drawHandSkeleton(ctx, groups, scale, offsetX, offsetY, focused = false) {
if (!Array.isArray(groups)) {
return;
}
const lineWidth = focused
? Math.max(3, Math.min(7, 2.5 * scale))
: Math.max(1.5, 3 * scale);
const outlineWidth = focused ? 3 : 1.5;
const outlineColor = focused
? "rgba(255,255,255,0.78)"
: "rgba(255,255,255,0.42)";
for (const hand of groups) {
if (!Array.isArray(hand)) {
continue;
}
for (const [a, b] of HAND_EDGES) {
const pointA = hand[a];
const pointB = hand[b];
if (!isValidKeypoint(pointA) || !isValidKeypoint(pointB)) {
continue;
}
const color = HAND_KEYPOINT_COLORS[b] || [255, 255, 255];
drawBoneWithOutline(
ctx,
pointA[0] * scale + offsetX,
pointA[1] * scale + offsetY,
pointB[0] * scale + offsetX,
pointB[1] * scale + offsetY,
`rgba(${color.join(", ")}, 0.9)`,
lineWidth,
outlineWidth,
outlineColor
);
}
}
}
renderSelectedPreview(preset) {
const canvas = this.container.querySelector(".openpose-gallery-selected-preview");
const frame = canvas?.closest(".openpose-preset-preview-frame");
if (!canvas || !preset) {
return;
}
canvas.width = Math.max(1, Math.round(frame?.clientWidth || 220));
canvas.height = Math.max(1, Math.round(frame?.clientHeight || 220));
const baseWidth = Number(preset.canvas_width ?? preset.width) || this.openpose.presetBaseWidth || 512;
const baseHeight = Number(preset.canvas_height ?? preset.height) || this.openpose.presetBaseHeight || 768;
this.openpose.renderPresetThumbnail(canvas, preset.keypoints, baseWidth, baseHeight);
const ctx = canvas.getContext("2d");
if (!ctx) {
return;
}
const padding = 10;
const scale = Math.min(
(canvas.width - padding * 2) / baseWidth,
(canvas.height - padding * 2) / baseHeight
);
const offsetX = (canvas.width - baseWidth * scale) / 2;
const offsetY = (canvas.height - baseHeight * scale) / 2;
this.drawHandSkeleton(ctx, preset.handLeftKeypoints, scale, offsetX, offsetY);
this.drawHandSkeleton(ctx, preset.handRightKeypoints, scale, offsetX, offsetY);
}
renderSelectedHandPreview(preset, side) {
const canvas = this.container.querySelector(".openpose-gallery-selected-preview");
const frame = canvas?.closest(".openpose-preset-preview-frame");
const groups = side === "right" ? preset?.handRightKeypoints : preset?.handLeftKeypoints;
if (!canvas || !Array.isArray(groups)) {
return;
}
canvas.width = Math.max(1, Math.round(frame?.clientWidth || 220));
canvas.height = Math.max(1, Math.round(frame?.clientHeight || 220));
const points = groups.flatMap((hand) => (
Array.isArray(hand) ? hand.filter(isValidKeypoint) : []
));
if (points.length === 0) {
this.renderSelectedPreview(preset);
return;
}
const ctx = canvas.getContext("2d");
if (!ctx) {
return;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
const previewSurface = this.openpose.getPreviewSurfaceFill();
if (previewSurface) {
ctx.fillStyle = previewSurface;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
const minX = Math.min(...points.map((point) => point[0]));
const maxX = Math.max(...points.map((point) => point[0]));
const minY = Math.min(...points.map((point) => point[1]));
const maxY = Math.max(...points.map((point) => point[1]));
const boxWidth = Math.max(1, maxX - minX);
const boxHeight = Math.max(1, maxY - minY);
const padding = 20;
const topPadding = 38;
const availableHeight = canvas.height - topPadding - padding;
const scale = Math.min(
(canvas.width - padding * 2) / boxWidth,
availableHeight / boxHeight
);
const offsetX = (canvas.width - boxWidth * scale) / 2 - minX * scale;
const offsetY = topPadding + (availableHeight - boxHeight * scale) / 2 - minY * scale;
this.drawHandSkeleton(ctx, groups, scale, offsetX, offsetY, true);
const label = t(`gallery.hand.${side}`);
ctx.font = "600 11px Arial, sans-serif";
ctx.textBaseline = "top";
const labelWidth = ctx.measureText(label).width + 12;
ctx.fillStyle = "rgba(0,0,0,0.58)";
ctx.fillRect(8, 8, labelWidth, 21);
ctx.fillStyle = "rgba(255,255,255,0.92)";
ctx.fillText(label, 14, 13);
}
insertSelectedPreset() {
if (!this.selectedPresetId) {
return;
}
if (this.openpose.presetSelect) {
this.openpose.presetSelect.value = this.selectedPresetId;
this.openpose.presetSelect.dispatchEvent(new Event("change", { bubbles: true }));
}
this.openpose.addPresetToCanvas(this.selectedPresetId);
this.openpose.setActiveTab("editor");
this.openpose.setEditorPane("canvas");
}
setSearchQuery(value) {
const nextQuery = String(value || "").trim();
if (nextQuery === this.searchQuery) {
return;
}
this.searchQuery = nextQuery;
this.compactRenderLimit = COMPACT_GALLERY_BATCH_SIZE;
this.refresh();
}
matchesPresetSearch(preset) {
return matchesGallerySearch([
preset?.displayFilename,
preset?.sourceFile,
preset?.galleryGroupTitle,
preset?.library,
preset?.label
], this.searchQuery);
}
updateStatsBadge(visiblePresets, totalPresets) {
const statsBadge = this.container.querySelector(".openpose-gallery-stats-badge");
if (!statsBadge) {
return;
}
const sourceFiles = new Set(visiblePresets.map((preset) => preset.sourceFile).filter(Boolean));
const libraries = new Set(visiblePresets.map((preset) => preset.library).filter(Boolean));
const visibleCount = visiblePresets.length;
const totalCount = totalPresets.length;
statsBadge.textContent = this.searchQuery
? t("gallery.stats.filtered", {
visible: visibleCount,
total: totalCount,
files: t("gallery.count.files", { count: sourceFiles.size }),
libraries: t("gallery.count.libraries", { count: libraries.size })
})
: formatGalleryStats(visibleCount, sourceFiles.size, libraries.size);
}
setViewMode(mode) {
const next = GALLERY_VIEW_MODES.has(mode) ? mode : "medium";
this.viewMode = next;
storeGalleryViewMode(next);
if (!this.galleryContainer) {
return;
}
this.galleryContainer.classList.remove(
"gallery-view--medium",
"gallery-view--large",
"gallery-view--small",
"gallery-view--tiles"
);
this.galleryContainer.classList.add(`gallery-view--${next}`);
}
getPreviewSize() {
if (this.viewMode === "large") {
return 280;
}
if (this.viewMode === "tiles") {
return 120;
}
if (this.viewMode === "small") {
return 96;
}
return 140;
}
getGroupTitle(sourceId, presets = []) {
const formatGalleryTitle = (rawTitle, icon) => {
if (!rawTitle || typeof rawTitle !== "string") {
return rawTitle;
}
const separator = "▸";
const formattedPath = rawTitle.split("/").join(` ${separator} `);
if (icon) {
return `${icon} ${formattedPath}`;
}
return formattedPath;
};
const explicitTitle = presets.find((preset) => preset?.galleryGroupTitle)?.galleryGroupTitle;
if (explicitTitle) {
return formatGalleryTitle(explicitTitle, "\u{1F4C1}");
}
const sourceFiles = [];
for (const preset of presets) {
if (preset?.sourceFile && typeof preset.sourceFile === "string") {
sourceFiles.push(preset.sourceFile);
continue;
}
if (!preset?.id || typeof preset.id !== "string") {
continue;
}
const splitIndex = preset.id.indexOf(":");
if (splitIndex === -1) {
continue;
}
const filename = preset.id.slice(0, splitIndex);
if (filename) {
sourceFiles.push(filename);
}
}
const uniqueFiles = Array.from(new Set(sourceFiles));
if (uniqueFiles.length === 1) {
const isCollection = presets.length > 1;
return formatGalleryTitle(`poses/${uniqueFiles[0]}`, isCollection ? "🧾" : "");
}
if (uniqueFiles.length > 1) {
const firstSlashIndex = uniqueFiles[0].lastIndexOf("/");
const baseDir = firstSlashIndex === -1 ? "" : uniqueFiles[0].slice(0, firstSlashIndex);
const sameDir = uniqueFiles.every((file) => {
const slashIndex = file.lastIndexOf("/");
const dir = slashIndex === -1 ? "" : file.slice(0, slashIndex);
return dir === baseDir;
});
if (sameDir) {
return formatGalleryTitle(baseDir ? `poses/${baseDir}` : "poses", "📁");
}
return formatGalleryTitle("poses", "📁");
}
if (!sourceId) {
return t("gallery.group.default");
}
if (sourceId.startsWith("group:")) {
const title = sourceId.slice("group:".length).trim();
return title || t("gallery.group.default");
}
const base = sourceId.replace(/\.json$/i, "").replace(/^.*[/\\]/, "");
return formatGalleryTitle(base.replace(/[_-]/g, " ").trim() || t("gallery.group.default"));
}
renderEmpty(message) {
if (!this.galleryContainer) {
return;
}
this.clearSelection();
this.galleryContainer.innerHTML = "";
const empty = document.createElement("div");
empty.className = "openpose-alert openpose-alert-info alert alert-info openpose-gallery-empty";
empty.setAttribute("role", "status");
const icon = document.createElement("span");
icon.className = "openpose-alert-icon";
icon.textContent = "\u{1F50D}";
const body = document.createElement("div");
body.className = "openpose-alert-body";
body.textContent = message;
empty.append(icon, body);
this.galleryContainer.appendChild(empty);
galleryOverlay.applyStyles(this.container);
}
updateLibraryWarning() {
const warning = this.container.querySelector(".openpose-gallery-library-warning");
const list = warning?.querySelector(".openpose-gallery-library-warning-list");
if (!warning || !list) {
return;
}
const unavailable = Array.isArray(this.openpose.unavailablePoseLibraries)
? this.openpose.unavailablePoseLibraries
: [];
list.replaceChildren();
for (const library of unavailable) {
if (!library?.path) {
continue;
}
const item = document.createElement("li");
item.textContent = library.reason
? `${library.path} — ${library.reason}`
: library.path;
list.appendChild(item);
}
warning.style.display = list.childElementCount > 0 ? "flex" : "none";
}
refresh() {
if (!this.galleryContainer) {
return;
}
this.stopThumbnailObserver();
this.setViewMode(this.viewMode);
this.updateLibraryWarning();
const op = this.openpose;
if (op.presetsLoading) {
this.renderLoading();
return;
}
if (!op.presets || op.presets.length === 0) {
this.renderEmpty(t("gallery.state.empty"));
return;
}
const allGalleryPresets = op.presets.filter((preset) => op.getPresetSourceId(preset) !== "Default");
const galleryPresets = allGalleryPresets.filter((preset) => this.matchesPresetSearch(preset));
const filteredEmptyPoseFiles = (this.emptyPoseFiles || []).filter(({ filename }) => (
matchesGallerySearch([filename], this.searchQuery)
));
if (allGalleryPresets.length === 0 && this.emptyPoseFiles.length === 0) {
this.renderEmpty(t("gallery.state.no_presets"));
return;
}
if (galleryPresets.length === 0 && filteredEmptyPoseFiles.length === 0) {
this.renderEmpty(t("gallery.state.no_search_match", { query: this.searchQuery }));
this.updateStatsBadge([], allGalleryPresets);
return;
}
this.galleryContainer.innerHTML = "";
const groups = new Map();
const order = [];
const previewSize = this.getPreviewSize();
const compact = this.container.classList.contains("ope-openpose-compact");
const renderLimit = compact ? this.compactRenderLimit : Number.POSITIVE_INFINITY;
let renderedCount = 0;
for (const preset of galleryPresets) {
const sourceId = preset.galleryGroupKey || op.getPresetSourceId(preset);
if (!groups.has(sourceId)) {
groups.set(sourceId, []);
order.push(sourceId);
}
groups.get(sourceId).push(preset);
}
order.forEach((sourceId) => {
const presets = groups.get(sourceId) || [];
const visiblePresets = presets.slice(0, Math.max(0, renderLimit - renderedCount));
if (!visiblePresets.length) {
return;
}
renderedCount += visiblePresets.length;
const section = document.createElement("div");
section.className = "openpose-gallery-section";
const title = document.createElement("div");
title.className = "openpose-gallery-title";
const titleText = document.createElement("span");
titleText.className = "openpose-gallery-title-text";
titleText.textContent = this.getGroupTitle(sourceId, presets);
title.appendChild(titleText);
const sourceFiles = new Set(presets.map((preset) => preset.sourceFile).filter(Boolean));
const isSingleCollection = sourceFiles.size === 1
&& this.collectionFiles.has(Array.from(sourceFiles)[0]);
const isDirectoryGroup = presets.some((preset) => (
preset.galleryGroupKey && preset.galleryGroupKey !== preset.sourceFile
));
const isCustomLibrary = presets.some((preset) => preset.customLibrary);
if (isSingleCollection || isDirectoryGroup || isCustomLibrary) {
const badges = document.createElement("span");
badges.className = "openpose-gallery-title-badges";
if (isCustomLibrary) {
const badge = document.createElement("span");
badge.className = "openpose-gallery-custom-path-pill";
badge.textContent = t("gallery.badge.custom_path");
badges.appendChild(badge);
}
if (isSingleCollection) {
const badge = document.createElement("span");
badge.className = "openpose-gallery-collection-pill";
badge.textContent = t("gallery.badge.collection");
badges.appendChild(badge);
}
if (isSingleCollection || isDirectoryGroup) {
const badge = document.createElement("span");
badge.className = "openpose-gallery-count-pill";
badge.textContent = t("gallery.count.poses", { count: presets.length });
badges.appendChild(badge);
}
title.appendChild(badges);
}
section.appendChild(title);
const carousel = document.createElement("div");
carousel.className = "openpose-gallery-carousel";
visiblePresets.forEach((preset) => {
const item = document.createElement("div");
item.className = "openpose-gallery-item";
item._galleryPresetId = preset.id;
item.tabIndex = 0;
item.setAttribute("role", "button");
item.title = preset.displayFilename || preset.sourceFile || "";
if (preset.sourceFile) {
item.dataset.sourceFile = preset.sourceFile;
}
if (preset.library) {
item.dataset.library = preset.library;
}
const normalizedName = op.normalizePoseName(preset.label || preset.id || t("gallery.fallback.pose"));
const {
faceCount,
leftHandCount,
rightHandCount,
personCount
} = getGalleryPresetDetails(preset);
const personLabel = t("gallery.count.people", { count: personCount });
const thumbnailSurface = document.createElement(compact ? "img" : "canvas");
this.queueThumbnail(thumbnailSurface, previewSize, (target) => {
op.renderPresetThumbnail(
target,
preset.keypoints,
preset.canvas_width || preset.width,
preset.canvas_height || preset.height
);
});
const label = document.createElement("div");
label.className = "openpose-gallery-item-title";
label.textContent = normalizedName;
const meta = document.createElement("div");
meta.className = "openpose-gallery-item-meta";
const metaName = document.createElement("div");
metaName.className = "openpose-gallery-item-meta-name";
metaName.textContent = normalizedName;
const metaSize = document.createElement("div");
metaSize.className = "openpose-gallery-item-meta-size";
metaSize.textContent = formatCanvasMetaSize(
preset.canvas_width ?? preset.width,
preset.canvas_height ?? preset.height
);
const metaPeople = document.createElement("div");
metaPeople.className = "openpose-gallery-item-meta-people";
metaPeople.textContent = personLabel;
const metaKp = document.createElement("div");
metaKp.className = "openpose-gallery-item-meta-kp";
if (faceCount > 0 || leftHandCount > 0 || rightHandCount > 0) {
metaKp.classList.add("openpose-gallery-item-meta-kp-indicators");
const addIndicatorBadge = (icon, count, titleText) => {
const badge = document.createElement("div");
badge.className = "openpose-gallery-kp-badge openpose-gallery-kp-badge-counts";
badge.title = titleText;
const iconSpan = document.createElement("span");
iconSpan.className = "openpose-gallery-kp-icon";
iconSpan.textContent = icon;
const countSpan = document.createElement("span");
countSpan.className = "openpose-gallery-kp-count";
countSpan.textContent = String(count);
badge.appendChild(iconSpan);
badge.appendChild(countSpan);
metaKp.appendChild(badge);
};
if (faceCount > 0) {
addIndicatorBadge("\u{1F642}", faceCount, t("gallery.tooltip.face_kps", { count: faceCount }));
}
if (leftHandCount > 0) {
addIndicatorBadge("\u{1F91A}", leftHandCount, t("gallery.tooltip.lhand_kps", { count: leftHandCount }));
}
if (rightHandCount > 0) {
addIndicatorBadge("\u270B", rightHandCount, t("gallery.tooltip.rhand_kps", { count: rightHandCount }));
}
} else {
metaKp.textContent = t("gallery.item.no_face_hands");
}
meta.appendChild(metaName);
meta.appendChild(metaSize);
meta.appendChild(metaPeople);
meta.appendChild(metaKp);
item.appendChild(thumbnailSurface);
item.appendChild(label);
item.appendChild(meta);
if (preset.galleryBadge === "nonstandard") {
const badge = document.createElement("div");
badge.className = "openpose-gallery-nonstandard";
badge.textContent = "!";
badge.title = t("gallery.badge.nonstandard_file");
item.appendChild(badge);
}
item.addEventListener("click", () => {
this.selectPreset(preset, item);
});
item.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") {
return;
}
event.preventDefault();
this.selectPreset(preset, item);
});
carousel.appendChild(item);
});
section.appendChild(carousel);
this.galleryContainer.appendChild(section);
});
// Render invalid files in a single "Invalid Files" category
const visibleEmptyPoseFiles = filteredEmptyPoseFiles.slice(0, Math.max(0, renderLimit - renderedCount));
if (visibleEmptyPoseFiles.length > 0) {
const section = document.createElement("div");
section.className = "openpose-gallery-section";
const title = document.createElement("div");
title.className = "openpose-gallery-title";
title.textContent = `\u{26A0}\u{FE0F} ${t("gallery.badge.invalid_files_strong")}`;
section.appendChild(title);
const carousel = document.createElement("div");
carousel.className = "openpose-gallery-carousel";
for (const { filename, reason } of visibleEmptyPoseFiles) {
const item = document.createElement("div");
item.className = "openpose-gallery-item";
item.title = `${filename}: ${reason}`;
const thumbnailSurface = document.createElement(compact ? "img" : "canvas");
this.queueThumbnail(thumbnailSurface, previewSize, (target) => {
const ctx = target.getContext("2d");
if (!ctx) {
return;
}
const previewSurface = op.getPreviewSurfaceFill();
ctx.clearRect(0, 0, target.width, target.height);
if (previewSurface) {
ctx.fillStyle = previewSurface;
ctx.fillRect(0, 0, target.width, target.height);
}
ctx.fillStyle = "#FFD700";
ctx.font = "bold 80px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("\u26A0\uFE0F", target.width / 2, target.height / 2);
});
const label = document.createElement("div");
label.className = "openpose-gallery-item-title";
label.textContent = filename;
const meta = document.createElement("div");
meta.className = "openpose-gallery-item-meta";
const metaName = document.createElement("div");
metaName.className = "openpose-gallery-item-meta-name";
metaName.textContent = filename;
const metaSize = document.createElement("div");
metaSize.className = "openpose-gallery-item-meta-size";
metaSize.textContent = formatCanvasMetaSize(null, null);
const metaInfo = document.createElement("div");
metaInfo.className = "openpose-gallery-item-meta-kp";
metaInfo.textContent = reason || t("gallery.state.invalid_file");
meta.appendChild(metaName);
meta.appendChild(metaSize);
meta.appendChild(metaInfo);
item.appendChild(thumbnailSurface);
item.appendChild(label);
item.appendChild(meta);
// Render warning sign on canvas
const ctx = thumbnailSurface.getContext?.("2d");
if (ctx) {
const previewSurface = op.getPreviewSurfaceFill();
ctx.clearRect(0, 0, thumbnailSurface.width, thumbnailSurface.height);
if (previewSurface) {
ctx.fillStyle = previewSurface;
ctx.fillRect(0, 0, thumbnailSurface.width, thumbnailSurface.height);
}
ctx.fillStyle = "#FFD700";
ctx.font = "bold 80px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("⚠️", thumbnailSurface.width / 2, thumbnailSurface.height / 2);
}
item.addEventListener("click", () => {
showToast("error", t("toast.invalid_pose_file_title"), `${filename}\n${reason}`);
});
carousel.appendChild(item);
}
renderedCount += visibleEmptyPoseFiles.length;
section.appendChild(carousel);
this.galleryContainer.appendChild(section);
}
const totalItems = galleryPresets.length + filteredEmptyPoseFiles.length;
if (compact && renderedCount < totalItems) {
const row = document.createElement("div");
row.className = "openpose-gallery-load-more-row";
const button = document.createElement("button");
button.type = "button";
button.className = "openpose-btn openpose-btn-secondary openpose-gallery-load-more";
const nextCount = Math.min(COMPACT_GALLERY_BATCH_SIZE, totalItems - renderedCount);
button.textContent = t("gallery.action.load_more", { count: nextCount });
button.addEventListener("click", () => {
const scrollTop = this.galleryContainer.scrollTop;
this.compactRenderLimit += COMPACT_GALLERY_BATCH_SIZE;
this.refresh();
requestAnimationFrame(() => {
this.galleryContainer.scrollTop = scrollTop;
});
});
row.appendChild(button);
this.galleryContainer.appendChild(row);
}
galleryOverlay.applyStyles(this.container);
this.startThumbnailObserver();
this.updateStatsBadge(galleryPresets, allGalleryPresets);
this.needsRefresh = false;
this.lastCompact = compact;
const selectedPreset = galleryPresets.find((preset) => preset.id === this.selectedPresetId);
if (selectedPreset) {
requestAnimationFrame(() => this.selectPreset(selectedPreset));
} else {
this.clearSelection();
}
}
refreshOnShow() {
const compact = this.container.classList.contains("ope-openpose-compact");
if (this.needsRefresh || this.lastCompact !== compact) {
this.refresh();
return;
}
requestAnimationFrame(() => this.startThumbnailObserver());
}
releaseThumbnails() {
this.stopThumbnailObserver(true);
}
}
export function setupGalleryManager(container, openposeInstance) {
return new GalleryManager(container, openposeInstance);
}
function setupGalleryCompactNavigation(container) {
const overlay = container?.querySelector(".openpose-gallery-overlay");
const buttons = Array.from(overlay?.querySelectorAll("[data-gallery-pane-target]") || []);
const panes = Array.from(overlay?.querySelectorAll("[data-gallery-pane]") || []);
if (!overlay || buttons.length === 0 || panes.length === 0) {
return null;
}
const setPane = (paneName) => {
const nextPane = paneName === "preview" ? "preview" : "browse";
buttons.forEach((button) => {
const active = button.dataset.galleryPaneTarget === nextPane;
button.classList.toggle("is-active", active);
button.setAttribute("aria-selected", active ? "true" : "false");
});
panes.forEach((pane) => {
pane.classList.toggle("is-active", pane.dataset.galleryPane === nextPane);
});
};
buttons.forEach((button) => {
if (button.dataset.galleryPaneReady) {
return;
}
button.dataset.galleryPaneReady = "1";
button.addEventListener("click", () => setPane(button.dataset.galleryPaneTarget));
});
if (!overlay.dataset.galleryCompactReady) {
overlay.dataset.galleryCompactReady = "1";
const showSelectedPreview = (event) => {
if (!container.classList.contains("ope-openpose-compact")) {
return;
}
if (event.type === "keydown" && event.key !== "Enter" && event.key !== " ") {
return;
}
const item = event.target.closest(".openpose-gallery-item");
if (item?._galleryPresetId) {
requestAnimationFrame(() => setPane("preview"));
}
};
overlay.addEventListener("click", showSelectedPreview);
overlay.addEventListener("keydown", showSelectedPreview);
}
setPane("browse");
return setPane;
}
function setupGalleryControls(container, openposeInstance, galleryManager) {
if (!container || !galleryManager) {
return;
}
const searchInput = container.querySelector('[data-action="gallery-search"]');
const clearSearch = container.querySelector('[data-action="gallery-search-clear"]');
if (searchInput && clearSearch && !searchInput.dataset.gallerySearchReady) {
searchInput.dataset.gallerySearchReady = "1";
searchInput.value = galleryManager.searchQuery || "";
let searchTimer = null;
const updateClearButton = () => {
clearSearch.hidden = searchInput.value.length === 0;
clearSearch.style.display = clearSearch.hidden ? "none" : "inline-flex";
};
const applySearch = () => {
searchTimer = null;
galleryManager.setSearchQuery(searchInput.value);
};
searchInput.addEventListener("input", () => {
updateClearButton();
if (searchTimer !== null) {
clearTimeout(searchTimer);
}
searchTimer = setTimeout(applySearch, 140);
});
searchInput.addEventListener("keydown", (event) => {
if (event.key !== "Escape" || searchInput.value.length === 0) {
return;
}
event.preventDefault();
event.stopPropagation();
if (searchTimer !== null) {
clearTimeout(searchTimer);
searchTimer = null;
}
searchInput.value = "";
updateClearButton();
galleryManager.setSearchQuery("");
});
clearSearch.addEventListener("click", () => {
if (searchTimer !== null) {
clearTimeout(searchTimer);
searchTimer = null;
}
searchInput.value = "";
updateClearButton();
galleryManager.setSearchQuery("");
searchInput.focus();
});
updateClearButton();
}
const viewToggle = container.querySelector('[data-action="gallery-toggle-view-mode"]');
if (viewToggle && !viewToggle.dataset.galleryViewReady) {
viewToggle.dataset.galleryViewReady = "1";
const viewOrder = ["large", "medium", "small", "tiles"];
const viewIcon = "\u{1F5BC}\u{FE0F}";
const viewLabels = {
medium: t("gallery.overlay.view.medium"),
large: t("gallery.view.large"),
small: t("gallery.view.small"),
tiles: t("gallery.view.tiles")
};
const updateLabel = () => {
const mode = galleryManager.viewMode || "medium";
viewToggle.textContent = `${viewIcon} ${viewLabels[mode] || viewLabels.medium}`;
};
updateLabel();
viewToggle.addEventListener("click", () => {
const current = galleryManager.viewMode || "medium";
const index = viewOrder.indexOf(current);
const next = viewOrder[(index + 1) % viewOrder.length];
galleryManager.setViewMode(next);
updateLabel();
if (openposeInstance?.activeTab === "gallery") {
galleryManager.refresh();
}
});
}
const insertButton = container.querySelector('[data-action="gallery-insert-pose"]');
if (insertButton && !insertButton.dataset.galleryInsertReady) {
insertButton.dataset.galleryInsertReady = "1";
insertButton.addEventListener("click", () => galleryManager.insertSelectedPreset());
}
container.querySelectorAll(".openpose-gallery-hand-row").forEach((row) => {
if (row.dataset.galleryHandReady) {
return;
}
row.dataset.galleryHandReady = "1";
const focusHand = () => {
if (row.classList.contains("is-available")) {
galleryManager.focusSelectedHand(row.dataset.galleryHand);
}
};
row.addEventListener("mouseenter", focusHand);
row.addEventListener("mouseleave", () => galleryManager.clearFocusedHand());
row.addEventListener("focus", focusHand);
row.addEventListener("blur", () => galleryManager.clearFocusedHand());
});
}
export function buildGalleryOverlayHtml() {
return `
<div class="openpose-overlay openpose-gallery-overlay" data-overlay="gallery">
<div class="openpose-view-tabs openpose-gallery-compact-tabs" role="tablist" aria-label="${t("gallery.label")}">
<button class="openpose-view-tab is-active" type="button" role="tab" aria-selected="true" data-gallery-pane-target="browse">${UiIcons.svg('layers', { size: 16, className: 'openpose-ui-icon' })}<span>${t("gallery.label")}</span></button>
<button class="openpose-view-tab" type="button" role="tab" aria-selected="false" data-gallery-pane-target="preview">${UiIcons.svg('canvas', { size: 16, className: 'openpose-ui-icon' })}<span>${t("gallery.preview.title")}</span></button>
</div>
<div class="openpose-sidebar openpose-gallery-sidebar" data-gallery-pane="preview">
<div class="openpose-sidebar-card">
<div class="openpose-preset-preview-frame">
<canvas class="openpose-preset-preview openpose-gallery-selected-preview" aria-label="${t("gallery.preview.selected_aria")}"></canvas>
</div>
<button class="openpose-btn openpose-primary-btn openpose-primary-action-btn openpose-gallery-insert-btn" type="button" data-action="gallery-insert-pose" disabled>${UiIcons.svg('plus', { size: 16, className: 'openpose-primary-action-icon' })}<span>${t("gallery.action.insert_pose")}</span></button>
<div class="openpose-gallery-details">
<div class="openpose-gallery-details-empty">${t("gallery.details.select_pose")}</div>
<div class="openpose-gallery-details-content" hidden>
<div class="openpose-gallery-details-name" data-gallery-detail="name"></div>
<div class="openpose-gallery-details-row">
<span>${t("gallery.details.file")}</span>
<strong class="openpose-gallery-details-path" data-gallery-detail="file"></strong>
</div>
<div class="openpose-gallery-details-row">
<span>${t("gallery.details.location")}</span>
<strong class="openpose-gallery-details-path" data-gallery-detail="location"></strong>
</div>
<div class="openpose-gallery-details-row">
<span>${t("gallery.details.canvas")}</span>
<strong data-gallery-detail="canvas"></strong>
</div>
<div class="openpose-gallery-details-row">
<span>${t("gallery.details.people")}</span>
<strong data-gallery-detail="people"></strong>
</div>
<div class="openpose-gallery-details-row">
<span>${t("gallery.details.body_keypoints")}</span>
<strong data-gallery-detail="body"></strong>
</div>
<div class="openpose-gallery-details-row">
<span>${t("gallery.details.face_keypoints")}</span>
<strong data-gallery-detail="face"></strong>
</div>
<div class="openpose-gallery-details-row openpose-gallery-hand-row" data-gallery-hand="left" tabindex="-1" aria-disabled="true">
<span>${t("gallery.details.left_hand")}</span>
<strong class="openpose-gallery-hand-value">
<span data-gallery-detail="leftHand"></span>
<span class="openpose-gallery-hand-zoom" hidden>${UiIcons.svg('zoomIn', { size: 14, className: 'openpose-ui-icon' })}</span>
</strong>
</div>
<div class="openpose-gallery-details-row openpose-gallery-hand-row" data-gallery-hand="right" tabindex="-1" aria-disabled="true">
<span>${t("gallery.details.right_hand")}</span>
<strong class="openpose-gallery-hand-value">
<span data-gallery-detail="rightHand"></span>
<span class="openpose-gallery-hand-zoom" hidden>${UiIcons.svg('zoomIn', { size: 14, className: 'openpose-ui-icon' })}</span>
</strong>
</div>
</div>
</div>
</div>
</div>
<div class="openpose-gallery-main is-active" data-gallery-pane="browse">
<div class="openpose-overlay-card openpose-gallery-card">
<div class="openpose-overlay-content openpose-gallery-wrapper">
<div class="openpose-gallery-header">
<div class="openpose-gallery-note-row">
<div class="openpose-gallery-note">${t("gallery.note.libraries")}</div>
<div class="openpose-gallery-actions">
<div class="openpose-gallery-search">
<input class="openpose-gallery-search-input openpose-gallery-header-ctrl" data-action="gallery-search" type="search" placeholder="${t("gallery.search.placeholder")}" aria-label="${t("gallery.search.aria")}" autocomplete="off" spellcheck="false" />
<button class="openpose-gallery-search-clear" data-action="gallery-search-clear" type="button" title="${t("gallery.search.clear_title")}" aria-label="${t("gallery.search.clear_aria")}" hidden>${UiIcons.svg('x', { size: 14, className: 'openpose-ui-icon' })}</button>
</div>
<span class="openpose-gallery-stats-badge openpose-gallery-header-ctrl">${formatGalleryStats(0, 0, 0)}</span>
<button class="openpose-btn openpose-btn-small openpose-gallery-view-toggle openpose-gallery-header-ctrl" data-action="gallery-toggle-view-mode">${t("gallery.overlay.view.medium")}</button>
<button class="openpose-btn openpose-btn-small openpose-refresh-btn openpose-gallery-header-ctrl" data-action="presets-reload" title="${t("gallery.action.reload_presets")}">\u{1F504}</button>
</div>
</div>
</div>
<div class="openpose-alert openpose-alert-warning alert alert-warning openpose-gallery-library-warning" style="display: none;">
<span class="openpose-alert-icon">\u{26A0}\u{FE0F}</span>
<div class="openpose-alert-body">
<strong>${t("gallery.warning.unavailable_title")}</strong>
<p>${t("gallery.warning.unavailable_body")}</p>
<ul class="openpose-gallery-library-warning-list"></ul>
</div>
</div>
<div class="openpose-gallery-content"></div>
</div>
</div>
</div>
</div>
`;
}
export function setupGalleryOverlayStyles(container) {
const resolveTheme = () => {
if (typeof window === "undefined") {
return null;
}
if (typeof window.getComfyTheme === "function") {
return window.getComfyTheme();
}
if (window.ComfyTheme && typeof window.ComfyTheme.getTheme === "function") {
return window.ComfyTheme.getTheme();
}
return null;
};
const theme = resolveTheme();
const themeColor = (key, cssVar) => (theme && theme[key] ? theme[key] : cssVar);
const borderColor = "rgba(255,255,255,0.3)";
const backgroundColor = themeColor("background", "var(--bg-color)");
const headerText = themeColor("text", "var(--fg-color)");
const overlayBg = toRgba(backgroundColor, 0.6) || backgroundColor;
const tileHoverRing = toRgba(headerText, 0.2) || headerText;
const captionText = borderColor;
const captionHoverText = themeColor("text", "var(--fg-color)");
const previewShadow = "0 1px 1px rgba(0,0,0,0.75)";
container.querySelectorAll(".openpose-gallery-overlay").forEach((overlay) => {
overlay.style.padding = "0";
});
container.querySelectorAll(".openpose-gallery-main").forEach((main) => {
main.style.display = "flex";
main.style.flex = "1 1 auto";
main.style.minWidth = "0";
main.style.minHeight = "0";
main.style.padding = "10px 10px 10px 6px";
main.style.boxSizing = "border-box";
});
// Gallery card: use same background as Pose Editor sidebars for consistency
container.querySelectorAll(".openpose-gallery-card").forEach((card) => {
card.style.width = "100%";
card.style.height = "100%";
card.style.overflow = "hidden";
card.style.background = "var(--openpose-panel-bg-secondary)";
card.style.border = "none";
card.style.borderRadius = "var(--openpose-card-radius)";
card.style.boxShadow = "0 1px 3px rgba(0,0,0,0.2), 0 1px 2px rgba(0,0,0,0.14)";
card.style.padding = "16px";
});
// Gallery wrapper: flex column layout to separate header from content
container.querySelectorAll(".openpose-gallery-wrapper").forEach((wrapper) => {
wrapper.style.display = "flex";
wrapper.style.flexDirection = "column";
wrapper.style.height = "100%";
wrapper.style.overflow = "hidden";
wrapper.style.gap = "0";
});
// Gallery header: fixed at top, doesn't scroll
container.querySelectorAll(".openpose-gallery-header").forEach((header) => {
header.style.flex = "0 0 auto";
header.style.paddingBottom = "10px";
});
container.querySelectorAll(".openpose-gallery-library-warning").forEach((warning) => {
warning.style.alignItems = "flex-start";
warning.style.marginBottom = "10px";
});
container.querySelectorAll(".openpose-gallery-library-warning-list").forEach((list) => {
list.style.margin = "6px 0 0";
list.style.paddingLeft = "20px";
});
// Unified sizing for all header controls (stats badge, view button, refresh button)
container.querySelectorAll(".openpose-gallery-header-ctrl").forEach((ctrl) => {
ctrl.style.display = "inline-flex";
ctrl.style.alignItems = "center";
ctrl.style.justifyContent = "center";
ctrl.style.height = "26px";
ctrl.style.minHeight = "26px";
ctrl.style.maxHeight = "26px";
ctrl.style.lineHeight = "1";
ctrl.style.boxSizing = "border-box";
ctrl.style.verticalAlign = "middle";
});
container.querySelectorAll(".openpose-gallery-main .openpose-btn:not(.openpose-primary-btn)").forEach((btn) => {
btn.style.padding = "6px 12px";
btn.style.border = "1px solid var(--openpose-border)";
btn.style.borderRadius = "4px";
btn.style.background = "var(--openpose-btn-bg)";
btn.style.color = "var(--openpose-text)";
btn.style.cursor = "pointer";
btn.style.fontFamily = "Arial, sans-serif";
btn.style.fontSize = "13px";
if (!btn.dataset.hoverReady) {
btn.dataset.hoverReady = "1";
btn.addEventListener("mouseenter", () => {
if (btn.disabled) {
return;
}
btn.style.background = "var(--openpose-btn-hover-bg)";
});
btn.addEventListener("mouseleave", () => {
if (btn.disabled) {
return;
}
btn.style.background = "var(--openpose-btn-bg)";
});
}
});
container.querySelectorAll(".openpose-gallery-overlay .openpose-btn-small").forEach((btn) => {
btn.style.padding = "0 10px";
btn.style.fontSize = "12px";
// height controlled by .openpose-gallery-header-ctrl when both classes present
});
container.querySelectorAll(".openpose-gallery-overlay .openpose-refresh-btn").forEach((btn) => {
btn.style.padding = "0 8px";
btn.style.minWidth = "26px";
// height is controlled by .openpose-gallery-header-ctrl
});
container.querySelectorAll(".openpose-gallery-stats-badge").forEach((badge) => {
// display, alignItems, height controlled by .openpose-gallery-header-ctrl
badge.style.padding = "0 8px";
badge.style.fontSize = "11px";
badge.style.fontFamily = "Arial, sans-serif";
badge.style.color = "var(--openpose-text-muted)";
badge.style.background = "var(--openpose-input-bg)";
badge.style.border = "1px solid var(--openpose-border)";
badge.style.borderRadius = "4px";
badge.style.whiteSpace = "nowrap";
});
container.querySelectorAll(".openpose-gallery-content").forEach((content) => {
content.style.display = "flex";
content.style.flexDirection = "column";
content.style.gap = "0";
// Make this the scrollable container
content.style.flex = "1 1 auto";
content.style.overflowY = "auto";
content.style.minHeight = "0";
});
container.querySelectorAll(".openpose-gallery-note-row").forEach((row) => {
row.style.display = "flex";
row.style.alignItems = "flex-start";
row.style.gap = "6px";
row.style.marginBottom = "6px";
});
container.querySelectorAll(".openpose-gallery-actions").forEach((actions) => {
actions.style.display = "flex";
actions.style.alignItems = "center";
actions.style.gap = "6px";
actions.style.marginLeft = "auto";
actions.style.flexShrink = "0";
});
container.querySelectorAll(".openpose-gallery-search").forEach((search) => {
search.style.position = "relative";
search.style.display = "flex";
search.style.alignItems = "center";
search.style.width = "240px";
search.style.minWidth = "150px";
});
container.querySelectorAll(".openpose-gallery-search-input").forEach((input) => {
input.style.width = "100%";
input.style.padding = "0 30px 0 9px";
input.style.border = "1px solid var(--openpose-border)";
input.style.borderRadius = "4px";
input.style.background = "var(--openpose-input-bg)";
input.style.color = "var(--openpose-input-text)";
input.style.fontFamily = "Arial, sans-serif";
input.style.fontSize = "12px";
input.style.outline = "none";
});
container.querySelectorAll(".openpose-gallery-search-clear").forEach((button) => {
button.style.position = "absolute";
button.style.right = "5px";
button.style.top = "50%";
button.style.width = "20px";
button.style.height = "20px";
button.style.padding = "3px";
button.style.alignItems = "center";
button.style.justifyContent = "center";
button.style.transform = "translateY(-50%)";
button.style.border = "0";
button.style.borderRadius = "3px";
button.style.background = "transparent";
button.style.color = "var(--openpose-text-muted)";
button.style.cursor = "pointer";
});
container.querySelectorAll(".openpose-gallery-note").forEach((note) => {
note.style.fontSize = "12px";
note.style.opacity = "0.85";
note.style.color = "var(--openpose-text-muted)";
note.style.flex = "1";
note.style.marginBottom = "0";
});
container.querySelectorAll(".openpose-gallery-section").forEach((section) => {
section.style.display = "flex";
section.style.flexDirection = "column";
section.style.gap = "8px";
section.style.padding = "0";
section.style.marginBottom = "20px";
section.style.border = "none";
section.style.borderRadius = "0";
section.style.background = "transparent";
section.style.boxShadow = "none";
section.style.overflow = "visible";
});
container.querySelectorAll(".openpose-gallery-title").forEach((title) => {
title.style.fontWeight = "600";
title.style.fontSize = "13px";
title.style.color = headerText;
title.style.padding = "0 0 6px 0";
title.style.display = "flex";
title.style.alignItems = "center";
title.style.gap = "8px";
title.style.margin = "0";
title.style.background = "transparent";
title.style.width = "100%";
title.style.boxSizing = "border-box";
title.style.border = "none";
});
container.querySelectorAll(".openpose-gallery-title-text").forEach((text) => {
text.style.flex = "1";
text.style.minWidth = "0";
});
container.querySelectorAll(".openpose-gallery-title-badges").forEach((badges) => {
badges.style.display = "flex";
badges.style.alignItems = "center";
badges.style.gap = "6px";
badges.style.marginLeft = "auto";
badges.style.flexShrink = "0";
});
container.querySelectorAll(".openpose-gallery-collection-pill").forEach((badge) => {
badge.style.fontSize = "10px";
badge.style.fontWeight = "600";
badge.style.letterSpacing = "0.5px";
badge.style.textTransform = "uppercase";
badge.style.padding = "3px 8px";
badge.style.borderRadius = "3px";
badge.style.background = "var(--openpose-primary-bg)";
badge.style.color = "var(--openpose-primary-text)";
badge.style.pointerEvents = "none";
badge.style.lineHeight = "1.2";
badge.style.opacity = "0.8";
});
container.querySelectorAll(".openpose-gallery-custom-path-pill").forEach((badge) => {
badge.style.fontSize = "10px";
badge.style.fontWeight = "600";
badge.style.letterSpacing = "0.5px";
badge.style.padding = "3px 8px";
badge.style.borderRadius = "3px";
badge.style.background = "rgba(62, 142, 244, 0.14)";
badge.style.border = "1px solid rgba(62, 142, 244, 0.40)";
badge.style.color = "var(--openpose-text)";
badge.style.pointerEvents = "none";
badge.style.lineHeight = "1.2";
});
container.querySelectorAll(".openpose-gallery-count-pill").forEach((badge) => {
badge.style.fontSize = "10px";
badge.style.fontWeight = "600";
badge.style.letterSpacing = "0.5px";
badge.style.textTransform = "uppercase";
badge.style.padding = "3px 8px";
badge.style.borderRadius = "3px";
badge.style.background = "var(--openpose-input-bg)";
badge.style.border = "1px solid var(--openpose-border)";
badge.style.color = "var(--openpose-text-muted)";
badge.style.pointerEvents = "none";
badge.style.lineHeight = "1.2";
});
container.querySelectorAll(".openpose-gallery-carousel").forEach((carousel) => {
// Base layout: grid for pose tiles
carousel.style.display = "grid";
carousel.style.gridTemplateColumns = "repeat(auto-fill, minmax(120px, 1fr))";
carousel.style.gap = "10px";
carousel.style.overflowX = "visible";
// Apply exact Render-style card styling from .openpose-render-style-section
carousel.style.padding = "16px";
carousel.style.border = "none";
carousel.style.borderRadius = "var(--openpose-card-radius)";
carousel.style.background = "linear-gradient(rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.035)), var(--openpose-panel-bg)";
carousel.style.boxSizing = "border-box";
carousel.style.boxShadow = "0 1px 2px rgba(0,0,0,0.2)";
});
container.querySelectorAll(".openpose-gallery-item").forEach((item) => {
item.style.display = "flex";
item.style.flexDirection = "column";
item.style.alignItems = "center";
item.style.gap = "0";
item.style.padding = "0";
item.style.borderRadius = "8px";
item.style.border = "none";
item.style.background = "transparent";
item.style.position = "relative";
item.style.aspectRatio = "1 / 1";
item.style.cursor = "pointer";
item.style.minWidth = "0";
item.style.width = "100%";
item.style.boxSizing = "border-box";
item.style.boxShadow = "none";
item.style.transition = "box-shadow 0.15s ease, background 0.15s ease";
if (!item.dataset.hoverReady) {
item.dataset.hoverReady = "1";
item.addEventListener("mouseenter", () => {
item.style.background = item.classList.contains("is-selected")
? "var(--openpose-gallery-selection-bg)"
: "rgba(0,0,0,0.15)";
item.style.boxShadow = item.classList.contains("is-selected")
? "none"
: "0 2px 6px rgba(0,0,0,0.2), 0 1px 3px rgba(0,0,0,0.14)";
item.querySelectorAll(".openpose-gallery-nonstandard, .openpose-gallery-collection").forEach((b) => {
b.style.opacity = "1";
});
});
item.addEventListener("mouseleave", () => {
item.style.background = item.classList.contains("is-selected")
? "var(--openpose-gallery-selection-bg)"
: "transparent";
item.style.boxShadow = "none";
item.querySelectorAll(".openpose-gallery-nonstandard, .openpose-gallery-collection").forEach((b) => {
b.style.opacity = "0.45";
});
});
}
});
container.querySelectorAll(".openpose-gallery-selected-preview").forEach((canvas) => {
canvas.style.cursor = "default";
});
container.querySelectorAll(".openpose-gallery-nonstandard").forEach((badge) => {
badge.style.position = "absolute";
badge.style.top = "6px";
badge.style.right = "6px";
badge.style.width = "18px";
badge.style.height = "18px";
badge.style.borderRadius = "999px";
badge.style.display = "flex";
badge.style.alignItems = "center";
badge.style.justifyContent = "center";
badge.style.background = "var(--openpose-error, #E74C3C)";
badge.style.color = "#fff";
badge.style.fontSize = "12px";
badge.style.fontWeight = "700";
badge.style.lineHeight = "1";
badge.style.boxShadow = "0 1px 2px rgba(0,0,0,0.35)";
badge.style.cursor = "pointer";
badge.style.pointerEvents = "auto";
badge.style.zIndex = "3";
badge.style.opacity = "0.45";
badge.style.transition = "opacity 0.15s ease";
});
container.querySelectorAll(".openpose-gallery-collection").forEach((badge) => {
badge.style.position = "absolute";
badge.style.top = "6px";
badge.style.right = "6px";
badge.style.width = "18px";
badge.style.height = "18px";
badge.style.borderRadius = "999px";
badge.style.display = "flex";
badge.style.alignItems = "center";
badge.style.justifyContent = "center";
badge.style.background = "var(--openpose-primary-bg, #2D8CFF)";
badge.style.color = "var(--openpose-primary-text, #fff)";
badge.style.fontSize = "11px";
badge.style.fontWeight = "700";
badge.style.lineHeight = "1";
badge.style.boxShadow = "0 1px 2px rgba(0,0,0,0.35)";
badge.style.cursor = "pointer";
badge.style.pointerEvents = "auto";
badge.style.zIndex = "3";
badge.style.opacity = "0.45";
badge.style.transition = "opacity 0.15s ease";
});
container.querySelectorAll(".openpose-gallery-kp-badge").forEach((badge) => {
badge.style.position = "static";
badge.style.width = "18px";
badge.style.height = "18px";
badge.style.borderRadius = "999px";
badge.style.display = "flex";
badge.style.alignItems = "center";
badge.style.justifyContent = "center";
badge.style.background = "var(--openpose-primary-bg, #2D8CFF)";
badge.style.color = "var(--openpose-primary-text, #fff)";
badge.style.fontSize = "11px";
badge.style.fontWeight = "700";
badge.style.lineHeight = "1";
badge.style.boxShadow = "0 1px 2px rgba(0,0,0,0.35)";
badge.style.cursor = "pointer";
badge.style.pointerEvents = "auto";
badge.style.zIndex = "3";
badge.style.opacity = "1";
badge.style.transition = "opacity 0.15s ease";
if (badge.classList.contains("openpose-gallery-kp-badge-counts")) {
badge.style.width = "auto";
badge.style.minWidth = "18px";
badge.style.padding = "0 5px";
badge.style.gap = "3px";
} else {
badge.style.width = "18px";
badge.style.padding = "0";
badge.style.gap = "0";
}
});
container.querySelectorAll(".openpose-gallery-item-meta-kp-indicators").forEach((row) => {
row.style.display = "flex";
row.style.alignItems = "center";
row.style.flexWrap = "wrap";
row.style.gap = "4px";
row.style.overflow = "visible";
row.style.whiteSpace = "normal";
});
container.querySelectorAll(".openpose-gallery-kp-count").forEach((count) => {
count.style.fontSize = "10px";
count.style.fontWeight = "600";
count.style.lineHeight = "1";
count.style.display = "inline-flex";
count.style.alignItems = "center";
});
container.querySelectorAll(".openpose-gallery-item-title").forEach((title) => {
title.style.fontSize = "11px";
title.style.color = captionText;
title.style.textAlign = "center";
title.style.maxWidth = "100%";
title.style.whiteSpace = "nowrap";
title.style.overflow = "hidden";
title.style.textOverflow = "ellipsis";
title.style.position = "absolute";
title.style.left = "0";
title.style.right = "0";
title.style.bottom = "0";
title.style.padding = "4px 6px";
title.style.background = "transparent";
title.style.boxSizing = "border-box";
title.style.borderBottomLeftRadius = "8px";
title.style.borderBottomRightRadius = "8px";
if (!title.title) {
title.title = title.textContent || "";
}
});
container.querySelectorAll(".openpose-gallery-item").forEach((item) => {
const title = item.querySelector(".openpose-gallery-item-title");
if (!title) {
return;
}
if (!item.dataset.captionHoverReady) {
item.dataset.captionHoverReady = "1";
item.addEventListener("mouseenter", () => {
title.style.color = captionHoverText;
});
item.addEventListener("mouseleave", () => {
title.style.color = captionText;
});
} else {
title.style.color = captionText;
}
});
container.querySelectorAll(".openpose-gallery-thumbnail").forEach((thumbnail) => {
thumbnail.style.width = "100%";
thumbnail.style.height = "100%";
thumbnail.style.objectFit = "contain";
thumbnail.style.borderRadius = "6px";
thumbnail.style.background = "var(--openpose-canvas-bg)";
thumbnail.style.border = "1px solid var(--openpose-canvas-border)";
thumbnail.style.boxShadow = previewShadow;
thumbnail.style.display = "block";
});
container.querySelectorAll(".openpose-gallery-warning").forEach((warning) => {
warning.style.display = "flex";
warning.style.flexDirection = "column";
warning.style.alignItems = "center";
warning.style.justifyContent = "center";
warning.style.gap = "6px";
warning.style.padding = "20px 16px";
warning.style.fontSize = "13px";
warning.style.color = "var(--openpose-text-muted)";
warning.style.opacity = "0.85";
warning.style.textAlign = "center";
});
container.querySelectorAll(".openpose-gallery-warning-reason").forEach((reason) => {
reason.style.fontSize = "11px";
reason.style.opacity = "0.7";
reason.style.fontStyle = "italic";
});
if (!container.querySelector('style[data-openpose-gallery-view="1"]')) {
const style = document.createElement("style");
style.dataset.openposeGalleryView = "1";
style.textContent = `
.openpose-gallery-sidebar .openpose-sidebar-card {
overflow: hidden;
}
.openpose-gallery-details {
min-height: 0;
overflow-y: auto;
margin-top: 4px;
padding-top: 12px;
border-top: 1px solid var(--openpose-border);
color: var(--openpose-text);
font-family: Arial, sans-serif;
}
.openpose-gallery-details-empty {
padding: 8px 2px;
color: var(--openpose-text-muted);
font-size: 12px;
line-height: 1.4;
}
.openpose-gallery-details-content {
display: flex;
flex-direction: column;
gap: 0;
}
.openpose-gallery-details-content[hidden],
.openpose-gallery-details-empty[hidden] {
display: none;
}
.openpose-gallery-details-name {
margin-bottom: 8px;
color: var(--openpose-text);
font-size: 14px;
font-weight: 700;
line-height: 1.3;
overflow-wrap: anywhere;
}
.openpose-gallery-details-row {
display: grid;
grid-template-columns: minmax(76px, 0.8fr) minmax(0, 1.2fr);
gap: 8px;
padding: 6px 2px;
border-top: 1px solid color-mix(in srgb, var(--openpose-border) 55%, transparent);
font-size: 11px;
line-height: 1.35;
}
.openpose-gallery-details-row span {
color: var(--openpose-text-muted);
}
.openpose-gallery-details-row strong {
min-width: 0;
color: var(--openpose-text);
font-weight: 500;
text-align: right;
overflow-wrap: anywhere;
}
.openpose-gallery-details-path {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow-wrap: normal !important;
}
.openpose-gallery-hand-row.is-available {
border-radius: 4px;
cursor: zoom-in;
transition: background 120ms ease, box-shadow 120ms ease;
}
.openpose-gallery-hand-row.is-available:hover,
.openpose-gallery-hand-row.is-available:focus-visible,
.openpose-gallery-hand-row.is-active {
background: color-mix(in srgb, var(--openpose-primary-bg) 18%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--openpose-primary-bg) 38%, transparent);
outline: none;
}
.openpose-gallery-hand-row.is-active > span {
color: var(--openpose-text);
}
.openpose-gallery-hand-value {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
}
.openpose-gallery-hand-value > span:first-child {
color: var(--openpose-text);
}
.openpose-gallery-hand-zoom {
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--openpose-primary-text);
}
.openpose-gallery-hand-zoom[hidden] {
display: none;
}
.openpose-gallery-content.gallery-view--large .openpose-gallery-carousel {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) !important;
gap: 12px !important;
}
.openpose-gallery-content.gallery-view--small .openpose-gallery-carousel {
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)) !important;
gap: 6px !important;
padding: 8px !important;
}
.openpose-gallery-content.gallery-view--tiles .openpose-gallery-carousel {
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)) !important;
gap: 8px !important;
}
.openpose-gallery-content.gallery-view--tiles .openpose-gallery-item {
aspect-ratio: auto !important;
height: auto !important;
display: flex !important;
flex-direction: row !important;
align-items: center !important;
gap: 12px !important;
padding: 6px 8px !important;
}
.openpose-gallery-content.gallery-view--tiles .openpose-gallery-item-title {
display: none !important;
}
.openpose-gallery-content.gallery-view--tiles .openpose-gallery-thumbnail {
width: 80px !important;
height: 80px !important;
flex: 0 0 auto !important;
}
.openpose-gallery-item-meta {
display: none;
}
.openpose-gallery-content.gallery-view--tiles .openpose-gallery-item-meta {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1 1 auto;
justify-content: center;
}
.openpose-gallery-item-meta-name {
font-size: 12px;
font-weight: 600;
color: var(--openpose-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.openpose-gallery-item-meta-size,
.openpose-gallery-item-meta-people,
.openpose-gallery-item-meta-kp {
font-size: 11px;
color: var(--openpose-text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.openpose-gallery-search-input::-webkit-search-cancel-button {
-webkit-appearance: none;
appearance: none;
}
.openpose-gallery-search-input:focus {
border-color: var(--openpose-primary-bg) !important;
box-shadow: 0 0 0 1px var(--openpose-primary-bg);
}
.openpose-gallery-search-clear:hover,
.openpose-gallery-search-clear:focus-visible {
background: var(--openpose-btn-hover-bg) !important;
color: var(--openpose-text) !important;
outline: none;
}
.openpose-gallery-load-more-row {
display: flex;
justify-content: center;
padding: 8px 16px 18px;
}
.openpose-gallery-load-more {
min-width: 160px;
}
`;
container.appendChild(style);
}
// Update gallery stats badge and collection badges
updateGalleryBadges(container);
}
function updateGalleryBadges(container) {
// Update stats badge with pose and file counts
const statsBadge = container.querySelector(".openpose-gallery-stats-badge");
if (statsBadge) {
const items = container.querySelectorAll(".openpose-gallery-item[data-source-file]");
const sourceFiles = new Set(Array.from(items, (item) => item.dataset.sourceFile).filter(Boolean));
const libraries = new Set(Array.from(items, (item) => item.dataset.library).filter(Boolean));
const poseCount = items.length;
const fileCount = sourceFiles.size;
const libraryCount = libraries.size;
statsBadge.textContent = formatGalleryStats(poseCount, fileCount, libraryCount);
}
// Update section pose counts after rendering
container.querySelectorAll(".openpose-gallery-count-pill").forEach((badge) => {
const section = badge.closest(".openpose-gallery-section");
if (section) {
const itemCount = section.querySelectorAll(".openpose-gallery-item").length;
badge.textContent = t("gallery.count.poses", { count: itemCount });
}
});
}
export const galleryOverlay = {
id: "gallery",
buildUI: buildGalleryOverlayHtml,
applyStyles: setupGalleryOverlayStyles,
initUI: setupGalleryOverlayStyles
};
const galleryState = {
manager: null,
fileMeta: new Map(),
setCompactPane: null
};
registerModule({
id: "gallery",
labelKey: "gallery.label",
order: 10,
slot: "overlay",
buildUI: buildGalleryOverlayHtml,
initUI: (container, openpose) => {
galleryState.manager = setupGalleryManager(container, openpose);
galleryOverlay.initUI(container);
galleryState.setCompactPane = setupGalleryCompactNavigation(container);
setupGalleryControls(container, openpose, galleryState.manager);
},
onActivate: ({ openpose }) => {
if (!openpose) {
return;
}
openpose.setSidebarsVisible(false);
openpose.setOverlayPlaceholderWidths(true);
openpose.setCanvasAreaVisible(true);
openpose.setSidebarControlsDisabled(true);
openpose.setBackgroundControlsEnabled(false);
galleryState.setCompactPane?.("browse");
galleryState.manager?.refreshOnShow();
},
onDeactivate: () => {
galleryState.manager?.releaseThumbnails();
},
onPresetsLoadStart: () => {
galleryState.fileMeta.clear();
if (galleryState.manager) {
galleryState.manager.markDirty();
galleryState.manager.collectionFiles.clear();
galleryState.manager.emptyPoseFiles = [];
galleryState.manager.renderLoading();
}
},
onPresetFileError: (info) => {
if (galleryState.manager && info?.filename) {
galleryState.manager.emptyPoseFiles.push({
filename: info.displayFilename || info.filename,
reason: info.reason || t("gallery.state.invalid_file")
});
}
},
onPresetFileLoaded: (info) => {
if (!info || !info.filename) {
return;
}
const payload = info.payload;
const isStandard = isStandardOpenPosePoseObject(payload);
const isCollection = !isStandard && isStandardOpenPoseCollectionPayload(payload);
const sourceFile = info.sourceFile || info.filename;
if (galleryState.manager && isCollection) {
galleryState.manager.collectionFiles.add(sourceFile);
}
const badge = isStandard ? null : (isCollection ? "collection" : "nonstandard");
galleryState.fileMeta.set(sourceFile, { badge });
},
decoratePreset: (preset, info) => {
if (!preset || !info?.filename) {
return;
}
const sourceFile = info.sourceFile || info.filename;
let meta = galleryState.fileMeta.get(sourceFile);
if (!meta && info.payload) {
const isStandard = isStandardOpenPosePoseObject(info.payload);
const isCollection = !isStandard && isStandardOpenPoseCollectionPayload(info.payload);
const badge = isStandard ? null : (isCollection ? "collection" : "nonstandard");
meta = { badge };
galleryState.fileMeta.set(sourceFile, meta);
if (galleryState.manager && isCollection) {
galleryState.manager.collectionFiles.add(sourceFile);
}
}
if (meta && meta.badge) {
preset.galleryBadge = meta.badge;
}
},
onPresetsLoaded: (info, context) => {
galleryState.manager?.clearSelection();
galleryState.manager?.markDirty();
if (context?.manager?.isActive("gallery")) {
galleryState.manager?.refresh();
}
},
summary: {
icon: UiIcons.svg('grid', { size: 14, className: 'openpose-sidebar-icon' }),
titleKey: "gallery.label",
descriptionKey: "gallery.summary.description"
},
emptyAction: {
icon: UiIcons.svg('grid', { size: 14, className: 'openpose-sidebar-icon' }),
textKey: "gallery.empty_action.text"
}
});