summaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/app/auth/login/page.tsx98
-rw-r--r--apps/web/src/app/auth/signup/page.tsx108
-rw-r--r--apps/web/src/lib/auth.ts2
-rw-r--r--apps/web/src/lib/ffmpeg-utils.ts197
-rw-r--r--apps/web/src/lib/media-processing.ts7
5 files changed, 408 insertions, 4 deletions
diff --git a/apps/web/src/app/auth/login/page.tsx b/apps/web/src/app/auth/login/page.tsx
new file mode 100644
index 00000000..5eadb85c
--- /dev/null
+++ b/apps/web/src/app/auth/login/page.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import { useSearchParams, useRouter } from "next/navigation";
+import { authClient } from "@/lib/auth-client";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Suspense, useState } from "react";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import Link from "next/link";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+
+function LoginForm() {
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const redirectUrl = searchParams.get("redirect");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [error, setError] = useState<string | null>(null);
+
+ const handleLogin = async () => {
+ setError(null);
+ const { error } = await authClient.signIn.email({
+ email,
+ password,
+ });
+
+ if (error) {
+ setError(error.message || "An unexpected error occurred.");
+ return;
+ }
+
+ router.push(redirectUrl || "/");
+ };
+
+ return (
+ <div className="flex flex-col space-y-4">
+ {error && (
+ <Alert variant="destructive">
+ <AlertTitle>Error</AlertTitle>
+ <AlertDescription>{error}</AlertDescription>
+ </Alert>
+ )}
+ <div className="space-y-2">
+ <Label htmlFor="email">Email</Label>
+ <Input
+ id="email"
+ type="email"
+ placeholder="m@example.com"
+ value={email}
+ onChange={(e) => setEmail(e.target.value)}
+ />
+ </div>
+ <div className="space-y-2">
+ <Label htmlFor="password">Password</Label>
+ <Input
+ id="password"
+ type="password"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ />
+ </div>
+ <Button onClick={handleLogin}>Login</Button>
+ </div>
+ );
+}
+
+export default function LoginPage() {
+ return (
+ <div className="flex h-screen items-center justify-center">
+ <Card className="w-[350px]">
+ <CardHeader>
+ <CardTitle>Login</CardTitle>
+ <CardDescription>
+ Enter your email and password to login.
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <Suspense fallback={<div>Loading...</div>}>
+ <LoginForm />
+ </Suspense>
+ <div className="mt-4 text-center text-sm">
+ Don't have an account?{" "}
+ <Link href="/auth/signup" className="underline">
+ Sign up
+ </Link>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ );
+}
diff --git a/apps/web/src/app/auth/signup/page.tsx b/apps/web/src/app/auth/signup/page.tsx
new file mode 100644
index 00000000..f328851c
--- /dev/null
+++ b/apps/web/src/app/auth/signup/page.tsx
@@ -0,0 +1,108 @@
+"use client";
+
+import { useSearchParams, useRouter } from "next/navigation";
+import { authClient } from "@/lib/auth-client";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Suspense, useState } from "react";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import Link from "next/link";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+
+function SignUpForm() {
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const redirectUrl = searchParams.get("redirect");
+ const [name, setName] = useState("");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [error, setError] = useState<string | null>(null);
+
+ const handleSignUp = async () => {
+ setError(null);
+ const { error } = await authClient.signUp.email({
+ name,
+ email,
+ password,
+ });
+
+ if (error) {
+ setError(error.message || "An unexpected error occurred.");
+ return;
+ }
+
+ router.push(redirectUrl || "/");
+ };
+
+ return (
+ <div className="flex flex-col space-y-4">
+ {error && (
+ <Alert variant="destructive">
+ <AlertTitle>Error</AlertTitle>
+ <AlertDescription>{error}</AlertDescription>
+ </Alert>
+ )}
+ <div className="space-y-2">
+ <Label htmlFor="name">Name</Label>
+ <Input
+ id="name"
+ type="text"
+ placeholder="John Doe"
+ value={name}
+ onChange={(e) => setName(e.target.value)}
+ />
+ </div>
+ <div className="space-y-2">
+ <Label htmlFor="email">Email</Label>
+ <Input
+ id="email"
+ type="email"
+ placeholder="m@example.com"
+ value={email}
+ onChange={(e) => setEmail(e.target.value)}
+ />
+ </div>
+ <div className="space-y-2">
+ <Label htmlFor="password">Password</Label>
+ <Input
+ id="password"
+ type="password"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ />
+ </div>
+ <Button onClick={handleSignUp}>Sign Up</Button>
+ </div>
+ );
+}
+
+export default function SignUpPage() {
+ return (
+ <div className="flex h-screen items-center justify-center">
+ <Card className="w-[350px]">
+ <CardHeader>
+ <CardTitle>Sign Up</CardTitle>
+ <CardDescription>Create an account to get started.</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <Suspense fallback={<div>Loading...</div>}>
+ <SignUpForm />
+ </Suspense>
+ <div className="mt-4 text-center text-sm">
+ Already have an account?{" "}
+ <Link href="/auth/login" className="underline">
+ Login
+ </Link>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ );
+}
diff --git a/apps/web/src/lib/auth.ts b/apps/web/src/lib/auth.ts
index d381e6a1..4cfaa6f2 100644
--- a/apps/web/src/lib/auth.ts
+++ b/apps/web/src/lib/auth.ts
@@ -14,7 +14,7 @@ export const auth = betterAuth({
},
},
emailAndPassword: {
- enabled: false,
+ enabled: true,
},
appName: "OpenCut",
trustedOrigins: ["http://localhost:3000"],
diff --git a/apps/web/src/lib/ffmpeg-utils.ts b/apps/web/src/lib/ffmpeg-utils.ts
new file mode 100644
index 00000000..56085a21
--- /dev/null
+++ b/apps/web/src/lib/ffmpeg-utils.ts
@@ -0,0 +1,197 @@
+import { FFmpeg } from '@ffmpeg/ffmpeg';
+import { toBlobURL } from '@ffmpeg/util';
+
+let ffmpeg: FFmpeg | null = null;
+
+export const initFFmpeg = async (): Promise<FFmpeg> => {
+ if (ffmpeg) return ffmpeg;
+
+ ffmpeg = new FFmpeg();
+
+ // Use locally hosted files instead of CDN
+ const baseURL = '/ffmpeg';
+
+ await ffmpeg.load({
+ coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
+ wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
+ });
+
+ return ffmpeg;
+};
+
+export const generateThumbnail = async (
+ videoFile: File,
+ timeInSeconds: number = 1
+): Promise<string> => {
+ const ffmpeg = await initFFmpeg();
+
+ const inputName = 'input.mp4';
+ const outputName = 'thumbnail.jpg';
+
+ // Write input file
+ await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
+
+ // Generate thumbnail at specific time
+ await ffmpeg.exec([
+ '-i', inputName,
+ '-ss', timeInSeconds.toString(),
+ '-vframes', '1',
+ '-vf', 'scale=320:240',
+ '-q:v', '2',
+ outputName
+ ]);
+
+ // Read output file
+ const data = await ffmpeg.readFile(outputName);
+ const blob = new Blob([data], { type: 'image/jpeg' });
+
+ // Cleanup
+ await ffmpeg.deleteFile(inputName);
+ await ffmpeg.deleteFile(outputName);
+
+ return URL.createObjectURL(blob);
+};
+
+export const trimVideo = async (
+ videoFile: File,
+ startTime: number,
+ endTime: number,
+ onProgress?: (progress: number) => void
+): Promise<Blob> => {
+ const ffmpeg = await initFFmpeg();
+
+ const inputName = 'input.mp4';
+ const outputName = 'output.mp4';
+
+ // Set up progress callback
+ if (onProgress) {
+ ffmpeg.on('progress', ({ progress }) => {
+ onProgress(progress * 100);
+ });
+ }
+
+ // Write input file
+ await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
+
+ const duration = endTime - startTime;
+
+ // Trim video
+ await ffmpeg.exec([
+ '-i', inputName,
+ '-ss', startTime.toString(),
+ '-t', duration.toString(),
+ '-c', 'copy', // Use stream copy for faster processing
+ outputName
+ ]);
+
+ // Read output file
+ const data = await ffmpeg.readFile(outputName);
+ const blob = new Blob([data], { type: 'video/mp4' });
+
+ // Cleanup
+ await ffmpeg.deleteFile(inputName);
+ await ffmpeg.deleteFile(outputName);
+
+ return blob;
+};
+
+export const getVideoInfo = async (videoFile: File): Promise<{
+ duration: number;
+ width: number;
+ height: number;
+ fps: number;
+}> => {
+ const ffmpeg = await initFFmpeg();
+
+ const inputName = 'input.mp4';
+
+ // Write input file
+ await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
+
+ // Get video info
+ await ffmpeg.exec(['-i', inputName, '-f', 'null', '-']);
+
+ // Note: In a real implementation, you'd parse the FFmpeg output
+ // For now, we'll return default values and enhance this later
+
+ // Cleanup
+ await ffmpeg.deleteFile(inputName);
+
+ return {
+ duration: 10, // Placeholder - would parse from FFmpeg output
+ width: 1920, // Placeholder
+ height: 1080, // Placeholder
+ fps: 30 // Placeholder
+ };
+};
+
+export const convertToWebM = async (
+ videoFile: File,
+ onProgress?: (progress: number) => void
+): Promise<Blob> => {
+ const ffmpeg = await initFFmpeg();
+
+ const inputName = 'input.mp4';
+ const outputName = 'output.webm';
+
+ // Set up progress callback
+ if (onProgress) {
+ ffmpeg.on('progress', ({ progress }) => {
+ onProgress(progress * 100);
+ });
+ }
+
+ // Write input file
+ await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
+
+ // Convert to WebM
+ await ffmpeg.exec([
+ '-i', inputName,
+ '-c:v', 'libvpx-vp9',
+ '-crf', '30',
+ '-b:v', '0',
+ '-c:a', 'libopus',
+ outputName
+ ]);
+
+ // Read output file
+ const data = await ffmpeg.readFile(outputName);
+ const blob = new Blob([data], { type: 'video/webm' });
+
+ // Cleanup
+ await ffmpeg.deleteFile(inputName);
+ await ffmpeg.deleteFile(outputName);
+
+ return blob;
+};
+
+export const extractAudio = async (
+ videoFile: File,
+ format: 'mp3' | 'wav' = 'mp3'
+): Promise<Blob> => {
+ const ffmpeg = await initFFmpeg();
+
+ const inputName = 'input.mp4';
+ const outputName = `output.${format}`;
+
+ // Write input file
+ await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
+
+ // Extract audio
+ await ffmpeg.exec([
+ '-i', inputName,
+ '-vn', // Disable video
+ '-acodec', format === 'mp3' ? 'libmp3lame' : 'pcm_s16le',
+ outputName
+ ]);
+
+ // Read output file
+ const data = await ffmpeg.readFile(outputName);
+ const blob = new Blob([data], { type: `audio/${format}` });
+
+ // Cleanup
+ await ffmpeg.deleteFile(inputName);
+ await ffmpeg.deleteFile(outputName);
+
+ return blob;
+}; \ No newline at end of file
diff --git a/apps/web/src/lib/media-processing.ts b/apps/web/src/lib/media-processing.ts
index c51dde55..d241f411 100644
--- a/apps/web/src/lib/media-processing.ts
+++ b/apps/web/src/lib/media-processing.ts
@@ -6,6 +6,7 @@ import {
getImageAspectRatio,
type MediaItem,
} from "@/stores/media-store";
+// import { generateThumbnail, getVideoInfo } from "./ffmpeg-utils"; // Temporarily disabled
export interface ProcessedMediaItem extends Omit<MediaItem, "id"> {}
@@ -33,7 +34,7 @@ export async function processMediaFiles(
// Get image aspect ratio
aspectRatio = await getImageAspectRatio(file);
} else if (fileType === "video") {
- // Generate thumbnail and get aspect ratio for videos
+ // Use basic thumbnail generation for now
const videoResult = await generateVideoThumbnail(file);
thumbnailUrl = videoResult.thumbnailUrl;
aspectRatio = videoResult.aspectRatio;
@@ -42,8 +43,8 @@ export async function processMediaFiles(
aspectRatio = 1;
}
- // Get duration for videos and audio
- if (fileType === "video" || fileType === "audio") {
+ // Get duration for videos and audio (if not already set by FFmpeg)
+ if ((fileType === "video" || fileType === "audio") && !duration) {
duration = await getMediaDuration(file);
}