Skip to content

Commit a1d2ac0

Browse files
committed
feat(desktop): add timeline slider to crop modal for frame-accurate cropping
Previously, the crop modal only showed the first frame of the video (a static screenshot). I couldn't crop based on content that appeared later in the recording, and I had to guess the crop and go back to the main timeline to check. This change adds a timeline slider to the crop modal, allowing users to scrub through the entire video and select any frame as their cropping reference. ## What's New - **Timeline slider** below the crop preview with current time and total duration - **Instant video scrubbing** using native <video> element with hardware-accelerated seeking - **Preload strategy** where video metadata preloads on editor open, and full video preloads on Crop button hover/focus for instant display - **Loading indicator** that shows a "Loading" pill while video buffers - **Smooth crossfade** with a 150ms transition from screenshot to video - **Screenshot fallback** that gracefully degrades if video fails to load ## Technical Notes - Uses raw source video (display.mp4) for scrubbing, not the rendered output with effects. This is intentional as crop applies to the background layer, and effects render on top. - Video is positioned absolute with object-contain to prevent layout shift while maintaining aspect ratio. ## Files Changed - [apps/desktop/src/routes/editor/Editor.tsx](cci:7://file:///Users/MAC/Desktop/Desktop%20-%20Poe%27s%20MacBook%20Pro/Engineering/Open%20Source/Cap/apps/desktop/src/routes/editor/Editor.tsx:0:0-0:0) - crop dialogue with video, slider, loading indicator - [apps/desktop/src/routes/editor/Player.tsx](cci:7://file:///Users/MAC/Desktop/Desktop%20-%20Poe%27s%20MacBook%20Pro/Engineering/Open%20Source/Cap/apps/desktop/src/routes/editor/Player.tsx:0:0-0:0) - preload on Crop button hover/focus - [apps/desktop/src/routes/editor/context.ts](cci:7://file:///Users/MAC/Desktop/Desktop%20-%20Poe%27s%20MacBook%20Pro/Engineering/Open%20Source/Cap/apps/desktop/src/routes/editor/context.ts:0:0-0:0) - preload metadata on editor mount - [apps/desktop/src/routes/editor/cropVideoPreloader.ts](cci:7://file:///Users/MAC/Desktop/Desktop%20-%20Poe%27s%20MacBook%20Pro/Engineering/Open%20Source/Cap/apps/desktop/src/routes/editor/cropVideoPreloader.ts:0:0-0:0) - new preload manager module ## How to Test 1. Open a recording in the editor 2. Hover over the "Crop" button (triggers video preload) 3. Click "Crop" to open the modal 4. Verify: - Loading indicator appears briefly (if video not yet cached) - Smooth crossfade from screenshot to video - Slider shows current time (left) and total duration (right) - Dragging slider updates video frame instantly - Crop bounds can be adjusted at any frame - Clicking "Save" persists the crop correctly
1 parent fd35e3e commit a1d2ac0

File tree

8 files changed

+377
-48
lines changed

8 files changed

+377
-48
lines changed

apps/desktop/src/App.tsx

