diff --git a/CHANGELOG.md b/CHANGELOG.md index 6561c97..a4370e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ The long form, with what was wrong before and how it was found, is in ### Added +- A Classic theme after the 2004 Mac app, beside Dark and Light: brushed-metal toolbar, Aqua + blue selection, red unread badges, a striped table and Lucida Grande. The theme button steps + through all three and remembers the choice. - A toolbar across the top, after the original iPodderX: add and unsubscribe, play, mark read and keep for the selected item, scan, a search box for what is showing, and Settings and Log. - Directory, Popular and All Subscriptions at the top of the feed list, opening in the main pane. @@ -28,8 +31,10 @@ The long form, with what was wrong before and how it was found, is in items show without pressing Scan. - A file deleted to save space, or by hand, looks as if it was never downloaded: no "reaped" label, just the Download button. The retention summary says "deleted", not "reaped". -- The Files pane and an item's own buttons are icons, with the words in their tooltips: save, - delete, view, download, mark read, keep, and open the original. +- Buttons are icons, with the words in their tooltips: the Files pane (save, delete, view, + download), an item's own buttons (mark read, keep, open the original), the feed header (scan, + download latest, mark all read, settings, unsubscribe), and the Settings, feed settings and + Download latest dialogs (save, download, cancel). ### Security diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index f23e1d7..19de1a0 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -29,6 +29,18 @@ test('the theme toggle actually changes the theme', async ({ page }) => { .not.toBe(before); }); +test('the theme button steps through dark, light and classic, and remembers', async ({ page }) => { + const theme = () => page.evaluate(() => document.documentElement.dataset.theme); + for (let i = 0; i < 3 && (await theme()) !== 'classic'; i++) await page.locator('#theme').click(); + expect(await theme()).toBe('classic'); + await expect(page.locator('#theme')).toHaveAttribute('title', /Classic.*Click for Dark/); + + await page.reload(); + await expect.poll(theme).toBe('classic'); + // The 2004 Mac app set its type in Lucida Grande. + expect(await page.evaluate(() => getComputedStyle(document.body).fontFamily)).toContain('Lucida Grande'); +}); + test('settings opens and saves the global schedule', async ({ page }) => { await page.locator('#prefs').click(); await expect(page.locator('#modal.on')).toBeVisible(); @@ -136,7 +148,7 @@ test('the filter tabs change what is listed', async ({ page }) => { test('a feed URL is editable and has a copy button', async ({ page }) => { await page.getByText('Test Show').click(); - await page.locator('.btn', { hasText: 'Settings' }).first().click(); + await page.locator('#content .acts [data-a="settings"]').click(); await expect(page.locator('#surl')).toHaveValue(/show\.xml/); await expect(page.locator('#scopy')).toBeVisible(); @@ -239,7 +251,7 @@ test('marking an OPML subscription read covers the feeds inside it', async ({ pa await expect(folder.locator('.badge')).not.toHaveText('0'); await folder.click(); - await page.locator('#content .acts button', { hasText: 'Mark all read' }).click(); + await page.locator('#content .acts [data-a="read"]').click(); await expect(folder.locator('.badge')).toHaveText('0'); }); diff --git a/web/index.html b/web/index.html index 5b94539..9d6f933 100644 --- a/web/index.html +++ b/web/index.html @@ -44,6 +44,26 @@ --bad:#b3402f; --shadow:0 8px 28px rgba(45,83,145,.14); } +/* Classic: the 2004 Mac app. Colours here; the chrome it needs is at the end of the sheet. */ +:root[data-theme="classic"] { + color-scheme:light; + --bg:#ffffff; + --panel:#e7ebf1; /* the source list's pale blue-grey */ + --panel2:#f2f2f2; + --raise:#d5dce7; + --line:#a9a9a9; + --fg:#000000; + --dim:#444444; + --faint:#666666; + --accent:#3875d7; /* Aqua selection blue */ + --accent2:#2a5db0; + --ink:#ffffff; + --good:#237a23; + --warn:#a15f00; + --bad:#c42b1c; + --shadow:0 4px 16px rgba(0,0,0,.28); + --r:5px; +} *{box-sizing:border-box} /* A rule that sets display beats the UA's [hidden], and several below do. */ [hidden]{display:none!important} @@ -427,6 +447,54 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu #logbox .lv{width:auto} #logbox .tg{display:none} } + +/* ---------- Classic: the 2004 Mac app ---------- */ +/* After the iPodderX screenshots: brushed-metal chrome, a pale blue-grey source list, Aqua blue + for whatever is selected, red unread badges, a striped table and Lucida Grande. */ +:root[data-theme="classic"] body{ + font-family:"Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Verdana,sans-serif;font-size:13px; +} +:root[data-theme="classic"] #topbar, +:root[data-theme="classic"] #status, +:root[data-theme="classic"] #player{background:linear-gradient(#ececec,#c9c9c9);border-color:#8e8e8e;color:#1a1a1a} +:root[data-theme="classic"] #status{text-align:center} +:root[data-theme="classic"] #sidebar{background:#e7ebf1;border-right-color:#a3a3a3} +:root[data-theme="classic"] #files{background:#f4f4f4} +:root[data-theme="classic"] .tgroup{background:none;border-color:#8e8e8e;border-radius:6px} +:root[data-theme="classic"] .tgroup button+button{border-left-color:#8e8e8e} +:root[data-theme="classic"] .tgroup button, +:root[data-theme="classic"] .btn, +:root[data-theme="classic"] .tabs{background:linear-gradient(#ffffff,#d9d9d9);border-color:#8e8e8e;color:#1a1a1a} +:root[data-theme="classic"] .tabs{border:1px solid #8e8e8e;border-radius:6px} +:root[data-theme="classic"] .tgroup button:hover:not(:disabled), +:root[data-theme="classic"] .btn:hover{background:linear-gradient(#f5f9ff,#cbdbf4)} +:root[data-theme="classic"] .btn.primary, +:root[data-theme="classic"] .tabs button.on, +:root[data-theme="classic"] .feed.sel, +:root[data-theme="classic"] .place.sel{background:linear-gradient(#86b0ec,#3a73cf);border-color:#2d5eae;color:#fff} +:root[data-theme="classic"] .feed.sel .txt small, +:root[data-theme="classic"] .place.sel .ico{color:#e6eeff} +:root[data-theme="classic"] #epSearch{border-radius:14px} +:root[data-theme="classic"] .badge{background:#c9302c;color:#fff} +:root[data-theme="classic"] .badge.zero{background:#b4bdc9;color:#fff} +:root[data-theme="classic"] .ephead, +:root[data-theme="classic"] .fhd{ + background:linear-gradient(#fbfbfb,#dcdcdc);color:#1a1a1a;text-transform:none;letter-spacing:0;font-size:11px; +} +:root[data-theme="classic"] .fhd{padding:3px 6px;border:1px solid #c2c2c2} +:root[data-theme="classic"] .ephead{border-bottom-color:#a3a3a3} +:root[data-theme="classic"] .ep{border-radius:0;margin:0} +:root[data-theme="classic"] #eps .ep:nth-child(even){background:#edf3fe} +:root[data-theme="classic"] .ep .t{color:#1c4fa8} +:root[data-theme="classic"] .ep.read .t{color:#333} +:root[data-theme="classic"] #eps .ep.sel{background:#3875d7;border-color:#3875d7} +:root[data-theme="classic"] .ep.sel .t, +:root[data-theme="classic"] .ep.sel .fd, +:root[data-theme="classic"] .ep.sel .date, +:root[data-theme="classic"] .ep.sel .line, +:root[data-theme="classic"] .ep.sel .st, +:root[data-theme="classic"] .ep.sel .fl{color:#fff} +:root[data-theme="classic"] .dt{background:linear-gradient(#80aae6,#3f78cf);color:#fff;padding:6px 12px;border-radius:4px} @@ -692,11 +760,11 @@ function renderFeed(){ ${f.group?`
From the OPML subscription ${esc(f.group)}
`:''}
- - - - - + + + + +
` : `
@@ -748,10 +816,10 @@ function renderGroup(f,kids){ ${gone?`
${gone} feed${gone===1?' is':'s are'} no longer listed in this OPML but kept because ${gone===1?'it has':'they have'} downloads.
`:''}
- - - - + + + +
@@ -1442,8 +1510,8 @@ async function prefsModal(){
Add and remove the people who can sign in, and choose who is an admin.
-
-
`); +
+
`); $('#gopml').onclick=opmlModal; $('#gusers').onclick=usersModal; $('#gsave').onclick=async()=>{ @@ -1539,8 +1607,8 @@ function settingsModal(f){ Where the files land. There is one copy however many people subscribe, so this is the same for everyone.`:''} -
-
`); +
+
`); $('#scopy').onclick=()=>copyText($('#surl').value,$('#scopy')); $('#ssave').onclick=async()=>{ const max=$('#smax').value; @@ -1566,8 +1634,8 @@ function downloadLatestModal(f){
Queued immediately, ignoring the per-scan limit.
-
-
`); +
+
`); $('#dgo').onclick=async()=>{ const n=Number($('#dcount').value)||5; try{ @@ -1640,11 +1708,18 @@ on('#logs','onclick',logsModal); $('#feedFilter').oninput=renderFeeds; $('#burger').onclick=()=>nav(!$('#sidebar').classList.contains('open')); $('#scrim').onclick=()=>nav(false); -$('#theme').onclick=()=>{ - const cur=document.documentElement.dataset.theme==='light'?'dark':'light'; - document.documentElement.dataset.theme=cur; localStorage.setItem('ipx.theme',cur); -}; -if(localStorage.getItem('ipx.theme')) document.documentElement.dataset.theme=localStorage.getItem('ipx.theme'); +// Dark, light, and the 2004 Mac app. The button steps through them; the choice is remembered. +const THEMES={dark:'Dark',light:'Light',classic:'Classic, the 2004 Mac app'}; +const THEME_ORDER=Object.keys(THEMES); +const nextTheme=t=>THEME_ORDER[(THEME_ORDER.indexOf(t)+1)%THEME_ORDER.length]; +function setTheme(t){ + if(!THEMES[t]) t='dark'; + document.documentElement.dataset.theme=t; + $('#theme').title=`Theme: ${THEMES[t]}. Click for ${THEMES[nextTheme(t)]}`; + try{ localStorage.setItem('ipx.theme',t); }catch{} +} +$('#theme').onclick=()=>setTheme(nextTheme(document.documentElement.dataset.theme)); +try{ setTheme(localStorage.getItem('ipx.theme')); }catch{ setTheme('dark'); } /* ---------------- live events ---------------- */ let sse;