style(magazine): separate driving-mode index rows with dividers and padding
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2ca79af530
commit
f120f3d4f4
@ -20,6 +20,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
- **Sentence timings now also work for interview renders.** Every recorded audio file carries `"interview": true` (set whenever a second voice is configured, even without speaker labels), and the timings endpoint and EPUB export discarded all such records, so the player and the EPUB fell back to estimated sentence positions for every current article. The record is now matched to the text again, for interview renders by the label-stripped length the renderer recorded, so `/timings` returns exact (or anchored) sentence times and speech onsets for these articles too. Records for text edited since the render are still ignored.
|
- **Sentence timings now also work for interview renders.** Every recorded audio file carries `"interview": true` (set whenever a second voice is configured, even without speaker labels), and the timings endpoint and EPUB export discarded all such records, so the player and the EPUB fell back to estimated sentence positions for every current article. The record is now matched to the text again, for interview renders by the label-stripped length the renderer recorded, so `/timings` returns exact (or anchored) sentence times and speech onsets for these articles too. Records for text edited since the render are still ignored.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- **Driving mode index rows are clearly separated.** Each row now has 13 px padding above and below and a thin divider line (the highlighted current row has none), replacing the bare gap, so multi-line titles with the play button, number, duration and status icon read as individual items. Hit areas stay 44 px and the 113-row list still renders at once.
|
||||||
- **Driving mode index rows have more breathing room and lighter play buttons.** Rows now have 9 px vertical gap between them, staying tappable and scrollable. Each row's play/resume/replay button is now a light circle with a blue icon (matching the magazine playlist), styled with a surface background, subtle border and the app's primary blue for the icon, instead of a solid blue circle; the visual circle is 34 px with a 44 px accessible touch target maintained via padding.
|
- **Driving mode index rows have more breathing room and lighter play buttons.** Rows now have 9 px vertical gap between them, staying tappable and scrollable. Each row's play/resume/replay button is now a light circle with a blue icon (matching the magazine playlist), styled with a surface background, subtle border and the app's primary blue for the icon, instead of a solid blue circle; the visual circle is 34 px with a 44 px accessible touch target maintained via padding.
|
||||||
- **Driving mode: one display menu instead of the three-icon switch; fewer controls in the dock.** The top bar now has *Share*, a *menu* button (shows the current view), *Index* and *Exit*. The menu opens a flyout with *Picture*, *Lyrics* and *Compact* plus *Show text*, *Show pictures* (moved from the dock) and *Minimize text and picture*; it works with arrow keys, Home/End, Enter and Escape, closes on an outside tap, keeps 44 px targets, stays inside the screen from 320 px wide and never covers the dock. The playback speed button moved to the end of the slider row, after the time. The dock is now just the slider row and the transport buttons. Picture zoom stays on the picture.
|
- **Driving mode: one display menu instead of the three-icon switch; fewer controls in the dock.** The top bar now has *Share*, a *menu* button (shows the current view), *Index* and *Exit*. The menu opens a flyout with *Picture*, *Lyrics* and *Compact* plus *Show text*, *Show pictures* (moved from the dock) and *Minimize text and picture*; it works with arrow keys, Home/End, Enter and Escape, closes on an outside tap, keeps 44 px targets, stays inside the screen from 320 px wide and never covers the dock. The playback speed button moved to the end of the slider row, after the time. The dock is now just the slider row and the transport buttons. Picture zoom stays on the picture.
|
||||||
- **Driving mode has a new layout that always keeps the controls on screen.** The overlay is now one full-height column: a top bar (share, a *Picture / Lyrics / Compact* switch, minimize, close), a content area that shrinks and scrolls inside itself, and a controls dock pinned to the bottom (position slider, time, previous / -15 / play / +15 / next, speed, text, images). On an iPhone SE (375x667) the controls used to fall below the screen, the share button covered the article title and the top of the title was cut off; now the dock is always fully visible from 320x568 up to tablets, in landscape the picture and the three lines sit side by side, and long titles wrap to two lines. *Picture* fills the area (double-tap, pinch, wheel, +/- keys or the zoom buttons to zoom, drag to pan, Escape or the reset button to reset); *Lyrics* shows the whole text, the spoken sentence large and centered, tap a sentence to jump there, and scrolling by hand pauses following until *Back to current*; *Compact* (default) is the picture plus previous / current / next line. The minimize button shrinks everything to just the current line. The choice is remembered. Transport buttons stay perfect circles.
|
- **Driving mode has a new layout that always keeps the controls on screen.** The overlay is now one full-height column: a top bar (share, a *Picture / Lyrics / Compact* switch, minimize, close), a content area that shrinks and scrolls inside itself, and a controls dock pinned to the bottom (position slider, time, previous / -15 / play / +15 / next, speed, text, images). On an iPhone SE (375x667) the controls used to fall below the screen, the share button covered the article title and the top of the title was cut off; now the dock is always fully visible from 320x568 up to tablets, in landscape the picture and the three lines sit side by side, and long titles wrap to two lines. *Picture* fills the area (double-tap, pinch, wheel, +/- keys or the zoom buttons to zoom, drag to pan, Escape or the reset button to reset); *Lyrics* shows the whole text, the spoken sentence large and centered, tap a sentence to jump there, and scrolling by hand pauses following until *Back to current*; *Compact* (default) is the picture plus previous / current / next line. The minimize button shrinks everything to just the current line. The choice is remembered. Transport buttons stay perfect circles.
|
||||||
|
|||||||
@ -289,7 +289,7 @@ turned into a podcast discussion between two or three voices you pick.
|
|||||||
large touch target and keyboard reachable; the speed also applies to the
|
large touch target and keyboard reachable; the speed also applies to the
|
||||||
playlist player.
|
playlist player.
|
||||||
**Hands-free:** while a track plays, the lock screen, notification shade, Bluetooth headset and car display show the article title, magazine and first-page artwork and offer play/pause, previous/next article and ±15 s (and seeking, where the device offers it). In driving mode the screen is kept awake while audio plays and allowed to sleep when it is paused or you leave. Both are optional browser features; where one is missing or refused, playback itself is unaffected. Opening driving mode resumes the first unfinished article at the position you left it.
|
**Hands-free:** while a track plays, the lock screen, notification shade, Bluetooth headset and car display show the article title, magazine and first-page artwork and offer play/pause, previous/next article and ±15 s (and seeking, where the device offers it). In driving mode the screen is kept awake while audio plays and allowed to sleep when it is paused or you leave. Both are optional browser features; where one is missing or refused, playback itself is unaffected. Opening driving mode resumes the first unfinished article at the position you left it.
|
||||||
**Layout and views:** driving mode is a top bar, a content area and a controls dock. The dock (position slider, time and playback speed, then previous, back 15 s, play/pause, forward 15 s, next) is always completely visible at the bottom and is never covered or pushed off screen, on phones from 320x568, tablets and in landscape. The top bar has *Share*, a *menu* button that shows the current view, *Index* and *Exit*. The menu opens a flyout: choose **Picture** (the picture fills the area), **Lyrics** (the whole text) or **Compact** (picture plus three lines, the default; in landscape side by side), and switch *Show text*, *Show pictures* and *Minimize text and picture* on or off. Keyboard: Enter or Arrow Down opens the menu, arrows/Home/End move, Enter chooses, Escape or a tap outside closes it and returns focus to the button. **Index:** the *Index* button opens a list of all articles of the magazine with their length (articles without audio are dimmed and marked *no audio*). The article that is playing is highlighted; tap it or press Enter on another to jump there while driving mode keeps playing. Escape or the close button closes the list (Tab stays inside it while open); the controls below stay visible and usable by touch. Lengths come from the already generated audio, nothing is recomputed. Each row also has a round **play button** (Play for unplayed, *Resume* with a circular-arrow icon for partly played, *Play again* with the replay icon for finished articles; it starts that article in driving mode and continues from the saved position), the author as *by Name* in smaller muted text under the title (omitted when unknown, long names are shortened), and a status icon on the right (empty circle: not played, dashed circle: partly played, check: finished) that also has the text *Not played / Partly played / Finished-Played* for screen readers and updates live while you listen. **Speed** is the button after the time next to the slider. **Picture zoom:** double-tap or double-click, pinch, mouse wheel, the + and - keys, or the zoom in / zoom out / reset buttons on the picture; drag (or arrow keys) to move a zoomed picture; Escape or the reset button returns to full view. **Lyrics:** the spoken sentence is large and kept in the middle, earlier and later ones are dimmed; tap or press Enter on a sentence to play from it; scrolling by hand stops the automatic following and shows *Back to current*. With reduced motion switched on in your device the text jumps instead of gliding. **Minimize** (in the menu) reduces the area to the title and the current line so the dock keeps minimal text; choose it again to expand. Your view and the minimized state are remembered in this browser only. Titles wrap to two lines (with an ellipsis beyond that), picture captions to two lines.
|
**Layout and views:** driving mode is a top bar, a content area and a controls dock. The dock (position slider, time and playback speed, then previous, back 15 s, play/pause, forward 15 s, next) is always completely visible at the bottom and is never covered or pushed off screen, on phones from 320x568, tablets and in landscape. The top bar has *Share*, a *menu* button that shows the current view, *Index* and *Exit*. The menu opens a flyout: choose **Picture** (the picture fills the area), **Lyrics** (the whole text) or **Compact** (picture plus three lines, the default; in landscape side by side), and switch *Show text*, *Show pictures* and *Minimize text and picture* on or off. Keyboard: Enter or Arrow Down opens the menu, arrows/Home/End move, Enter chooses, Escape or a tap outside closes it and returns focus to the button. **Index:** the *Index* button opens a list of all articles of the magazine with their length (articles without audio are dimmed and marked *no audio*). The article that is playing is highlighted; tap it or press Enter on another to jump there while driving mode keeps playing. Every row is its own item, separated from the next by a thin divider line and generous space above and below (the playing row is highlighted instead), so long two- or three-line titles do not run into each other. Escape or the close button closes the list (Tab stays inside it while open); the controls below stay visible and usable by touch. Lengths come from the already generated audio, nothing is recomputed. Each row also has a round **play button** (Play for unplayed, *Resume* with a circular-arrow icon for partly played, *Play again* with the replay icon for finished articles; it starts that article in driving mode and continues from the saved position), the author as *by Name* in smaller muted text under the title (omitted when unknown, long names are shortened), and a status icon on the right (empty circle: not played, dashed circle: partly played, check: finished) that also has the text *Not played / Partly played / Finished-Played* for screen readers and updates live while you listen. **Speed** is the button after the time next to the slider. **Picture zoom:** double-tap or double-click, pinch, mouse wheel, the + and - keys, or the zoom in / zoom out / reset buttons on the picture; drag (or arrow keys) to move a zoomed picture; Escape or the reset button returns to full view. **Lyrics:** the spoken sentence is large and kept in the middle, earlier and later ones are dimmed; tap or press Enter on a sentence to play from it; scrolling by hand stops the automatic following and shows *Back to current*. With reduced motion switched on in your device the text jumps instead of gliding. **Minimize** (in the menu) reduces the area to the title and the current line so the dock keeps minimal text; choose it again to expand. Your view and the minimized state are remembered in this browser only. Titles wrap to two lines (with an ellipsis beyond that), picture captions to two lines.
|
||||||
**Three lines of text:** driving mode shows the previous sentence (small), the current one (large, the spoken word marked) and the next (small) in a box of fixed height, so nothing moves around it. Tap or press Enter on the previous or next line to jump to that sentence. The text is deliberately not announced by screen readers sentence by sentence; the play/pause button and the position slider stay the way to control it. With reduced motion switched on in your device, lines change without animation. On a phone (375 px wide) the sentences wrap and a very long current sentence scrolls inside its box. Turn the text off with *Show text* in the menu to see only the picture.
|
**Three lines of text:** driving mode shows the previous sentence (small), the current one (large, the spoken word marked) and the next (small) in a box of fixed height, so nothing moves around it. Tap or press Enter on the previous or next line to jump to that sentence. The text is deliberately not announced by screen readers sentence by sentence; the play/pause button and the position slider stay the way to control it. With reduced motion switched on in your device, lines change without animation. On a phone (375 px wide) the sentences wrap and a very long current sentence scrolls inside its box. Turn the text off with *Show text* in the menu to see only the picture.
|
||||||
**Text and picture follow the voice:** for audio generated since sentence times were recorded, the highlighted sentence (playlist and driving mode) and the picture shown come from the exact time each sentence was spoken, so they stay right after the slider, the 15-second buttons, a tap on a sentence, the lock screen or a resume. Captions are shown but are not in the MP3, so they are never highlighted. The highlight lights up a sentence just before its first word is heard (each sentence starts with a short silence, which is not counted). Audio from before sentence times were recorded, and audio whose text was edited afterwards, has no usable record and is followed by an estimate from sentence length (less exact); regenerate the audio to get exact sync. Interview-voice audio is covered like any other.
|
**Text and picture follow the voice:** for audio generated since sentence times were recorded, the highlighted sentence (playlist and driving mode) and the picture shown come from the exact time each sentence was spoken, so they stay right after the slider, the 15-second buttons, a tap on a sentence, the lock screen or a resume. Captions are shown but are not in the MP3, so they are never highlighted. The highlight lights up a sentence just before its first word is heard (each sentence starts with a short silence, which is not counted). Audio from before sentence times were recorded, and audio whose text was edited afterwards, has no usable record and is followed by an estimate from sentence length (less exact); regenerate the audio to get exact sync. Interview-voice audio is covered like any other.
|
||||||
**Word by word:** the first time an article is played, the app asks the forced-aligner service (the separate `tts-aligner` container, see below) to measure where each word starts and ends and stores that beside the audio. Playback never waits for it: until it is ready (usually well under a minute for a magazine article) the marked word is estimated from word length as before, and it switches to the measured word on its own. The marked word is the last one that has started, so it stays lit through a pause; the *Lyrics* view now marks the spoken word too (as do *Compact* and the playlist text). If the service is not running, nothing changes for you and the app tries again later (at most every 10 minutes per article). Prerequisites: `docker compose up -d tts-aligner` (first start downloads about 1.2 GB of model weights into the `aligner-models` volume; after that it needs no internet). `ALIGNER_URL` empty turns word timing off. Privacy: text and audio go only to your own container on the internal Docker network. Accessibility: the marking is visual only and adds no announcements; with reduced motion nothing animates. API: `POST /api/magazine/{doc}/articles/{id}/align-words` starts it explicitly, `GET` shows progress.
|
**Word by word:** the first time an article is played, the app asks the forced-aligner service (the separate `tts-aligner` container, see below) to measure where each word starts and ends and stores that beside the audio. Playback never waits for it: until it is ready (usually well under a minute for a magazine article) the marked word is estimated from word length as before, and it switches to the measured word on its own. The marked word is the last one that has started, so it stays lit through a pause; the *Lyrics* view now marks the spoken word too (as do *Compact* and the playlist text). If the service is not running, nothing changes for you and the app tries again later (at most every 10 minutes per article). Prerequisites: `docker compose up -d tts-aligner` (first start downloads about 1.2 GB of model weights into the `aligner-models` volume; after that it needs no internet). `ALIGNER_URL` empty turns word timing off. Privacy: text and audio go only to your own container on the internal Docker network. Accessibility: the marking is visual only and adds no announcements; with reduced motion nothing animates. API: `POST /api/magazine/{doc}/articles/{id}/align-words` starts it explicitly, `GET` shows progress.
|
||||||
|
|||||||
@ -5765,11 +5765,14 @@ details.mag-report-group summary { cursor: pointer; margin-bottom: 6px; }
|
|||||||
.mag-driving-index-head { flex: none; display: flex; align-items: center; justify-content: space-between; padding: 4px 8px 4px 16px; }
|
.mag-driving-index-head { flex: none; display: flex; align-items: center; justify-content: space-between; padding: 4px 8px 4px 16px; }
|
||||||
.mag-driving-index-head h3 { margin: 0; font-size: 17px; }
|
.mag-driving-index-head h3 { margin: 0; font-size: 17px; }
|
||||||
.mag-driving-index-head button { width: 44px; height: 44px; border: 0; border-radius: 50%; background: transparent; color: var(--text); font-size: 22px; cursor: pointer; }
|
.mag-driving-index-head button { width: 44px; height: 44px; border: 0; border-radius: 50%; background: transparent; color: var(--text); font-size: 22px; cursor: pointer; }
|
||||||
.mag-driving-index-list { list-style: none; margin: 0; padding: 0 8px 8px; flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: 9px; }
|
.mag-driving-index-list { list-style: none; margin: 0; padding: 0 8px 8px; flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: 0; }
|
||||||
.mag-driving-index-list .ix-item { display: flex; align-items: center; gap: 6px; border-radius: 10px; }
|
/* every row is its own item: 13 px above and below, and a thin divider between rows (the highlighted row drops its divider) */
|
||||||
|
.mag-driving-index-list .ix-item { display: flex; align-items: center; gap: 6px; border-radius: 10px; padding: 13px 4px; border-bottom: 1px solid var(--border); }
|
||||||
|
.mag-driving-index-list .ix-item:last-child { border-bottom-color: transparent; }
|
||||||
|
.mag-driving-index-list .ix-item:has(.ix-row[aria-current="true"]) { border-bottom-color: transparent; }
|
||||||
.mag-driving-index-list .ix-item:has(.ix-row[aria-current="true"]) { background: var(--accent); color: var(--on-accent, #fff); }
|
.mag-driving-index-list .ix-item:has(.ix-row[aria-current="true"]) { background: var(--accent); color: var(--on-accent, #fff); }
|
||||||
.mag-driving-index-list button { min-height: 44px; border: 0; background: transparent; color: var(--text); font-size: 15px; cursor: pointer; }
|
.mag-driving-index-list button { min-height: 44px; border: 0; background: transparent; color: var(--text); font-size: 15px; cursor: pointer; }
|
||||||
.mag-driving-index-list .ix-row { flex: 1 1 auto; min-width: 0; padding: 6px 8px 6px 0; display: flex; align-items: center; gap: 8px; text-align: left; border-radius: 10px; }
|
.mag-driving-index-list .ix-row { flex: 1 1 auto; min-width: 0; min-height: 44px; padding: 0 8px 0 0; display: flex; align-items: center; gap: 8px; text-align: left; border-radius: 10px; }
|
||||||
.mag-driving-index-list .ix-play { flex: none; width: 44px; height: 44px; min-height: 44px; padding: 5px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
|
.mag-driving-index-list .ix-play { flex: none; width: 44px; height: 44px; min-height: 44px; padding: 5px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
|
||||||
background: var(--surface); color: var(--accent); font-size: 22px; box-sizing: border-box; border: 1px solid var(--border); position: relative; }
|
background: var(--surface); color: var(--accent); font-size: 22px; box-sizing: border-box; border: 1px solid var(--border); position: relative; }
|
||||||
.mag-driving-index-list .ix-play .mdi { display: inline-flex; width: 34px; height: 34px; align-items: center; justify-content: center; border-radius: 50%; }
|
.mag-driving-index-list .ix-play .mdi { display: inline-flex; width: 34px; height: 34px; align-items: center; justify-content: center; border-radius: 50%; }
|
||||||
|
|||||||
@ -3083,6 +3083,25 @@ test.describe('Driving mode layout and views', () => {
|
|||||||
await expect.poll(current).toBe('article-001');
|
await expect.poll(current).toBe('article-001');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('index rows are separated: divider line and 12-14 px padding per row, current row highlighted without divider', async ({ page }) => {
|
||||||
|
await setupIndex(page);
|
||||||
|
await page.setViewportSize({ width: 375, height: 667 });
|
||||||
|
await open(page);
|
||||||
|
await page.locator('#mag-driving-indexbtn').click();
|
||||||
|
const g = await page.evaluate(() => {
|
||||||
|
const items = [...document.querySelectorAll('#mag-driving-index-list .ix-item')];
|
||||||
|
const cs = (e) => getComputedStyle(e);
|
||||||
|
return items.map((li) => { const c = cs(li), row = li.querySelector('.ix-row');
|
||||||
|
return { pt: parseFloat(c.paddingTop), pb: parseFloat(c.paddingBottom), bb: parseFloat(c.borderBottomWidth), bc: c.borderBottomColor,
|
||||||
|
current: row.getAttribute('aria-current') === 'true', rowH: row.getBoundingClientRect().height, play: li.querySelector('.ix-play').getBoundingClientRect().height }; });
|
||||||
|
});
|
||||||
|
expect(g.length).toBeGreaterThan(1);
|
||||||
|
for (const it of g) { expect(it.pt).toBeGreaterThanOrEqual(12); expect(it.pt).toBeLessThanOrEqual(14); expect(it.pb).toBeGreaterThanOrEqual(12); expect(it.rowH).toBeGreaterThanOrEqual(43.5); expect(it.play).toBeGreaterThanOrEqual(43.5); }
|
||||||
|
const plain = g.find((it) => !it.current && it !== g[g.length - 1]);
|
||||||
|
expect(plain.bb).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(g.find((it) => it.current).bc).toBe('rgba(0, 0, 0, 0)');
|
||||||
|
});
|
||||||
|
|
||||||
test('index with 113 articles renders quickly and scrolls the current one into view', async ({ page }) => {
|
test('index with 113 articles renders quickly and scrolls the current one into view', async ({ page }) => {
|
||||||
await setupIndex(page, 113);
|
await setupIndex(page, 113);
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
await page.setViewportSize({ width: 375, height: 667 });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user