@hienlh/ppm 0.18.7 → 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.
Files changed (60) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/assets/skills/ppm/SKILL.md +1 -1
  3. package/assets/skills/ppm/references/cli-reference.md +4 -4
  4. package/assets/skills/ppm/references/http-api.md +2 -1
  5. package/dist/web/assets/{ai-resource-editor-DZQUyrsU.js → ai-resource-editor-BydGrxjb.js} +1 -1
  6. package/dist/web/assets/{audio-preview-GGksv6U8.js → audio-preview-DzBGq9Lk.js} +1 -1
  7. package/dist/web/assets/{chat-tab-Xd2WMjdX.js → chat-tab-pBGQ9SCY.js} +3 -3
  8. package/dist/web/assets/{code-editor-DqXYJWAB.js → code-editor-XviNXdzi.js} +2 -2
  9. package/dist/web/assets/{conflict-editor-Dg_JIcKq.js → conflict-editor-CRConSGG.js} +1 -1
  10. package/dist/web/assets/{csv-preview-DsJ_a2Op.js → csv-preview-BtCGfmrq.js} +1 -1
  11. package/dist/web/assets/{database-viewer-CzjnrnMz.js → database-viewer-2M5sKRqb.js} +1 -1
  12. package/dist/web/assets/{diff-viewer-DUNkO-ll.js → diff-viewer-BMr7vOvZ.js} +1 -1
  13. package/dist/web/assets/{docx-preview-DujpUDkE.js → docx-preview-BpG2kr_-.js} +1 -1
  14. package/dist/web/assets/{explorer-body-DSDJUQs7.js → explorer-body-DXufyj-9.js} +1 -1
  15. package/dist/web/assets/{explorer-window-content-CWYBapkj.js → explorer-window-content-B3krk4G1.js} +1 -1
  16. package/dist/web/assets/{extension-webview-DBTFznQp.js → extension-webview-12M45CGq.js} +1 -1
  17. package/dist/web/assets/{git-log-panel-D8ZQdoYl.js → git-log-panel-Cp4k3tX_.js} +1 -1
  18. package/dist/web/assets/{glide-data-grid-uIjdTT7c.js → glide-data-grid-B0BB0VDl.js} +1 -1
  19. package/dist/web/assets/{group-chat-tab-B3sryFdA.js → group-chat-tab-CcG_NXIS.js} +1 -1
  20. package/dist/web/assets/{image-preview-B0VXSpYu.js → image-preview-3HfuxA0N.js} +1 -1
  21. package/dist/web/assets/{index-DgbOF3xa.js → index-BAVBuKmT.js} +4 -4
  22. package/dist/web/assets/{keybindings-store-FyolpNND.js → keybindings-store-oMJmtpH4.js} +1 -1
  23. package/dist/web/assets/{markdown-renderer-BaBQ6Wm6.js → markdown-renderer-DPcoPjmO.js} +1 -1
  24. package/dist/web/assets/{markdown-renderer-feiuhJ9n.js → markdown-renderer-Dg5HE9Zq.js} +1 -1
  25. package/dist/web/assets/media-url-DlYsHq0S.js +1 -0
  26. package/dist/web/assets/{mobile-explorer-sheet-Zxjl-e4c.js → mobile-explorer-sheet-CEVcU964.js} +1 -1
  27. package/dist/web/assets/{notification-store-DnGpxgkF.js → notification-store-B8AqxwRT.js} +1 -1
  28. package/dist/web/assets/{pdf-preview-xzAA4-kD.js → pdf-preview-BslNoDFw.js} +1 -1
  29. package/dist/web/assets/{postgres-viewer-Dkec6GyK.js → postgres-viewer-BtSXQ3S4.js} +1 -1
  30. package/dist/web/assets/{settings-tab-Bd8e6iqY.js → settings-tab-COSUzGl8.js} +1 -1
  31. package/dist/web/assets/{sql-query-editor-Bz1XrUqI.js → sql-query-editor-CkvpmXyA.js} +1 -1
  32. package/dist/web/assets/{sqlite-viewer-D5-NZw6b.js → sqlite-viewer-CJbP6ruh.js} +1 -1
  33. package/dist/web/assets/{system-monitor-tab-A0jvt5Qw.js → system-monitor-tab-DThAFSd_.js} +1 -1
  34. package/dist/web/assets/{team-member-sheet-5Oaqm8I_.js → team-member-sheet-M83B4znI.js} +2 -2
  35. package/dist/web/assets/{team-member-window-content-DSpZ2zhL.js → team-member-window-content-Cxh-HF2V.js} +1 -1
  36. package/dist/web/assets/{terminal-tab-DXazqejW.js → terminal-tab-gQCs_fyx.js} +1 -1
  37. package/dist/web/assets/{tool-cards-Bgp8iceS.js → tool-cards-BG2fz83z.js} +2 -2
  38. package/dist/web/assets/{use-monaco-theme-CpF3A7JL.js → use-monaco-theme-DaqI7O-a.js} +1 -1
  39. package/dist/web/assets/{use-open-team-member-CUvV02fA.js → use-open-team-member-DKpstvLS.js} +1 -1
  40. package/dist/web/assets/{use-websocket-TnL2I1io.js → use-websocket-KVhDtbwK.js} +1 -1
  41. package/dist/web/assets/video-preview-CVEUDZBj.js +1 -0
  42. package/dist/web/index.html +1 -1
  43. package/dist/web/sw.js +1 -1
  44. package/package.json +1 -1
  45. package/src/server/helpers/media-route-handlers.ts +22 -3
  46. package/src/server/routes/files.ts +5 -2
  47. package/src/server/routes/fs-browse.ts +5 -2
  48. package/src/services/edge-forwarder.ts +191 -191
  49. package/src/services/fs-path-guard.service.ts +17 -1
  50. package/src/services/media-transcode/transcode-stream.ts +27 -5
  51. package/src/services/windows-process-tree.ts +351 -351
  52. package/src/web/components/editor/video-player/video-player.tsx +54 -22
  53. package/src/web/lib/media-url.ts +30 -4
  54. package/.release-run.sh +0 -5
  55. package/dist/web/assets/media-url-CUpN5vI1.js +0 -1
  56. package/dist/web/assets/video-preview-CwtLPELG.js +0 -1
  57. package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
  58. package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
  59. package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
  60. 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
