PWA for iPhone and iPad: PNG icons, splash screens, manifest; stay signed in for 180 days
build-container / build (push) Successful in 1m56s
- iOS ignores SVG touch icons: add 120/152/167/180 apple-touch-icons, 192/512 and maskable manifest icons, and 25 splash screens (every iPhone size, iPad portrait+landscape incl. 12.9") - Icons and splash screens are public so iOS can fetch them without a session - Manifest: id, scope, categories, shortcuts; iOS standalone polish (no callout, no overscroll) - Sessions last SESSION_DAYS (default 180) and renew daily while in use - tools/make-icons.py regenerates the artwork Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
No files matched your search
@@ -11,3 +11,5 @@ AUDIOBOOKS_URL=https://audiobooks.necloud.us
|
||||
HUB_SETUP_CODE=
|
||||
VERSE_URL=https://versebyversewithnate.us/admin
|
||||
PODCAST_URL=https://podcast.necloud.us/
|
||||
# Days you stay signed in without using the app (it renews every time you do)
|
||||
SESSION_DAYS=180
|
||||
@@ -42,6 +42,13 @@ e.g. hub.necloud.us. Settings: `SEAFILE_URL`, `ONLYOFFICE_URL` env vars in docke
|
||||
- **Mail**: deep links to Gmail, Outlook.com, Microsoft 365 and iCloud webmail (inbox + compose).
|
||||
Providers forbid framing, and a real unified inbox would need Google/Azure app registrations.
|
||||
|
||||
## Install on iPhone / iPad (PWA)
|
||||
|
||||
Open the site in **Safari** (it must be the `https://` address), tap **Share -> Add to Home Screen**. The home-screen app has proper
|
||||
icons (iPhone, iPad), a splash screen for each iPhone and iPad size (including iPad Pro 12.9"), and opens full screen without Safari's bars.
|
||||
iOS keeps a separate sign-in for the home-screen app, so sign in once inside it with your passkey; it then stays signed in
|
||||
(`SESSION_DAYS`, default 180, renewed whenever you use it). Regenerate the artwork with `python3 tools/make-icons.py` (needs Pillow).
|
||||
|
||||
## Build and deploy with Gitea
|
||||
|
||||
`.gitea/workflows/build.yml` builds the container on every push to `main` (and `v*` tags) and pushes it to
|
||||
|
||||
@@ -29,7 +29,9 @@ const save = () => writeJson('passkeys.json', store);
|
||||
const SETUP_CODE = process.env.HUB_SETUP_CODE || crypto.randomBytes(4).toString('hex');
|
||||
if (!store.creds.length) console.log(`[auth] No passkeys yet. Setup code: ${SETUP_CODE}`);
|
||||
|
||||
const SESSION_DAYS = 30;
|
||||
// Sessions last SESSION_DAYS and slide forward: using the app renews them, so you only sign in again after that long idle.
|
||||
const SESSION_DAYS = Math.max(1, parseInt(process.env.SESSION_DAYS, 10) || 180);
|
||||
const LIFETIME = SESSION_DAYS * 864e5;
|
||||
const mac = v => crypto.createHmac('sha256', secret).update(v).digest('base64url');
|
||||
const cookies = req => Object.fromEntries((req.headers.cookie || '').split(/; */).filter(Boolean).map(c => { const i = c.indexOf('='); return [c.slice(0, i), decodeURIComponent(c.slice(i + 1))]; }));
|
||||
const isHttps = req => (req.headers['x-forwarded-proto'] || '') === 'https' || (process.env.PUBLIC_URL || '').startsWith('https://');
|
||||
@@ -40,9 +42,15 @@ function isAuthed(req) {
|
||||
const want = mac('s' + exp);
|
||||
return sig.length === want.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(want)) && Number(exp) > Date.now();
|
||||
}
|
||||
function startSession(req, res) {
|
||||
const exp = Date.now() + SESSION_DAYS * 864e5;
|
||||
res.setHeader('Set-Cookie', `hub_session=${exp}.${mac('s' + exp)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_DAYS * 86400}${isHttps(req) ? '; Secure' : ''}`);
|
||||
const sessionCookie = req => { const exp = Date.now() + LIFETIME;
|
||||
return `hub_session=${exp}.${mac('s' + exp)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_DAYS * 86400}${isHttps(req) ? '; Secure' : ''}`; };
|
||||
// Add a Set-Cookie without clobbering ones other code already set on this response.
|
||||
const addCookie = (res, c) => { const cur = res.getHeader('Set-Cookie'); res.setHeader('Set-Cookie', cur ? [].concat(cur, c) : c); };
|
||||
function startSession(req, res) { addCookie(res, sessionCookie(req)); }
|
||||
/** Renew the session cookie when it is more than a day old, so an active session never runs out. */
|
||||
function refresh(req, res) {
|
||||
const [exp] = String(cookies(req).hub_session || '').split('.');
|
||||
if (Number(exp) - Date.now() < LIFETIME - 864e5) addCookie(res, sessionCookie(req));
|
||||
}
|
||||
|
||||
const challenges = new Map(); // cid -> { challenge, kind, exp }
|
||||
@@ -100,7 +108,7 @@ async function handle(req, res) {
|
||||
try {
|
||||
if (p === '/api/auth/state') return send(res, 200, { registered: store.creds.length > 0, authed: isAuthed(req) });
|
||||
if (p === '/api/auth/logout' && req.method === 'POST') {
|
||||
res.setHeader('Set-Cookie', 'hub_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0');
|
||||
addCookie(res, 'hub_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0');
|
||||
return send(res, 200, { ok: true });
|
||||
}
|
||||
const body = req.method === 'POST' || req.method === 'DELETE' ? JSON.parse((await readBody(req)) || '{}') : {};
|
||||
@@ -177,4 +185,4 @@ async function handle(req, res) {
|
||||
} catch (e) { send(res, 400, { error: e.message }); }
|
||||
}
|
||||
|
||||
module.exports = { handle, isAuthed, secret };
|
||||
module.exports = { handle, isAuthed, refresh, addCookie, secret, SESSION_DAYS };
|
||||
@@ -20,6 +20,7 @@ services:
|
||||
ONLYOFFICE_JWT_SECRET: ${ONLYOFFICE_JWT_SECRET:-} # same value as the OnlyOffice server's JWT secret
|
||||
PUBLIC_URL: ${PUBLIC_URL:-} # e.g. https://hub.necloud.us (OnlyOffice posts saved edits here)
|
||||
HUB_SETUP_CODE: ${HUB_SETUP_CODE:-} # first passkey; if empty, a code is printed in the log
|
||||
SESSION_DAYS: ${SESSION_DAYS:-180} # how long you stay signed in when idle; using the app renews it
|
||||
COOKIE_SECRET: ${COOKIE_SECRET:-} # optional; auto-generated and kept in /data if empty
|
||||
volumes:
|
||||
- hub-data:/data # passkeys, calendar links and the session secret live here
|
||||
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 979 B |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,25 @@
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-2732x2048.png" media="(device-width: 1366px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-2388x1668.png" media="(device-width: 1194px) and (device-height: 834px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-1668x2224.png" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-2224x1668.png" media="(device-width: 1112px) and (device-height: 834px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-2360x1640.png" media="(device-width: 1180px) and (device-height: 820px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-2160x1620.png" media="(device-width: 1080px) and (device-height: 810px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-1488x2266.png" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-2266x1488.png" media="(device-width: 1133px) and (device-height: 744px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-2048x1536.png" media="(device-width: 1024px) and (device-height: 768px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
@@ -8,9 +8,41 @@
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="NE Hub">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="application-name" content="NE Hub">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="icon" href="/icon.svg">
|
||||
<link rel="apple-touch-icon" href="/icon.svg">
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png">
|
||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/icons/apple-touch-icon-120.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/icons/apple-touch-icon-152.png">
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="/icons/apple-touch-icon-167.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon-180.png">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-2732x2048.png" media="(device-width: 1366px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-2388x1668.png" media="(device-width: 1194px) and (device-height: 834px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-1668x2224.png" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-2224x1668.png" media="(device-width: 1112px) and (device-height: 834px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-2360x1640.png" media="(device-width: 1180px) and (device-height: 820px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-2160x1620.png" media="(device-width: 1080px) and (device-height: 810px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-1488x2266.png" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-2266x1488.png" media="(device-width: 1133px) and (device-height: 744px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-2048x1536.png" media="(device-width: 1024px) and (device-height: 768px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
<title>NE Hub</title>
|
||||
</head>
|
||||
|
||||
@@ -4,8 +4,43 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#0f1115">
|
||||
<link rel="icon" href="/icon.svg">
|
||||
<link rel="apple-touch-icon" href="/icon.svg">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="NE Hub">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png">
|
||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/icons/apple-touch-icon-120.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/icons/apple-touch-icon-152.png">
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="/icons/apple-touch-icon-167.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon-180.png">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-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="/icons/splash-2732x2048.png" media="(device-width: 1366px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-2388x1668.png" media="(device-width: 1194px) and (device-height: 834px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-1668x2224.png" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-2224x1668.png" media="(device-width: 1112px) and (device-height: 834px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-2360x1640.png" media="(device-width: 1180px) and (device-height: 820px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-2160x1620.png" media="(device-width: 1080px) and (device-height: 810px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-1488x2266.png" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-2266x1488.png" media="(device-width: 1133px) and (device-height: 744px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/icons/splash-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="/icons/splash-2048x1536.png" media="(device-width: 1024px) and (device-height: 768px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
<title>Sign in · NE Hub</title>
|
||||
<style>
|
||||
|
||||
@@ -1,11 +1,95 @@
|
||||
{
|
||||
"id": "/",
|
||||
"name": "NE Hub",
|
||||
"short_name": "NE Hub",
|
||||
"description": "Files, documents, music, calendar, tasks and more in one place.",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"display_override": [
|
||||
"standalone",
|
||||
"minimal-ui"
|
||||
],
|
||||
"orientation": "any",
|
||||
"background_color": "#0f1115",
|
||||
"theme_color": "#0f1115",
|
||||
"categories": [
|
||||
"productivity",
|
||||
"utilities"
|
||||
],
|
||||
"lang": "en",
|
||||
"icons": [
|
||||
{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" }
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Files",
|
||||
"url": "/#/files",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Calendar",
|
||||
"url": "/#/calendar",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Tasks",
|
||||
"url": "/#/tasks",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Music",
|
||||
"url": "/#/music",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -240,3 +240,9 @@ textarea { padding: 12px 14px; resize: vertical; font-family: inherit; }
|
||||
.donetask .tick { color: var(--accent); }
|
||||
.donetask .tick circle { fill: var(--accent); stroke: var(--accent); }
|
||||
.donetask .tick path { stroke: var(--accent-t); }
|
||||
|
||||
/* iOS home-screen app polish */
|
||||
html { -webkit-text-size-adjust: 100%; overscroll-behavior: none; }
|
||||
body { -webkit-touch-callout: none; }
|
||||
button, a, .row { -webkit-user-select: none; user-select: none; }
|
||||
input, textarea, [contenteditable], .sheet p, .card p, #view .sub { -webkit-user-select: text; user-select: text; }
|
||||
@@ -1,6 +1,6 @@
|
||||
// App-shell cache only; Seafile/API traffic always goes to the network.
|
||||
const CACHE = 'hub-v3';
|
||||
const SHELL = ['/', '/style.css', '/app.js', '/icon.svg', '/manifest.webmanifest'];
|
||||
const CACHE = 'hub-v4';
|
||||
const SHELL = ['/style.css', '/app.js', '/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];
|
||||
|
||||
self.addEventListener('install', e => {
|
||||
e.waitUntil(caches.open(CACHE).then(c => c.addAll(SHELL)));
|
||||
|
||||
@@ -166,7 +166,7 @@ const unsigned = v => { const i = String(v || '').lastIndexOf('.'); return i > 0
|
||||
const parseCookies = req => Object.fromEntries((req.headers.cookie || '').split(/; */).filter(Boolean).map(c => { const i = c.indexOf('='); return [c.slice(0, i), decodeURIComponent(c.slice(i + 1))]; }));
|
||||
const setCookies = (req, res, obj, maxAge = 31536000) => {
|
||||
const secure = (req.headers['x-forwarded-proto'] || '') === 'https' ? '; Secure' : '';
|
||||
res.setHeader('Set-Cookie', Object.entries(obj).map(([k, v]) => `${k}=${encodeURIComponent(v)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${v === '' ? 0 : maxAge}${secure}`));
|
||||
for (const [k, v] of Object.entries(obj)) auth.addCookie(res, `${k}=${encodeURIComponent(v)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${v === '' ? 0 : maxAge}${secure}`);
|
||||
};
|
||||
const plexTv = async (path, token, opts = {}) => {
|
||||
const r = await fetch('https://plex.tv' + path, { ...opts, headers: { ...PLEX_HEADERS, ...(token ? { 'X-Plex-Token': token } : {}) } });
|
||||
@@ -289,8 +289,9 @@ http.createServer((req, res) => {
|
||||
if (pathname.startsWith('/cal/feed/')) return cal.feed(req, res); // subscribe feed: protected by its own secret token, not the session
|
||||
// Everything else needs a passkey session, except the sign-in page and its assets,
|
||||
// and OnlyOffice's save callback (which carries its own signed token).
|
||||
if (auth.isAuthed(req)) auth.refresh(req, res);
|
||||
const open = ['/login', '/login.js', '/webauthn.js', '/style.css', '/icon.svg', '/manifest.webmanifest', '/sw.js', '/api/oo/callback'];
|
||||
if (!open.includes(pathname) && !auth.isAuthed(req)) {
|
||||
if (!open.includes(pathname) && !pathname.startsWith('/icons/') && !auth.isAuthed(req)) {
|
||||
if (/^\/(api|sf|plex)\//.test(pathname)) { res.writeHead(401, { 'content-type': 'application/json', 'x-hub-auth': 'required' }); return res.end('{"error":"Sign in required"}'); }
|
||||
res.writeHead(302, { location: '/login' }); return res.end();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generates the PWA icons and iOS splash screens into public/icons. Needs Pillow: pip install pillow"""
|
||||
import os
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
OUT = os.path.join(os.path.dirname(__file__), '..', 'public', 'icons')
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
SS = 4 # supersampling for smooth edges
|
||||
|
||||
def glyph(size, fg_scale=0.56, bg=((0x5b, 0x86, 0xff), (0x3a, 0x5c, 0xe6)), rounded=0.0):
|
||||
"""The app mark: four rounded tiles on a blue gradient. rounded>0 rounds the outer corners (for splash screens)."""
|
||||
S = size * SS
|
||||
img = Image.new('RGB', (S, S))
|
||||
px = img.load() if False else None
|
||||
# vertical gradient
|
||||
top, bot = bg
|
||||
grad = Image.new('RGB', (1, S))
|
||||
for y in range(S):
|
||||
t = y / (S - 1)
|
||||
grad.putpixel((0, y), tuple(int(top[i] + (bot[i] - top[i]) * t) for i in range(3)))
|
||||
img = grad.resize((S, S))
|
||||
overlay = Image.new('RGBA', (S, S), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(overlay)
|
||||
g = S * fg_scale # glyph box
|
||||
gap = g * 0.10
|
||||
tile = (g - gap) / 2
|
||||
x0 = (S - g) / 2
|
||||
r = tile * 0.24
|
||||
for i, (cx, cy, a) in enumerate([(0, 0, 255), (1, 0, 180), (0, 1, 180), (1, 1, 255)]):
|
||||
x = x0 + cx * (tile + gap); y = x0 + cy * (tile + gap)
|
||||
d.rounded_rectangle([x, y, x + tile, y + tile], radius=r, fill=(255, 255, 255, a))
|
||||
img = Image.alpha_composite(img.convert('RGBA'), overlay)
|
||||
if rounded:
|
||||
mask = Image.new('L', (S, S), 0)
|
||||
ImageDraw.Draw(mask).rounded_rectangle([0, 0, S - 1, S - 1], radius=S * rounded, fill=255)
|
||||
out = Image.new('RGBA', (S, S), (0, 0, 0, 0)); out.paste(img, (0, 0), mask); img = out
|
||||
return img.resize((size, size), Image.LANCZOS)
|
||||
|
||||
def save(img, name, flatten=True):
|
||||
if flatten and img.mode == 'RGBA': # iOS fills transparency with black, so icons must be opaque
|
||||
bg = Image.new('RGB', img.size, (0x3a, 0x5c, 0xe6)); bg.paste(img, mask=img.split()[3]); img = bg
|
||||
img.save(os.path.join(OUT, name), optimize=True)
|
||||
|
||||
# ---- Icons
|
||||
for n in (180, 167, 152, 120):
|
||||
save(glyph(n), f'apple-touch-icon-{n}.png')
|
||||
save(glyph(180), 'apple-touch-icon.png') # iOS default lookup
|
||||
save(glyph(192), 'icon-192.png'); save(glyph(512), 'icon-512.png')
|
||||
save(glyph(512, fg_scale=0.50), 'icon-maskable-512.png') # artwork stays inside the 80% safe zone
|
||||
save(glyph(192, fg_scale=0.50), 'icon-maskable-192.png')
|
||||
save(glyph(32), 'favicon-32.png')
|
||||
|
||||
# ---- iOS splash screens: dark background with the app mark. (width, height, css width, css height, pixel ratio)
|
||||
BG = (0x0f, 0x11, 0x15)
|
||||
PHONES = [(1320, 2868, 440, 956, 3), (1206, 2622, 402, 874, 3), (1290, 2796, 430, 932, 3), (1179, 2556, 393, 852, 3), (1284, 2778, 428, 926, 3),
|
||||
(1170, 2532, 390, 844, 3), (1125, 2436, 375, 812, 3), (1242, 2688, 414, 896, 3), (828, 1792, 414, 896, 2), (1242, 2208, 414, 736, 3), (750, 1334, 375, 667, 2)]
|
||||
PADS = [(2048, 2732, 1024, 1366, 2), (1668, 2388, 834, 1194, 2), (1668, 2224, 834, 1112, 2), (1640, 2360, 820, 1180, 2), (1620, 2160, 810, 1080, 2),
|
||||
(1488, 2266, 744, 1133, 2), (1536, 2048, 768, 1024, 2)]
|
||||
links = []
|
||||
def splash(w, h, cw, ch, pr, orient):
|
||||
img = Image.new('RGB', (w, h), BG)
|
||||
m = int(min(w, h) * 0.22)
|
||||
mark = glyph(m, rounded=0.225)
|
||||
img.paste(mark, ((w - m) // 2, (h - m) // 2), mark)
|
||||
name = f'splash-{w}x{h}.png'
|
||||
img.save(os.path.join(OUT, name), optimize=True)
|
||||
links.append((name, cw, ch, pr, orient))
|
||||
for (w, h, cw, ch, pr) in PHONES: splash(w, h, cw, ch, pr, 'portrait')
|
||||
for (w, h, cw, ch, pr) in PADS:
|
||||
splash(w, h, cw, ch, pr, 'portrait')
|
||||
splash(h, w, ch, cw, pr, 'landscape') # the same device on its side
|
||||
|
||||
with open(os.path.join(OUT, 'splash-links.html'), 'w') as f:
|
||||
for name, cw, ch, pr, o in links:
|
||||
f.write(f'<link rel="apple-touch-startup-image" href="/icons/{name}" media="(device-width: {cw}px) and (device-height: {ch}px) and (-webkit-device-pixel-ratio: {pr}) and (orientation: {o})">\n')
|
||||
print(len(links), 'splash screens')
|
||||