Lines changed: 120 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,55 @@ const MainPage = lazy(() => import("./routes/(window-chrome)/(main)"));
2323
const NewMainPage = lazy(() => import("./routes/(window-chrome)/new-main"));
2424
const SetupPage = lazy(() => import("./routes/(window-chrome)/setup"));
2525
const SettingsLayout = lazy(() => import("./routes/(window-chrome)/settings"));
26-
const SettingsGeneralPage = lazy(() => import("./routes/(window-chrome)/settings/general"));
27-
const SettingsRecordingsPage = lazy(() => import("./routes/(window-chrome)/settings/recordings"));
28-
const SettingsScreenshotsPage = lazy(() => import("./routes/(window-chrome)/settings/screenshots"));
29-
const SettingsHotkeysPage = lazy(() => import("./routes/(window-chrome)/settings/hotkeys"));
30-
const SettingsChangelogPage = lazy(() => import("./routes/(window-chrome)/settings/changelog"));
31-
const SettingsFeedbackPage = lazy(() => import("./routes/(window-chrome)/settings/feedback"));
32-
const SettingsExperimentalPage = lazy(() => import("./routes/(window-chrome)/settings/experimental"));
33-
const SettingsLicensePage = lazy(() => import("./routes/(window-chrome)/settings/license"));
34-
const SettingsIntegrationsPage = lazy(() => import("./routes/(window-chrome)/settings/integrations"));
35-
const SettingsS3ConfigPage = lazy(() => import("./routes/(window-chrome)/settings/integrations/s3-config"));
26+
const SettingsGeneralPage = lazy(
27+
() => import("./routes/(window-chrome)/settings/general"),
28+
);
29+
const SettingsRecordingsPage = lazy(
30+
() => import("./routes/(window-chrome)/settings/recordings"),
31+
);
32+
const SettingsScreenshotsPage = lazy(
33+
() => import("./routes/(window-chrome)/settings/screenshots"),
34+
);
35+
const SettingsHotkeysPage = lazy(
36+
() => import("./routes/(window-chrome)/settings/hotkeys"),
37+
);
38+
const SettingsChangelogPage = lazy(
39+
() => import("./routes/(window-chrome)/settings/changelog"),
40+
);
41+
const SettingsFeedbackPage = lazy(
42+
() => import("./routes/(window-chrome)/settings/feedback"),
43+
);
44+
const SettingsExperimentalPage = lazy(
45+
() => import("./routes/(window-chrome)/settings/experimental"),
46+
);
47+
const SettingsLicensePage = lazy(
48+
() => import("./routes/(window-chrome)/settings/license"),
49+
);
50+
const SettingsIntegrationsPage = lazy(
51+
() => import("./routes/(window-chrome)/settings/integrations"),
52+
);
53+
const SettingsS3ConfigPage = lazy(
54+
() => import("./routes/(window-chrome)/settings/integrations/s3-config"),
55+
);
3656
const UpgradePage = lazy(() => import("./routes/(window-chrome)/upgrade"));
3757
const UpdatePage = lazy(() => import("./routes/(window-chrome)/update"));
3858
const CameraPage = lazy(() => import("./routes/camera"));
3959
const CaptureAreaPage = lazy(() => import("./routes/capture-area"));
4060
const DebugPage = lazy(() => import("./routes/debug"));
4161
const EditorPage = lazy(() => import("./routes/editor"));
42-
const InProgressRecordingPage = lazy(() => import("./routes/in-progress-recording"));
62+
const InProgressRecordingPage = lazy(
63+
() => import("./routes/in-progress-recording"),
64+
);
4365
const ModeSelectPage = lazy(() => import("./routes/mode-select"));
4466
const NotificationsPage = lazy(() => import("./routes/notifications"));
4567
const RecordingsOverlayPage = lazy(() => import("./routes/recordings-overlay"));
4668
const ScreenshotEditorPage = lazy(() => import("./routes/screenshot-editor"));
47-
const TargetSelectOverlayPage = lazy(() => import("./routes/target-select-overlay"));
48-
const WindowCaptureOccluderPage = lazy(() => import("./routes/window-capture-occluder"));
69+
const TargetSelectOverlayPage = lazy(
70+
() => import("./routes/target-select-overlay"),
71+
);
72+
const WindowCaptureOccluderPage = lazy(
73+
() => import("./routes/window-capture-occluder"),
74+
);
4975

5076
const queryClient = new QueryClient({
5177
defaultOptions: {
@@ -64,7 +90,18 @@ const queryClient = new QueryClient({
6490

6591
export default function App() {
6692
// #region agent log
67-
fetch('http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.tsx:App',message:'App component rendering',data:{pathname:location.pathname},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'C'})}).catch(()=>{});
93+
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
94+
method: "POST",
95+
headers: { "Content-Type": "application/json" },
96+
body: JSON.stringify({
97+
location: "App.tsx:App",
98+
message: "App component rendering",
99+
data: { pathname: location.pathname },
100+
timestamp: Date.now(),
101+
sessionId: "debug-session",
102+
hypothesisId: "C",
103+
}),
104+
}).catch(() => {});
68105
// #endregion
69106
return (
70107
<QueryClientProvider client={queryClient}>
@@ -77,14 +114,36 @@ export default function App() {
77114

78115
function Inner() {
79116
// #region agent log
80-
fetch('http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.tsx:Inner',message:'Inner component rendering',data:{pathname:location.pathname},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'C'})}).catch(()=>{});
117+
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
118+
method: "POST",
119+
headers: { "Content-Type": "application/json" },
120+
body: JSON.stringify({
121+
location: "App.tsx:Inner",
122+
message: "Inner component rendering",
123+
data: { pathname: location.pathname },
124+
timestamp: Date.now(),
125+
sessionId: "debug-session",
126+
hypothesisId: "C",
127+
}),
128+
}).catch(() => {});
81129
// #endregion
82130
const currentWindow = getCurrentWebviewWindow();
83131
createThemeListener(currentWindow);
84132

