Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"@ffmpeg/ffmpeg": "^0.12.10",
"@ffmpeg/util": "^0.12.2",
"bun": "^1.3.14",
"clsx": "^2.1.1",
"focus-trap-react": "^12.0.1",
"jszip": "^3.10.1",
Expand All @@ -32,6 +33,7 @@
"@types/node": "^25",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^6.0.2",
"eslint": "^9",
"eslint-config-next": "^15.1.8",
"eslint-plugin-jsx-a11y": "^6.10.2",
Expand Down
Binary file added public/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 53 additions & 0 deletions public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "Reframe — Browser-based Video Editor",
"short_name": "Reframe",
"description": "Free, open-source video editor that runs entirely in your browser. Resize, trim, rotate, and export videos offline.",
"id": "/",
"start_url": "/",
"display": "standalone",
"background_color": "#12100e",
"theme_color": "#e63946",
"orientation": "any",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"screenshots": [
{
"src": "/screenshots/desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide",
"label": "Reframe Desktop Video Editor"
},
{
"src": "/screenshots/mobile.png",
"sizes": "720x1280",
"type": "image/png",
"form_factor": "narrow",
"label": "Reframe Mobile Video Editor"
}
]
}
Binary file added public/screenshots/desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/screenshots/mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 113 additions & 0 deletions public/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
const CACHE_NAME = "reframe-pwa-cache-v1";

const PRECACHE_ASSETS = [
"/",
"/index.html",
"/favicon.svg",
"/manifest.json",
"/robots.txt",
"/sitemap.xml",
"/sounds/export-complete.mp3",
"/screenshots/desktop.png",
"/screenshots/mobile.png"
];

// Install Event: cache static shell assets resiliently
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
// Fetch each asset individually to prevent any 404 error (e.g. dev-mode missing files)
// from blocking the entire service worker installation.
const cachePromises = PRECACHE_ASSETS.map((asset) => {
return fetch(asset)
.then((response) => {
if (response.ok) {
return cache.put(asset, response);
}
})
.catch((err) => {
console.warn(`[SW Install] Skip precaching ${asset} due to:`, err);
});
});
return Promise.all(cachePromises);
}).then(() => {
return self.skipWaiting();
})
);
});

// Activate Event: clean old caches
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
);
}).then(() => self.clients.claim())
);
});

// Fetch Event: apply custom caching strategies
self.addEventListener("fetch", (event) => {
const request = event.request;
if (request.method !== "GET") return;

const url = new URL(request.url);
const isHtml = request.mode === "navigate" || url.pathname.endsWith(".html") || !url.pathname.includes(".");

if (isHtml) {
// Network-First strategy for HTML navigations
event.respondWith(
fetch(request)
.then((response) => {
if (response.status === 200) {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseClone);
});
}
return response;
})
.catch(() => {
return caches.match(request).then((cachedResponse) => {
if (cachedResponse) return cachedResponse;
// Safer static fallback supporting both / and /index.html structures
return caches.match("/").then((res1) => res1 || caches.match("/index.html"));
});
})
);
} else {
// Cache-First strategy for assets (WASM, JS, CSS, Media)
event.respondWith(
caches.match(request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}

return fetch(request).then((response) => {
const isCacheable = response.status === 200 || response.status === 0;

// Explicitly check for Next.js chunks, CDN resources, and static assets
const isNextAsset = url.pathname.startsWith("/_next/") ||
url.pathname.endsWith(".js") ||
url.pathname.endsWith(".mjs") ||
url.pathname.endsWith(".css");

const shouldCache = isCacheable && (
url.origin === self.location.origin ||
isNextAsset ||
url.hostname.includes("jsdelivr.net")
);

if (shouldCache) {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseClone);
});
}
return response;
});
})
);
}
});
49 changes: 49 additions & 0 deletions scripts/resize_icons.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Add-Type -AssemblyName System.Drawing

$sourceImage = "C:\Users\verma\.gemini\antigravity-ide\brain\4edb9a97-81fe-47c8-8ab1-caac8ad98a39\reframe_logo_1779370016792.png"
$publicDir = "c:\Users\verma\reframe\public"

if (-not (Test-Path $sourceImage)) {
Write-Error "Source image not found at $sourceImage"
exit 1
}

Write-Host "Loading source image..."
$srcBitmap = New-Object System.Drawing.Bitmap($sourceImage)

# Resize helper function
function Resize-Image {
param(
[System.Drawing.Bitmap]$src,
[int]$width,
[int]$height,
[string]$outputPath
)
Write-Host "Resizing to $width x $height..."
$dest = New-Object System.Drawing.Bitmap($width, $height)
$graphics = [System.Drawing.Graphics]::FromImage($dest)

# Configure high-quality scaling
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$graphics.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality

# Draw resized
$graphics.DrawImage($src, 0, 0, $width, $height)

# Save
$dest.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)

# Cleanup
$graphics.Dispose()
$dest.Dispose()
Write-Host "Saved to $outputPath"
}

# Create icons
Resize-Image $srcBitmap 192 192 "$publicDir\icon-192.png"
Resize-Image $srcBitmap 512 512 "$publicDir\icon-512.png"

$srcBitmap.Dispose()
Write-Host "All icons generated successfully!"
67 changes: 67 additions & 0 deletions scripts/resize_screenshots.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
Add-Type -AssemblyName System.Drawing

$publicDir = "c:\Users\verma\reframe\public\screenshots"
$desktopSrc = "$publicDir\desktop.png"
$mobileSrc = "$publicDir\mobile.png"