- const resumeRef = useRef(true);
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
- // A detached <video> keeps its network fetch alive until garbage-collected, which
63
- // would leave ffmpeg encoding for nobody. Emptying the source aborts the load now.
64
- // `isConnected` guards StrictMode's simulated unmount, where the element stays in the
65
- // DOM and React would not re-apply the src attribute afterwards.
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
- return () => { if (v && !v.isConnected) { v.pause(); v.removeAttribute("src"); v.load(); } };
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: () => { const v = videoRef.current; if (!v) return; if (v.paused) void v.play().catch(() => {}); else v.pause(); },
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
- if (error) {
117
- return (
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 && !resumeRef.current) v.pause();
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
@@ -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
- /** Live ffmpeg → fragmented-MP4 stream starting at `start` seconds. */
34
- export function transcodeMediaUrl(filePath: string, projectName: string, start = 0): string {
35
- const base = mediaBase("transcode", filePath, projectName);
36
- return withToken(start > 0 ? `${base}&start=${start.toFixed(3)}` : base);
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 {
package/.release-run.sh DELETED
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Temporary runner: invokes the release script with bun on PATH. Deleted after use.
3
- export PATH="$HOME/.bun/bin:$PATH"
4
- cd /c/Users/PC/ppm || exit 1
5
- exec bash scripts/release.sh 0.18.7
@@ -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-DgbOF3xa.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};