Merge branch 'feat/desktop-reader-brightness' into chore/reconcile-ui-review
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1094,6 +1094,28 @@
|
||||
|
||||
{/if}
|
||||
|
||||
<!-- Desktop brightness. On mobile this control lives in the reader
|
||||
settings sheet; desktop has no sheet, so it sits inline here.
|
||||
Shares the `brightness` state and the same `--reader-dim`
|
||||
effect, so a value set on either form factor stays in sync. -->
|
||||
<label class="brightness-field desktop-control">
|
||||
<Sun size={16} aria-hidden="true" />
|
||||
<span class="visually-hidden">Brightness</span>
|
||||
<input
|
||||
type="range"
|
||||
class="brightness-slider"
|
||||
min="0.3"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={brightness}
|
||||
oninput={(e) => {
|
||||
brightness = Number((e.currentTarget as HTMLInputElement).value);
|
||||
}}
|
||||
aria-label="Brightness"
|
||||
data-testid="reader-brightness-desktop"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="reader-settings-btn mobile-control"
|
||||
@@ -2181,6 +2203,20 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Inline desktop brightness control in the reader nav. The slider is
|
||||
full-width inside the mobile settings sheet; here it's capped so it
|
||||
sits neatly beside the mode toggle. */
|
||||
.brightness-field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.brightness-field .brightness-slider {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
/* Reader-nav sits at the viewport top — the global mobile
|
||||
chrome is hidden on this route so there's no app bar above
|
||||
|
||||
Reference in New Issue
Block a user