Fix floating viewer compare modes with the Studio player

- Tag the Studio player's video with the media class the floating viewer
  uses, so zoom, pan, exposure/channel tone and overlay guides apply to it
  in A/B, side by side and grid like they do to the other slots.
- Stop the A/B divider above the player controls instead of drawing over
  them.
- Keep B's aspect ratio when comparing images of different sizes in the
  Studio viewer.
This commit is contained in:
Ewald ALOEBOETOE
2026-11-04 13:39:16 +00:00
parent e7b2d306ab
commit 39defc3471
13 changed files with 54 additions and 23 deletions
+3
View File
@@ -46,6 +46,9 @@ All notable changes to this project are documented in this file.
- **Floating viewer: one player everywhere**: A/B, side-by-side and grid compare now also use the Studio player for the primary video / audio (the other slots stay bare, synced elements). The legacy video controls (in/out, Nuke-style bar, ~2 000 lines, their styles and tests) are gone; the asset sidebar keeps its small preview controls. GIF / animated WebP keep their own simple player.
### Fixed (follow-ups)
- **Floating viewer compare modes ignored the Studio player's video**: zoom, pan, exposure / channel tone and the overlay guides only addressed bare media elements, so in A/B, side-by-side and grid the primary video (the Studio player) stayed untouched while the other slots changed. The player's video now takes part in all of them.
- **Floating viewer A/B divider drew over the player controls**: it now stops above the control bar (and extends fully again when the controls auto-hide).
- **Studio compare of different sizes**: B keeps its own aspect ratio instead of being stretched to A's frame (wipe, blend and side by side).
- **Floating viewer topbar button position**: the *Viewer* button sometimes landed at the far end of the actionbar after reloading the interface. The Run group is rendered asynchronously inside the actionbar and its late arrival was never observed; the button now re-anchors next to Run as soon as it appears.
- **Floating viewer A/B hid the player**: with the Studio player on A, the B layer covered the right half of its controls. A now sits on top and only its picture is clipped at the divider, so the controls stay visible and usable everywhere.
- **Light dismiss in the Studio viewer**: clicking the empty area around the image or video (not the picture, a tool, the inspector or the player bar) closes the viewer again, as the classic viewer did. Dragging to pan never closes it.
@@ -4,7 +4,7 @@ import { c as S, l as C, m as w, p as ee, s as T, t as E, u as D, v as O } from
import { r as k } from "./events-1iwo5blr.js";
import { C as A, D as j, E as M, T as N, _ as P, a as F, b as I, d as L, f as R, g as z, h as B, m as te, n as V, o as ne, p as H, r as re, s as ie, t as U, v as ae, w as oe, y as se } from "./tooltipShortcuts-Ury2eTek.js";
import { I as ce } from "./mjr-primevue-C07IsEdU.js";
import { C as le, S as ue, n as de, t as fe, w as pe } from "./genInfo-DZ8thzbX.js";
import { C as le, S as ue, n as de, t as fe, w as pe } from "./genInfo-Dk8cerKp.js";
// ui/features/viewer/videoSync.ts
var me = () => {
try {
@@ -524,7 +524,8 @@ function je(e) {
frameCount: e.frameCount ?? null,
initialMuted: !!e.initialMuted,
initialRate: e.initialRate,
compact: e.compact !== !1
compact: e.compact !== !1,
mediaClass: e.mediaClass
}) });
r.mount(t);
let i = !1;
@@ -696,7 +697,8 @@ function He(e, { initialMuted: t = !1, initialPlaybackRate: n = 1, fill: r = !1
initialMuted: e?._isPreview ? !0 : t,
initialRate: n,
compact: !0,
fill: r
fill: r,
mediaClass: "mjr-mfv-media"
});
} catch (e) {
return console.debug?.(e), null;
@@ -1,5 +1,5 @@
import { Vn as e, t } from "./config-C9a-wT5M.js";
import { t as n } from "./floatingViewerManager-DVF62mxS.js";
import { t as n } from "./floatingViewerManager-B_I-SRbd.js";
import { r } from "./events-1iwo5blr.js";
// ui/features/viewer/LiveStreamTracker.ts
var i = !1, a = null, o = null, s = null, c = null, l = null, u = null, d = null, f = null, p = 0, m = 0, h = !1, g = 400, _ = "kj_preview_override", v = /* @__PURE__ */ new Set([
@@ -1,5 +1,5 @@
import { D as e, E as t, It as n, Jn as r, Lt as i, T as a, Wt as o, b as s, dn as c, dr as l, fr as u, g as d, h as f, i as p, in as m, l as h, ln as g, p as _, st as v, ur as y, w as b, xn as x, y as ee } from "./config-C9a-wT5M.js";
import { t as S } from "./floatingViewerManager-DVF62mxS.js";
import { t as S } from "./floatingViewerManager-B_I-SRbd.js";
import { A as te, N as ne, a as C, d as w, i as T, k as E, m as D, n as O, o as re, p as k, r as A, t as j, v as ie, y as M } from "./generationSectionState-DuXxSw-j.js";
import { n as N, t as P } from "./events-1iwo5blr.js";
import { $ as ae, B as oe, C as F, D as se, E as I, H as ce, J as le, K as L, M as R, O as z, P as B, Q as V, S as ue, T as H, V as de, W as U, X as W, b as fe, gt as G, ht as K, j as pe, k as q, mt as me, ot as he, pt as J, st as Y, y as ge } from "./mjr-primevue-C07IsEdU.js";
@@ -1385,7 +1385,7 @@ var xn = {
setup(e) {
let t = e, n = Y(0), i = Y(!1), a = null;
function c() {
return a ||= import("./floatingViewerManager-DVF62mxS.js").then((e) => e.n), a;
return a ||= import("./floatingViewerManager-B_I-SRbd.js").then((e) => e.n), a;
}
function l() {
return (Array.isArray(t.inputFile?.previewCandidates) ? t.inputFile.previewCandidates : [])[n.value] || "";
@@ -1,9 +1,9 @@
import { T as e, Wt as t, Yn as n, f as r, in as i, ir as a, s as o, u as s } from "./config-C9a-wT5M.js";
import { a as c, i as l, r as u } from "./floatingViewerManager-DVF62mxS.js";
import { a as c, i as l, r as u } from "./floatingViewerManager-B_I-SRbd.js";
import { i as d, r as f, t as p } from "./events-1iwo5blr.js";
import { a as ee, i as m, n as h, r as g, t as _ } from "./TagsEditor-CUU3SlW5.js";
import { a as ee, i as m, n as h, r as g, t as _ } from "./TagsEditor-MUk741GH.js";
import { $ as te, A as v, B as ne, C as y, D as b, E as x, H as S, J as re, K as C, M as w, O as T, P as E, Q as D, S as O, T as k, U as A, V as ie, W as j, X as M, Y as N, b as ae, gt as P, ht as F, j as I, k as L, lt as oe, mt as R, ot as se, pt as z, q as B, rt as ce, st as V, w as le, z as H } from "./mjr-primevue-C07IsEdU.js";
import { C as U, _ as W, a as ue, b as de, c as G, d as fe, f as pe, g as me, h as he, i as ge, l as _e, m as ve, n as ye, o as K, p as be, r as xe, s as q, t as Se, u as Ce, v as we, x as Te, y as Ee } from "./genInfo-DZ8thzbX.js";
import { C as U, _ as W, a as ue, b as de, c as G, d as fe, f as pe, g as me, h as he, i as ge, l as _e, m as ve, n as ye, o as K, p as be, r as xe, s as q, t as Se, u as Ce, v as we, x as Te, y as Ee } from "./genInfo-Dk8cerKp.js";
// ui/vue/components/viewer/FloatingViewerHost.vue
var De = /* @__PURE__ */ E({
__name: "FloatingViewerHost",
@@ -201,7 +201,7 @@ var he = /* @__PURE__ */ e({
teardownFloatingViewerManager: () => Ue
}), S = null, ge = null;
async function _e() {
return S || (ge ||= import("./FloatingViewer-CexKk67v.js").then((e) => (S = e.FloatingViewer, S)), ge);
return S || (ge ||= import("./FloatingViewer--x61GAzy.js").then((e) => (S = e.FloatingViewer, S)), ge);
}
var C = null, ve = null;
async function ye() {
@@ -1536,7 +1536,8 @@ var it = ["src"], at = ["src"], ot = {
compareSrc: {},
compare: {},
wipe: {},
lightDismiss: { type: Boolean }
lightDismiss: { type: Boolean },
mediaClass: {}
},
emits: [
"ended",
@@ -1874,7 +1875,7 @@ var it = ["src"], at = ["src"], ot = {
}, [d("video", {
ref_key: "media",
ref: o,
class: "mjr-studio-player__media",
class: T(["mjr-studio-player__media", t.mediaClass]),
src: t.src,
playsinline: "",
preload: "auto",
@@ -1885,7 +1886,7 @@ var it = ["src"], at = ["src"], ot = {
onProgress: Pe,
onEnded: Fe,
onError: Ie
}, null, 40, it), Se.value ? (y(), w("video", {
}, null, 42, it), Se.value ? (y(), w("video", {
ref_key: "mediaB",
ref: oe,
key: `b:${t.compareSrc}`,
+10 -10
View File
File diff suppressed because one or more lines are too long
@@ -214,6 +214,9 @@ export function buildFloatingViewerStudioPlayer(
initialRate: initialPlaybackRate,
compact: true,
fill,
// Same hook as the bare elements: zoom, pan, exposure / channel tone
// and the overlay guides all address `.mjr-mfv-media`.
mediaClass: "mjr-mfv-media",
});
} catch (e: any) {
console.debug?.(e);
@@ -24,6 +24,8 @@ export interface StudioPlayerHostOptions {
compact?: boolean;
/** Fill the slot of a compare layout (A/B, side by side, grid). */
fill?: boolean;
/** Extra class for the <video>, so host-level zoom / tone controls can find it. */
mediaClass?: string;
}
export interface StudioPlayerHandle {
@@ -59,6 +61,7 @@ export function mountStudioPlayerHost(options: StudioPlayerHostOptions): HTMLEle
initialMuted: !!options.initialMuted,
initialRate: options.initialRate,
compact: options.compact !== false,
mediaClass: options.mediaClass,
}),
});
app.mount(host);
@@ -42,6 +42,8 @@ describe("floating viewer media player bridge", () => {
expect(root?.classList.contains("mjr-mfv-player-host--studio")).toBe(true);
expect(root?._mjrMediaControlsHandle?.destroy).toBeTypeOf("function");
expect(root?.querySelector(".mjr-studio-player video")).toBeTruthy();
// Zoom / pan / tone controls address the media through this class.
expect(root?.querySelector(".mjr-studio-player video.mjr-mfv-media")).toBeTruthy();
expect(mountFloatingViewerSimplePlayerMock).not.toHaveBeenCalled();
root._mjrMediaControlsHandle.destroy();
});
+14
View File
@@ -9269,3 +9269,17 @@ body.mjr-mfv-popout-body {
.mjr-mfv-ab-container.has-studio-a .mjr-mfv-ab-divider {
z-index: 10;
}
/* The divider stops above the player controls instead of drawing over them
(clip-path also removes its hit area there). */
.mjr-mfv-ab-container.has-studio-a .mjr-mfv-ab-divider {
clip-path: inset(0 -20px 72px -20px);
}
.mjr-mfv-ab-container.has-studio-a:has(.mjr-studio-player.controls-hidden) .mjr-mfv-ab-divider {
clip-path: none;
}
/* B keeps its own aspect ratio inside A's frame (A/B of different sizes). */
.mjr-studio-stage__img.is-b,
.mjr-studio-stage__side .mjr-studio-stage__img {
object-fit: contain;
}
@@ -43,6 +43,8 @@ const props = defineProps<{
wipe?: number;
/** Clicking the bare background (not the picture) emits `dismiss` instead of toggling play. */
lightDismiss?: boolean;
/** Extra class on the <video> (the floating viewer's zoom / pan / tone controls target it). */
mediaClass?: string;
}>();
const emit = defineEmits<{
@@ -607,6 +609,7 @@ defineExpose({
<video
ref="media"
class="mjr-studio-player__media"
:class="mediaClass"
:src="src"
playsinline
preload="auto"