85133
onMount(() => {
86134
// #region agent log
87-
fetch('http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.tsx:Inner:onMount',message:'Inner onMount',data:{pathname:location.pathname},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'C'})}).catch(()=>{});
135+
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
136+
method: "POST",
137+
headers: { "Content-Type": "application/json" },
138+
body: JSON.stringify({
139+
location: "App.tsx:Inner:onMount",
140+
message: "Inner onMount",
141+
data: { pathname: location.pathname },
142+
timestamp: Date.now(),
143+
sessionId: "debug-session",
144+
hypothesisId: "C",
145+
}),
146+
}).catch(() => {});
88147
// #endregion
89148
initAnonymousUser();
90149
});
@@ -116,7 +175,27 @@ function Inner() {
116175

117176
onMount(() => {
118177
// #region agent log
119-
fetch('http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.tsx:Router:root:onMount',message:'Router root onMount',data:{pathname:location.pathname,matchCount:matches().length,autoShowFlags:matches().map(m=>m.route.info?.AUTO_SHOW_WINDOW)},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'C,D'})}).catch(()=>{});
178+
fetch(
179+
"http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23",
180+
{
181+
method: "POST",
182+
headers: { "Content-Type": "application/json" },
183+
body: JSON.stringify({
184+
location: "App.tsx:Router:root:onMount",
185+
message: "Router root onMount",
186+
data: {
187+
pathname: location.pathname,
188+
matchCount: matches().length,
189+
autoShowFlags: matches().map(
190+
(m) => m.route.info?.AUTO_SHOW_WINDOW,
191+
),
192+
},
193+
timestamp: Date.now(),
194+
sessionId: "debug-session",
195+
hypothesisId: "C,D",
196+
}),
197+
},
198+
).catch(() => {});
120199
// #endregion
121200
for (const match of matches()) {
122201
if (match.route.info?.AUTO_SHOW_WINDOW === false) return;
@@ -150,10 +229,19 @@ function Inner() {
150229
<Route path="/hotkeys" component={SettingsHotkeysPage} />
151230
<Route path="/changelog" component={SettingsChangelogPage} />
152231
<Route path="/feedback" component={SettingsFeedbackPage} />
153-
<Route path="/experimental" component={SettingsExperimentalPage} />
232+
<Route
233+
path="/experimental"
234+
component={SettingsExperimentalPage}
235+
/>
154236
<Route path="/license" component={SettingsLicensePage} />
155-
<Route path="/integrations" component={SettingsIntegrationsPage} />
156-
<Route path="/integrations/s3-config" component={SettingsS3ConfigPage} />
237+
<Route
238+
path="/integrations"
239+
component={SettingsIntegrationsPage}
240+
/>
241+
<Route
242+
path="/integrations/s3-config"
243+
component={SettingsS3ConfigPage}
244+
/>
157245
</Route>
158246
<Route path="/upgrade" component={UpgradePage} />
159247
<Route path="/update" component={UpdatePage} />
@@ -162,13 +250,22 @@ function Inner() {
162250
<Route path="/capture-area" component={CaptureAreaPage} />
163251
<Route path="/debug" component={DebugPage} />
164252
<Route path="/editor" component={EditorPage} />
165-
<Route path="/in-progress-recording" component={InProgressRecordingPage} />
253+
<Route
254+
path="/in-progress-recording"
255+
component={InProgressRecordingPage}
256+
/>
166257
<Route path="/mode-select" component={ModeSelectPage} />
167258
<Route path="/notifications" component={NotificationsPage} />
168259
<Route path="/recordings-overlay" component={RecordingsOverlayPage} />
169260
<Route path="/screenshot-editor" component={ScreenshotEditorPage} />
170-
<Route path="/target-select-overlay" component={TargetSelectOverlayPage} />
171-
<Route path="/window-capture-occluder" component={WindowCaptureOccluderPage} />
261+
<Route
262+
path="/target-select-overlay"
263+
component={TargetSelectOverlayPage}
264+
/>
265+
<Route
266+
path="/window-capture-occluder"
267+
component={WindowCaptureOccluderPage}
268+
/>
172269
</Router>
173270
</CapErrorBoundary>
174271
</>

apps/desktop/src/routes/(window-chrome).tsx

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,34 @@ export default function (props: RouteSectionProps) {
1717
let unlistenResize: UnlistenFn | undefined;
1818

1919
// #region agent log
20-
fetch('http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'(window-chrome).tsx:render',message:'WindowChrome component rendering',data:{pathname:location.pathname},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'C'})}).catch(()=>{});
20+
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
21+
method: "POST",
22+
headers: { "Content-Type": "application/json" },
23+
body: JSON.stringify({
24+
location: "(window-chrome).tsx:render",
25+
message: "WindowChrome component rendering",
26+
data: { pathname: location.pathname },
27+
timestamp: Date.now(),
28+
sessionId: "debug-session",
29+
hypothesisId: "C",
30+
}),
31+
}).catch(() => {});
2132
// #endregion
2233

2334
onMount(async () => {
2435
// #region agent log
25-
fetch('http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'(window-chrome).tsx:onMount',message:'WindowChrome onMount',data:{pathname:location.pathname},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'C'})}).catch(()=>{});
36+
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
37+
method: "POST",
38+
headers: { "Content-Type": "application/json" },
39+
body: JSON.stringify({
40+
location: "(window-chrome).tsx:onMount",
41+
message: "WindowChrome onMount",
42+
data: { pathname: location.pathname },
43+
timestamp: Date.now(),
44+
sessionId: "debug-session",
45+
hypothesisId: "C",
46+
}),
47+
}).catch(() => {});
2648
// #endregion
2749
console.log("window chrome mounted");
2850
unlistenResize = await initializeTitlebar();
@@ -94,11 +116,33 @@ function Header() {
94116

95117
function Inner(props: ParentProps) {
96118
// #region agent log
97-
fetch('http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'(window-chrome).tsx:Inner:render',message:'Inner component rendering',data:{pathname:location.pathname},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'C'})}).catch(()=>{});
119+
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
120+
method: "POST",
121+
headers: { "Content-Type": "application/json" },
122+
body: JSON.stringify({
123+
location: "(window-chrome).tsx:Inner:render",
124+
message: "Inner component rendering",
125+
data: { pathname: location.pathname },
126+
timestamp: Date.now(),
127+
sessionId: "debug-session",
128+
hypothesisId: "C",
129+
}),
130+
}).catch(() => {});
98131
// #endregion
99132
onMount(() => {
100133
// #region agent log
101-
fetch('http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'(window-chrome).tsx:Inner:onMount',message:'Inner onMount, about to show window',data:{pathname:location.pathname},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'B,C'})}).catch(()=>{});
134+
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
135+
method: "POST",
136+
headers: { "Content-Type": "application/json" },
137+
body: JSON.stringify({
138+
location: "(window-chrome).tsx:Inner:onMount",
139+
message: "Inner onMount, about to show window",
140+
data: { pathname: location.pathname },
141+
timestamp: Date.now(),
142+
sessionId: "debug-session",
143+
hypothesisId: "B,C",
144+
}),
145+
}).catch(() => {});
102146
// #endregion
103147
if (location.pathname !== "/") getCurrentWindow().show();
104148
});

apps/desktop/src/routes/(window-chrome)/setup.tsx

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,60 @@ export default function () {
9595

9696
const handleContinue = () => {
9797
// #region agent log
98-
fetch('http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'setup.tsx:handleContinue',message:'handleContinue called',data:{},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'A'})}).catch(()=>{});
98+
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
99+
method: "POST",
100+
headers: { "Content-Type": "application/json" },
101+
body: JSON.stringify({
102+
location: "setup.tsx:handleContinue",
103+
message: "handleContinue called",
104+
data: {},
105+
timestamp: Date.now(),
106+
sessionId: "debug-session",
107+
hypothesisId: "A",
108+
}),
109+
}).catch(() => {});
99110
// #endregion
100-
commands.showWindow({ Main: { init_target_mode: null } }).then(() => {
101-
// #region agent log
102-
fetch('http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'setup.tsx:handleContinue:then',message:'showWindow resolved, closing setup',data:{},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'A,B'})}).catch(()=>{});
103-
// #endregion
104-
getCurrentWindow().close();
105-
}).catch((err) => {
106-
// #region agent log
107-
fetch('http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'setup.tsx:handleContinue:catch',message:'showWindow failed',data:{error:String(err)},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'A'})}).catch(()=>{});
108-
// #endregion
109-
});
111+
commands
112+
.showWindow({ Main: { init_target_mode: null } })
113+
.then(() => {
114+
// #region agent log
115+
fetch(
116+
"http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23",
117+
{
118+
method: "POST",
119+
headers: { "Content-Type": "application/json" },
120+
body: JSON.stringify({
121+
location: "setup.tsx:handleContinue:then",
122+
message: "showWindow resolved, closing setup",
123+
data: {},
124+
timestamp: Date.now(),
125+
sessionId: "debug-session",
126+
hypothesisId: "A,B",
127+
}),
128+
},
129+
).catch(() => {});
130+
// #endregion
131+
getCurrentWindow().close();
132+
})
133+
.catch((err) => {
134+
// #region agent log
135+
fetch(
136+
"http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23",
137+
{
138+
method: "POST",
139+
headers: { "Content-Type": "application/json" },
140+
body: JSON.stringify({
141+
location: "setup.tsx:handleContinue:catch",
142+
message: "showWindow failed",
143+
data: { error: String(err) },
144+
timestamp: Date.now(),
145+
sessionId: "debug-session",
146+
hypothesisId: "A",
147+
}),
148+
},
149+
).catch(() => {});
150+
// #endregion
151+
});
110152
};
111153

112154
return (

0 commit comments

Comments
 (0)