diff options
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/app/favicon.ico | bin | 4286 -> 0 bytes | |||
| -rw-r--r-- | apps/web/src/app/layout.tsx | 37 | ||||
| -rw-r--r-- | apps/web/src/app/metadata.ts | 66 | ||||
| -rw-r--r-- | apps/web/src/components/editor/audio-waveform.tsx | 115 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline.tsx | 37 | ||||
| -rw-r--r-- | apps/web/src/components/header.tsx | 2 | ||||
| -rw-r--r-- | apps/web/src/stores/playback-store.ts | 6 |
7 files changed, 217 insertions, 46 deletions
diff --git a/apps/web/src/app/favicon.ico b/apps/web/src/app/favicon.ico Binary files differdeleted file mode 100644 index cb20127b..00000000 --- a/apps/web/src/app/favicon.ico +++ /dev/null diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 5d33100d..ee045647 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,4 +1,3 @@ -import type { Metadata } from "next"; import { Inter } from "next/font/google"; import { ThemeProvider } from "next-themes"; import { Analytics } from "@vercel/analytics/react"; @@ -6,46 +5,14 @@ import Script from "next/script"; import "./globals.css"; import { Toaster } from "../components/ui/sonner"; import { TooltipProvider } from "../components/ui/tooltip"; +import { baseMetaData } from "./metadata"; const inter = Inter({ subsets: ["latin"], variable: "--font-inter", }); -export const metadata: Metadata = { - title: "OpenCut", - description: - "A simple but powerful video editor that gets the job done. In your browser.", - openGraph: { - title: "OpenCut", - description: - "A simple but powerful video editor that gets the job done. In your browser.", - url: "https://opencut.app", - siteName: "OpenCut", - locale: "en_US", - type: "website", - images: [ - { - url: "https://opencut.app/opengraph-image.jpg", - width: 1200, - height: 630, - alt: "OpenCut", - }, - ], - }, - twitter: { - card: "summary_large_image", - title: "OpenCut", - description: - "A simple but powerful video editor that gets the job done. In your browser.", - creator: "@opencutapp", - images: ["/opengraph-image.jpg"], - }, - robots: { - index: true, - follow: true, - }, -}; +export const metadata = baseMetaData; export default function RootLayout({ children, diff --git a/apps/web/src/app/metadata.ts b/apps/web/src/app/metadata.ts new file mode 100644 index 00000000..4e387014 --- /dev/null +++ b/apps/web/src/app/metadata.ts @@ -0,0 +1,66 @@ +import { Metadata } from "next"; + +const title = "OpenCut"; +const description = "A simple but powerful video editor that gets the job done. In your browser."; +const openGraphImageUrl = "https://opencut.app/opengraph-image.jpg"; +const twitterImageUrl = "/opengraph-image.jpg"; + +export const baseMetaData: Metadata = { + title: title, + description: description, + openGraph: { + title: title, + description: description, + url: "https://opencut.app", + siteName: "OpenCut", + locale: "en_US", + type: "website", + images: [ + { + url: openGraphImageUrl, + width: 1200, + height: 630, + alt: "OpenCut", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: title, + description: description, + creator: "@opencutapp", + images: [twitterImageUrl], + }, + robots: { + index: true, + follow: true, + }, + icons: { + icon: [ + { url: "/favicon.ico" }, + { url: "/icons/favicon-16x16.png", sizes: "16x16", type: "image/png" }, + { url: "/icons/favicon-32x32.png", sizes: "32x32", type: "image/png" }, + { url: "/icons/favicon-96x96.png", sizes: "96x96", type: "image/png" }, + ], + apple: [ + { url: "/icons/apple-icon-57x57.png", sizes: "57x57", type: "image/png" }, + { url: "/icons/apple-icon-60x60.png", sizes: "60x60", type: "image/png" }, + { url: "/icons/apple-icon-72x72.png", sizes: "72x72", type: "image/png" }, + { url: "/icons/apple-icon-76x76.png", sizes: "76x76", type: "image/png" }, + { url: "/icons/apple-icon-114x114.png", sizes: "114x114", type: "image/png" }, + { url: "/icons/apple-icon-120x120.png", sizes: "120x120", type: "image/png" }, + { url: "/icons/apple-icon-144x144.png", sizes: "144x144", type: "image/png" }, + { url: "/icons/apple-icon-152x152.png", sizes: "152x152", type: "image/png" }, + { url: "/icons/apple-icon-180x180.png", sizes: "180x180", type: "image/png" }, + ], + shortcut: ["/favicon.ico"] + }, + appleWebApp: { + capable: true, + title: title, + }, + manifest: "/manifest.json", + other: { + "msapplication-config": "/browserconfig.xml" + } +}; diff --git a/apps/web/src/components/editor/audio-waveform.tsx b/apps/web/src/components/editor/audio-waveform.tsx new file mode 100644 index 00000000..75a5ae06 --- /dev/null +++ b/apps/web/src/components/editor/audio-waveform.tsx @@ -0,0 +1,115 @@ +import React, { useEffect, useRef, useState } from 'react'; +import WaveSurfer from 'wavesurfer.js'; + +interface AudioWaveformProps { + audioUrl: string; + height?: number; + className?: string; +} + +const AudioWaveform: React.FC<AudioWaveformProps> = ({ + audioUrl, + height = 32, + className = '' +}) => { + const waveformRef = useRef<HTMLDivElement>(null); + const wavesurfer = useRef<WaveSurfer | null>(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + let mounted = true; + + const initWaveSurfer = async () => { + if (!waveformRef.current || !audioUrl) return; + + try { + // Clean up any existing instance + if (wavesurfer.current) { + try { + wavesurfer.current.destroy(); + } catch (e) { + // Silently ignore destroy errors + } + wavesurfer.current = null; + } + + wavesurfer.current = WaveSurfer.create({ + container: waveformRef.current, + waveColor: 'rgba(255, 255, 255, 0.6)', + progressColor: 'rgba(255, 255, 255, 0.9)', + cursorColor: 'transparent', + barWidth: 2, + barGap: 1, + height: height, + normalize: true, + interact: false, + }); + + // Event listeners + wavesurfer.current.on('ready', () => { + if (mounted) { + setIsLoading(false); + setError(false); + } + }); + + wavesurfer.current.on('error', (err) => { + console.error('WaveSurfer error:', err); + if (mounted) { + setError(true); + setIsLoading(false); + } + }); + + await wavesurfer.current.load(audioUrl); + + } catch (err) { + console.error('Failed to initialize WaveSurfer:', err); + if (mounted) { + setError(true); + setIsLoading(false); + } + } + }; + + initWaveSurfer(); + + return () => { + mounted = false; + if (wavesurfer.current) { + try { + wavesurfer.current.destroy(); + } catch (e) { + // Silently ignore destroy errors + } + wavesurfer.current = null; + } + }; + }, [audioUrl, height]); + + if (error) { + return ( + <div className={`flex items-center justify-center ${className}`} style={{ height }}> + <span className="text-xs text-foreground/60">Audio unavailable</span> + </div> + ); + } + + return ( + <div className={`relative ${className}`}> + {isLoading && ( + <div className="absolute inset-0 flex items-center justify-center"> + <span className="text-xs text-foreground/60">Loading...</span> + </div> + )} + <div + ref={waveformRef} + className={`w-full transition-opacity duration-200 ${isLoading ? 'opacity-0' : 'opacity-100'}`} + style={{ height }} + /> + </div> + ); +}; + +export default AudioWaveform;
\ No newline at end of file diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index ce0ffa30..c1b8f3e8 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -35,6 +35,7 @@ import { SelectTrigger,
SelectValue,
} from "../ui/select";
+import AudioWaveform from "./audio-waveform";
export function Timeline() {
// Timeline shows all tracks (video, audio, effects) and their clips.
@@ -566,15 +567,6 @@ export function Timeline() { onMouseLeave={() => setIsInTimeline(false)}
onWheel={handleWheel}
>
- {/* Show overlay when dragging media over the timeline */}
- {isDragOver && (
- <div className="absolute inset-0 z-20 flex items-center justify-center pointer-events-none backdrop-blur-lg">
- <div>
- Drop media here to add to timeline
- </div>
- </div>
- )}
-
{/* Toolbar */}
<div className="border-b flex items-center px-2 py-1 gap-1">
<TooltipProvider delayDuration={500}>
@@ -953,6 +945,17 @@ export function Timeline() { )}
</>
)}
+ {isDragOver && (
+ <div
+ className="absolute left-0 right-0 border-2 border-dashed border-accent flex items-center justify-center text-muted-foreground"
+ style={{
+ top: `${tracks.length * 60}px`,
+ height: "60px",
+ }}
+ >
+ <div>Drop media here to add a new track</div>
+ </div>
+ )}
</div>
</div>
</div>
@@ -1626,7 +1629,21 @@ function TimelineTrackContent({ );
}
- // Fallback for audio or videos without thumbnails
+ if (mediaItem.type === "audio") {
+ return (
+ <div className="w-full h-full flex items-center gap-2">
+ <div className="flex-1 min-w-0">
+ <AudioWaveform
+ audioUrl={mediaItem.url}
+ height={24}
+ className="w-full"
+ />
+ </div>
+ </div>
+ );
+ }
+
+ // Fallback for videos without thumbnails
return (
<span className="text-xs text-foreground/80 truncate">{clip.name}</span>
);
diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index d99a4fc5..74ba9359 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -50,7 +50,7 @@ export function Header() { ) : (
<Link href="https://github.com/OpenCut-app/OpenCut" target="_blank">
<Button size="sm" className="text-sm ml-4">
- GitHub
+ GitHub {star}+
<ArrowRight className="h-4 w-4" />
</Button>
</Link>
diff --git a/apps/web/src/stores/playback-store.ts b/apps/web/src/stores/playback-store.ts index 0127edff..0cc01cc1 100644 --- a/apps/web/src/stores/playback-store.ts +++ b/apps/web/src/stores/playback-store.ts @@ -21,7 +21,13 @@ const startTimer = (store: any) => { const newTime = state.currentTime + delta * state.speed; if (newTime >= state.duration) { + // When video completes, pause and reset playhead to start state.pause(); + state.setCurrentTime(0); + // Notify video elements to sync with reset + window.dispatchEvent( + new CustomEvent("playback-seek", { detail: { time: 0 } }) + ); } else { state.setCurrentTime(newTime); // Notify video elements to sync |
