diff --git a/packages/webapp/public/logo-maskable-144.png b/packages/webapp/public/logo-maskable-144.png index a14e6f97..13650b33 100644 Binary files a/packages/webapp/public/logo-maskable-144.png and b/packages/webapp/public/logo-maskable-144.png differ diff --git a/packages/webapp/public/manifest.webmanifest b/packages/webapp/public/manifest.webmanifest deleted file mode 100644 index e5b4b8d5..00000000 --- a/packages/webapp/public/manifest.webmanifest +++ /dev/null @@ -1,81 +0,0 @@ -{ - "id": "com.nesbox", - "name": "NESBox", - "short_name": "NESBox", - "categories": ["entertainment", "games"], - "description": "Enjoy game", - "scope": "/", - "start_url": "/?utm_source=web_app_manifest", - "background_color": "black", - "theme_color": "black", - "display": "standalone", - "orientation": "landscape", - "file_handlers": [ - { - "action": "/emulator", - "name": "NES file", - "accept": { - "application/octet-stream": [".nes"] - } - } - ], - "launch_handler": { - "route_to": "existing-client-retain" - }, - "shortcuts": [ - { - "name": "All Games", - "url": "/games" - }, - { - "name": "My Favorites", - "url": "/favorites" - } - ], - "icons": [ - { - "src": "/logo-32.png", - "sizes": "32x32", - "type": "image/png" - }, - { - "src": "/logo-96.png", - "sizes": "96x96", - "type": "image/png" - }, - { - "src": "/logo-maskable-144.png", - "sizes": "144x144", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/logo-144.png", - "sizes": "144x144", - "type": "image/png" - } - ], - "screenshots": [ - { - "src": "https://raw.githubusercontent.com/mantou132/nesbox/master/screenshots/homepage.png", - "sizes": "1280x861", - "type": "image/png", - "platform": "wide", - "label": "Homescreen of NESBox" - }, - { - "src": "https://raw.githubusercontent.com/mantou132/nesbox/master/screenshots/playing.png", - "sizes": "1280x861", - "type": "image/png", - "platform": "wide", - "label": "Playing game in NESBox" - }, - { - "src": "https://raw.githubusercontent.com/mantou132/nesbox/master/screenshots/settings.png", - "sizes": "1280x861", - "type": "image/png", - "platform": "wide", - "label": "Settings in NESBox" - } - ] -} diff --git a/packages/webapp/public/sw.js b/packages/webapp/public/sw.js index b817bf3a..ee1254ac 100644 --- a/packages/webapp/public/sw.js +++ b/packages/webapp/public/sw.js @@ -8,7 +8,7 @@ const sw = self; const cacheName = 'console'; -const assetsToCache = [...new Set([sw.location.pathname, '/'])].map((path) => sw.location.origin + path); +const assetsToCache = [...new Set(['/'])].map((path) => location.origin + path); sw.addEventListener('activate', (event) => { event.waitUntil(sw.registration.navigationPreload?.enable() || Promise.resolve()); @@ -16,7 +16,7 @@ sw.addEventListener('activate', (event) => { sw.addEventListener('install', () => { sw.skipWaiting(); - sw.caches.open(cacheName).then((cache) => { + caches.open(cacheName).then((cache) => { cache.addAll(assetsToCache); }); }); @@ -29,6 +29,7 @@ sw.addEventListener('push', function (event) { payload = { title: event.data.text() }; } event.waitUntil( + // 需要在 client 请求权限 sw.registration.showNotification(payload.title, { icon: '/logo-144.png', body: payload.body, @@ -44,16 +45,36 @@ sw.addEventListener('notificationclick', function (event) { event.notification.close(); }); +/** + * + * @param {FetchEvent} event + */ +function handleShareTarget(event) { + event.respondWith( + (async () => { + const formData = await event.request.formData(); + const file = formData.get('file'); + // TODO: add to caches -> client open emulator + return Response.redirect('/', 303); + })(), + ); +} + sw.addEventListener('fetch', (event) => { const { request } = event; const requestUrl = new URL(request.url); - const isSomeOrigin = requestUrl.origin === sw.location.origin; + const isSomeOrigin = requestUrl.origin === location.origin; const isApiFetch = (isSomeOrigin && requestUrl.pathname.startsWith('/api')) || requestUrl.origin.includes('api.'); + if (requestUrl.pathname === '/_share_target') { + handleShareTarget(event); + return; + } + if (request.method !== 'GET') return; if (!isSomeOrigin && !isApiFetch) return; - const getCache = () => sw.caches.match(request, { ignoreSearch: request.mode === 'navigate' }); + const getCache = () => caches.match(request, { ignoreSearch: request.mode === 'navigate' }); event.respondWith( (async function () { @@ -64,7 +85,7 @@ sw.addEventListener('fetch', (event) => { .then(async (response) => { if (response.ok) { const responseCache = response.clone(); - sw.caches.open(cacheName).then((cache) => cache.put(request, responseCache)); + caches.open(cacheName).then((cache) => cache.put(request, responseCache)); return response; } else { const cache = await getCache(); diff --git a/packages/webapp/src/drop.ts b/packages/webapp/src/drop.ts index 2a0d872f..2f683dae 100644 --- a/packages/webapp/src/drop.ts +++ b/packages/webapp/src/drop.ts @@ -7,6 +7,7 @@ import { setNesFile } from 'src/configure'; window.launchQueue?.setConsumer(async (launchParams: any) => { if (!launchParams.files.length) return; + // 为啥刷新还是会触发 // https://github.com/WICG/file-system-access/blob/master/EXPLAINER.md#example-code const files = await Promise.all(launchParams.files.map((h: any) => h.getFile())); diff --git a/packages/webapp/src/index.html b/packages/webapp/src/index.html index a5165d72..732575af 100644 --- a/packages/webapp/src/index.html +++ b/packages/webapp/src/index.html @@ -8,7 +8,6 @@ /> NESBox - diff --git a/packages/webapp/src/index.ts b/packages/webapp/src/index.ts index 5b16d51f..d1706fae 100644 --- a/packages/webapp/src/index.ts +++ b/packages/webapp/src/index.ts @@ -223,3 +223,13 @@ if (COMMAND === 'build') { addEventListener('load', () => { logger.info('Loaded!'); }); + +// Installed +matchMedia(mediaQuery.PWA).addEventListener('change', ({ matches }) => { + if (matches) { + const w = 1024; + const h = 640; + resizeTo(w, h); + moveTo((screen.width - w) / 2, (screen.height - h) / 2); + } +}); diff --git a/packages/webapp/src/modules/meta.ts b/packages/webapp/src/modules/meta.ts index e550cf46..110fb788 100644 --- a/packages/webapp/src/modules/meta.ts +++ b/packages/webapp/src/modules/meta.ts @@ -4,7 +4,7 @@ import { mediaQuery } from '@mantou/gem/helper/mediaquery'; import { i18n } from 'src/i18n/basic'; import { themeStore } from 'src/theme'; import { canonicalOrigin, isSafari } from 'src/constants'; -import { navStore } from 'src/configure'; +import { configure, navStore } from 'src/configure'; import 'duoyun-ui/elements/title'; import 'duoyun-ui/elements/reflect'; @@ -15,6 +15,10 @@ const style = createCSSSheet(css` } `); +type State = { + manifest?: string; +}; + /** * @customElement m-meta */ @@ -24,8 +28,22 @@ const style = createCSSSheet(css` @connectStore(i18n.store) @connectStore(history.store) @adoptedStyle(style) -export class ModuleMetaElement extends GemElement { +export class ModuleMetaElement extends GemElement { + state: State = {}; + mounted = () => { + addEventListener('load', async () => { + const { getWebManifestURL } = await import('src/webmanifest'); + this.effect( + () => { + this.setState({ manifest: getWebManifestURL() }); + }, + () => [i18n.currentLanguage, configure.theme], + ); + }); + }; + render = () => { + const { manifest } = this.state; return html` @@ -35,6 +53,7 @@ export class ModuleMetaElement extends GemElement { /> + ${manifest ? html`` : ''} `; }; diff --git a/packages/webapp/src/webmanifest.ts b/packages/webapp/src/webmanifest.ts new file mode 100644 index 00000000..bc002d2c --- /dev/null +++ b/packages/webapp/src/webmanifest.ts @@ -0,0 +1,110 @@ +import { utf8ToB64 } from 'duoyun-ui/lib/encode'; +import { routes } from 'src/routes'; + +import { COMMAND } from 'src/constants'; +import { i18n } from 'src/i18n/basic'; + +export function getWebManifestURL() { + return `data:application/json;base64,${utf8ToB64( + JSON.stringify(genWebManifest(), (_, value) => + typeof value === 'string' && value.startsWith('/') ? new URL(value, location.origin).href : value, + ), + )}`; +} + +export function genWebManifest() { + return { + id: 'com.nesbox' + (COMMAND === 'serve' ? '.dev' : ''), + name: i18n.get('global.title') + (COMMAND === 'serve' ? '(DEV)' : ''), + short_name: i18n.get('global.title'), + categories: ['entertainment', 'games'], + description: i18n.get('global.sloganDesc').replaceAll('\n', ','), + scope: '/', + start_url: `${routes.games.pattern}?utm_source=web_app_manifest`, + background_color: 'black', + theme_color: 'black', + display: 'standalone', + orientation: 'landscape', + file_handlers: [ + { + action: routes.emulator.pattern, + name: 'Game File', + accept: { + 'application/octet-stream': ['.nes'], + }, + }, + ], + share_target: { + action: '/_share_target', + method: 'POST', + enctype: 'multipart/form-data', + params: { + files: [ + { + name: 'nes', + accept: ['application/octet-stream', '.nes'], + }, + ], + }, + }, + launch_handler: { + client_mode: 'navigate-new', + }, + shortcuts: [ + { + name: i18n.get('page.games.title'), + url: routes.games.pattern, + }, + { + name: i18n.get('page.favorites.title'), + url: routes.favorites.pattern, + }, + ], + icons: [ + { + src: '/logo-32.png', + sizes: '32x32', + type: 'image/png', + }, + { + src: '/logo-96.png', + sizes: '96x96', + type: 'image/png', + }, + { + src: '/logo-maskable-144.png', + sizes: '144x144', + type: 'image/png', + purpose: 'maskable', + }, + { + src: '/logo-144.png', + sizes: '144x144', + type: 'image/png', + }, + ], + screenshots: [ + { + src: 'https://raw.githubusercontent.com/mantou132/nesbox/master/screenshots/homepage.png', + sizes: '1280x861', + type: 'image/png', + form_factor: 'wide', + label: 'HomeScreen of NESBox', + }, + { + src: 'https://raw.githubusercontent.com/mantou132/nesbox/master/screenshots/playing.png', + sizes: '1280x861', + type: 'image/png', + form_factor: 'wide', + label: 'Playing game in NESBox', + }, + { + src: 'https://raw.githubusercontent.com/mantou132/nesbox/master/screenshots/settings.png', + sizes: '1280x861', + type: 'image/png', + form_factor: 'wide', + label: 'Settings in NESBox', + }, + ], + }; +}