-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(desktop): add timeline slider to crop modal for frame-accurate cropping #1451
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
59b3998
fd35e3e
a1d2ac0
f1a6d66
3127103
62110cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,3 +50,4 @@ tauri.windows.conf.json | |
| # Cursor | ||
| .cursor | ||
| .env*.local | ||
| .docs/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,16 @@ | ||
| // @refresh reload | ||
| import { mount, StartClient } from "@solidjs/start/client"; | ||
| import { type } from "@tauri-apps/plugin-os"; | ||
|
|
||
| document.documentElement.classList.add(`platform-${type()}`); | ||
| mount(() => <StartClient />, document.getElementById("app")!); | ||
| async function initApp() { | ||
| try { | ||
| const { type } = await import("@tauri-apps/plugin-os"); | ||
| const osType = type(); | ||
| document.documentElement.classList.add(`platform-${osType}`); | ||
| } catch (error) { | ||
| console.error("Failed to get OS type:", error); | ||
| } | ||
|
|
||
| mount(() => <StartClient />, document.getElementById("app")!); | ||
| } | ||
|
|
||
| initApp(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,7 +44,15 @@ import { ExportDialog } from "./ExportDialog"; | |
| import { Header } from "./Header"; | ||
| import { PlayerContent } from "./Player"; | ||
| import { Timeline } from "./Timeline"; | ||
| import { Dialog, DialogContent, EditorButton, Input, Subfield } from "./ui"; | ||
| import { | ||
| Dialog, | ||
| DialogContent, | ||
| EditorButton, | ||
| Input, | ||
| Slider, | ||
| Subfield, | ||
| } from "./ui"; | ||
| import { formatTime } from "./utils"; | ||
|
|
||
| const DEFAULT_TIMELINE_HEIGHT = 260; | ||
| const MIN_PLAYER_CONTENT_HEIGHT = 320; | ||
|
|
@@ -414,13 +422,61 @@ function Dialogs() { | |
| })()} | ||
| > | ||
| {(dialog) => { | ||
| const { setProject: setState, editorInstance } = | ||
| useEditorContext(); | ||
| const { | ||
| setProject: setState, | ||
| editorInstance, | ||
| editorState, | ||
| totalDuration, | ||
| project, | ||
| } = useEditorContext(); | ||
| const display = editorInstance.recordings.segments[0].display; | ||
|
|
||
| let cropperRef: CropperRef | undefined; | ||
| let videoRef: HTMLVideoElement | undefined; | ||
| const [crop, setCrop] = createSignal(CROP_ZERO); | ||
| const [aspect, setAspect] = createSignal<Ratio | null>(null); | ||
| const [previewTime, setPreviewTime] = createSignal( | ||
| editorState.playbackTime, | ||
| ); | ||
| const [videoLoaded, setVideoLoaded] = createSignal(false); | ||
|
|
||
| const currentSegment = createMemo(() => { | ||
| const time = previewTime(); | ||
| let elapsed = 0; | ||
| for (const seg of project.timeline?.segments ?? []) { | ||
| const segDuration = (seg.end - seg.start) / seg.timescale; | ||
| if (time < elapsed + segDuration) { | ||
| return { | ||
| index: seg.recordingSegment ?? 0, | ||
| localTime: seg.start / seg.timescale + (time - elapsed), | ||
| }; | ||
| } | ||
| elapsed += segDuration; | ||
| } | ||
| return { index: 0, localTime: 0 }; | ||
| }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Slider at end position shows wrong frameThe |
||
|
|
||
| const videoSrc = createMemo(() => | ||
| convertFileSrc( | ||
| `${editorInstance.path}/content/segments/segment-${currentSegment().index}/display.mp4`, | ||
| ), | ||
| ); | ||
|
|
||
| createEffect( | ||
| on( | ||
| () => currentSegment().index, | ||
| () => { | ||
| setVideoLoaded(false); | ||
| }, | ||
| { defer: true }, | ||
| ), | ||
| ); | ||
|
|
||
| createEffect(() => { | ||
| if (videoRef && videoLoaded()) { | ||
| videoRef.currentTime = currentSegment().localTime; | ||
| } | ||
| }); | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const initialBounds = { | ||
| x: dialog().position.x, | ||
|
|
@@ -582,16 +638,60 @@ function Dialogs() { | |
| allowLightMode={true} | ||
| onContextMenu={(e) => showCropOptionsMenu(e, true)} | ||
| > | ||
| <img | ||
| class="shadow pointer-events-none max-h-[70vh]" | ||
| alt="screenshot" | ||
| src={convertFileSrc( | ||
| `${editorInstance.path}/screenshots/display.jpg`, | ||
| )} | ||
| /> | ||
| <div class="relative"> | ||
| <img | ||
| class="shadow pointer-events-none max-h-[70vh]" | ||
| alt="screenshot" | ||
| src={convertFileSrc( | ||
| `${editorInstance.path}/screenshots/display.jpg`, | ||
| )} | ||
| style={{ | ||
| opacity: videoLoaded() ? 0 : 1, | ||
| transition: "opacity 150ms ease-out", | ||
| }} | ||
| /> | ||
| <video | ||
| ref={videoRef} | ||
| src={videoSrc()} | ||
| class="absolute inset-0 w-full h-full object-contain shadow pointer-events-none" | ||
| preload="auto" | ||
| muted | ||
| onLoadedData={() => setVideoLoaded(true)} | ||
| onError={() => setVideoLoaded(false)} | ||
| style={{ | ||
| opacity: videoLoaded() ? 1 : 0, | ||
| transition: "opacity 150ms ease-out", | ||
| }} | ||
| /> | ||
| <Show when={!videoLoaded()}> | ||
| <div class="absolute bottom-2 right-2 flex items-center gap-1.5 bg-gray-500/80 rounded-full px-2 py-1"> | ||
| <IconLucideLoaderCircle class="size-3 text-white animate-spin" /> | ||
| <span class="text-[10px] text-white font-medium"> | ||
| Loading | ||
| </span> | ||
| </div> | ||
| </Show> | ||
| </div> | ||
| </Cropper> | ||
| </div> | ||
| </div> | ||
| <div class="flex items-center gap-3 mt-4 px-2"> | ||
| <span class="text-gray-11 text-sm tabular-nums min-w-[3rem]"> | ||
| {formatTime(previewTime())} | ||
| </span> | ||
| <Slider | ||
| class="flex-1" | ||
| minValue={0} | ||
| maxValue={totalDuration()} | ||
| step={1 / FPS} | ||
| value={[previewTime()]} | ||
| onChange={([v]) => setPreviewTime(v)} | ||
| aria-label="Video timeline" | ||
| /> | ||
| <span class="text-gray-11 text-sm tabular-nums min-w-[3rem] text-right"> | ||
| {formatTime(totalDuration())} | ||
| </span> | ||
| </div> | ||
| </Dialog.Content> | ||
| <Dialog.Footer> | ||
| <Button | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: CapSoftware/Cap
Length of output: 1862
🏁 Script executed:
Repository: CapSoftware/Cap
Length of output: 2006
🏁 Script executed:
Repository: CapSoftware/Cap
Length of output: 1107
🏁 Script executed:
Repository: CapSoftware/Cap
Length of output: 2610
🏁 Script executed:
Repository: CapSoftware/Cap
Length of output: 265
🏁 Script executed:
Repository: CapSoftware/Cap
Length of output: 3350
🏁 Script executed:
Repository: CapSoftware/Cap
Length of output: 4244
Confirm that mandatory microphone and camera permissions at startup align with product goals.
The Setup window requires users to grant all four permissions (screen recording, accessibility, microphone, and camera) before accessing the main application. The "Continue" button is disabled until every permission shows as granted or not needed. Users cannot proceed without granting all permissions, even if they only intend to use screen recording features.
While the PR indicates this expansion is intentional, verify with product/design that forcing microphone and camera grants at initial onboarding is the desired UX pattern. This creates a hard gate on features that users may not need, potentially causing drop-off during onboarding. If these permissions are truly optional features, consider deferring their requests until users attempt to use those specific features.
🤖 Prompt for AI Agents