web: make the studio installable, and give it a shell that opens offline
The three things a browser asks for, without a plugin: a manifest in
public/ (name, /app as the start, three icons cut from the one piece of
art this repo has), a service worker, and the two metas iOS reads
instead of the manifest.
The worker caches the shell — /, /app, /library, all one document under
the SPA fallback — and the hashed assets the build emits. A navigation
is network-first, so a deploy is never pinned behind the cache; a
hashed asset or the wasm is cache-first, because under a given build
those never change. /api and any non-GET go straight out: a worker is a
cache, not a proxy. nginx serves sw.js and manifest.json `no-cache`
(both names outlive their contents) with the isolation headers the
worker script needs under COEP.
The offer is the app's own dialog, not Chromium's mini-infobar: the
event is held, and it is spent either after the visitor has been in the
studio two minutes or the moment an export lands — the point at which
the app has done their work. Safari never fires the event, so it gets
the Share > Add to Home Screen line instead. A refusal is remembered and
never asked again.
node scripts/make-icons.mjs 192x192 39785B / 512x512 159296B / maskable 512x512 123723B
node scripts/pwa-check.mjs manifest 3 icons · worker activated · shell cached
· offline reload of /app paints
off (https://localhost:8090) same four, through nginx
This commit is contained in:
@@ -13,6 +13,13 @@ export const en: Dict = {
|
||||
'nav.back': 'Back to home',
|
||||
'nav.guest': 'Guest',
|
||||
|
||||
// The install offer (see vi.ts).
|
||||
'install.title': 'Install RecipesCam',
|
||||
'install.body': 'Add it to your home screen to open it faster — smoother, offline, full screen like an app.',
|
||||
'install.yes': 'INSTALL',
|
||||
'install.no': 'NO THANKS',
|
||||
'install.ios': 'Tap the Share button in the toolbar, then choose “Add to Home Screen”.',
|
||||
|
||||
'act.undo': 'UNDO',
|
||||
'act.redo': 'REDO',
|
||||
'act.reset': 'RESET',
|
||||
@@ -218,9 +225,32 @@ export const en: Dict = {
|
||||
'photos.failed': 'That did not work: {msg}',
|
||||
'photos.noLabels': 'No strip labels yet',
|
||||
|
||||
'lib.title': 'Library',
|
||||
'lib.hint':
|
||||
'Pick a photo folder on this machine. Nothing is uploaded — the page only remembers where the folder is and keeps one thumbnail per frame.',
|
||||
'lib.add': 'ADD FOLDER',
|
||||
'lib.stop': 'STOP SCAN',
|
||||
'lib.rescan': 'RESCAN',
|
||||
'lib.remove': 'REMOVE',
|
||||
'lib.reconnect': 'GRANT ACCESS AGAIN',
|
||||
'lib.all': 'ALL',
|
||||
'lib.open': 'Open in the studio',
|
||||
'lib.edited': 'edited',
|
||||
'lib.size': '{mb} MB',
|
||||
'lib.count': '{n} photos',
|
||||
'lib.scanning': 'Scanning {done}/{total} — {added} new…',
|
||||
'lib.scanned': 'Scanned {folder}: {added}/{total} new frames.',
|
||||
'lib.empty': 'Nothing in this folder yet.',
|
||||
'lib.noFolders': 'No folder yet. Hit ADD FOLDER and pick a photo folder on this machine.',
|
||||
'lib.missing': 'Could not open {name} — the file has been moved or deleted.',
|
||||
'lib.failed': 'That folder could not be read.',
|
||||
'lib.unsupported':
|
||||
'This browser will not let a page read a folder from the disk (Chrome, Edge, Opera and Brave will). Photos still edit fine — drag and drop them into the studio.',
|
||||
|
||||
'nav.admin': 'Admin',
|
||||
'nav.photos': 'My photos',
|
||||
'nav.profile': 'Profile',
|
||||
'nav.library': 'Library',
|
||||
'nav.studio': 'Studio',
|
||||
'adm.title': 'Strip moderation',
|
||||
'adm.subtitle': 'Photos contributed by users. Delete one to pull it off the landing page.',
|
||||
|
||||
@@ -17,9 +17,19 @@ export const vi = {
|
||||
'nav.admin': 'Quản trị',
|
||||
'nav.photos': 'Ảnh của tôi',
|
||||
'nav.profile': 'Hồ sơ',
|
||||
'nav.library': 'Thư viện',
|
||||
'nav.studio': 'Studio',
|
||||
'nav.guest': 'Khách',
|
||||
|
||||
// The install offer (src/ui/InstallPrompt.tsx). The only dialog that asks for
|
||||
// something the visitor did not come for, so the copy sells what they get out of
|
||||
// it and the refusal is one word away.
|
||||
'install.title': 'Cài RecipesCam lên màn hình chính',
|
||||
'install.body': 'Cài lên màn hình chính để mở nhanh hơn — mượt mà hơn, offline, toàn màn hình như một app.',
|
||||
'install.yes': 'ĐỒNG Ý',
|
||||
'install.no': 'KHÔNG CẢM ƠN',
|
||||
'install.ios': 'Bấm nút Chia sẻ trên thanh địa chỉ, rồi chọn "Thêm vào màn hình chính".',
|
||||
|
||||
'act.undo': 'HOÀN TÁC',
|
||||
'act.redo': 'LÀM LẠI',
|
||||
'act.reset': 'ĐẶT LẠI',
|
||||
@@ -228,6 +238,28 @@ export const vi = {
|
||||
'photos.failed': 'Không thực hiện được: {msg}',
|
||||
'photos.noLabels': 'Chưa có nhãn dải phim',
|
||||
|
||||
'lib.title': 'Thư viện',
|
||||
'lib.hint':
|
||||
'Chọn thư mục ảnh trên máy. Ảnh không được tải lên đâu cả — trang chỉ nhớ đường dẫn và giữ một ảnh thu nhỏ cho mỗi tấm.',
|
||||
'lib.add': 'THÊM THƯ MỤC',
|
||||
'lib.stop': 'DỪNG QUÉT',
|
||||
'lib.rescan': 'QUÉT LẠI',
|
||||
'lib.remove': 'BỎ',
|
||||
'lib.reconnect': 'CẤP LẠI QUYỀN',
|
||||
'lib.all': 'TẤT CẢ',
|
||||
'lib.open': 'Mở trong studio',
|
||||
'lib.edited': 'đã chỉnh',
|
||||
'lib.size': '{mb} MB',
|
||||
'lib.count': '{n} ảnh',
|
||||
'lib.scanning': 'Đang quét {done}/{total} — thêm {added}…',
|
||||
'lib.scanned': 'Đã quét {folder}: {added}/{total} ảnh mới.',
|
||||
'lib.empty': 'Chưa có ảnh nào trong thư mục này.',
|
||||
'lib.noFolders': 'Chưa có thư mục nào. Bấm THÊM THƯ MỤC để chọn một thư mục ảnh trên máy.',
|
||||
'lib.missing': 'Không mở được {name} — tệp đã bị di chuyển hoặc xoá.',
|
||||
'lib.failed': 'Không đọc được thư mục này.',
|
||||
'lib.unsupported':
|
||||
'Trình duyệt này không cho phép app đọc thư mục trên máy (cần Chrome, Edge, Opera hoặc Brave). Ảnh vẫn chỉnh được bình thường bằng cách kéo & thả vào studio.',
|
||||
|
||||
'adm.title': 'Quản trị dải phim',
|
||||
'adm.subtitle': 'Ảnh do người dùng đóng góp. Xoá một ảnh để gỡ nó khỏi trang chủ.',
|
||||
'adm.upload': 'THÊM ẢNH',
|
||||
|
||||
Reference in New Issue
Block a user