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>
31 lines
842 B
Rust
31 lines
842 B
Rust
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(),
|
|
}
|
|
}
|
|
}
|