# Verify they exist
if (-not (Test-Path $desktopSrc) -or -not (Test-Path $mobileSrc)) {
Write-Error "Source screenshots not found!"
exit 1
}

# 1. Desktop: Crop 1024x1024 to 1024x576, then resize to 1280x720
Write-Host "Processing desktop screenshot..."
$bmpDesktopSrc = New-Object System.Drawing.Bitmap($desktopSrc)
$bmpDesktopDest = New-Object System.Drawing.Bitmap(1280, 720)
$graphDesktop = [System.Drawing.Graphics]::FromImage($bmpDesktopDest)

# Settings
$graphDesktop.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$graphDesktop.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$graphDesktop.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$graphDesktop.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality

# Source crop rectangle: 16:9 from center
# Width: 1024, Height: 576. Y offset: (1024-576)/2 = 224
$srcRectDesktop = New-Object System.Drawing.Rectangle(0, 224, 1024, 576)
$destRectDesktop = New-Object System.Drawing.Rectangle(0, 0, 1280, 720)

$graphDesktop.DrawImage($bmpDesktopSrc, $destRectDesktop, $srcRectDesktop, [System.Drawing.GraphicsUnit]::Pixel)

$graphDesktop.Dispose()
$bmpDesktopSrc.Dispose()

# Delete old and save new
if (Test-Path $desktopSrc) { Remove-Item $desktopSrc -Force }
$bmpDesktopDest.Save($desktopSrc, [System.Drawing.Imaging.ImageFormat]::Png)
$bmpDesktopDest.Dispose()
Write-Host "Desktop screenshot processed successfully!"

# 2. Mobile: Crop 1024x1024 to 576x1024, then resize to 720x1280
Write-Host "Processing mobile screenshot..."
$bmpMobileSrc = New-Object System.Drawing.Bitmap($mobileSrc)
$bmpMobileDest = New-Object System.Drawing.Bitmap(720, 1280)
$graphMobile = [System.Drawing.Graphics]::FromImage($bmpMobileDest)

# Settings
$graphMobile.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$graphMobile.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$graphMobile.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$graphMobile.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality

# Source crop rectangle: 9:16 from center
# Height: 1024, Width: 576. X offset: (1024-576)/2 = 224
$srcRectMobile = New-Object System.Drawing.Rectangle(224, 0, 576, 1024)
$destRectMobile = New-Object System.Drawing.Rectangle(0, 0, 720, 1280)

$graphMobile.DrawImage($bmpMobileSrc, $destRectMobile, $srcRectMobile, [System.Drawing.GraphicsUnit]::Pixel)

$graphMobile.Dispose()
$bmpMobileSrc.Dispose()

# Delete old and save new
if (Test-Path $mobileSrc) { Remove-Item $mobileSrc -Force }
$bmpMobileDest.Save($mobileSrc, [System.Drawing.Imaging.ImageFormat]::Png)
$bmpMobileDest.Dispose()
Write-Host "Mobile screenshot processed successfully!"
52 changes: 43 additions & 9 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ import ErrorBoundary from "@/components/ErrorBoundary";
import "./globals.css";
import { ThemeProvider } from "@/components/ThemeProvider";
import { ThemeToggle } from "@/components/ThemeToggle";
import { InstallButton } from "@/components/InstallButton";
import ScrollToTop from "@/components/ScrollToTop";
import BrandLogo from "@/components/BrandLogo";

export const viewport = {
themeColor: "#e63946",
};

export const metadata: Metadata = {
title: "Reframe — Resize, trim, and export videos in your browser",
description: "Free, open-source video editor that runs entirely in your browser. No login, no uploads, no ads. Resize for any platform, trim, rotate, adjust speed, and export.",
manifest: "/manifest.json",
keywords: [
"video editor",
"browser video editor",
Expand Down Expand Up @@ -49,6 +55,20 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<head>
<link rel="manifest" href="/manifest.json" />
<script
dangerouslySetInnerHTML={{
__html: `
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
window.deferredPrompt = e;
window.dispatchEvent(new CustomEvent('deferredpromptready'));
});
`,
}}
/>
<link rel="preconnect" href="https://cdn.jsdelivr.net" />
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net" />
<script
dangerouslySetInnerHTML={{
__html: `(function() {
Expand Down Expand Up @@ -77,15 +97,29 @@ export default function RootLayout({
<ThemeProvider>
<ErrorBoundary>
<header
role="banner"
className="sticky top-0 z-50 flex items-center justify-between border-b border-[var(--border)] bg-[var(--bg)]/95 px-6 py-3 backdrop-blur"
>
<div className="flex items-center gap-2">
<BrandLogo size={24} />
<h1 className="text-lg font-semibold">Reframe</h1>
</div>
<ThemeToggle />
</header>
role="banner"
className="sticky top-0 z-50 flex items-center justify-between border-b border-[var(--border)] bg-[var(--bg)]/95 px-6 py-3 backdrop-blur"
>
<div className="flex items-center gap-2">
<BrandLogo size={24} />
<h1 className="text-lg font-semibold">Reframe</h1>
</div>

<div className="flex items-center gap-3">
<InstallButton />

<a
href="https://github.com/magic-peach/reframe"
target="_blank"
rel="noopener noreferrer"
className="hidden min-[400px]:inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full border border-[var(--border)] bg-[var(--surface)] text-[10px] font-heading font-semibold uppercase tracking-wider hover:bg-[var(--border)] transition-all"
>
⭐ Star
</a>

<ThemeToggle />
</div>
</header>
<main id="main-content" tabIndex={-1}>
{children}
</main>
Expand Down
2 changes: 1 addition & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ export default function Home() {
<Footer />
</>
);
}
}
Loading
Loading