feat: Add installable Android PWA and native Android Web Share Target support

This commit is contained in:
chrisf committed 2026-10-22 17:46:24 -06:00
1 parent 44d1dec42b
commit 94ae568385
12 files changed
+241 -19

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

+51 -6
View File
@@ -1,17 +1,46 @@
{
"name": "Everclone",
"name": "Everclone — Notes & Knowledge Base",
"short_name": "Everclone",
"description": "Self-hosted Evernote clone for notes, web clips, and attachments",
"description": "Self-hosted Evernote clone for rich notes, web clips, and attachments",
"id": "/",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#1a1a1a",
"display_override": ["window-controls-overlay", "standalone", "minimal-ui"],
"background_color": "#18181b",
"theme_color": "#00a82d",
"orientation": "any",
"categories": ["productivity", "utilities", "notes"],
"icons": [
{
"src": "/icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any maskable"
"purpose": "any"
},
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"share_target": {
@@ -25,9 +54,25 @@
"files": [
{
"name": "files",
"accept": ["image/*", "audio/*", "application/pdf", "text/*"]
"accept": ["image/*", "audio/*", "video/*", "application/pdf", "text/*", "*/*"]
}
]
}
}
},
"shortcuts": [
{
"name": "New Note",
"short_name": "New Note",
"description": "Create a new note in Everclone",
"url": "/?action=new-note",
"icons": [{ "src": "/icon-192.png", "sizes": "192x192" }]
},
{
"name": "Search Notes",
"short_name": "Search",
"description": "Search notes across your library",
"url": "/?action=search",
"icons": [{ "src": "/icon-192.png", "sizes": "192x192" }]
}
]
}
+74
View File
@@ -0,0 +1,74 @@
// Everclone PWA Service Worker (v1.0.0)
const CACHE_NAME = 'everclone-cache-v1';
const PRECACHE_URLS = [
'/',
'/manifest.json',
'/icon.svg',
'/icon-192.png',
'/icon-512.png',
'/apple-touch-icon.png'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(PRECACHE_URLS).catch(() => {});
})
);
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
);
})
);
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
// CRITICAL: Never intercept POST/PUT/DELETE requests!
// This ensures Android Web Share Target multipart POSTs stream directly to /api/hooks/note
if (event.request.method !== 'GET') {
return;
}
const url = new URL(event.request.url);
// Bypass API endpoints and file downloads to always get fresh data
if (url.pathname.startsWith('/api/')) {
return;
}
// Network-first strategy for navigation and static assets with cache fallback
event.respondWith(
fetch(event.request)
.then((networkResponse) => {
if (
networkResponse &&
networkResponse.status === 200 &&
(url.pathname === '/' || url.pathname.startsWith('/_next/static/'))
) {
const responseClone = networkResponse.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
}
return networkResponse;
})
.catch(() => {
return caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
if (event.request.mode === 'navigate') {
return caches.match('/');
}
return new Response('Network error occurred', { status: 503, statusText: 'Offline' });
});
})
);
});