Merge branch 'OpenCut-app:main' into feat/media-panel-filter

This commit is contained in:
George
2025-06-24 20:06:08 +08:00
committed by GitHub
2 changed files with 76 additions and 45 deletions

View File

@ -524,17 +524,24 @@ export function Timeline() {
onClick={toggle} onClick={toggle}
className="mr-2" className="mr-2"
> >
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />} {isPlaying ? (
<Pause className="h-4 w-4" />
) : (
<Play className="h-4 w-4" />
)}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{isPlaying ? "Pause (Space)" : "Play (Space)"}</TooltipContent> <TooltipContent>
{isPlaying ? "Pause (Space)" : "Play (Space)"}
</TooltipContent>
</Tooltip> </Tooltip>
<div className="w-px h-6 bg-border mx-1" /> <div className="w-px h-6 bg-border mx-1" />
{/* Time Display */} {/* Time Display */}
<div className="text-xs text-muted-foreground font-mono px-2"> <div className="text-xs text-muted-foreground font-mono px-2">
{Math.floor(currentTime * 10) / 10}s / {Math.floor(duration * 10) / 10}s {Math.floor(currentTime * 10) / 10}s /{" "}
{Math.floor(duration * 10) / 10}s
</div> </div>
<div className="w-px h-6 bg-border mx-1" /> <div className="w-px h-6 bg-border mx-1" />
@ -606,7 +613,11 @@ export function Timeline() {
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant="text" size="icon" onClick={handleDuplicateSelected}> <Button
variant="text"
size="icon"
onClick={handleDuplicateSelected}
>
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@ -710,17 +721,19 @@ export function Timeline() {
return ( return (
<div <div
key={i} key={i}
className={`absolute top-0 bottom-0 ${isMainMarker className={`absolute top-0 bottom-0 ${
? "border-l border-muted-foreground/40" isMainMarker
: "border-l border-muted-foreground/20" ? "border-l border-muted-foreground/40"
}`} : "border-l border-muted-foreground/20"
}`}
style={{ left: `${time * 50 * zoomLevel}px` }} style={{ left: `${time * 50 * zoomLevel}px` }}
> >
<span <span
className={`absolute top-1 left-1 text-xs ${isMainMarker className={`absolute top-1 left-1 text-xs ${
? "text-muted-foreground font-medium" isMainMarker
: "text-muted-foreground/70" ? "text-muted-foreground font-medium"
}`} : "text-muted-foreground/70"
}`}
> >
{(() => { {(() => {
const formatTime = (seconds: number) => { const formatTime = (seconds: number) => {
@ -762,18 +775,8 @@ export function Timeline() {
{/* Tracks Area */} {/* Tracks Area */}
<div className="flex-1 flex overflow-hidden"> <div className="flex-1 flex overflow-hidden">
{/* Track Labels */} {/* Track Labels */}
<div className="w-48 flex-shrink-0 border-r bg-background overflow-y-auto"> {tracks.length > 0 && (
{tracks.length === 0 ? ( <div className="w-48 flex-shrink-0 border-r bg-background overflow-y-auto">
<div className="flex flex-col items-center justify-center h-full py-8 text-center px-4">
<div className="w-12 h-12 rounded-full bg-muted/30 flex items-center justify-center mb-3">
<SplitSquareHorizontal className="h-6 w-6 text-muted-foreground" />
</div>
<p className="text-sm text-muted-foreground">No tracks</p>
<p className="text-xs text-muted-foreground/70 mt-1">
Drop media to create tracks
</p>
</div>
) : (
<div className="flex flex-col"> <div className="flex flex-col">
{tracks.map((track) => ( {tracks.map((track) => (
<div <div
@ -782,7 +785,7 @@ export function Timeline() {
onContextMenu={(e) => { onContextMenu={(e) => {
e.preventDefault(); e.preventDefault();
setContextMenu({ setContextMenu({
type: 'track', type: "track",
trackId: track.id, trackId: track.id,
x: e.clientX, x: e.clientX,
y: e.clientY, y: e.clientY,
@ -795,8 +798,8 @@ export function Timeline() {
track.type === "video" track.type === "video"
? "bg-blue-500" ? "bg-blue-500"
: track.type === "audio" : track.type === "audio"
? "bg-green-500" ? "bg-green-500"
: "bg-purple-500" : "bg-purple-500"
}`} }`}
/> />
<span className="ml-2 text-sm font-medium truncate"> <span className="ml-2 text-sm font-medium truncate">
@ -811,12 +814,16 @@ export function Timeline() {
</div> </div>
))} ))}
</div> </div>
)} </div>
</div> )}
{/* Timeline Tracks Content */} {/* Timeline Tracks Content */}
<div className="flex-1 relative overflow-hidden"> <div className="flex-1 relative overflow-hidden">
<div className="w-full h-[600px] overflow-hidden flex" ref={timelineRef} style={{ position: 'relative' }}> <div
className="w-full h-[600px] overflow-hidden flex"
ref={timelineRef}
style={{ position: "relative" }}
>
{/* Timeline grid and clips area (with left margin for sidebar) */} {/* Timeline grid and clips area (with left margin for sidebar) */}
<div <div
className="relative flex-1" className="relative flex-1"
@ -852,7 +859,7 @@ export function Timeline() {
onContextMenu={(e) => { onContextMenu={(e) => {
e.preventDefault(); e.preventDefault();
setContextMenu({ setContextMenu({
type: 'track', type: "track",
trackId: track.id, trackId: track.id,
x: e.clientX, x: e.clientX,
y: e.clientY, y: e.clientY,
@ -893,19 +900,23 @@ export function Timeline() {
style={{ left: contextMenu.x, top: contextMenu.y }} style={{ left: contextMenu.x, top: contextMenu.y }}
onContextMenu={(e) => e.preventDefault()} onContextMenu={(e) => e.preventDefault()}
> >
{contextMenu.type === 'track' ? ( {contextMenu.type === "track" ? (
// Track context menu // Track context menu
<> <>
<button <button
className="flex items-center w-full px-3 py-2 hover:bg-accent hover:text-accent-foreground transition-colors text-left" className="flex items-center w-full px-3 py-2 hover:bg-accent hover:text-accent-foreground transition-colors text-left"
onClick={() => { onClick={() => {
const track = tracks.find(t => t.id === contextMenu.trackId); const track = tracks.find(
(t) => t.id === contextMenu.trackId
);
if (track) toggleTrackMute(track.id); if (track) toggleTrackMute(track.id);
setContextMenu(null); setContextMenu(null);
}} }}
> >
{(() => { {(() => {
const track = tracks.find(t => t.id === contextMenu.trackId); const track = tracks.find(
(t) => t.id === contextMenu.trackId
);
return track?.muted ? ( return track?.muted ? (
<> <>
<Volume2 className="h-4 w-4 mr-2" /> <Volume2 className="h-4 w-4 mr-2" />
@ -939,14 +950,23 @@ export function Timeline() {
className="flex items-center w-full px-3 py-2 hover:bg-accent hover:text-accent-foreground transition-colors text-left" className="flex items-center w-full px-3 py-2 hover:bg-accent hover:text-accent-foreground transition-colors text-left"
onClick={() => { onClick={() => {
if (contextMenu.clipId) { if (contextMenu.clipId) {
const track = tracks.find(t => t.id === contextMenu.trackId); const track = tracks.find(
const clip = track?.clips.find(c => c.id === contextMenu.clipId); (t) => t.id === contextMenu.trackId
);
const clip = track?.clips.find(
(c) => c.id === contextMenu.clipId
);
if (clip && track) { if (clip && track) {
const splitTime = currentTime; const splitTime = currentTime;
const effectiveStart = clip.startTime; const effectiveStart = clip.startTime;
const effectiveEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); const effectiveEnd =
clip.startTime +
(clip.duration - clip.trimStart - clip.trimEnd);
if (splitTime > effectiveStart && splitTime < effectiveEnd) { if (
splitTime > effectiveStart &&
splitTime < effectiveEnd
) {
updateClipTrim( updateClipTrim(
track.id, track.id,
clip.id, clip.id,
@ -958,7 +978,8 @@ export function Timeline() {
name: clip.name + " (split)", name: clip.name + " (split)",
duration: clip.duration, duration: clip.duration,
startTime: splitTime, startTime: splitTime,
trimStart: clip.trimStart + (splitTime - effectiveStart), trimStart:
clip.trimStart + (splitTime - effectiveStart),
trimEnd: clip.trimEnd, trimEnd: clip.trimEnd,
}); });
toast.success("Clip split successfully"); toast.success("Clip split successfully");
@ -977,14 +998,21 @@ export function Timeline() {
className="flex items-center w-full px-3 py-2 hover:bg-accent hover:text-accent-foreground transition-colors text-left" className="flex items-center w-full px-3 py-2 hover:bg-accent hover:text-accent-foreground transition-colors text-left"
onClick={() => { onClick={() => {
if (contextMenu.clipId) { if (contextMenu.clipId) {
const track = tracks.find(t => t.id === contextMenu.trackId); const track = tracks.find(
const clip = track?.clips.find(c => c.id === contextMenu.clipId); (t) => t.id === contextMenu.trackId
);
const clip = track?.clips.find(
(c) => c.id === contextMenu.clipId
);
if (clip && track) { if (clip && track) {
useTimelineStore.getState().addClipToTrack(track.id, { useTimelineStore.getState().addClipToTrack(track.id, {
mediaId: clip.mediaId, mediaId: clip.mediaId,
name: clip.name + " (copy)", name: clip.name + " (copy)",
duration: clip.duration, duration: clip.duration,
startTime: clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd) + 0.1, startTime:
clip.startTime +
(clip.duration - clip.trimStart - clip.trimEnd) +
0.1,
trimStart: clip.trimStart, trimStart: clip.trimStart,
trimEnd: clip.trimEnd, trimEnd: clip.trimEnd,
}); });
@ -1002,7 +1030,10 @@ export function Timeline() {
className="flex items-center w-full px-3 py-2 text-destructive hover:bg-destructive/10 transition-colors text-left" className="flex items-center w-full px-3 py-2 text-destructive hover:bg-destructive/10 transition-colors text-left"
onClick={() => { onClick={() => {
if (contextMenu.clipId) { if (contextMenu.clipId) {
removeClipFromTrack(contextMenu.trackId, contextMenu.clipId); removeClipFromTrack(
contextMenu.trackId,
contextMenu.clipId
);
toast.success("Clip deleted"); toast.success("Clip deleted");
} }
setContextMenu(null); setContextMenu(null);

View File

@ -1,5 +1,5 @@
import { db } from "@/lib/db"; import { db } from "@opencut/db";
import { waitlist } from "@/lib/db/schema"; import { waitlist } from "@opencut/db/schema";
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
export async function getWaitlistCount() { export async function getWaitlistCount() {