@hienlh/ppm 0.18.6 → 0.18.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +2 -1
- package/dist/web/assets/{ai-resource-editor-C59ti0ED.js → ai-resource-editor-BydGrxjb.js} +1 -1
- package/dist/web/assets/{audio-preview-GGksv6U8.js → audio-preview-DzBGq9Lk.js} +1 -1
- package/dist/web/assets/{chat-tab-CM5hUNpk.js → chat-tab-pBGQ9SCY.js} +3 -3
- package/dist/web/assets/{code-editor-2yL9lCL1.js → code-editor-XviNXdzi.js} +2 -2
- package/dist/web/assets/{conflict-editor-H1zmUXSQ.js → conflict-editor-CRConSGG.js} +1 -1
- package/dist/web/assets/{csv-preview-Dzzyx_Gv.js → csv-preview-BtCGfmrq.js} +1 -1
- package/dist/web/assets/{database-viewer-CiVA--mM.js → database-viewer-2M5sKRqb.js} +1 -1
- package/dist/web/assets/{diff-viewer-69nl3mqO.js → diff-viewer-BMr7vOvZ.js} +1 -1
- package/dist/web/assets/{docx-preview-CFvopEBf.js → docx-preview-BpG2kr_-.js} +1 -1
- package/dist/web/assets/{explorer-body-DyPXovdU.js → explorer-body-DXufyj-9.js} +1 -1
- package/dist/web/assets/{explorer-window-content-CXFdvs_i.js → explorer-window-content-B3krk4G1.js} +1 -1
- package/dist/web/assets/{extension-webview-DIoKwGV4.js → extension-webview-12M45CGq.js} +1 -1
- package/dist/web/assets/{git-log-panel-Copm8mzy.js → git-log-panel-Cp4k3tX_.js} +1 -1
- package/dist/web/assets/{glide-data-grid-D0L8SZVt.js → glide-data-grid-B0BB0VDl.js} +1 -1
- package/dist/web/assets/{group-chat-tab-D9cazEAI.js → group-chat-tab-CcG_NXIS.js} +1 -1
- package/dist/web/assets/{image-preview-vFeN8o43.js → image-preview-3HfuxA0N.js} +1 -1
- package/dist/web/assets/{index-BewcuJw5.js → index-BAVBuKmT.js} +5 -5
- package/dist/web/assets/{keybindings-store-ruNjhVT3.js → keybindings-store-oMJmtpH4.js} +1 -1
- package/dist/web/assets/{markdown-renderer-B46VJEHH.js → markdown-renderer-DPcoPjmO.js} +1 -1
- package/dist/web/assets/{markdown-renderer-CYiMPviS.js → markdown-renderer-Dg5HE9Zq.js} +1 -1
- package/dist/web/assets/media-url-DlYsHq0S.js +1 -0
- package/dist/web/assets/{mobile-explorer-sheet-BkMZxqs3.js → mobile-explorer-sheet-CEVcU964.js} +1 -1
- package/dist/web/assets/{notification-store-B3IK7PXf.js → notification-store-B8AqxwRT.js} +1 -1
- package/dist/web/assets/{pdf-preview-DU_NRMno.js → pdf-preview-BslNoDFw.js} +1 -1
- package/dist/web/assets/{postgres-viewer-C0h3Zfk9.js → postgres-viewer-BtSXQ3S4.js} +1 -1
- package/dist/web/assets/{settings-tab-Cep8gjxG.js → settings-tab-COSUzGl8.js} +1 -1
- package/dist/web/assets/{sql-query-editor-DJofTC9F.js → sql-query-editor-CkvpmXyA.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-D7bskeyk.js → sqlite-viewer-CJbP6ruh.js} +1 -1
- package/dist/web/assets/{system-monitor-tab-CxgFsQx9.js → system-monitor-tab-DThAFSd_.js} +1 -1
- package/dist/web/assets/{team-member-sheet-Dn5NGkcp.js → team-member-sheet-M83B4znI.js} +2 -2
- package/dist/web/assets/{team-member-window-content-DS9V4trr.js → team-member-window-content-Cxh-HF2V.js} +1 -1
- package/dist/web/assets/{terminal-tab-CMs8AyFJ.js → terminal-tab-gQCs_fyx.js} +1 -1
- package/dist/web/assets/{tool-cards-HHRTIlFv.js → tool-cards-BG2fz83z.js} +2 -2
- package/dist/web/assets/{use-monaco-theme-BLt-n9r1.js → use-monaco-theme-DaqI7O-a.js} +1 -1
- package/dist/web/assets/{use-open-team-member-BwtzTuxf.js → use-open-team-member-DKpstvLS.js} +1 -1
- package/dist/web/assets/{use-websocket-DA3Zwl3Z.js → use-websocket-KVhDtbwK.js} +1 -1
- package/dist/web/assets/video-preview-CVEUDZBj.js +1 -0
- package/dist/web/index.html +1 -1
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/server/helpers/media-route-handlers.ts +22 -3
- package/src/server/routes/files.ts +5 -2
- package/src/server/routes/fs-browse.ts +5 -2
- package/src/services/edge-forwarder.ts +191 -191
- package/src/services/fs-ops/fs-ops-upload.service.ts +17 -1
- package/src/services/fs-path-guard.service.ts +17 -1
- package/src/services/media-transcode/transcode-stream.ts +27 -5
- package/src/services/windows-process-tree.ts +351 -351
- package/src/web/components/editor/video-player/video-player.tsx +54 -22
- package/src/web/components/os-explorer/actions/explorer-actions-upload.ts +6 -0
- package/src/web/lib/media-url.ts +30 -4
- package/dist/web/assets/media-url-CUpN5vI1.js +0 -1
- package/dist/web/assets/video-preview-BZhTYMFX.js +0 -1
- package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.exe +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { Loader2 } from "lucide-react";
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
|
-
import { rawMediaUrl, transcodeMediaUrl } from "@/lib/media-url";
|
|
4
|
+
import { newMediaSessionId, rawMediaUrl, stopTranscode, transcodeMediaUrl } from "@/lib/media-url";
|
|
5
5
|
import { VideoPlayerControls } from "./video-player-controls";
|
|
6
6
|
import { SPEED_STEPS } from "./video-settings-menu";
|
|
7
7
|
import { useVideoKeyboardShortcuts, type VideoPlayerActions } from "./use-video-keyboard-shortcuts";
|
|
@@ -30,7 +30,13 @@ export function VideoPlayer({ filePath, projectName, mode, probeDuration = null,
|
|
|
30
30
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
31
31
|
const stageRef = useRef<HTMLDivElement>(null);
|
|
32
32
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
33
|
-
|
|
33
|
+
// Whether the user wants playback. Read after a transcode restart to decide between
|
|
34
|
+
// resuming and staying paused; derived from user actions, not `video.paused`, because
|
|
35
|
+
// the element reports paused while a fresh stream is still loading.
|
|
36
|
+
const wantPlayRef = useRef(true);
|
|
37
|
+
// One id per mounted player: the server replaces (kills) this player's previous
|
|
38
|
+
// ffmpeg job on every seek and stops it when told the player is gone.
|
|
39
|
+
const [sessionId] = useState(newMediaSessionId);
|
|
34
40
|
|
|
35
41
|
const [start, setStart] = useState(0);
|
|
36
42
|
const [elapsed, setElapsed] = useState(0);
|
|
@@ -47,7 +53,7 @@ export function VideoPlayer({ filePath, projectName, mode, probeDuration = null,
|
|
|
47
53
|
const [error, setError] = useState<string | null>(null);
|
|
48
54
|
|
|
49
55
|
const isTranscode = mode === "transcode";
|
|
50
|
-
const src = isTranscode ? transcodeMediaUrl(filePath, projectName, start) : rawMediaUrl(filePath, projectName);
|
|
56
|
+
const src = isTranscode ? transcodeMediaUrl(filePath, projectName, start, sessionId) : rawMediaUrl(filePath, projectName);
|
|
51
57
|
const duration = isTranscode ? probeDuration : nativeDuration;
|
|
52
58
|
const position = scrub ?? (isTranscode ? start + elapsed : elapsed);
|
|
53
59
|
|
|
@@ -59,14 +65,26 @@ export function VideoPlayer({ filePath, projectName, mode, probeDuration = null,
|
|
|
59
65
|
useEffect(() => { const v = videoRef.current; if (v) { v.volume = volume; v.muted = muted; } }, [volume, muted]);
|
|
60
66
|
useEffect(() => { const v = videoRef.current; if (v) v.playbackRate = speed; }, [speed, src]);
|
|
61
67
|
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
68
|
+
// Leaving the player must stop its ffmpeg job explicitly: a detached <video> keeps its
|
|
69
|
+
// fetch alive until garbage-collected, and a proxy such as Cloudflare Tunnel keeps the
|
|
70
|
+
// origin request open even after the browser has dropped it. Emptying the source aborts
|
|
71
|
+
// the local load; the DELETE tells the server. `isConnected` guards StrictMode's
|
|
72
|
+
// simulated unmount, where the element stays in the DOM and React would not re-apply
|
|
73
|
+
// the src attribute afterwards. `pagehide` covers reload/close, where cleanups never run.
|
|
66
74
|
useEffect(() => {
|
|
67
75
|
const v = videoRef.current;
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
const stop = () => { if (isTranscode) stopTranscode(filePath, projectName, sessionId); };
|
|
77
|
+
window.addEventListener("pagehide", stop);
|
|
78
|
+
return () => {
|
|
79
|
+
window.removeEventListener("pagehide", stop);
|
|
80
|
+
if (v && !v.isConnected) {
|
|
81
|
+
v.pause();
|
|
82
|
+
v.removeAttribute("src");
|
|
83
|
+
v.load();
|
|
84
|
+
stop();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}, [filePath, projectName, isTranscode, sessionId]);
|
|
70
88
|
|
|
71
89
|
useEffect(() => {
|
|
72
90
|
const onFs = () => setFullscreen(document.fullscreenElement === rootRef.current);
|
|
@@ -90,12 +108,17 @@ export function VideoPlayer({ filePath, projectName, mode, probeDuration = null,
|
|
|
90
108
|
if (!isTranscode) { if (v) v.currentTime = clamped; return; }
|
|
91
109
|
// Restarting ffmpeg for a sub-second nudge is wasteful.
|
|
92
110
|
if (Math.abs(clamped - (start + (v?.currentTime ?? 0))) < 0.5) return;
|
|
93
|
-
resumeRef.current = !(v?.paused ?? false);
|
|
94
111
|
setStart(clamped);
|
|
95
112
|
}, [duration, isTranscode, start]);
|
|
96
113
|
|
|
97
114
|
const actions = useMemo<VideoPlayerActions>(() => ({
|
|
98
|
-
togglePlay: () => {
|
|
115
|
+
togglePlay: () => {
|
|
116
|
+
const v = videoRef.current;
|
|
117
|
+
if (!v) return;
|
|
118
|
+
wantPlayRef.current = v.paused;
|
|
119
|
+
if (v.paused) void v.play().catch(() => {});
|
|
120
|
+
else v.pause();
|
|
121
|
+
},
|
|
99
122
|
seekBy: (d) => seekTo((isTranscode ? start : 0) + (videoRef.current?.currentTime ?? 0) + d),
|
|
100
123
|
toggleFullscreen: () => {
|
|
101
124
|
if (document.fullscreenElement) void document.exitFullscreen();
|
|
@@ -113,14 +136,8 @@ export function VideoPlayer({ filePath, projectName, mode, probeDuration = null,
|
|
|
113
136
|
const onKeyDown = useVideoKeyboardShortcuts(actions);
|
|
114
137
|
const fitted = fittedMaxSize(transform, box);
|
|
115
138
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<div className="flex flex-col items-center justify-center h-full gap-3 text-text-secondary p-4 text-center">
|
|
119
|
-
<p className="text-sm">{error}</p>
|
|
120
|
-
<Button variant="outline" size="sm" onClick={() => { setError(null); setStart((s) => s + 0.001); }}>Retry</Button>
|
|
121
|
-
</div>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
139
|
+
// Retry re-requests the same position; the nudge makes the URL differ so the element reloads.
|
|
140
|
+
const retry = () => { setError(null); setStart((s) => s + 0.001); };
|
|
124
141
|
|
|
125
142
|
return (
|
|
126
143
|
<div
|
|
@@ -144,10 +161,13 @@ export function VideoPlayer({ filePath, projectName, mode, probeDuration = null,
|
|
|
144
161
|
preload={isTranscode ? "auto" : "metadata"}
|
|
145
162
|
style={{ transform: toCssTransform(transform), maxWidth: fitted.maxWidth || undefined, maxHeight: fitted.maxHeight || undefined }}
|
|
146
163
|
onLoadedMetadata={(e) => {
|
|
164
|
+
// Re-apply user settings: a fresh load (seek restart, retry) resets the element.
|
|
147
165
|
const v = e.currentTarget;
|
|
148
166
|
v.playbackRate = speed;
|
|
167
|
+
v.volume = volume;
|
|
168
|
+
v.muted = muted;
|
|
149
169
|
if (!isTranscode && Number.isFinite(v.duration)) setNativeDuration(v.duration);
|
|
150
|
-
if (isTranscode && !
|
|
170
|
+
if (isTranscode && !wantPlayRef.current) v.pause();
|
|
151
171
|
}}
|
|
152
172
|
onPlay={() => setPlaying(true)}
|
|
153
173
|
onPause={() => setPlaying(false)}
|
|
@@ -155,13 +175,25 @@ export function VideoPlayer({ filePath, projectName, mode, probeDuration = null,
|
|
|
155
175
|
onPlaying={() => setWaiting(false)}
|
|
156
176
|
onCanPlay={() => setWaiting(false)}
|
|
157
177
|
onTimeUpdate={(e) => setElapsed(e.currentTarget.currentTime)}
|
|
158
|
-
onEnded={() => setPlaying(false)}
|
|
178
|
+
onEnded={() => { setPlaying(false); wantPlayRef.current = false; }}
|
|
159
179
|
onError={() => {
|
|
160
180
|
if (!isTranscode) onNativeError?.();
|
|
161
181
|
else setError("Transcoding failed. ffmpeg may have stopped or the file is corrupt.");
|
|
162
182
|
}}
|
|
163
183
|
/>
|
|
164
|
-
{waiting && <Loader2 className="absolute size-8 animate-spin text-white/80 pointer-events-none" />}
|
|
184
|
+
{waiting && !error && <Loader2 className="absolute size-8 animate-spin text-white/80 pointer-events-none" />}
|
|
185
|
+
{/* Overlay rather than a replacement tree: the <video> stays mounted so volume,
|
|
186
|
+
mute and speed survive a retry. */}
|
|
187
|
+
{error && (
|
|
188
|
+
<div
|
|
189
|
+
className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/85 text-text-secondary p-4 text-center"
|
|
190
|
+
onClick={(e) => e.stopPropagation()}
|
|
191
|
+
onDoubleClick={(e) => e.stopPropagation()}
|
|
192
|
+
>
|
|
193
|
+
<p className="text-sm">{error}</p>
|
|
194
|
+
<Button variant="outline" size="sm" className="h-11 px-4" onClick={retry}>Retry</Button>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
165
197
|
</div>
|
|
166
198
|
|
|
167
199
|
<VideoPlayerControls
|
|
@@ -69,6 +69,12 @@ async function uploadResolvingCollisions(
|
|
|
69
69
|
try {
|
|
70
70
|
await fsApi.remove(destination, false);
|
|
71
71
|
} catch (e) {
|
|
72
|
+
// The entry vanished between the collision and the confirmation — nothing to trash, and
|
|
73
|
+
// the upload below is exactly what the user asked for.
|
|
74
|
+
if (e instanceof FsError && e.code === "ENOENT") {
|
|
75
|
+
await uploadFileXhr(destination, entry.file, true, (p) => onProgress(p.loaded), signal);
|
|
76
|
+
return "done";
|
|
77
|
+
}
|
|
72
78
|
if (!(e instanceof FsError) || e.code !== "NO_TRASH") throw e;
|
|
73
79
|
// No trash backend on this host — ask before doing something permanent instead of
|
|
74
80
|
// silently downgrading to it (mirrors `transfer()`'s own NO_TRASH handling).
|
package/src/web/lib/media-url.ts
CHANGED
|
@@ -30,10 +30,36 @@ export function rawMediaUrl(filePath: string, projectName: string): string {
|
|
|
30
30
|
return withToken(mediaBase("raw", filePath, projectName));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Live ffmpeg → fragmented-MP4 stream starting at `start` seconds.
|
|
35
|
+
* `sessionId` identifies the player instance: the server kills that player's previous
|
|
36
|
+
* job before starting this one, so a seek never leaves an orphaned ffmpeg behind —
|
|
37
|
+
* even through proxies (Cloudflare Tunnel) that do not propagate client disconnects.
|
|
38
|
+
*/
|
|
39
|
+
export function transcodeMediaUrl(filePath: string, projectName: string, start = 0, sessionId?: string): string {
|
|
40
|
+
let url = mediaBase("transcode", filePath, projectName);
|
|
41
|
+
if (start > 0) url += `&start=${start.toFixed(3)}`;
|
|
42
|
+
if (sessionId) url += `&sid=${encodeURIComponent(sessionId)}`;
|
|
43
|
+
return withToken(url);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Tell the server the player is gone so its ffmpeg job stops now. `keepalive` lets the
|
|
48
|
+
* request complete even when fired from a page that is unloading.
|
|
49
|
+
*/
|
|
50
|
+
export function stopTranscode(filePath: string, projectName: string, sessionId: string): void {
|
|
51
|
+
const prefix = isExternalPath(filePath) ? "/api/fs" : `${projectUrl(projectName)}/files`;
|
|
52
|
+
const token = getAuthToken();
|
|
53
|
+
void fetch(`${prefix}/transcode?sid=${encodeURIComponent(sessionId)}`, {
|
|
54
|
+
method: "DELETE",
|
|
55
|
+
keepalive: true,
|
|
56
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
57
|
+
}).catch(() => {});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Short random id for one mounted player. */
|
|
61
|
+
export function newMediaSessionId(): string {
|
|
62
|
+
return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
37
63
|
}
|
|
38
64
|
|
|
39
65
|
export interface MediaProbeInfo {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as e,n as t,r as n}from"./api-client-BSB14J4w.js";function r(e){return/^(\/|[A-Za-z]:[/\\])/.test(e)}function i(t,n,i){return`${r(n)?`/api/fs`:`${e(i)}/files`}/${t}?path=${encodeURIComponent(n)}`}function a(e){let t=n();return t?`${e}&token=${encodeURIComponent(t)}`:e}function o(e,t){return a(i(`raw`,e,t))}function s(e,t,n=0){let r=i(`transcode`,e,t);return a(n>0?`${r}&start=${n.toFixed(3)}`:r)}function c(e,n){return t.get(i(`probe`,e,n))}export{o as n,s as r,c as t};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{o as e}from"./rolldown-runtime-FhOqtrmT.js";import{S as t,x as n}from"./vendor-markdown-Ls5AcU91.js";import"./vendor-ui-CaeeFO0B.js";import{t as r}from"./createLucideIcon-D8pyde1A.js";import{t as i}from"./button-B4MWPjWj.js";import{t as a}from"./download-D9UYwY0-.js";import{t as o}from"./file-exclamation-point-a8f3_w7a.js";import"./api-client-BSB14J4w.js";import{Bt as s,Hn as c,Ht as l,Ln as u,Lt as d,On as f,Rt as p,Ut as m,Vn as h,Vt as g,Wn as _,er as v,qt as y,zt as b}from"./index-BewcuJw5.js";import{n as x,r as ee,t as S}from"./media-url-CUpN5vI1.js";import{o as C}from"./can-open-in-ppm-DXGOSUQc.js";var w=r(`flip-horizontal-2`,[[`path`,{d:`m3 7 5 5-5 5V7`,key:`couhi7`}],[`path`,{d:`m21 7-5 5 5 5V7`,key:`6ouia7`}],[`path`,{d:`M12 20v2`,key:`1lh1kg`}],[`path`,{d:`M12 14v2`,key:`8jcxud`}],[`path`,{d:`M12 8v2`,key:`1woqiv`}],[`path`,{d:`M12 2v2`,key:`tus03m`}]]),T=r(`flip-vertical-2`,[[`path`,{d:`m17 3-5 5-5-5h10`,key:`1ftt6x`}],[`path`,{d:`m17 21-5-5-5 5h10`,key:`1m0wmu`}],[`path`,{d:`M4 12H2`,key:`rhcxmi`}],[`path`,{d:`M10 12H8`,key:`s88cx1`}],[`path`,{d:`M16 12h-2`,key:`10asgb`}],[`path`,{d:`M22 12h-2`,key:`14jgyd`}]]),E=r(`maximize`,[[`path`,{d:`M8 3H5a2 2 0 0 0-2 2v3`,key:`1dcmit`}],[`path`,{d:`M21 8V5a2 2 0 0 0-2-2h-3`,key:`1e4gt3`}],[`path`,{d:`M3 16v3a2 2 0 0 0 2 2h3`,key:`wsl5sc`}],[`path`,{d:`M16 21h3a2 2 0 0 0 2-2v-3`,key:`18trek`}]]),D=r(`minimize`,[[`path`,{d:`M8 3v3a2 2 0 0 1-2 2H3`,key:`hohbtr`}],[`path`,{d:`M21 8h-3a2 2 0 0 1-2-2V3`,key:`5jw1f3`}],[`path`,{d:`M3 16h3a2 2 0 0 1 2 2v3`,key:`198tvr`}],[`path`,{d:`M16 21v-3a2 2 0 0 1 2-2h3`,key:`ph8mxp`}]]),O=r(`pause`,[[`rect`,{x:`14`,y:`3`,width:`5`,height:`18`,rx:`1`,key:`kaeet6`}],[`rect`,{x:`5`,y:`3`,width:`5`,height:`18`,rx:`1`,key:`1wsw3u`}]]),k=r(`volume-2`,[[`path`,{d:`M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z`,key:`uqj9uw`}],[`path`,{d:`M16 9a5 5 0 0 1 0 6`,key:`1q6k2b`}],[`path`,{d:`M19.364 18.364a9 9 0 0 0 0-12.728`,key:`ijwkga`}]]),A=r(`volume-1`,[[`path`,{d:`M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z`,key:`uqj9uw`}],[`path`,{d:`M16 9a5 5 0 0 1 0 6`,key:`1q6k2b`}]]),j=r(`volume-x`,[[`path`,{d:`M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z`,key:`uqj9uw`}],[`line`,{x1:`22`,x2:`16`,y1:`9`,y2:`15`,key:`1ewh16`}],[`line`,{x1:`16`,x2:`22`,y1:`9`,y2:`15`,key:`5ykzw1`}]]),M=e(t(),1),N={rotation:0,flipH:!1,flipV:!1};function P(e){return{...e,rotation:(e.rotation+90)%360}}function F(e){return{...e,rotation:(e.rotation+270)%360}}function I(e){return e.rotation===0&&!e.flipH&&!e.flipV}function L(e){let t=[];return e.rotation&&t.push(`rotate(${e.rotation}deg)`),e.flipH&&t.push(`scaleX(-1)`),e.flipV&&t.push(`scaleY(-1)`),t.length?t.join(` `):`none`}function R(e,t){return e.rotation===90||e.rotation===270?{maxWidth:t.height,maxHeight:t.width}:{maxWidth:t.width,maxHeight:t.height}}var z=[[`Space / K`,`Play / pause`],[`← / →`,`Seek 5s`],[`J / L or Shift+← / →`,`Seek 10s`],[`↑ / ↓`,`Volume`],[`M`,`Mute`],[`F`,`Fullscreen`],[`< / >`,`Speed`],[`R / Shift+R`,`Rotate`]];function B(e){return(0,M.useCallback)(t=>{let n=t.target;if(n.tagName===`INPUT`||n.tagName===`TEXTAREA`||n.isContentEditable||t.ctrlKey||t.metaKey||t.altKey)return;let r=!0;switch(t.key){case` `:case`k`:case`K`:e.togglePlay();break;case`ArrowLeft`:e.seekBy(t.shiftKey?-10:-5);break;case`ArrowRight`:e.seekBy(t.shiftKey?10:5);break;case`j`:case`J`:e.seekBy(-10);break;case`l`:case`L`:e.seekBy(10);break;case`ArrowUp`:e.volumeBy(.1);break;case`ArrowDown`:e.volumeBy(-.1);break;case`m`:case`M`:e.toggleMute();break;case`f`:case`F`:e.toggleFullscreen();break;case`<`:case`,`:e.speedStep(-1);break;case`>`:case`.`:e.speedStep(1);break;case`r`:e.rotate(1);break;case`R`:e.rotate(-1);break;default:r=!1}r&&t.preventDefault()},[e])}var V=n(),H=[.25,.5,.75,1,1.25,1.5,2,3];function U({speed:e,onSpeed:t,transform:n,onRotate:r,onFlipH:a,onFlipV:o,onResetTransform:_}){return(0,V.jsxs)(d,{children:[(0,V.jsx)(y,{asChild:!0,children:(0,V.jsxs)(i,{variant:`ghost`,size:`icon`,className:`size-11 shrink-0 relative`,"aria-label":`Playback settings`,children:[(0,V.jsx)(u,{className:`size-5`}),e!==1&&(0,V.jsxs)(`span`,{className:`absolute -top-0.5 -right-0.5 text-[10px] leading-none px-1 rounded bg-primary text-primary-foreground tabular-nums`,children:[e,`x`]})]})}),(0,V.jsxs)(p,{align:`end`,className:`w-60 max-h-[70vh] overflow-y-auto`,children:[(0,V.jsx)(s,{children:`Speed`}),(0,V.jsx)(g,{value:String(e),onValueChange:e=>t(Number(e)),children:(0,V.jsx)(`div`,{className:`grid grid-cols-4 gap-1 px-1 pb-1`,children:H.map(e=>(0,V.jsxs)(l,{value:String(e),className:`justify-center pl-2 min-h-11 text-xs tabular-nums data-[state=checked]:bg-accent [&>span:first-child]:hidden`,children:[e,`x`]},e))})}),(0,V.jsx)(m,{}),(0,V.jsx)(s,{children:`Orientation`}),(0,V.jsxs)(b,{className:`min-h-11`,onSelect:e=>{e.preventDefault(),r(1)},children:[(0,V.jsx)(h,{className:`size-4`}),` Rotate right `,(0,V.jsx)(`span`,{className:`ml-auto text-xs text-text-subtle`,children:`R`})]}),(0,V.jsxs)(b,{className:`min-h-11`,onSelect:e=>{e.preventDefault(),r(-1)},children:[(0,V.jsx)(c,{className:`size-4`}),` Rotate left `,(0,V.jsx)(`span`,{className:`ml-auto text-xs text-text-subtle`,children:`⇧R`})]}),(0,V.jsxs)(b,{className:`min-h-11`,onSelect:e=>{e.preventDefault(),a()},children:[(0,V.jsx)(w,{className:`size-4`}),` Flip horizontal `,n.flipH&&(0,V.jsx)(`span`,{className:`ml-auto text-xs`,children:`✓`})]}),(0,V.jsxs)(b,{className:`min-h-11`,onSelect:e=>{e.preventDefault(),o()},children:[(0,V.jsx)(T,{className:`size-4`}),` Flip vertical `,n.flipV&&(0,V.jsx)(`span`,{className:`ml-auto text-xs`,children:`✓`})]}),!I(n)&&(0,V.jsxs)(b,{className:`min-h-11`,onSelect:_,children:[(0,V.jsx)(f,{className:`size-4`}),` Reset orientation`]}),(0,V.jsx)(m,{}),(0,V.jsx)(s,{children:`Keyboard`}),(0,V.jsx)(`div`,{className:`px-2 pb-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs text-text-secondary`,children:z.map(([e,t])=>(0,V.jsxs)(`div`,{className:`contents`,children:[(0,V.jsx)(`kbd`,{className:`font-mono text-text-subtle whitespace-nowrap`,children:e}),(0,V.jsx)(`span`,{children:t})]},e))})]})]})}function W(e){let t=e.muted||e.volume===0?j:e.volume<.5?A:k,n=t=>e.onSeek(Number(t.target.value));return(0,V.jsxs)(`div`,{className:`flex items-center gap-1 px-2 py-1 bg-surface border-t border-border`,children:[(0,V.jsx)(i,{variant:`ghost`,size:`icon`,className:`size-11 shrink-0`,onClick:e.onTogglePlay,"aria-label":e.playing?`Pause`:`Play`,children:e.playing?(0,V.jsx)(O,{className:`size-5`}):(0,V.jsx)(_,{className:`size-5`})}),(0,V.jsxs)(`span`,{className:`text-xs tabular-nums text-text-secondary shrink-0 min-w-[4.5rem] text-center`,children:[G(e.position),e.duration?` / ${G(e.duration)}`:``]}),e.duration?(0,V.jsx)(`input`,{type:`range`,min:0,max:e.duration,step:.1,value:Math.min(e.position,e.duration),"aria-label":`Seek`,className:`flex-1 h-11 accent-primary cursor-pointer min-w-0`,onChange:t=>e.onScrub(Number(t.target.value)),onPointerUp:n,onKeyUp:n,onTouchEnd:n}):(0,V.jsx)(`div`,{className:`flex-1`}),(0,V.jsxs)(`div`,{className:`flex items-center shrink-0`,children:[(0,V.jsx)(i,{variant:`ghost`,size:`icon`,className:`size-11`,onClick:e.onToggleMute,"aria-label":e.muted?`Unmute`:`Mute`,children:(0,V.jsx)(t,{className:`size-5`})}),(0,V.jsx)(`input`,{type:`range`,min:0,max:1,step:.05,value:e.muted?0:e.volume,"aria-label":`Volume`,className:`hidden sm:block w-20 h-11 accent-primary cursor-pointer`,onChange:t=>e.onVolume(Number(t.target.value))})]}),(0,V.jsx)(U,{speed:e.speed,onSpeed:e.onSpeed,transform:e.transform,onRotate:e.onRotate,onFlipH:e.onFlipH,onFlipV:e.onFlipV,onResetTransform:e.onResetTransform}),(0,V.jsx)(i,{variant:`ghost`,size:`icon`,className:`size-11 shrink-0`,onClick:e.onToggleFullscreen,"aria-label":e.fullscreen?`Exit fullscreen`:`Fullscreen`,children:e.fullscreen?(0,V.jsx)(D,{className:`size-5`}):(0,V.jsx)(E,{className:`size-5`})})]})}function G(e){let t=Math.max(0,Math.floor(Number.isFinite(e)?e:0)),n=Math.floor(t/3600),r=Math.floor(t%3600/60),i=t%60,a=n?String(r).padStart(2,`0`):String(r);return`${n?`${n}:`:``}${a}:${String(i).padStart(2,`0`)}`}function K({filePath:e,projectName:t,mode:n,probeDuration:r=null,onNativeError:a}){let o=(0,M.useRef)(null),s=(0,M.useRef)(null),c=(0,M.useRef)(null),l=(0,M.useRef)(!0),[u,d]=(0,M.useState)(0),[f,p]=(0,M.useState)(0),[m,h]=(0,M.useState)(null),[g,_]=(0,M.useState)(!1),[y,b]=(0,M.useState)(!0),[S,C]=(0,M.useState)(null),[w,T]=(0,M.useState)(1),[E,D]=(0,M.useState)(!1),[O,k]=(0,M.useState)(1),[A,j]=(0,M.useState)(N),[I,z]=(0,M.useState)(!1),[U,G]=(0,M.useState)({width:0,height:0}),[K,q]=(0,M.useState)(null),J=n===`transcode`,Y=J?ee(e,t,u):x(e,t),X=J?r:m,te=S??(J?u+f:f);(0,M.useEffect)(()=>{p(0),b(!0),q(null)},[Y]),(0,M.useEffect)(()=>{d(0),h(null),j(N)},[e,n]),(0,M.useEffect)(()=>{let e=c.current;e&&(e.volume=w,e.muted=E)},[w,E]),(0,M.useEffect)(()=>{let e=c.current;e&&(e.playbackRate=O)},[O,Y]),(0,M.useEffect)(()=>{let e=c.current;return()=>{e&&!e.isConnected&&(e.pause(),e.removeAttribute(`src`),e.load())}},[]),(0,M.useEffect)(()=>{let e=()=>z(document.fullscreenElement===o.current);return document.addEventListener(`fullscreenchange`,e),()=>document.removeEventListener(`fullscreenchange`,e)},[]),(0,M.useEffect)(()=>{let e=s.current;if(!e)return;let t=new ResizeObserver(()=>G({width:e.clientWidth,height:e.clientHeight}));return t.observe(e),()=>t.disconnect()},[]);let Z=(0,M.useCallback)(e=>{let t=c.current,n=Math.max(0,X?Math.min(e,X-.5):e);if(C(null),!J){t&&(t.currentTime=n);return}Math.abs(n-(u+(t?.currentTime??0)))<.5||(l.current=!(t?.paused??!1),d(n))},[X,J,u]),Q=(0,M.useMemo)(()=>({togglePlay:()=>{let e=c.current;e&&(e.paused?e.play().catch(()=>{}):e.pause())},seekBy:e=>Z((J?u:0)+(c.current?.currentTime??0)+e),toggleFullscreen:()=>{document.fullscreenElement?document.exitFullscreen():o.current?.requestFullscreen?.()},toggleMute:()=>D(e=>!e),volumeBy:e=>{D(!1),T(t=>Math.min(1,Math.max(0,Math.round((t+e)*100)/100)))},speedStep:e=>k(t=>{let n=H.indexOf(t);return H[Math.min(H.length-1,Math.max(0,(n===-1?3:n)+e))]??1}),rotate:e=>j(t=>e===1?P(t):F(t))}),[Z,J,u]),ne=B(Q),$=R(A,U);return K?(0,V.jsxs)(`div`,{className:`flex flex-col items-center justify-center h-full gap-3 text-text-secondary p-4 text-center`,children:[(0,V.jsx)(`p`,{className:`text-sm`,children:K}),(0,V.jsx)(i,{variant:`outline`,size:`sm`,onClick:()=>{q(null),d(e=>e+.001)},children:`Retry`})]}):(0,V.jsxs)(`div`,{ref:o,"data-testid":`video-player`,tabIndex:0,onKeyDown:ne,className:`flex flex-col h-full bg-black outline-none focus-visible:ring-1 focus-visible:ring-primary/60`,children:[(0,V.jsxs)(`div`,{ref:s,className:`relative flex-1 min-h-0 flex items-center justify-center overflow-hidden`,onClick:e=>{o.current?.focus(),e.detail===1&&Q.togglePlay()},onDoubleClick:Q.toggleFullscreen,children:[(0,V.jsx)(`video`,{ref:c,src:Y,autoPlay:!0,playsInline:!0,preload:J?`auto`:`metadata`,style:{transform:L(A),maxWidth:$.maxWidth||void 0,maxHeight:$.maxHeight||void 0},onLoadedMetadata:e=>{let t=e.currentTarget;t.playbackRate=O,!J&&Number.isFinite(t.duration)&&h(t.duration),J&&!l.current&&t.pause()},onPlay:()=>_(!0),onPause:()=>_(!1),onWaiting:()=>b(!0),onPlaying:()=>b(!1),onCanPlay:()=>b(!1),onTimeUpdate:e=>p(e.currentTarget.currentTime),onEnded:()=>_(!1),onError:()=>{J?q(`Transcoding failed. ffmpeg may have stopped or the file is corrupt.`):a?.()}}),y&&(0,V.jsx)(v,{className:`absolute size-8 animate-spin text-white/80 pointer-events-none`})]}),(0,V.jsx)(W,{playing:g,position:te,duration:X,volume:w,muted:E,speed:O,transform:A,fullscreen:I,onTogglePlay:Q.togglePlay,onScrub:C,onSeek:Z,onVolume:e=>{T(e),D(e===0)},onToggleMute:Q.toggleMute,onSpeed:k,onRotate:Q.rotate,onFlipH:()=>j(e=>({...e,flipH:!e.flipH})),onFlipV:()=>j(e=>({...e,flipV:!e.flipV})),onResetTransform:()=>j(N),onToggleFullscreen:Q.toggleFullscreen})]})}var q=new Set([`mp4`,`m4v`,`webm`,`ogg`,`ogv`,`mov`]);function J({filePath:e,projectName:t}){let n=C(e),[r,i]=(0,M.useState)(()=>q.has(n)?{kind:`native`}:{kind:`probing`});return(0,M.useEffect)(()=>{i(q.has(C(e))?{kind:`native`}:{kind:`probing`})},[e]),(0,M.useEffect)(()=>{if(r.kind!==`probing`)return;let n=!1;return S(e,t).then(e=>{n||i(e.transcodable?{kind:`transcode`,probe:e}:{kind:`unsupported`,probe:e})}).catch(()=>{n||i({kind:`unsupported`,probe:null})}),()=>{n=!0}},[r.kind,e,t]),r.kind===`probing`?(0,V.jsx)(`div`,{className:`flex items-center justify-center h-full`,children:(0,V.jsx)(v,{className:`size-5 animate-spin text-text-subtle`})}):r.kind===`transcode`?(0,V.jsx)(K,{filePath:e,projectName:t,mode:`transcode`,probeDuration:r.probe.duration}):r.kind===`unsupported`?(0,V.jsx)(Y,{filePath:e,projectName:t,probe:r.probe}):(0,V.jsx)(K,{filePath:e,projectName:t,mode:`native`,onNativeError:()=>i({kind:`probing`})})}function Y({filePath:e,projectName:t,probe:n}){let r=[n?.video?.codec,n?.audio?.codec].filter(Boolean).join(` + `);return(0,V.jsxs)(`div`,{className:`flex flex-col items-center justify-center h-full gap-3 text-text-secondary p-6 text-center`,children:[(0,V.jsx)(o,{className:`size-10 text-text-subtle`}),(0,V.jsxs)(`p`,{className:`text-sm`,children:[`This video cannot be played in the browser`,r?` (${r})`:``,`.`]}),(0,V.jsxs)(`p`,{className:`text-xs text-text-subtle max-w-sm`,children:[`Install `,(0,V.jsx)(`code`,{children:`ffmpeg`}),` on the PPM server (e.g. `,(0,V.jsx)(`code`,{children:`winget install ffmpeg`}),`, `,(0,V.jsx)(`code`,{children:`brew install ffmpeg`}),`,`,` `,(0,V.jsx)(`code`,{children:`apt install ffmpeg`}),`) and restart PPM to transcode it on the fly.`]}),(0,V.jsx)(i,{asChild:!0,variant:`outline`,size:`sm`,className:`h-11 px-4`,children:(0,V.jsxs)(`a`,{href:`${x(e,t)}&download=true`,download:!0,children:[(0,V.jsx)(a,{className:`size-4 mr-2`}),` Download file`]})})]})}export{J as VideoPreview};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|