Skip to content
Merged
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
55 changes: 55 additions & 0 deletions .github/workflows/android-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Build Android AAB

on:
workflow_dispatch:

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- uses: actions/setup-node@v5
with:
node-version: 24

- name: Install Bubblewrap
run: npm install -g @bubblewrap/cli

- name: Decode keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > "$RUNNER_TEMP/keystore.jks"

- name: Inject signing config into twa-manifest
run: |
jq \
--arg path "$RUNNER_TEMP/keystore.jks" \
'.signingKey.path = $path' \
twa-manifest.json > twa-manifest.tmp.json
mv twa-manifest.tmp.json twa-manifest.json

- name: Init TWA project
run: bubblewrap init --skipPwaValidation

- name: Build release AAB
env:
BUBBLEWRAP_KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
BUBBLEWRAP_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: bubblewrap build --skipPwaValidation

- name: Upload AAB
uses: actions/upload-artifact@v4
with:
name: video-shrinker-${{ github.run_number }}
path: |
app-release-bundle.aab
app-release.apk
if-no-files-found: warn
retention-days: 90
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
11 changes: 7 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"typescript": "~6.0.2",
"vite": "^8.1.1",
"vite-plugin-pwa": "^1.3.0",
"vite-plugin-static-copy": "^4.1.1"
"vite-plugin-static-copy": "^4.1.1",
"workbox-expiration": "^7.4.1",
"workbox-precaching": "^7.4.1",
"workbox-routing": "^7.4.1",
"workbox-strategies": "^7.4.1"
}
}
15 changes: 15 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ function App() {
[reset],
);

useEffect(() => {
if (!location.search.includes('share-target')) return;
history.replaceState(null, '', location.pathname);
(async () => {
const cache = await caches.open('share-target');
const response = await cache.match('/share-target-file');
if (!response) return;
const name = decodeURIComponent(response.headers.get('X-File-Name') ?? 'shared-video');
const type = response.headers.get('Content-Type') ?? 'video/mp4';
const blob = await response.blob();
await cache.delete('/share-target-file');
handleFile(new File([blob], name, { type }));
})();
}, [handleFile]);

const handleConvert = useCallback(async () => {
if (!file) return;
setStatus('converting');
Expand Down
44 changes: 44 additions & 0 deletions src/sw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { type PrecacheEntry, cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'
import { CacheFirst } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'

declare const self: ServiceWorkerGlobalScope & {
__WB_MANIFEST: Array<string | PrecacheEntry>
}

cleanupOutdatedCaches()
precacheAndRoute(self.__WB_MANIFEST)

registerRoute(
({ url }) => /\/ffmpeg-core\/ffmpeg-core\.(js|wasm)$/.test(url.pathname),
new CacheFirst({
cacheName: 'ffmpeg-core',
plugins: [new ExpirationPlugin({ maxEntries: 4, maxAgeSeconds: 60 * 60 * 24 * 365 })],
}),
)

self.addEventListener('fetch', (event: FetchEvent) => {
const url = new URL(event.request.url)
if (event.request.method !== 'POST' || !url.pathname.endsWith('/share-target')) return

event.respondWith(
(async () => {
const formData = await event.request.formData()
const file = formData.get('video') as File | null
if (file) {
const cache = await caches.open('share-target')
await cache.put(
'/share-target-file',
new Response(file, {
headers: {
'Content-Type': file.type,
'X-File-Name': encodeURIComponent(file.name),
},
}),
)
}
return Response.redirect(`${url.origin}/video-shrinker/?share-target=1`, 303)
})(),
)
})
3 changes: 2 additions & 1 deletion tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
"include": ["src"],
"exclude": ["src/sw.ts"]
}
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.sw.json" }
]
}
23 changes: 23 additions & 0 deletions tsconfig.sw.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.sw.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "WebWorker"],
"module": "esnext",
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,

/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/sw.ts"]
}
39 changes: 39 additions & 0 deletions twa-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"packageId": "io.github.dubsector.videoshrinker",
"host": "dubsector.github.io",
"startUrl": "/video-shrinker/",
"manifestUrl": "https://dubsector.github.io/video-shrinker/manifest.webmanifest",
"name": "Video Shrinker",
"launcherName": "Video Shrinker",
"display": "standalone",
"themeColor": "#5865F2",
"backgroundColor": "#ffffff",
"navigationColor": "#5865F2",
"navigationColorDark": "#5865F2",
"navigationDividerColor": "#5865F2",
"navigationDividerColorDark": "#5865F2",
"iconUrl": "https://dubsector.github.io/video-shrinker/pwa-512x512.png",
"maskableIconUrl": "https://dubsector.github.io/video-shrinker/maskable-icon-512x512.png",
"monochromeIconUrl": "https://dubsector.github.io/video-shrinker/pwa-512x512.png",
"appVersion": "1",
"appVersionCode": 1,
"signingKey": {
"path": "",
"alias": "video-shrinker"
},
"enableNotifications": false,
"shortcuts": [],
"generatorApp": "bubblewrap-cli",
"webManifestUrl": "https://dubsector.github.io/video-shrinker/manifest.webmanifest",
"fallbackType": "customtabs",
"features": {},
"alphaDependencies": {
"enabled": false
},
"enableSiteSettingsShortcut": true,
"isChromeOSOnly": false,
"isMetaQuest": false,
"fullScopeUrl": "https://dubsector.github.io/video-shrinker/",
"minSdkVersion": 21,
"fingerprints": []
}
30 changes: 15 additions & 15 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export default defineConfig({
// schedule instead of forcing a reload that could interrupt an
// in-progress conversion.
injectRegister: null,
strategies: 'injectManifest',
srcDir: 'src',
filename: 'sw.ts',
includeAssets: ['favicon.svg'],
manifest: {
name: 'Video Shrinker',
Expand All @@ -53,22 +56,19 @@ export default defineConfig({
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
{ src: 'maskable-icon-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
],
},
workbox: {
// The ffmpeg.wasm binary (~32MB) is only needed by the rare CPU
// fallback path, so it's cached at runtime on first use (below)
// rather than bloating the initial install with a large precache.
globPatterns: ['**/*.{js,css,html,ico,png,svg,webmanifest}'],
runtimeCaching: [
{
urlPattern: /\/ffmpeg-core\/ffmpeg-core\.(js|wasm)$/,
handler: 'CacheFirst',
options: {
cacheName: 'ffmpeg-core',
expiration: { maxEntries: 4, maxAgeSeconds: 60 * 60 * 24 * 365 },
},
share_target: {
action: '/video-shrinker/share-target',
method: 'POST',
enctype: 'multipart/form-data',
params: {
files: [
{
name: 'video',
accept: ['video/mp4', 'video/webm', 'video/quicktime', 'video/x-msvideo', 'video/x-matroska', 'video/mpeg', 'video/3gpp'],
},
],
},
],
},
},
}),
],
Expand Down