Chantier F - Typage Python - ruff S101 déplacé en per-file-ignores (pyproject.toml) - pyrightconfig.json : mode standard confirmé Chantier G - Assertions & bare-except - 16 blocs except documentés dans config.py - --cov-branch activé, seuil coverage 60→68% Chantier H - Cache settings - Stratégie documentée dans settings.py - _resolve_index_dir() logguée dans config.py Chantier I - Split gros fichiers JS - client.js 1509L → client.js 406L + clientAuth.js + clientOps.js + fetchUtils.js - toolbar.js 927L → toolbar.js 458L + toolbarActions.js + toolbarControls.js - FloatingViewer.js → FloatingViewer.impl.js + floatingViewer*(8 modules) - Viewer_impl.js → ViewerRuntime.js + viewerShell.js + viewerThemeStyles.js + viewerOverlayDismiss.js - model3dRenderer.js → model3dRenderer.impl.js + model3dCore.js + model3dLoaders.js + model3dSupport.js - dialogs.js → dialogs.js + DialogTemplates.js Chantier J - Tests frontend (Phases 1-2) - use_drag_drop.vitest.mjs : 5 tests - use_grid_state.vitest.mjs : 18 tests - security_client_headers, security_deep_merge, security_runtime_state, runtime_store, dialog_templates - Total: 80 fichiers test / 350 tests vitest Chantier K - Pinia migration - runtimeState.js entièrement migré vers Pinia - getOptionalRuntimeStore.js + runtimeEnrichmentState.js ajoutés Chantier L - Durcissement sécurité - tests/security/test_rate_limit.py : 8 tests rate-limit (state machine complète) - clientAuth.js : docstring sessionStorage security boundary - three.js 0.183.2 audité : 0 CVE (Snyk) Chantier M - Simplification config - deps.py, tool_detect.py, utils.py nettoyés docs: PLAN_REFACTO_V2_LONG_TERME.md ajouté et clôturé .gitignore: coverage/ ajouté
835 lines
30 KiB
JavaScript
835 lines
30 KiB
JavaScript
import { safeCall } from "./lifecycle.js";
|
|
import { createIconButton } from "../../components/buttons.js";
|
|
import { t } from "../../app/i18n.js";
|
|
import { appendTooltipHint } from "../../utils/tooltipShortcuts.js";
|
|
|
|
export function createViewerToolbarControls({
|
|
VIEWER_MODES,
|
|
state,
|
|
onToolsChanged,
|
|
onCompareModeChanged,
|
|
onExportFrame,
|
|
onCopyFrame,
|
|
onAudioVizModeChanged,
|
|
getCanAB,
|
|
} = {}) {
|
|
const DEFAULT_TOOL_STATE = {
|
|
channel: "rgb",
|
|
exposureEV: 0,
|
|
gamma: 1.0,
|
|
analysisMode: "none",
|
|
scopesMode: "off",
|
|
gridMode: 0,
|
|
overlayMaskEnabled: false,
|
|
overlayMaskOpacity: 0.65,
|
|
overlayFormat: "image",
|
|
probeEnabled: false,
|
|
loupeEnabled: false,
|
|
hudEnabled: true,
|
|
distractionFree: false,
|
|
genInfoOpen: true,
|
|
abCompareMode: "wipe",
|
|
audioVisualizerMode: "artistic",
|
|
};
|
|
|
|
const toolsRow = document.createElement("div");
|
|
toolsRow.className = "mjr-viewer-tools";
|
|
toolsRow.style.cssText = `
|
|
display: block;
|
|
padding: 8px 8px 6px;
|
|
border-top: 0.8px solid rgba(196, 202, 210, 0.16);
|
|
background: rgba(12, 14, 19, 0.22);
|
|
`;
|
|
|
|
const toolsDeck = document.createElement("div");
|
|
toolsDeck.className = "mjr-viewer-tools-deck";
|
|
toolsDeck.style.cssText =
|
|
"display:flex; flex-wrap:nowrap; gap:8px; align-items:center; min-width:0; overflow-x:auto; overflow-y:hidden;";
|
|
|
|
const createToolsPanel = ({ key, eyebrow, title } = {}) => {
|
|
const panel = document.createElement("section");
|
|
panel.className = "mjr-viewer-tools-panel";
|
|
if (key) panel.dataset.panel = String(key);
|
|
panel.style.cssText =
|
|
"display:flex; flex-direction:column; gap:4px; min-width:0; padding:5px 6px; border-radius:10px; border:1px solid rgba(255,255,255,0.08); background:linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.02)); box-shadow:inset 0 1px 0 rgba(255,255,255,0.04); flex:0 0 auto;";
|
|
|
|
const head = document.createElement("div");
|
|
head.className = "mjr-viewer-tools-panel-head";
|
|
head.style.cssText =
|
|
"display:none; align-items:flex-start; justify-content:space-between; gap:6px; min-width:0;";
|
|
|
|
const heading = document.createElement("div");
|
|
heading.className = "mjr-viewer-tools-panel-heading";
|
|
heading.style.cssText = "display:flex; flex-direction:column; gap:1px; min-width:0;";
|
|
|
|
const eyebrowEl = document.createElement("span");
|
|
eyebrowEl.className = "mjr-viewer-tools-panel-eyebrow";
|
|
eyebrowEl.textContent = eyebrow || "";
|
|
|
|
const titleEl = document.createElement("span");
|
|
titleEl.className = "mjr-viewer-tools-panel-title";
|
|
titleEl.textContent = title || "";
|
|
|
|
const body = document.createElement("div");
|
|
body.className = "mjr-viewer-tools-panel-body";
|
|
body.style.cssText = "display:flex; align-items:center; flex-wrap:nowrap; gap:6px; min-width:0;";
|
|
|
|
heading.appendChild(eyebrowEl);
|
|
heading.appendChild(titleEl);
|
|
head.appendChild(heading);
|
|
panel.appendChild(head);
|
|
panel.appendChild(body);
|
|
|
|
return { panel, body, head, heading, eyebrowEl, titleEl };
|
|
};
|
|
|
|
const gradePanel = createToolsPanel({ key: "grade", eyebrow: "Image", title: "Adjustments" });
|
|
const overlayPanel = createToolsPanel({ key: "overlay", eyebrow: "Viewer", title: "Guides & Compare" });
|
|
const inspectPanel = createToolsPanel({ key: "inspect", eyebrow: "Inspect", title: "Probe" });
|
|
const actionPanel = createToolsPanel({ key: "actions", eyebrow: "Actions", title: "Reset & Export" });
|
|
const infoPanel = createToolsPanel({ key: "info", eyebrow: "Infos", title: "Help" });
|
|
|
|
const toolsActions = document.createElement("div");
|
|
toolsActions.className = "mjr-viewer-tools-actions";
|
|
toolsActions.style.cssText =
|
|
"display:flex; align-items:center; justify-content:flex-start; gap:6px; flex-wrap:wrap; min-width:0;";
|
|
|
|
const toolsMeta = document.createElement("div");
|
|
toolsMeta.className = "mjr-viewer-tools-meta";
|
|
toolsMeta.style.cssText =
|
|
"display:flex; align-items:center; justify-content:flex-start; gap:6px; flex-wrap:nowrap; min-width:0;";
|
|
|
|
const toolsGroup = ({ key, label, accentRgb } = {}) => {
|
|
const g = document.createElement("div");
|
|
g.className = "mjr-viewer-tools-group";
|
|
if (key) g.dataset.group = String(key);
|
|
if (accentRgb) g.style.setProperty("--mjr-group-accent", String(accentRgb));
|
|
g.style.cssText =
|
|
"display:flex; align-items:center; gap:6px; padding:2px 6px; border-radius:8px; border:1px solid rgba(196,202,210,0.14); background:rgba(10,12,16,0.22);";
|
|
if (label) {
|
|
const l = document.createElement("span");
|
|
l.className = "mjr-viewer-tools-group-label";
|
|
l.textContent = label;
|
|
l.style.cssText = "font-size: 10px; color: rgba(255,255,255,0.7);";
|
|
g.appendChild(l);
|
|
}
|
|
return g;
|
|
};
|
|
|
|
const createSelect = (title, options) => {
|
|
const s = document.createElement("select");
|
|
s.title = title || "";
|
|
s.className = "mjr-viewer-tools-select";
|
|
s.style.cssText = `
|
|
height: 24px;
|
|
padding: 0 6px;
|
|
border-radius: 6px;
|
|
border: 0.8px solid rgba(196, 202, 210, 0.24);
|
|
background: linear-gradient(180deg, rgba(210, 214, 220, 0.06), rgba(210, 214, 220, 0.02));
|
|
color: rgba(230,233,238,0.92);
|
|
font-size: 11px;
|
|
outline: none;
|
|
`;
|
|
for (const o of options || []) {
|
|
const opt = document.createElement("option");
|
|
opt.value = String(o.value);
|
|
opt.textContent = String(o.label);
|
|
s.appendChild(opt);
|
|
}
|
|
return s;
|
|
};
|
|
|
|
const createRange = (title, { min, max, step, value }) => {
|
|
const wrap = document.createElement("div");
|
|
wrap.className = "mjr-viewer-tools-range";
|
|
wrap.style.cssText = "display:flex; align-items:center; gap:6px;";
|
|
|
|
const input = document.createElement("input");
|
|
input.type = "range";
|
|
input.className = "mjr-viewer-tools-range-input";
|
|
input.min = String(min);
|
|
input.max = String(max);
|
|
input.step = String(step);
|
|
input.value = String(value);
|
|
input.title = title || "";
|
|
input.style.cssText = `
|
|
width: 92px;
|
|
accent-color: rgba(255,255,255,0.85);
|
|
`;
|
|
|
|
const out = document.createElement("span");
|
|
out.style.cssText =
|
|
"font-size: 11px; color: rgba(255,255,255,0.9); min-width: 38px; text-align: right;";
|
|
out.textContent = String(value);
|
|
|
|
wrap.appendChild(input);
|
|
wrap.appendChild(out);
|
|
|
|
return { wrap, input, out };
|
|
};
|
|
|
|
const createToggle = (label, title, { iconClass = null, accentRgb = null } = {}) => {
|
|
const b = document.createElement("button");
|
|
b.type = "button";
|
|
b.className = "mjr-viewer-tool-btn";
|
|
b.setAttribute("aria-label", title || label || "Toggle");
|
|
b.setAttribute("aria-pressed", "false");
|
|
if (accentRgb) {
|
|
try {
|
|
b.dataset.accentRgb = String(accentRgb);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
if (iconClass) {
|
|
const icon = document.createElement("span");
|
|
icon.className = `pi ${iconClass}`.trim();
|
|
icon.setAttribute("aria-hidden", "true");
|
|
icon.style.fontSize = "14px";
|
|
b.appendChild(icon);
|
|
|
|
const sr = document.createElement("span");
|
|
sr.textContent = label || "";
|
|
sr.style.cssText =
|
|
"position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0;";
|
|
b.appendChild(sr);
|
|
} else {
|
|
b.textContent = label;
|
|
}
|
|
b.title = title || "";
|
|
b.style.cssText = `
|
|
height: 24px;
|
|
padding: 0 8px;
|
|
border-radius: 6px;
|
|
border: 0.8px solid rgba(196, 202, 210, 0.24);
|
|
background: linear-gradient(180deg, rgba(210,214,220,0.06), rgba(210,214,220,0.02));
|
|
color: rgba(230,233,238,0.92);
|
|
cursor: pointer;
|
|
font-size: 11px;
|
|
user-select: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
position: relative;
|
|
`;
|
|
b.dataset.active = "0";
|
|
const setActive = (active) => {
|
|
const on = Boolean(active);
|
|
b.dataset.active = on ? "1" : "0";
|
|
try {
|
|
b.setAttribute("aria-pressed", on ? "true" : "false");
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
const rgb = String(b.dataset?.accentRgb || "").trim();
|
|
if (on && rgb) {
|
|
b.style.background = `rgba(${rgb}, 0.12)`;
|
|
b.style.borderColor = `rgba(${rgb}, 0.38)`;
|
|
b.style.boxShadow = `0 0 0 0.8px rgba(${rgb}, 0.12) inset`;
|
|
} else {
|
|
b.style.background = on ? "rgba(214,218,224,0.12)" : "rgba(210,214,220,0.06)";
|
|
b.style.borderColor = on ? "rgba(214,218,224,0.38)" : "rgba(196,202,210,0.24)";
|
|
b.style.boxShadow = "";
|
|
}
|
|
};
|
|
setActive(false);
|
|
return { b, setActive };
|
|
};
|
|
|
|
const channelsSelect = createSelect("Channel View", [
|
|
{ value: "rgb", label: "RGB" },
|
|
{ value: "r", label: "R" },
|
|
{ value: "g", label: "G" },
|
|
{ value: "b", label: "B" },
|
|
{ value: "a", label: "Alpha" },
|
|
{ value: "l", label: "Luma" },
|
|
]);
|
|
channelsSelect.title = t("tooltip.colorChannels", "View color channels or luminance");
|
|
|
|
const exposureCtl = createRange("Exposure (EV)", { min: -10, max: 10, step: 0.1, value: 0 });
|
|
const gammaCtl = createRange("Gamma", { min: 0.1, max: 3.0, step: 0.01, value: 1.0 });
|
|
|
|
// Accents (RGB): comfy-friendly, subtle but distinct.
|
|
const ACCENT = Object.freeze({
|
|
channel: "120, 180, 255",
|
|
exposure: "255, 200, 70",
|
|
gamma: "190, 150, 255",
|
|
analysis: "255, 140, 80",
|
|
zebra: "255, 90, 90",
|
|
overlay: "110, 240, 190",
|
|
probe: "120, 255, 170",
|
|
loupe: "180, 140, 255",
|
|
compare: "90, 220, 220",
|
|
geninfo: "200, 170, 255",
|
|
audioviz: "255, 150, 80",
|
|
});
|
|
|
|
const SELECT_STYLE_DEFAULT = Object.freeze({
|
|
borderColor: "rgba(255,255,255,0.14)",
|
|
background: "rgba(255,255,255,0.08)",
|
|
boxShadow: "",
|
|
});
|
|
|
|
const setSelectHighlighted = (selectEl, { accentRgb, active, title } = {}) => {
|
|
try {
|
|
if (!selectEl) return;
|
|
if (title) selectEl.title = String(title);
|
|
const on = Boolean(active);
|
|
if (!on) {
|
|
selectEl.style.borderColor = SELECT_STYLE_DEFAULT.borderColor;
|
|
selectEl.style.background = SELECT_STYLE_DEFAULT.background;
|
|
selectEl.style.boxShadow = SELECT_STYLE_DEFAULT.boxShadow;
|
|
return;
|
|
}
|
|
const rgb = String(accentRgb || "").trim();
|
|
if (!rgb) return;
|
|
selectEl.style.borderColor = `rgba(${rgb},0.55)`;
|
|
selectEl.style.background = `rgba(${rgb},0.14)`;
|
|
selectEl.style.boxShadow = `0 0 0 1px rgba(${rgb},0.14) inset`;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
};
|
|
|
|
const setChannelSelectStyle = (channel) => {
|
|
try {
|
|
const ch = String(channel || "rgb");
|
|
const baseBorder = "rgba(255,255,255,0.14)";
|
|
const baseBg = "rgba(255,255,255,0.08)";
|
|
channelsSelect.style.boxShadow = "";
|
|
if (ch === "r") {
|
|
channelsSelect.style.borderColor = "rgba(255,90,90,0.60)";
|
|
channelsSelect.style.background = "rgba(255,90,90,0.14)";
|
|
channelsSelect.style.boxShadow = "0 0 0 1px rgba(255,90,90,0.14) inset";
|
|
return;
|
|
}
|
|
if (ch === "g") {
|
|
channelsSelect.style.borderColor = "rgba(90,255,140,0.55)";
|
|
channelsSelect.style.background = "rgba(90,255,140,0.12)";
|
|
channelsSelect.style.boxShadow = "0 0 0 1px rgba(90,255,140,0.12) inset";
|
|
return;
|
|
}
|
|
if (ch === "b") {
|
|
channelsSelect.style.borderColor = "rgba(90,160,255,0.60)";
|
|
channelsSelect.style.background = "rgba(90,160,255,0.12)";
|
|
channelsSelect.style.boxShadow = "0 0 0 1px rgba(90,160,255,0.12) inset";
|
|
return;
|
|
}
|
|
if (ch === "l") {
|
|
channelsSelect.style.borderColor = "rgba(255,210,90,0.60)";
|
|
channelsSelect.style.background = "rgba(255,210,90,0.12)";
|
|
channelsSelect.style.boxShadow = "0 0 0 1px rgba(255,210,90,0.12) inset";
|
|
return;
|
|
}
|
|
if (ch === "a") {
|
|
channelsSelect.style.borderColor = "rgba(220,220,220,0.35)";
|
|
channelsSelect.style.background = "rgba(255,255,255,0.10)";
|
|
channelsSelect.style.boxShadow = "0 0 0 1px rgba(255,255,255,0.08) inset";
|
|
return;
|
|
}
|
|
// RGB (default): subtle tricolor.
|
|
if (ch === "rgb") {
|
|
channelsSelect.style.borderColor = "rgba(255,255,255,0.22)";
|
|
channelsSelect.style.background =
|
|
"linear-gradient(90deg, rgba(255,90,90,0.16), rgba(90,255,140,0.14), rgba(90,160,255,0.16))";
|
|
channelsSelect.style.boxShadow = "0 0 0 1px rgba(255,255,255,0.10) inset";
|
|
return;
|
|
}
|
|
channelsSelect.style.borderColor = baseBorder;
|
|
channelsSelect.style.background = baseBg;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
};
|
|
|
|
const setValueHighlighted = (el, { accentRgb, active } = {}) => {
|
|
try {
|
|
if (!el) return;
|
|
const on = Boolean(active);
|
|
if (!on) {
|
|
el.style.color = "rgba(255,255,255,0.9)";
|
|
return;
|
|
}
|
|
const rgb = String(accentRgb || "").trim();
|
|
if (!rgb) return;
|
|
el.style.color = `rgb(${rgb})`;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
};
|
|
|
|
const setGroupHighlighted = (groupEl, { accentRgb, active } = {}) => {
|
|
try {
|
|
if (!groupEl) return;
|
|
const on = Boolean(active);
|
|
if (!on) {
|
|
groupEl.style.background = "";
|
|
groupEl.style.borderColor = "transparent";
|
|
groupEl.style.boxShadow = "";
|
|
return;
|
|
}
|
|
const rgb = String(accentRgb || "").trim();
|
|
if (!rgb) return;
|
|
groupEl.style.background = `rgba(${rgb},0.10)`;
|
|
groupEl.style.borderColor = `rgba(${rgb},0.38)`;
|
|
groupEl.style.boxShadow = `0 0 0 1px rgba(${rgb},0.12) inset`;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
};
|
|
|
|
const zebraToggle = createToggle("Zebra", "Zebra Highlights (Z)", {
|
|
iconClass: "pi-bars",
|
|
accentRgb: ACCENT.zebra,
|
|
});
|
|
const scopesToggle = createToggle("Scopes", "Scopes overlay", {
|
|
iconClass: "pi-chart-bar",
|
|
accentRgb: ACCENT.analysis,
|
|
});
|
|
const scopesSelect = createSelect("Scopes", [
|
|
{ value: "off", label: "Off" },
|
|
{ value: "hist", label: "Histogram" },
|
|
{ value: "wave", label: "Waveform" },
|
|
{ value: "both", label: "Both" },
|
|
]);
|
|
scopesSelect.title = t("tooltip.scopesHistogram", "Show histogram/waveform scopes");
|
|
const gridToggle = createToggle("Grid", "Grid (G)", {
|
|
iconClass: "pi-th-large",
|
|
accentRgb: ACCENT.overlay,
|
|
});
|
|
const gridModeSelect = createSelect("Grid Overlay", [
|
|
{ value: 0, label: "Off" },
|
|
{ value: 1, label: "Thirds" },
|
|
{ value: 2, label: "Center" },
|
|
{ value: 3, label: "Safe" },
|
|
{ value: 4, label: "Golden" },
|
|
]);
|
|
gridModeSelect.title = appendTooltipHint(
|
|
t("tooltip.gridOverlay", "Grid overlay (rule of thirds, center)"),
|
|
"G",
|
|
);
|
|
const maskToggle = createToggle("Mask", "Format mask (dim outside)", {
|
|
iconClass: "pi-stop",
|
|
accentRgb: ACCENT.overlay,
|
|
});
|
|
const formatSelect = createSelect("Format", [
|
|
{ value: "image", label: "Image" },
|
|
{ value: "16:9", label: "16:9" },
|
|
{ value: "1:1", label: "1:1" },
|
|
{ value: "4:3", label: "4:3" },
|
|
{ value: "2.39", label: "2.39" },
|
|
{ value: "9:16", label: "9:16" },
|
|
]);
|
|
formatSelect.title = t("tooltip.aspectRatioMask", "Aspect ratio overlay mask");
|
|
const maskOpacityCtl = createRange("Mask Opacity", {
|
|
min: 0,
|
|
max: 0.9,
|
|
step: 0.05,
|
|
value: 0.65,
|
|
});
|
|
const probeToggle = createToggle("Probe", "Pixel Probe (I)", {
|
|
iconClass: "pi-eye",
|
|
accentRgb: ACCENT.probe,
|
|
});
|
|
const loupeToggle = createToggle("Loupe", "Loupe (L)", {
|
|
iconClass: "pi-search-plus",
|
|
accentRgb: ACCENT.loupe,
|
|
});
|
|
const hudToggle = createToggle("HUD", "Viewer HUD", {
|
|
iconClass: "pi-info-circle",
|
|
accentRgb: ACCENT.overlay,
|
|
});
|
|
const focusToggle = createToggle("Focus", "Distraction-free mode (X)", {
|
|
iconClass: "pi-window-maximize",
|
|
accentRgb: ACCENT.overlay,
|
|
});
|
|
const genInfoToggle = createToggle(
|
|
"Gen",
|
|
appendTooltipHint("Generation info (prompt/model)", "D"),
|
|
{
|
|
iconClass: "pi-book",
|
|
accentRgb: ACCENT.geninfo,
|
|
},
|
|
);
|
|
|
|
const compareModeSelect = createSelect("A/B Compare Mode", [
|
|
{ value: "wipe", label: "Wipe (H)" },
|
|
{ value: "wipeV", label: "Wipe (V)" },
|
|
{ value: "difference", label: "Difference" },
|
|
{ value: "absdiff", label: "AbsDiff" },
|
|
{ value: "add", label: "Add" },
|
|
{ value: "subtract", label: "Subtract" },
|
|
{ value: "multiply", label: "Multiply" },
|
|
{ value: "screen", label: "Screen" },
|
|
]);
|
|
compareModeSelect.title = t("tooltip.compareBlendMode", "Compare blend mode");
|
|
const audioVizModeSelect = createSelect("Audio Visualizer", [
|
|
{ value: "simple", label: "Simple" },
|
|
{ value: "artistic", label: "Artistic" },
|
|
]);
|
|
audioVizModeSelect.title = t("tooltip.audioVisualizer", "Audio visualizer mode");
|
|
|
|
const resetGradeBtn = createIconButton(
|
|
"Reset",
|
|
t("tooltip.resetPlayerControls", "Reset all viewer controls"),
|
|
);
|
|
resetGradeBtn.style.height = "26px";
|
|
resetGradeBtn.style.fontSize = "11px";
|
|
resetGradeBtn.style.padding = "0 8px";
|
|
resetGradeBtn.classList?.add?.("mjr-viewer-tool-btn", "mjr-viewer-tool-btn--reset");
|
|
resetGradeBtn.classList?.add?.("mjr-viewer-tools-action", "mjr-viewer-tools-action--primary");
|
|
resetGradeBtn.style.marginLeft = "auto";
|
|
|
|
const exportBtn = document.createElement("button");
|
|
exportBtn.type = "button";
|
|
exportBtn.title = t("tooltip.exportFrame", "Save current frame as PNG");
|
|
exportBtn.setAttribute("aria-label", t("tooltip.exportFrame", "Save frame as PNG"));
|
|
exportBtn.className = "mjr-viewer-tool-btn mjr-viewer-tool-btn--reset";
|
|
exportBtn.style.cssText =
|
|
"height:24px; padding:0 8px; display:inline-flex; align-items:center; justify-content:center;";
|
|
const exportIcon = document.createElement("span");
|
|
exportIcon.className = "pi pi-download";
|
|
exportIcon.setAttribute("aria-hidden", "true");
|
|
exportIcon.style.fontSize = "14px";
|
|
exportBtn.appendChild(exportIcon);
|
|
exportBtn.classList?.add?.("mjr-viewer-tools-action");
|
|
try {
|
|
exportBtn.style.display = "none";
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
const copyBtn = document.createElement("button");
|
|
copyBtn.type = "button";
|
|
copyBtn.title = t("tooltip.copyFrame", "Copy current frame to clipboard");
|
|
copyBtn.setAttribute("aria-label", t("tooltip.copyFrame", "Copy frame to clipboard"));
|
|
copyBtn.className = "mjr-viewer-tool-btn mjr-viewer-tool-btn--reset";
|
|
copyBtn.style.cssText =
|
|
"height:24px; padding:0 8px; display:inline-flex; align-items:center; justify-content:center;";
|
|
const copyIcon = document.createElement("span");
|
|
copyIcon.className = "pi pi-copy";
|
|
copyIcon.setAttribute("aria-hidden", "true");
|
|
copyIcon.style.fontSize = "14px";
|
|
copyBtn.appendChild(copyIcon);
|
|
copyBtn.classList?.add?.("mjr-viewer-tools-action");
|
|
try {
|
|
copyBtn.style.display = "none";
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
const chGroup = toolsGroup({ key: "channel", label: "Channel", accentRgb: ACCENT.channel });
|
|
chGroup.appendChild(channelsSelect);
|
|
gradePanel.body.appendChild(chGroup);
|
|
|
|
const expGroup = toolsGroup({ key: "exposure", label: "EV", accentRgb: ACCENT.exposure });
|
|
expGroup.appendChild(exposureCtl.wrap);
|
|
gradePanel.body.appendChild(expGroup);
|
|
|
|
const gamGroup = toolsGroup({ key: "gamma", label: "Gamma", accentRgb: ACCENT.gamma });
|
|
gamGroup.appendChild(gammaCtl.wrap);
|
|
gradePanel.body.appendChild(gamGroup);
|
|
|
|
const resetExposure = () => {
|
|
try {
|
|
state.exposureEV = 0;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
exposureCtl.input.value = "0";
|
|
exposureCtl.out.textContent = "0.0EV";
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
safeCall(onToolsChanged);
|
|
};
|
|
|
|
const resetGamma = () => {
|
|
try {
|
|
state.gamma = 1.0;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
gammaCtl.input.value = "1";
|
|
gammaCtl.out.textContent = "1.00";
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
safeCall(onToolsChanged);
|
|
};
|
|
|
|
const resetViewerTools = () => {
|
|
try {
|
|
Object.assign(state, DEFAULT_TOOL_STATE);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
safeCall(onCompareModeChanged);
|
|
safeCall(onAudioVizModeChanged);
|
|
safeCall(onToolsChanged);
|
|
};
|
|
|
|
// Clickable labels/value to reset (Nuke-like UX).
|
|
try {
|
|
const evLabel = expGroup.querySelector?.(".mjr-viewer-tools-group-label");
|
|
if (evLabel) {
|
|
evLabel.title = t("tooltip.resetExposure", "Reset EV to 0");
|
|
evLabel.style.cursor = "pointer";
|
|
evLabel.style.userSelect = "none";
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
exposureCtl.out.title = t("tooltip.resetExposure", "Reset EV to 0");
|
|
exposureCtl.out.style.cursor = "pointer";
|
|
exposureCtl.out.style.userSelect = "none";
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
const gLabel = gamGroup.querySelector?.(".mjr-viewer-tools-group-label");
|
|
if (gLabel) {
|
|
gLabel.title = t("tooltip.resetGamma", "Reset Gamma to 1.00");
|
|
gLabel.style.cursor = "pointer";
|
|
gLabel.style.userSelect = "none";
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
gammaCtl.out.title = t("tooltip.resetGamma", "Reset Gamma to 1.00");
|
|
gammaCtl.out.style.cursor = "pointer";
|
|
gammaCtl.out.style.userSelect = "none";
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
const anaGroup = toolsGroup({ key: "analysis", label: "Analysis", accentRgb: ACCENT.analysis });
|
|
anaGroup.appendChild(zebraToggle.b);
|
|
anaGroup.appendChild(scopesToggle.b);
|
|
anaGroup.appendChild(scopesSelect);
|
|
gradePanel.body.appendChild(anaGroup);
|
|
|
|
const ovGuidesGroup = toolsGroup({
|
|
key: "overlay-guides",
|
|
label: "Guides",
|
|
accentRgb: ACCENT.overlay,
|
|
});
|
|
ovGuidesGroup.appendChild(gridToggle.b);
|
|
ovGuidesGroup.appendChild(gridModeSelect);
|
|
ovGuidesGroup.appendChild(maskToggle.b);
|
|
ovGuidesGroup.appendChild(formatSelect);
|
|
ovGuidesGroup.appendChild(maskOpacityCtl.wrap);
|
|
overlayPanel.body.appendChild(ovGuidesGroup);
|
|
|
|
const ovInspectGroup = toolsGroup({
|
|
key: "overlay-inspect",
|
|
label: "Inspect",
|
|
accentRgb: ACCENT.overlay,
|
|
});
|
|
const inspectButtons = [probeToggle.b, loupeToggle.b, hudToggle.b, focusToggle.b, genInfoToggle.b];
|
|
inspectButtons.forEach((btn, index) => {
|
|
if (index > 0) btn.style.marginLeft = "4px";
|
|
ovInspectGroup.appendChild(btn);
|
|
});
|
|
inspectPanel.body.appendChild(ovInspectGroup);
|
|
|
|
const cmpGroup = toolsGroup({ key: "compare", label: "Compare", accentRgb: ACCENT.compare });
|
|
cmpGroup.style.borderRadius = "8px";
|
|
cmpGroup.style.padding = "4px 6px";
|
|
cmpGroup.style.border = "1px solid transparent";
|
|
cmpGroup.style.transition =
|
|
"background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease";
|
|
cmpGroup.appendChild(compareModeSelect);
|
|
overlayPanel.body.appendChild(cmpGroup);
|
|
|
|
const audGroup = toolsGroup({
|
|
key: "audio-viz",
|
|
label: "Audio Viz",
|
|
accentRgb: ACCENT.audioviz,
|
|
});
|
|
audGroup.appendChild(audioVizModeSelect);
|
|
overlayPanel.body.appendChild(audGroup);
|
|
|
|
toolsActions.appendChild(resetGradeBtn);
|
|
toolsActions.appendChild(exportBtn);
|
|
toolsActions.appendChild(copyBtn);
|
|
|
|
const model3dHint = document.createElement("div");
|
|
model3dHint.className = "mjr-viewer-tools-group mjr-viewer-tools-group--3d";
|
|
model3dHint.textContent = "LMB rotate \u00b7 RMB pan \u00b7 Scroll zoom";
|
|
model3dHint.style.cssText = [
|
|
"display:none",
|
|
"align-items:center",
|
|
"padding:2px 8px",
|
|
"border-radius:999px",
|
|
"border:1px solid rgba(255,255,255,0.12)",
|
|
"background:rgba(255,255,255,0.06)",
|
|
"color:rgba(255,255,255,0.55)",
|
|
"font-size:10px",
|
|
"font-weight:400",
|
|
"letter-spacing:0.01em",
|
|
].join(";");
|
|
toolsMeta.appendChild(model3dHint);
|
|
|
|
// Shortcut help (discoverability)
|
|
const helpWrap = document.createElement("div");
|
|
helpWrap.style.cssText = "position: relative; display:inline-flex; align-items:center;";
|
|
helpWrap.className = "mjr-viewer-tools-action";
|
|
helpWrap.style.marginLeft = "4px";
|
|
|
|
const helpBtn = document.createElement("button");
|
|
helpBtn.type = "button";
|
|
helpBtn.title = t("tooltip.viewerShortcuts", "Viewer shortcuts");
|
|
helpBtn.setAttribute("aria-label", t("tooltip.viewerShortcuts", "Viewer shortcuts"));
|
|
helpBtn.style.cssText = `
|
|
height: 24px;
|
|
padding: 0 8px;
|
|
border-radius: 6px;
|
|
border: 1px solid rgba(255,255,255,0.14);
|
|
background: rgba(255,255,255,0.08);
|
|
color: rgba(255,255,255,0.92);
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
`;
|
|
const helpIcon = document.createElement("span");
|
|
helpIcon.className = "pi pi-question-circle";
|
|
helpIcon.setAttribute("aria-hidden", "true");
|
|
helpIcon.style.fontSize = "14px";
|
|
helpBtn.appendChild(helpIcon);
|
|
|
|
const helpPop = document.createElement("div");
|
|
helpPop.className = "mjr-viewer-help";
|
|
helpPop.style.cssText = `
|
|
position: absolute;
|
|
right: 0;
|
|
top: 32px;
|
|
min-width: 260px;
|
|
max-width: 360px;
|
|
padding: 10px 12px;
|
|
border-radius: 8px;
|
|
background: rgba(0,0,0,0.88);
|
|
border: 1px solid rgba(255,255,255,0.16);
|
|
color: rgba(255,255,255,0.92);
|
|
font-size: 12px;
|
|
line-height: 1.4;
|
|
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
|
|
display: none;
|
|
z-index: 10002;
|
|
`;
|
|
try {
|
|
const title = document.createElement("div");
|
|
title.textContent = "Shortcuts";
|
|
title.style.cssText = "font-weight:600; margin-bottom:6px;";
|
|
|
|
const grid = document.createElement("div");
|
|
grid.style.cssText = "display:grid; grid-template-columns: 1fr 1fr; gap: 4px 10px;";
|
|
|
|
const addRow = (key, label) => {
|
|
const row = document.createElement("div");
|
|
const k = document.createElement("span");
|
|
k.style.cssText = "opacity:.75;";
|
|
k.textContent = String(key || "");
|
|
row.appendChild(k);
|
|
row.appendChild(document.createTextNode(` ${String(label || "")}`));
|
|
grid.appendChild(row);
|
|
};
|
|
|
|
addRow("Esc", "Close");
|
|
addRow("Space", "Play/Pause");
|
|
addRow("+", "Zoom In");
|
|
addRow("-", "Zoom Out");
|
|
addRow("Alt+1", "1:1 Zoom");
|
|
addRow("G", "Grid");
|
|
addRow("D", "Gen Info");
|
|
addRow("Z", "Zebra");
|
|
addRow("I", "Probe");
|
|
addRow("L", "Loupe");
|
|
addRow("X", "Focus Mode");
|
|
addRow("C", "Copy Color");
|
|
addRow("[ / ]", "Speed -/+");
|
|
addRow("\\", "Speed 1x");
|
|
addRow("←/→", "Prev/Next");
|
|
addRow("0-5", "Rating");
|
|
|
|
helpPop.appendChild(title);
|
|
helpPop.appendChild(grid);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
helpWrap.appendChild(helpBtn);
|
|
helpWrap.appendChild(helpPop);
|
|
toolsMeta.appendChild(helpWrap);
|
|
|
|
actionPanel.body.appendChild(toolsActions);
|
|
infoPanel.body.appendChild(toolsMeta);
|
|
|
|
toolsDeck.appendChild(gradePanel.panel);
|
|
toolsDeck.appendChild(overlayPanel.panel);
|
|
toolsDeck.appendChild(inspectPanel.panel);
|
|
toolsDeck.appendChild(actionPanel.panel);
|
|
toolsDeck.appendChild(infoPanel.panel);
|
|
toolsRow.appendChild(toolsDeck);
|
|
|
|
|
|
return {
|
|
toolsRow,
|
|
gradePanel,
|
|
overlayPanel,
|
|
inspectPanel,
|
|
actionPanel,
|
|
infoPanel,
|
|
toolsActions,
|
|
toolsMeta,
|
|
chGroup,
|
|
expGroup,
|
|
gamGroup,
|
|
anaGroup,
|
|
ovGuidesGroup,
|
|
ovInspectGroup,
|
|
cmpGroup,
|
|
audGroup,
|
|
model3dHint,
|
|
helpWrap,
|
|
helpBtn,
|
|
helpPop,
|
|
channelsSelect,
|
|
exposureCtl,
|
|
gammaCtl,
|
|
zebraToggle,
|
|
scopesToggle,
|
|
scopesSelect,
|
|
gridToggle,
|
|
gridModeSelect,
|
|
maskToggle,
|
|
formatSelect,
|
|
maskOpacityCtl,
|
|
probeToggle,
|
|
loupeToggle,
|
|
hudToggle,
|
|
focusToggle,
|
|
genInfoToggle,
|
|
compareModeSelect,
|
|
audioVizModeSelect,
|
|
resetGradeBtn,
|
|
exportBtn,
|
|
copyBtn,
|
|
resetExposure,
|
|
resetGamma,
|
|
resetViewerTools,
|
|
ACCENT,
|
|
setSelectHighlighted,
|
|
setChannelSelectStyle,
|
|
setValueHighlighted,
|
|
setGroupHighlighted,
|
|
DEFAULT_TOOL_STATE,
|
|
};
|
|
}
|