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?`โ†—`:''} + `; }