diff --git a/.env.example b/.env.example index f11f398..3d0722b 100644 --- a/.env.example +++ b/.env.example @@ -3,8 +3,9 @@ PRIVATE_DATABASE_URL="postgres://root:mysecretpassword@localhost:5432/local" PRIVATE_BETTER_AUTH_SECRET=mysecretpassword # Google OAuth -GOOGLE_CLIENT_ID=your_google_client_id_here -GOOGLE_CLIENT_SECRET=your_google_client_secret_here +PRIVATE_GOOGLE_CLIENT_ID=your_google_client_id_here +PRIVATE_GOOGLE_CLIENT_SECRET=your_google_client_secret_here # AI Generation (OpenRouter) OPENROUTER_API_KEY=your_openrouter_api_key_here + diff --git a/Dockerfile b/Dockerfile index 885ca9f..c32ae70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM node:20-alpine AS builder +FROM node:20-slim AS builder WORKDIR /app @@ -15,20 +15,35 @@ RUN pnpm install --frozen-lockfile # Copy source code COPY . . -# Prepare the app -RUN pnpm run prepare - # Build the app RUN pnpm run build # Production stage -FROM nginx:alpine +# Playwright official image includes all browser dependencies +FROM mcr.microsoft.com/playwright:v1.50.0-jammy + +WORKDIR /app + +# Install FFmpeg +RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/* + +# Copy built application +COPY --from=builder /app/build ./build +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml +COPY --from=builder /app/pnpm-workspace.yaml ./pnpm-workspace.yaml + +# Install only production dependencies +RUN npm install -g pnpm && pnpm install --prod --frozen-lockfile -# Copy built static files to nginx -COPY --from=builder /app/build /usr/share/nginx/html +# Environment variables +ENV NODE_ENV=production +ENV PORT=3000 +# Use 0.0.0.0 to allow access from outside the container +ENV HOST=0.0.0.0 # Expose port -EXPOSE 80 +EXPOSE 3000 -# Start nginx -CMD ["nginx", "-g", "daemon off;"] +# Start the Node.js server +CMD ["node", "build/index.js"] diff --git a/docker-compose.yml b/docker-compose.yml index 5ec5392..cf4726d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,12 @@ services: - db: - image: postgres - restart: always + app: + build: . ports: - - 5432:5432 + - '3000:3000' environment: - POSTGRES_USER: root - POSTGRES_PASSWORD: mysecretpassword - POSTGRES_DB: local - volumes: - - pgdata:/var/lib/postgresql/data -volumes: - pgdata: + - PUBLIC_BASE_URL=${PUBLIC_BASE_URL} + - PRIVATE_DATABASE_URL=${PRIVATE_DATABASE_URL} + - PRIVATE_BETTER_AUTH_SECRET=${PRIVATE_BETTER_AUTH_SECRET} + - PRIVATE_GOOGLE_CLIENT_ID=${PRIVATE_GOOGLE_CLIENT_ID} + - PRIVATE_GOOGLE_CLIENT_SECRET=${PRIVATE_GOOGLE_CLIENT_SECRET} + - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} diff --git a/package.json b/package.json index bfd6373..0a04c25 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/typography": "^0.5.18", "@tailwindcss/vite": "^4.1.13", + "@types/fluent-ffmpeg": "^2.1.28", "@types/node": "^25.0.10", "@vitest/browser": "^3.2.4", "bits-ui": "^2.15.4", @@ -42,6 +43,7 @@ "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.14.0", + "fluent-ffmpeg": "^2.1.3", "globals": "^17.1.0", "mdsvex": "^0.12.6", "paneforge": "^1.0.2", @@ -68,17 +70,15 @@ "@openrouter/ai-sdk-provider": "^2.0.2", "@resvg/resvg-js": "^2.6.2", "@sveltejs/adapter-static": "^3.0.10", - "@types/dompurify": "^3.0.5", "@vercel/mcp-adapter": "^1.0.0", "ai": "^6.0.49", "better-auth": "^1.3.27", "bezier-easing": "^2.1.0", "dompurify": "^3.3.1", - "lucide-svelte": "^0.563.0", "mediabunny": "^1.30.1", "nanoid": "^5.1.6", "postgres": "^3.4.7", - "runed": "^0.34.0", + "runed": "^0.37.1", "schema-dts": "^1.1.5", "svelte-component-to-image": "^2.0.5", "svelte-sonner": "^1.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef4383d..71ec047 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,9 +23,6 @@ importers: '@sveltejs/adapter-static': specifier: ^3.0.10 version: 3.0.10(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))) - '@types/dompurify': - specifier: ^3.0.5 - version: 3.2.0 '@vercel/mcp-adapter': specifier: ^1.0.0 version: 1.0.0(@modelcontextprotocol/sdk@1.25.2(hono@4.11.7)(zod@4.3.6)) @@ -41,9 +38,6 @@ importers: dompurify: specifier: ^3.3.1 version: 3.3.1 - lucide-svelte: - specifier: ^0.563.0 - version: 0.563.0(svelte@5.48.2) mediabunny: specifier: ^1.30.1 version: 1.30.1 @@ -54,8 +48,8 @@ importers: specifier: ^3.4.7 version: 3.4.7 runed: - specifier: ^0.34.0 - version: 0.34.0(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) + specifier: ^0.37.1 + version: 0.37.1(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(zod@4.3.6) schema-dts: specifier: ^1.1.5 version: 1.1.5 @@ -102,6 +96,9 @@ importers: '@tailwindcss/vite': specifier: ^4.1.13 version: 4.1.14(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) + '@types/fluent-ffmpeg': + specifier: ^2.1.28 + version: 2.1.28 '@types/node': specifier: ^25.0.10 version: 25.0.10 @@ -129,6 +126,9 @@ importers: eslint-plugin-svelte: specifier: ^3.14.0 version: 3.14.0(eslint@9.39.2(jiti@2.6.1))(svelte@5.48.2) + fluent-ffmpeg: + specifier: ^2.1.3 + version: 2.1.3 globals: specifier: ^17.1.0 version: 17.1.0 @@ -1211,13 +1211,12 @@ packages: '@types/dom-webcodecs@0.1.13': resolution: {integrity: sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==} - '@types/dompurify@3.2.0': - resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} - deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/fluent-ffmpeg@2.1.28': + resolution: {integrity: sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1430,6 +1429,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + async@0.2.10: + resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -2029,6 +2031,11 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + fluent-ffmpeg@2.1.3: + resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} + engines: {node: '>=18'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -2337,11 +2344,6 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lucide-svelte@0.563.0: - resolution: {integrity: sha512-pjZKw7TpQcamfQrx7YdbOHgmrcNeKiGGMD0tKZQaVktwSsbqw28CsKc2Q97ttwjytiCWkJyOa8ij2Q+Og0nPfQ==} - peerDependencies: - svelte: ^3 || ^4 || ^5.0.0-next.42 - lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -2755,8 +2757,8 @@ packages: peerDependencies: svelte: ^5.7.0 - runed@0.34.0: - resolution: {integrity: sha512-hdDCoxWCuOCa7HnuU2ihu2tXuAOacNXtvTDDZ02km+rguHZBtglzAoo3dVYtssZjFsooY9xawvYX9HmDJqaPTA==} + runed@0.35.1: + resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} peerDependencies: '@sveltejs/kit': ^2.21.0 svelte: ^5.7.0 @@ -2764,14 +2766,17 @@ packages: '@sveltejs/kit': optional: true - runed@0.35.1: - resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} + runed@0.37.1: + resolution: {integrity: sha512-MeFY73xBW8IueWBm012nNFIGy19WUGPLtknavyUPMpnyt350M47PhGSGrGoSLbidwn+Zlt/O0cp8/OZE3LASWA==} peerDependencies: '@sveltejs/kit': ^2.21.0 svelte: ^5.7.0 + zod: ^4.1.0 peerDependenciesMeta: '@sveltejs/kit': optional: true + zod: + optional: true sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} @@ -3221,6 +3226,10 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -4177,12 +4186,12 @@ snapshots: '@types/dom-webcodecs@0.1.13': {} - '@types/dompurify@3.2.0': - dependencies: - dompurify: 3.3.1 - '@types/estree@1.0.8': {} + '@types/fluent-ffmpeg@2.1.28': + dependencies: + '@types/node': 25.0.10 + '@types/json-schema@7.0.15': {} '@types/mdast@4.0.4': @@ -4432,6 +4441,8 @@ snapshots: assertion-error@2.0.1: {} + async@0.2.10: {} + axobject-query@4.1.0: {} balanced-match@1.0.2: {} @@ -5004,6 +5015,11 @@ snapshots: flatted@3.3.3: {} + fluent-ffmpeg@2.1.3: + dependencies: + async: 0.2.10 + which: 1.3.1 + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -5250,10 +5266,6 @@ snapshots: loupe@3.2.1: {} - lucide-svelte@0.563.0(svelte@5.48.2): - dependencies: - svelte: 5.48.2 - lz-string@1.5.0: {} magic-string@0.30.19: @@ -5593,7 +5605,7 @@ snapshots: esm-env: 1.2.2 svelte: 5.48.2 - runed@0.34.0(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): + runed@0.35.1(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): dependencies: dequal: 2.0.3 esm-env: 1.2.2 @@ -5602,7 +5614,7 @@ snapshots: optionalDependencies: '@sveltejs/kit': 2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) - runed@0.35.1(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): + runed@0.37.1(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(zod@4.3.6): dependencies: dequal: 2.0.3 esm-env: 1.2.2 @@ -5610,6 +5622,7 @@ snapshots: svelte: 5.48.2 optionalDependencies: '@sveltejs/kit': 2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) + zod: 4.3.6 sade@1.8.1: dependencies: @@ -6078,6 +6091,10 @@ snapshots: whatwg-mimetype@4.0.0: {} + which@1.3.1: + dependencies: + isexe: 2.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/src/app.d.ts b/src/app.d.ts index c77bfc5..6bfcec2 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -3,6 +3,17 @@ import type { Session, User } from 'better-auth'; // for information about these interfaces +interface DevMotionAPI { + ready: Promise; + seek: (time: number) => void; + getConfig: () => { + width: number; + height: number; + duration: number; + fps: number; + }; +} + declare global { namespace App { // interface Error {} @@ -14,6 +25,10 @@ declare global { // interface PageState {} // interface Platform {} } + + interface Window { + __DEVMOTION__?: DevMotionAPI; + } } declare module 'svelte/elements' { diff --git a/src/lib/components/ai/ai-chat.svelte b/src/lib/components/ai/ai-chat.svelte index 1120837..2ca7aeb 100644 --- a/src/lib/components/ai/ai-chat.svelte +++ b/src/lib/components/ai/ai-chat.svelte @@ -1,7 +1,7 @@ diff --git a/src/lib/components/editor/canvas/canvas-controls.svelte b/src/lib/components/editor/canvas/canvas-controls.svelte index b8c5515..d708946 100644 --- a/src/lib/components/editor/canvas/canvas-controls.svelte +++ b/src/lib/components/editor/canvas/canvas-controls.svelte @@ -1,6 +1,6 @@ diff --git a/src/lib/components/ui/radio-group/index.ts b/src/lib/components/ui/radio-group/index.ts new file mode 100644 index 0000000..919676b --- /dev/null +++ b/src/lib/components/ui/radio-group/index.ts @@ -0,0 +1,10 @@ +import Root from './radio-group.svelte'; +import Item from './radio-group-item.svelte'; + +export { + Root, + Item, + // + Root as RadioGroup, + Item as RadioGroupItem +}; diff --git a/src/lib/components/ui/radio-group/radio-group-item.svelte b/src/lib/components/ui/radio-group/radio-group-item.svelte new file mode 100644 index 0000000..fe4bc4b --- /dev/null +++ b/src/lib/components/ui/radio-group/radio-group-item.svelte @@ -0,0 +1,31 @@ + + + + {#snippet children({ checked })} +
+ {#if checked} + + {/if} +
+ {/snippet} +
diff --git a/src/lib/components/ui/radio-group/radio-group.svelte b/src/lib/components/ui/radio-group/radio-group.svelte new file mode 100644 index 0000000..ee6a890 --- /dev/null +++ b/src/lib/components/ui/radio-group/radio-group.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/layers/components/BrowserLayer.svelte b/src/lib/layers/components/BrowserLayer.svelte index 74fe985..875bebd 100644 --- a/src/lib/layers/components/BrowserLayer.svelte +++ b/src/lib/layers/components/BrowserLayer.svelte @@ -1,7 +1,7 @@ diff --git a/src/routes/render/[id]/+page.server.ts b/src/routes/render/[id]/+page.server.ts new file mode 100644 index 0000000..3e8c089 --- /dev/null +++ b/src/routes/render/[id]/+page.server.ts @@ -0,0 +1,27 @@ +import { db } from '$lib/server/db'; +import { project } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { error } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { validateRenderToken } from '$lib/server/render-token'; + +export const load: PageServerLoad = async ({ params, url }) => { + const token = url.searchParams.get('token'); + + // Validate render token (internal use only) + if (!token || !validateRenderToken(token, params.id)) { + error(403, 'Invalid or missing render token'); + } + + const result = await db.query.project.findFirst({ + where: eq(project.id, params.id) + }); + + if (!result) { + error(404, 'Project not found'); + } + + return { + project: result.data + }; +}; diff --git a/src/routes/render/[id]/+page.svelte b/src/routes/render/[id]/+page.svelte new file mode 100644 index 0000000..00caf8a --- /dev/null +++ b/src/routes/render/[id]/+page.svelte @@ -0,0 +1,122 @@ + + + + Render - {project.name} + + + + +
+ +
+ {#each project.layers as layer (layer.id)} + {@const { transform, style, customProps } = getLayerRenderData(layer)} + {@const component = getLayerComponent(layer.type)} + + + {/each} +
+
+ +