feat: continuous reader mode with persisted preference

Add a vertical-scroll continuous mode to the reader alongside the
existing single-page mode. A segmented toggle in the reader top bar
switches between them; in continuous mode a gap selector
(None/Small/Medium/Large → 0/12/32/64px) controls the spacing
between stacked pages. Settings page mirrors the same controls.

Backend: new user_preferences table (one row per user, lazily
inserted, ON DELETE CASCADE) and GET/PATCH /api/v1/auth/me/preferences
gated by the existing CurrentUser extractor. Allowed values are
enforced both by API validation and table-level CHECK constraints.
Eight integration tests cover defaults, persistence, partial
updates, validation errors, auth, per-user isolation, and cascade.

Frontend: a new preferences store mirrors the theme-store pattern
with a localStorage shadow so anonymous browsers get a consistent
experience and logged-in users don't flash defaults while the
server response is in flight. Server values that the frontend
doesn't recognize (forward-compat) are ignored rather than poisoning
the UI; non-401 PATCH errors revert the optimistic local update;
logout clears the shadow so user A's settings don't follow user B
on a shared browser.

In continuous mode native scrolling handles Space/PageDown/arrows;
Home/End remain wired and call scrollIntoView() so jumping to chapter
bounds stays one keystroke. Single-page mode (chevrons, arrow-key
pagination, next-page preload) is unchanged.

Versions bumped 0.13.0 → 0.14.0 in lockstep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-17 13:15:03 +02:00
parent 567d56bfa1
commit 60cc7712fa
18 changed files with 1287 additions and 6 deletions

View File

@@ -5,6 +5,7 @@ pub mod manga;
pub mod page;
pub mod session;
pub mod user;
pub mod user_preferences;
pub use api_token::ApiToken;
pub use bookmark::{Bookmark, BookmarkSummary};
@@ -13,3 +14,4 @@ pub use manga::Manga;
pub use page::Page;
pub use session::Session;
pub use user::User;
pub use user_preferences::UserPreferences;

View File

@@ -0,0 +1,30 @@
use chrono::{DateTime, Utc};
use serde::Serialize;
use sqlx::FromRow;
use uuid::Uuid;
pub const READER_MODE_SINGLE: &str = "single";
pub const READER_MODE_CONTINUOUS: &str = "continuous";
pub const READER_MODES: &[&str] = &[READER_MODE_SINGLE, READER_MODE_CONTINUOUS];
pub const READER_GAPS: &[&str] = &["none", "small", "medium", "large"];
#[derive(Debug, Clone, Serialize, FromRow, PartialEq, Eq)]
pub struct UserPreferences {
#[serde(skip)]
pub user_id: Uuid,
pub reader_mode: String,
pub reader_page_gap: String,
pub updated_at: DateTime<Utc>,
}
impl UserPreferences {
pub fn defaults_for(user_id: Uuid) -> Self {
Self {
user_id,
reader_mode: READER_MODE_SINGLE.to_string(),
reader_page_gap: "none".to_string(),
updated_at: Utc::now(),
}
}
}