feat(pwa): align app branding and add startup screen
No files matched your search
|
Before Width: | Height: | Size: 202 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 263 B After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 429 B After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 634 B After Width: | Height: | Size: 5.3 KiB |
@@ -28,6 +28,43 @@
|
||||
<link rel="apple-touch-icon" href="./icons/apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="reader" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<!-- startup-images:start -->
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-640x1136.png" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1136x640.png" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-750x1334.png" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1334x750.png" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1242x2208.png" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2208x1242.png" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1125x2436.png" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2436x1125.png" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1170x2532.png" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2532x1170.png" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1179x2556.png" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2556x1179.png" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1206x2622.png" media="(device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2622x1206.png" media="(device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-828x1792.png" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1792x828.png" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1242x2688.png" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2688x1242.png" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1284x2778.png" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2778x1284.png" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1290x2796.png" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2796x1290.png" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1320x2868.png" media="(device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2868x1320.png" media="(device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1536x2048.png" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2048x1536.png" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1620x2160.png" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2160x1620.png" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1640x2360.png" media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2360x1640.png" media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-1668x2388.png" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2388x1668.png" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2048x2732.png" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
|
||||
<link rel="apple-touch-startup-image" href="./startup/startup-2732x2048.png" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
|
||||
<!-- startup-images:end -->
|
||||
<title>reader</title>
|
||||
<style>
|
||||
/* First paint only: prevents a white flash before the bundle applies the
|
||||
@@ -42,10 +79,13 @@
|
||||
html[data-theme='dark'] body {
|
||||
background: #16171a;
|
||||
}
|
||||
.app-startup { position: fixed; inset: 0; display: grid; place-items: center; background: #fbfaf7; z-index: 9999; }
|
||||
.app-startup img { width: 96px; height: 96px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="app-startup" class="app-startup" role="status" aria-label="正在启动 reader"><img src="./icons/icon-192.png" width="96" height="96" alt="" /></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,7 +6,10 @@
|
||||
"lang": "zh-CN",
|
||||
"start_url": "./",
|
||||
"scope": "./",
|
||||
"display_override": ["window-controls-overlay", "standalone"],
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"launch_handler": { "client_mode": "navigate-existing" },
|
||||
"background_color": "#fbfaf7",
|
||||
"theme_color": "#fbfaf7",
|
||||
"icons": [
|
||||
|
||||
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -42,4 +42,4 @@ window.__readerFlush = () => {
|
||||
|
||||
void app.start().catch((err: unknown) => {
|
||||
root.textContent = `客户端启动失败:${err instanceof Error ? err.message : String(err)}`;
|
||||
});
|
||||
}).finally(() => document.getElementById('app-startup')?.remove());
|
||||
@@ -1,6 +1,8 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { registerPwa, watchPwaUpdates } from '../src/core/pwa.ts';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
let registration: ServiceWorkerRegistration;
|
||||
let serviceWorker: ServiceWorkerContainer;
|
||||
@@ -126,6 +128,16 @@ it('keeps ordinary HTTP and unsupported browsers usable', () => {
|
||||
expect(registerPwa).not.toThrow();
|
||||
});
|
||||
|
||||
it('ships standalone launch metadata and a first-paint startup shell', () => {
|
||||
const manifest = JSON.parse(readFileSync(resolve(process.cwd(), 'public/manifest.webmanifest'), 'utf8'));
|
||||
expect(manifest.display).toBe('standalone');
|
||||
expect(manifest.display_override).toContain('standalone');
|
||||
expect(manifest.launch_handler.client_mode).toBe('navigate-existing');
|
||||
const html = readFileSync(resolve(process.cwd(), 'index.html'), 'utf8');
|
||||
expect(html).toContain('id="app-startup"');
|
||||
expect(html).toContain('apple-touch-startup-image');
|
||||
});
|
||||
|
||||
it('reports failed registration without an unhandled rejection', async () => {
|
||||
const error = new Error('storage unavailable');
|
||||
register.mockRejectedValueOnce(error);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// One PWA icon supplies APK launcher sizes and iOS startup images.
|
||||
// Run: node web/tools/pwa/branding.mjs
|
||||
import { chromium } from 'playwright';
|
||||
import { readFile, mkdir, writeFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
const root = new URL('../../', import.meta.url);
|
||||
const icon = await readFile(new URL('public/icons/icon-512.png', root));
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setContent('<canvas></canvas>');
|
||||
async function render(relative, width, height, size) {
|
||||
const bytes = await page.evaluate(async ({ source, width, height, size }) => {
|
||||
const img = new Image(); img.src = source; await img.decode();
|
||||
const canvas = document.querySelector('canvas'); canvas.width = width; canvas.height = height;
|
||||
const ctx = canvas.getContext('2d'); ctx.fillStyle = '#fbfaf7'; ctx.fillRect(0, 0, width, height);
|
||||
ctx.drawImage(img, (width - size) / 2, (height - size) / 2, size, size);
|
||||
return canvas.toDataURL('image/png').split(',')[1];
|
||||
}, { source: `data:image/png;base64,${icon.toString('base64')}`, width, height, size });
|
||||
const dest = new URL(relative, root); await mkdir(new URL('./', dest), { recursive: true });
|
||||
await writeFile(dest, Buffer.from(bytes, 'base64'));
|
||||
}
|
||||
for (const [density, size] of Object.entries({ mdpi: 48, hdpi: 72, xhdpi: 96, xxhdpi: 144, xxxhdpi: 192 })) {
|
||||
await render(`../android/app/src/main/res/mipmap-${density}/ic_launcher.png`, size, size, size);
|
||||
}
|
||||
const devices = [[320,568,2],[375,667,2],[414,736,3],[375,812,3],[390,844,3],[393,852,3],[402,874,3],[414,896,2],[414,896,3],[428,926,3],[430,932,3],[440,956,3],[768,1024,2],[810,1080,2],[820,1180,2],[834,1194,2],[1024,1366,2]];
|
||||
const links = [];
|
||||
for (const [w,h,dpr] of devices) {
|
||||
for (const orientation of ['portrait', 'landscape']) {
|
||||
const width = (orientation === 'portrait' ? w : h) * dpr;
|
||||
const height = (orientation === 'portrait' ? h : w) * dpr;
|
||||
const name = `startup-${width}x${height}.png`;
|
||||
await render(`public/startup/${name}`, width, height, 96 * dpr);
|
||||
links.push(` <link rel="apple-touch-startup-image" href="./startup/${name}" media="(device-width: ${w}px) and (device-height: ${h}px) and (-webkit-device-pixel-ratio: ${dpr}) and (orientation: ${orientation})" />`);
|
||||
}
|
||||
}
|
||||
const htmlPath = new URL('index.html', root);
|
||||
const html = await readFile(htmlPath, 'utf8');
|
||||
await writeFile(htmlPath, html.replace(/<!-- startup-images:start -->[\s\S]*?<!-- startup-images:end -->/, `<!-- startup-images:start -->\n${links.join('\n')}\n <!-- startup-images:end -->`));
|
||||
console.log('Generated Android launcher icons and iOS startup images.');
|
||||
} finally { await browser.close(); }
|
||||