From 1e51bdec7cc5f7d85c84e95e19124bee27a8e73d Mon Sep 17 00:00:00 2001 From: th
loading…
Failed to load: ${escapeHtml(e.message)}
${escapeHtml(code)}
${escapeHtml(c)}
+ src = src.replace(/(^|\n)((?:> [^\n]*\n?)+)/g, (_m, pre, block) => { + const inner = block.replace(/^> ?/gm, '').trim(); + return `${pre}${inner.replace(/\n/g, '')}\n`; + }); + + // Lists: grouped runs of "- " / "* " / "N." items + src = src.replace(/(^|\n)((?:[-*]\s+[^\n]+\n?)+)/g, (_m, pre, block) => { + const items = block.trim().split(/\n/).map(l => l.replace(/^[-*]\s+/, '')).map(i => `${i}`).join(''); + return `${pre}${items}\n`; + }); + src = src.replace(/(^|\n)((?:\d+\.\s+[^\n]+\n?)+)/g, (_m, pre, block) => { + const items = block.trim().split(/\n/).map(l => l.replace(/^\d+\.\s+/, '')).map(i => `${i}`).join(''); + return `${pre}${items}\n`; + }); + + // Bold + italic (do bold first to avoid partial capture) + src = src.replace(/\*\*([^*\n]+)\*\*/g, '$1'); + src = src.replace(/(?$1'); + + // Paragraphs: wrap any run of non-block-level lines. Block-level tags + // we've already emitted stay as-is. + const blocks = src.split(/\n{2,}/); + return blocks.map(b => { + const t = b.trim(); + if (!t) return ''; + if (/^<(h[1-6]|ul|ol|pre|blockquote|hr|img)/.test(t)) return t; + return `${t.replace(/\n/g, '')}`; + }).join('\n'); +} + +function escapeAttr(s) { + return String(s ?? '').replace(/["&<>]/g, c => ({ '"': '"', '&': '&', '<': '<', '>': '>' }[c])); +} + +$('read-back')?.addEventListener('click', closeReader); +$('read-delete')?.addEventListener('click', async () => { + if (!readCurrentUid) return; + if (!await relayConfirm('Remove this item from your read list?', { okLabel: 'Remove', danger: true })) return; + const uid = readCurrentUid; + try { + await api('api/read/' + encodeURIComponent(uid), { method: 'DELETE' }); + closeReader(); + loadReadList(); + toast('removed'); + } catch (e) { + toast(e.message || 'failed', true); + } +}); + +// Browser back should close the reader first, then leave the view. +window.addEventListener('keydown', e => { + if (e.key === 'Escape' && readCurrentUid) { + e.preventDefault(); + closeReader(); + } +}); diff --git a/relay/ui/layout.php b/relay/ui/layout.php index 23e37b6..28db37c 100644 --- a/relay/ui/layout.php +++ b/relay/ui/layout.php @@ -28,6 +28,7 @@ if (!defined('RELAY_BOOTSTRAPPED')) { http_response_code(404); exit; } inbox chat shopping + read account users logout @@ -234,6 +235,26 @@ if (!defined('RELAY_BOOTSTRAPPED')) { http_response_code(404); exit; } + + + + + + No items yet. Toggle Read later on a card in hyprcrd to send it here. + + + + + + + + + + + + + + diff --git a/relay/ui/style.css b/relay/ui/style.css index cab2c70..6a3a925 100644 --- a/relay/ui/style.css +++ b/relay/ui/style.css @@ -839,3 +839,210 @@ header .nav-btn .ph { font-size:11px; vertical-align:-1px; margin-right:2px; } .auth-form h2 { font-size: 16px; margin-bottom: 8px; } .auth-form .auth-icon { font-size: 44px; } } + +/* ───────────────────────────────────────────────────────────────── + * Read view — list + distraction-free zen reader. Mobile-first; the + * article width is capped tight for comfortable line-lengths and the + * type scale is larger than anywhere else in the UI because this is + * meant for actual reading, not scanning. + * ───────────────────────────────────────────────────────────────── */ + +.read-wrap { + max-width: 720px; + margin: 0 auto; + padding: 24px 16px 48px; +} + +.read-empty { + text-align: center; + padding: 64px 24px; + color: var(--dim); + font-size: 13px; + line-height: 1.6; +} +.read-empty-icon { font-size: 48px; display: block; margin: 0 auto 16px; color: var(--muted); } + +.read-items { + list-style: none; + display: flex; + flex-direction: column; + gap: 10px; +} +.read-item { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 14px 16px; + cursor: pointer; + transition: border-color 0.15s, background 0.15s; + display: flex; + gap: 14px; + align-items: center; +} +.read-item:hover { border-color: var(--accent); background: var(--raised); } +.read-item-thumb { + width: 64px; height: 64px; flex-shrink: 0; + border-radius: var(--radius); + background: var(--bg) center/cover no-repeat; + border: 1px solid var(--border); +} +.read-item-thumb-placeholder { + display: flex; align-items: center; justify-content: center; + color: var(--dim); + font-size: 24px; +} +.read-item-body { flex: 1; min-width: 0; } +.read-item-title { + color: var(--bright); + font-size: 14px; + font-weight: 500; + line-height: 1.4; + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.read-item-meta { + font-family: var(--pixel); + font-size: 9px; + color: var(--dim); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Reader overlay: covers the main content area; header stays visible + * so the user can navigate back via the burger / nav buttons. */ +.read-reader { + position: fixed; + inset: 56px 0 0 0; + background: var(--bg); + z-index: 80; + overflow-y: auto; + display: flex; + flex-direction: column; +} +.read-reader-bar { + position: sticky; + top: 0; + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + background: color-mix(in srgb, var(--bg) 92%, transparent); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border-bottom: 1px solid var(--border); + z-index: 2; +} +.read-reader-btn { + background: transparent; + border: 1px solid var(--border); + color: var(--text); + width: 36px; height: 36px; + border-radius: var(--radius); + cursor: pointer; + display: flex; align-items: center; justify-content: center; + font-size: 16px; + transition: border-color 0.15s, color 0.15s; +} +.read-reader-btn:hover { border-color: var(--accent); color: var(--accent); } +.read-reader-btn-danger:hover { border-color: var(--danger); color: var(--danger); } + +.read-article { + max-width: 680px; + width: 100%; + margin: 0 auto; + padding: 24px 20px 96px; + font-size: 17px; + line-height: 1.7; + color: var(--text); +} +.read-article .read-hero { + display: block; + width: 100%; + max-height: 360px; + object-fit: cover; + border-radius: var(--radius-lg); + margin-bottom: 24px; + background: var(--surface); +} +.read-article h1.read-title { + font-family: var(--mono); + font-size: 26px; + line-height: 1.25; + font-weight: 700; + color: var(--bright); + margin-bottom: 6px; +} +.read-article .read-date { + font-family: var(--pixel); + font-size: 10px; + color: var(--dim); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 28px; +} +.read-article h1, .read-article h2, .read-article h3 { + color: var(--bright); + line-height: 1.3; + margin-top: 28px; + margin-bottom: 12px; +} +.read-article h2 { font-size: 20px; } +.read-article h3 { font-size: 17px; } +.read-article p { margin: 0 0 16px; } +.read-article a { color: var(--accent); text-decoration: underline; } +.read-article ul, .read-article ol { padding-left: 24px; margin: 0 0 16px; } +.read-article li { margin-bottom: 6px; } +.read-article code { + font-family: var(--mono); + font-size: 0.9em; + background: var(--surface); + padding: 1px 5px; + border-radius: 3px; +} +.read-article pre { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 14px 16px; + overflow-x: auto; + margin: 0 0 16px; +} +.read-article pre code { background: none; padding: 0; } +.read-article blockquote { + border-left: 3px solid var(--accent); + padding-left: 14px; + color: var(--muted); + margin: 0 0 16px; +} +.read-article img { + max-width: 100%; + height: auto; + border-radius: var(--radius); + margin: 12px 0; +} +.read-article hr { + border: none; + border-top: 1px solid var(--border); + margin: 28px 0; +} +.read-article table { + border-collapse: collapse; + margin: 0 0 16px; + width: 100%; +} +.read-article th, .read-article td { + border: 1px solid var(--border); + padding: 8px 10px; + text-align: left; +} + +/* Mobile: pad tighter, bump touch targets */ +@media (max-width: 640px) { + .read-article { font-size: 16px; padding: 20px 16px 96px; } + .read-article h1.read-title { font-size: 22px; } + .read-reader { inset: 52px 0 0 0; } + .read-reader-btn { width: 40px; height: 40px; font-size: 18px; } + .read-item-thumb { width: 52px; height: 52px; } +} diff --git a/src/Database.php b/src/Database.php index 5d62087..8d9f856 100644 --- a/src/Database.php +++ b/src/Database.php @@ -124,6 +124,13 @@ class Database if (!in_array('relay_uid', $colNames)) { self::$pdo->exec('ALTER TABLE cards ADD COLUMN relay_uid TEXT DEFAULT NULL'); } + // Read-later push: when 1, saves propagate the card to the relay's + // /api/read endpoint so the user can read it in the mobile-friendly + // zen reader. Explicitly NOT tag-based (so reading-mode is + // independent of whatever tags the user already organises by). + if (!in_array('read_push', $colNames)) { + self::$pdo->exec('ALTER TABLE cards ADD COLUMN read_push INTEGER NOT NULL DEFAULT 0'); + } // Relay pushed message IDs (skip on own poll) self::$pdo->exec('
${inner.replace(/\n/g, '')}
${t.replace(/\n/g, '')}
No items yet. Toggle Read later on a card in hyprcrd to send it here.