From 8a309eb6528a64e5bd596bfaf562359041ffca86 Mon Sep 17 00:00:00 2001 From: rays Date: Fri, 11 Sep 2026 14:57:03 +0000 Subject: [PATCH] Files pane and item buttons are icons, with the words in tooltips Save, delete, view and download in the Files pane, and mark read, keep and open the original under an item's title, are icons now. The word is in each one's title and aria-label, so it is still there on hover and for screen readers. Tests find them by title or action, not text. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn --- CHANGELOG.md | 2 ++ tests/ui/app.spec.js | 21 +++++++++++---------- web/index.html | 30 +++++++++++++++++++----------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c4dd42..6561c97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ 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. ### Security diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index fe327dd..f23e1d7 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -78,7 +78,7 @@ test('the three panes are there and the item text lands in the bottom one', asyn const downloaded = page.locator('.ep', { hasText: 'downloaded' }).first(); await downloaded.click(); await expect(page.locator('#files audio')).toBeVisible(); - await expect(page.locator('#files .encbox .btn', { hasText: 'Save' })).toBeVisible(); + await expect(page.locator('#files .encbox [title="Save to this computer"]')).toBeVisible(); // Selecting another item replaces the pane rather than stacking. await page.locator('.ep', { hasText: 'First Episode' }).click(); @@ -101,8 +101,8 @@ test('a downloaded file that is not audio gets no player', async ({ page }) => { await expect(page.locator('#files .encbox')).toContainText('image'); await expect(page.locator('#files .encbox')).toContainText('downloaded'); // Still offered as a file, just not as an episode: viewable and keepable. - await expect(page.locator('#files .btn', { hasText: 'Save' })).toBeVisible(); - const view = page.locator('#files a', { hasText: 'View' }); + await expect(page.locator('#files [title="Save to this computer"]')).toBeVisible(); + const view = page.locator('#files a[title="View in a new tab"]'); await expect(view).toHaveAttribute('target', '_blank'); await expect(view).toHaveAttribute('rel', /noopener/); await expect(view).toHaveAttribute('href', /\/media\/\d+/); @@ -213,7 +213,7 @@ test('inside an OPML, feeds with unread items are listed first', async ({ page } await side.getByText('Grouped Show').click(); const gs = page.locator('.ep', { hasText: 'Grouped Ep' }); await gs.click(); - await page.locator('#detail button', { hasText: 'Mark unread' }).click(); + await page.locator('#detail button[title="Mark unread"]').click(); await expect(gs).not.toHaveClass(/read/); // Aardvark comes first alphabetically and in the OPML, so only the unread sort puts @@ -281,9 +281,9 @@ test('opening an item marks it read, and the toggle flips it back', async ({ pag // Another test may have opened this item already, so start from a known state: the // toggle in the text below flips it back -- which used to recurse until the stack blew. await row().click(); - await page.locator('#detail button', { hasText: 'Mark unread' }).click(); + await page.locator('#detail button[title="Mark unread"]').click(); await expect(row()).not.toHaveClass(/read/); - await expect(page.locator('#detail button', { hasText: 'Mark read' })).toBeVisible(); + await expect(page.locator('#detail button[title="Mark read"]')).toBeVisible(); // Opening it is reading it. await row().click(); @@ -375,8 +375,9 @@ test('deleting a shared file warns that it is everyone\'s copy', async ({ page } await expect(row).toBeVisible({ timeout: 20_000 }); await row.click(); - const del = page.locator('#files button', { hasText: 'Delete' }); - await expect(del).toHaveText('Delete for everyone'); + // An icon now; what it does, and to whom, is in its tooltip. + const del = page.locator('#files button[data-a="del"]'); + await expect(del).toHaveAttribute('title', /^Delete for everyone \(shared with 1 other person/); // Two prompts: the page's own, then the server's, because someone else has not played // it. Accept the first, decline the second, and the file must survive. @@ -657,12 +658,12 @@ test('a deleted file looks as if it was never downloaded', async ({ page }) => { const row = page.locator('.ep', { hasText: 'An Article' }); await expect(row).toBeVisible({ timeout: 20_000 }); await row.click(); - await page.locator('#files button', { hasText: 'Delete' }).click(); + await page.locator('#files button[data-a="del"]').click(); // No "reaped", no chip at all: just the way to get it again. await expect(row).not.toContainText('downloaded'); await expect(row).not.toContainText(/reaped/i); await row.click(); await expect(page.locator('#files')).not.toContainText(/reaped/i); - await expect(page.locator('#files .btn', { hasText: 'Download' })).toBeVisible(); + await expect(page.locator('#files button[title="Download to the server"]')).toBeVisible(); }); diff --git a/web/index.html b/web/index.html index aa707fb..5b94539 100644 --- a/web/index.html +++ b/web/index.html @@ -214,6 +214,8 @@ a.btn{text-decoration:none;color:inherit} .btn.primary{background:var(--accent);border-color:var(--accent);color:var(--ink);font-weight:600} .btn.primary:hover{filter:brightness(1.08)} .btn.danger:hover{border-color:var(--bad);color:var(--bad)} +/* An icon in place of a word; the word is in its tooltip. */ +.btn.ico{padding:4px 9px;min-width:32px;font-size:14px;line-height:1.25;text-align:center} .toolbar{ display:flex;gap:10px;align-items:center;margin-bottom:12px;flex-wrap:wrap; position:sticky;top:0;background:var(--bg);padding:6px 0 8px;z-index:3; @@ -983,9 +985,12 @@ function showDetail(e){ ${num?`${num}`:''} ${dateOf(e.published)} ${e.duration?`${clock(e.duration)}`:''} - - - ${e.link?`Open original \u2197`:''} + + + ${e.link?`\u2197`:''}
${(e.description&&e.description.trim())||'No show notes.'}
${narrow?encs:''}`; @@ -1006,16 +1011,19 @@ function encBox(x){ // One file serves everyone reading the feed, so deleting is not a private act. const f=S.feeds.find(y=>y.id===x.feed_id); const shared=f&&f.subscribers>1; - const delBtn=``; + // Icons, with the words in the tooltip and for screen readers. + const delLabel=shared + ? `Delete for everyone (shared with ${f.subscribers-1} other ${f.subscribers===2?'person':'people'} reading this feed)` + : 'Delete file'; + const delBtn=``; + const saveBtn=`๐Ÿ’พ`; if(x.path && !isPlayable(x)){ // On disk, but not audio or video: view it, keep it, or remove it -- no player. return `
${esc(kindOf(x))} downloaded${size?' \u00b7 '+size:''} - View - Save + โ†— + ${saveBtn} ${delBtn}
`; } @@ -1023,7 +1031,7 @@ function encBox(x){ return `
${size} - Save + ${saveBtn} ${delBtn}
`; } @@ -1036,8 +1044,8 @@ function encBox(x){ ${x.state==='reaped'?'':`${x.state==='skipped'?kindOf(x):esc(x.state)}`} ${esc(kindOf(x))}${size?' \u00b7 '+size:''} ${x.state==='error'&&x.last_error?`${esc(x.last_error)}`:''} - ${viewable?`View`:''} - + ${viewable?`โ†—`:''} + `; }