Skip to content

Commit 4c6aee0

Browse files
committed
feat(view): videoplayer fullscreen helper with genie expand-from-source
Adds a fullscreen-overlay entry point to view/VideoPlayer alongside the existing inline createPlayer: VideoPlayer.renderFullScreenPlayer(srcElement, { src, poster, ... }) Opens a viewport-covering overlay with a large autoplaying player that expands out of `srcElement` (Mac-dock-genie style via FLIP transform) and contracts back to it on close. Click on the dimmed backdrop, the close (×) button, or pressing Escape closes the overlay; clicks on the video itself stay live so the user can scrub / unmute / fullscreen without dismissing. Stylesheet lives in src/styles/VideoPlayer.less (imported from brackets.less) instead of being injected from JS — same place every other view module's styles live. The AI panel's old in-place .ai-video-lightbox rules are removed since the AI panel now just calls renderFullScreenPlayer.
1 parent 365f18b commit 4c6aee0

4 files changed

Lines changed: 236 additions & 25 deletions

File tree

src/styles/Extn-AIChatPanel.less

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,10 @@
644644
}
645645
}
646646

647+
/* Fullscreen video overlay opened from the AI panel intro thumbnail.
648+
Position fixed so it covers the entire app viewport (not just the
649+
AI panel column). The inner player is sized to use most of the
650+
screen while preserving 16:9 letterboxing inside the dark backdrop. */
647651
/* ── Assistant message — markdown content ───────────────────────────── */
648652
.ai-msg-assistant {
649653
.ai-msg-content {
@@ -2822,24 +2826,6 @@
28222826
overflow-wrap: anywhere;
28232827
}
28242828

2825-
.ai-intro-video-player {
2826-
display: block;
2827-
width: 100%;
2828-
border: 1px solid rgba(255, 255, 255, 0.08);
2829-
border-radius: 8px;
2830-
overflow: hidden;
2831-
background: rgba(0, 0, 0, 0.25);
2832-
aspect-ratio: 16 / 9;
2833-
2834-
video {
2835-
display: block;
2836-
width: 100%;
2837-
height: 100%;
2838-
object-fit: cover;
2839-
background: rgba(0, 0, 0, 0.25);
2840-
}
2841-
}
2842-
28432829
.ai-intro-video-thumb {
28442830
position: relative;
28452831
display: block;

src/styles/VideoPlayer.less

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* GNU AGPL-3.0 License
3+
*
4+
* Copyright (c) 2021 - present core.ai . All rights reserved.
5+
*
6+
* This program is free software: you can redistribute it and/or modify it under
7+
* the terms of the GNU Affero General Public License as published by the Free
8+
* Software Foundation, either version 3 of the License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
11+
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License along
15+
* with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
16+
*
17+
*/
18+
19+
/* Styles for view/VideoPlayer.js — both the inline createPlayer wrapper
20+
* and the renderFullScreenPlayer overlay (genie-style expand-from-source
21+
* animation). The fullscreen overlay's player-expand animation is
22+
* JS-driven (FLIP transform) so it can target the actual source rect at
23+
* click time; only the backdrop fade is keyframe-driven. */
24+
25+
@keyframes phx-video-fs-backdrop-in {
26+
from { background-color: rgba(0, 0, 0, 0); }
27+
to { background-color: rgba(0, 0, 0, 0.88); }
28+
}
29+
30+
.phx-video-fullscreen-overlay {
31+
position: fixed;
32+
inset: 0;
33+
z-index: 1000;
34+
background: rgba(0, 0, 0, 0.88);
35+
display: flex;
36+
align-items: center;
37+
justify-content: center;
38+
cursor: pointer;
39+
padding: 24px;
40+
box-sizing: border-box;
41+
animation: phx-video-fs-backdrop-in 220ms ease-out;
42+
43+
.phx-video-fullscreen-player {
44+
cursor: default;
45+
width: min(90vw, calc((90vh - 48px) * 16 / 9));
46+
max-width: 90vw;
47+
max-height: 90vh;
48+
aspect-ratio: 16 / 9;
49+
background: #000;
50+
border-radius: 8px;
51+
overflow: hidden;
52+
box-shadow: 0 6px 32px rgba(0, 0, 0, 0.6);
53+
transform-origin: center;
54+
will-change: transform, opacity;
55+
56+
video {
57+
display: block;
58+
width: 100%;
59+
height: 100%;
60+
background: #000;
61+
}
62+
}
63+
64+
.phx-video-fullscreen-close {
65+
position: absolute;
66+
top: 14px;
67+
right: 14px;
68+
width: 36px;
69+
height: 36px;
70+
border: 0;
71+
border-radius: 50%;
72+
background: rgba(0, 0, 0, 0.55);
73+
color: #fff;
74+
font-size: 16px;
75+
line-height: 1;
76+
cursor: pointer;
77+
display: flex;
78+
align-items: center;
79+
justify-content: center;
80+
transition: background-color 0.15s ease, transform 0.15s ease;
81+
82+
&:hover {
83+
background: rgba(0, 0, 0, 0.8);
84+
transform: scale(1.05);
85+
}
86+
}
87+
}

src/styles/brackets.less

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
@import "Extn-Terminal.less";
5555
@import "UserProfile.less";
5656
@import "phoenix-pro.less";
57+
@import "VideoPlayer.less";
5758

5859
/* Overall layout */
5960

src/view/VideoPlayer.js

Lines changed: 144 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,20 @@
1919
// @INCLUDE_IN_API_DOCS
2020

2121
/**
22-
* Tiny shared HTML5 `<video>` widget. Wraps a single `<video>` element in a
23-
* div so callers can drop a configured player into their UI without
24-
* re-deriving sensible defaults each time. Defaults (controls + muted +
25-
* playsinline + preload="metadata") are tuned for inline product videos —
26-
* the kind of "watch this short clip" surface where the user has not yet
27-
* signalled intent to play.
22+
* Tiny shared HTML5 `<video>` widget. Two entry points:
23+
*
24+
* createPlayer(options) — returns a configured `<video>` wrapper the
25+
* caller can drop anywhere in their UI.
26+
*
27+
* renderFullScreenPlayer(srcElement, options) — opens a viewport-
28+
* covering overlay with a large auto-playing player that
29+
* expands out of `srcElement` (genie-style) and contracts back
30+
* on close. Useful when an inline thumbnail should expand into
31+
* a focused fullscreen view on click.
2832
*/
2933
define(function (require, exports, module) {
3034

35+
const Strings = require("strings");
3136

3237
/**
3338
* Build a `<video>` element wrapped in a div with sensible Phoenix
@@ -94,5 +99,137 @@ define(function (require, exports, module) {
9499
return $wrap;
95100
}
96101

97-
exports.createPlayer = createPlayer;
102+
/**
103+
* Open a viewport-covering overlay with a large autoplaying video that
104+
* expands out of `srcElement` (Mac-dock-genie style) and contracts
105+
* back to it on close. Click on the dimmed backdrop, the close (×)
106+
* button, or pressing Escape closes the overlay.
107+
*
108+
* Defaults: muted, autoplay, controls, preload="auto" (so the bytes
109+
* stream while the open animation runs and the user can hit play
110+
* straight away). Override via `options`.
111+
*
112+
* @param {HTMLElement|jQuery} srcElement Element the lightbox should
113+
* expand from / contract back to. Used only for the source rect;
114+
* not modified.
115+
* @param {Object} options See createPlayer's options;
116+
* additionally honours all the same player flags. `src` required.
117+
* @returns {{ close: Function }} Handle exposing a programmatic close.
118+
*/
119+
function renderFullScreenPlayer(srcElement, options) {
120+
options = options || {};
121+
if (!options.src) {
122+
throw new Error("VideoPlayer.renderFullScreenPlayer: options.src is required");
123+
}
124+
125+
const srcEl = srcElement && srcElement.jquery ? srcElement[0] : srcElement;
126+
const originRect = srcEl ? srcEl.getBoundingClientRect() : null;
127+
128+
const $overlay = $('<div class="phx-video-fullscreen-overlay"></div>');
129+
const $player = createPlayer({
130+
src: options.src,
131+
poster: options.poster,
132+
// Fullscreen defaults — overridable via options.
133+
muted: options.muted !== false,
134+
controls: options.controls !== false,
135+
autoplay: options.autoplay !== false,
136+
loop: options.loop === true,
137+
preload: options.preload || "auto",
138+
className: "phx-video-fullscreen-player"
139+
});
140+
const closeLabel = (Strings && Strings.CLOSE) || "Close";
141+
const $closeBtn = $(
142+
'<button class="phx-video-fullscreen-close" type="button">' +
143+
'<i class="fa-solid fa-xmark"></i>' +
144+
'</button>'
145+
).attr("title", closeLabel).attr("aria-label", closeLabel);
146+
147+
const ANIM_OPEN_MS = 280;
148+
const ANIM_CLOSE_MS = 220;
149+
let isClosing = false;
150+
151+
// Compute the transform that snaps the centered final-position
152+
// player back over the origin rect — used for both the initial
153+
// collapsed state and the close animation.
154+
function transformToOrigin() {
155+
if (!originRect) { return null; }
156+
const r = $player[0].getBoundingClientRect();
157+
if (!r.width || !r.height) { return null; }
158+
const dx = (originRect.left + originRect.width / 2) -
159+
(r.left + r.width / 2);
160+
const dy = (originRect.top + originRect.height / 2) -
161+
(r.top + r.height / 2);
162+
const scale = Math.min(
163+
originRect.width / r.width,
164+
originRect.height / r.height
165+
);
166+
return "translate(" + dx + "px," + dy + "px) scale(" + scale + ")";
167+
}
168+
169+
function close() {
170+
if (isClosing) { return; }
171+
isClosing = true;
172+
$(document).off("keydown.phxVideoFullscreen");
173+
const collapseT = transformToOrigin();
174+
if (collapseT) {
175+
$player.css({
176+
transition: "transform " + ANIM_CLOSE_MS +
177+
"ms cubic-bezier(0.4, 0, 1, 1), opacity " +
178+
ANIM_CLOSE_MS + "ms ease",
179+
transform: collapseT,
180+
opacity: 0
181+
});
182+
$closeBtn.css({
183+
transition: "opacity " + (ANIM_CLOSE_MS - 40) + "ms ease",
184+
opacity: 0
185+
});
186+
$overlay.css({
187+
transition: "background-color " + ANIM_CLOSE_MS + "ms ease",
188+
backgroundColor: "rgba(0,0,0,0)"
189+
});
190+
setTimeout(function () { $overlay.remove(); }, ANIM_CLOSE_MS + 20);
191+
} else {
192+
$overlay.remove();
193+
}
194+
}
195+
196+
$overlay.on("click", close);
197+
$player.on("click", function (e) { e.stopPropagation(); });
198+
$closeBtn.on("click", function (e) { e.stopPropagation(); close(); });
199+
$(document).on("keydown.phxVideoFullscreen", function (e) {
200+
if (e.key === "Escape") { close(); }
201+
});
202+
203+
$overlay.append($player).append($closeBtn);
204+
$("body").append($overlay);
205+
206+
// Two-frame FLIP open animation: lay out at the natural centered
207+
// position so we can measure the destination rect, snap visually
208+
// back to the origin with a transform, force a paint, then animate
209+
// to identity. Result: the lightbox appears to expand out of the
210+
// source element.
211+
const startT = transformToOrigin();
212+
if (startT) {
213+
$player.css({
214+
transform: startT,
215+
opacity: 0.6,
216+
transition: "none"
217+
});
218+
// Force reflow so the next style write transitions instead
219+
// of collapsing into a single paint.
220+
void $player[0].offsetWidth;
221+
$player.css({
222+
transition: "transform " + ANIM_OPEN_MS +
223+
"ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity " +
224+
(ANIM_OPEN_MS - 80) + "ms ease",
225+
transform: "translate(0px, 0px) scale(1)",
226+
opacity: 1
227+
});
228+
}
229+
230+
return { close: close };
231+
}
232+
233+
exports.createPlayer = createPlayer;
234+
exports.renderFullScreenPlayer = renderFullScreenPlayer;
98235
});

0 commit comments

Comments
 (0)