From f83ac6767636e81470c28d1cd248da54523bca30 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 25 Jun 2026 20:01:05 +0200 Subject: [PATCH] feat(reader): surface brightness on desktop, not just the mobile sheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The brightness slider lived only in the mobile reader settings sheet, so desktop users couldn't dim — and could be left stuck dimmed by a value a phone had persisted (brightness is shared via localStorage and the --reader-dim effect is form-factor agnostic). Add an inline brightness slider to the desktop reader nav, marked `desktop-control` alongside the existing mode toggle and gap select. It binds the same `brightness` state, so a value set on either form factor stays in sync. Also mocks `/me/read-progress` in the reader-mode e2e setup — without it the GET/PUT fell through to the (absent) backend and stalled the reader load, which is why those specs couldn't run headless. The spec is now hermetic and covers the new desktop control (it dims/undims, and a phone-persisted value is recoverable). Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/Cargo.lock | 2 +- backend/Cargo.toml | 2 +- frontend/e2e/reader-mode.spec.ts | 68 +++++++++++++++++++ frontend/package.json | 2 +- .../[id]/chapter/[chapter_id]/+page.svelte | 36 ++++++++++ 5 files changed, 107 insertions(+), 3 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 332ac16..77e9e38 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1517,7 +1517,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mangalord" -version = "0.88.0" +version = "0.89.0" dependencies = [ "anyhow", "argon2", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 88ade25..765d388 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mangalord" -version = "0.88.0" +version = "0.89.0" edition = "2021" default-run = "mangalord" diff --git a/frontend/e2e/reader-mode.spec.ts b/frontend/e2e/reader-mode.spec.ts index b31cd8b..012bc90 100644 --- a/frontend/e2e/reader-mode.spec.ts +++ b/frontend/e2e/reader-mode.spec.ts @@ -66,6 +66,15 @@ async function mockReaderApis(page: Page) { body: JSON.stringify({ error: { code: 'unauthenticated', message: '' } }) }) ); + // Anonymous reader: read-progress reads/writes are unauthenticated. Without + // this the GET/PUT fall through to the (absent) backend and stall the load. + await page.route('**/api/v1/me/read-progress/**', (route) => + route.fulfill({ + status: 401, + contentType: 'application/json', + body: JSON.stringify({ error: { code: 'unauthenticated', message: '' } }) + }) + ); await page.route(`**/api/v1/mangas/${mangaId}`, (route) => route.fulfill({ status: 200, @@ -126,6 +135,7 @@ test.beforeEach(async ({ context }) => { try { localStorage.removeItem('mangalord-reader-mode'); localStorage.removeItem('mangalord-reader-gap'); + localStorage.removeItem('mangalord-reader-brightness'); } catch { // ignore — about:blank doesn't expose localStorage yet. } @@ -181,6 +191,64 @@ test('gap select updates the inline gap on the continuous container', async ({ p await expect(container).toHaveAttribute('style', /gap:\s*64px/); }); +test('desktop reader nav exposes a brightness control that dims and undims the reader', async ({ + page +}) => { + // The brightness slider used to live only in the mobile settings sheet, + // so desktop users couldn't dim — and could be stuck dimmed by a value a + // phone had persisted. The desktop nav now carries its own slider. + await mockReaderApis(page); + await page.goto(`/manga/${mangaId}/chapter/${chapterId}`); + + const slider = page.getByTestId('reader-brightness-desktop'); + await expect(slider).toBeVisible(); + + const dim = () => + page.evaluate(() => + document.documentElement.style.getPropertyValue('--reader-dim') + ); + + // Default brightness is full → no dim overlay. + await expect.poll(async () => Number(await dim())).toBe(0); + + // Dimming raises the overlay opacity… + await slider.fill('0.3'); + await expect.poll(async () => Number(await dim())).toBeGreaterThan(0); + + // …and restoring it clears the dim, so a desktop user is never stuck. + await slider.fill('1'); + await expect.poll(async () => Number(await dim())).toBe(0); +}); + +test('a brightness value persisted by a phone is recoverable on desktop', async ({ + page, + context +}) => { + // Seed a dimmed value as if set on mobile, then open the reader on a + // desktop viewport: the dim applies and the desktop control reflects it. + await context.addInitScript(() => { + try { + localStorage.setItem('mangalord-reader-brightness', '0.3'); + } catch { + // about:blank — ignore + } + }); + await mockReaderApis(page); + await page.goto(`/manga/${mangaId}/chapter/${chapterId}`); + + const slider = page.getByTestId('reader-brightness-desktop'); + await expect(slider).toHaveValue('0.3'); + await expect + .poll(async () => + Number( + await page.evaluate(() => + document.documentElement.style.getPropertyValue('--reader-dim') + ) + ) + ) + .toBeGreaterThan(0); +}); + test('reader-mode preference set on one page is honored when the reader opens', async ({ page, context diff --git a/frontend/package.json b/frontend/package.json index 18e80b3..50639c3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mangalord-frontend", - "version": "0.88.0", + "version": "0.89.0", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.svelte b/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.svelte index cd2b865..c59893f 100644 --- a/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.svelte +++ b/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.svelte @@ -1094,6 +1094,28 @@ {/if} + + +