Speed becomes a per-USER-per-BOOK preference that syncs across devices, with a user-level default for books not yet played: - new audio_playback_speed table (mirrors reading_speed's shape; a preference, NOT a measurement and NOT a reading_progress column) + users.default_audio_speed, schema in place - chapters endpoint resolves per-book row → user default → 1.0 and ships playback_speed + default_playback_speed (zero extra round trips; default powers the dropdown marker) - PUT /api/media-items/:id/audio-speed upserts (range-checked; the client SPEEDS list stays the real gate; library visibility gates writes) - the user default rides the profile path like theme/timezone: profile form gains a 'Default Playback Speed' select (server-side SpeedOptions mirrors the web SPEEDS list), GET /profile carries it - web: restorePosition applies the resolved server speed (localStorage audio_speed demotes to a first-paint cache); setSpeed PUTs fire-and-forget with a failure toast; the Speed dropdown gains a '(default)' row marker and a 'Make N× my default' action with a success toast ALSO fixes a pre-existing profile-page bug surfaced by the §8 form verification: the password section's current_password input was required INSIDE the shared form, so HTML5 validation blocked every Save Changes submission (dead since the shell migration). The password flow keeps its own gates (disabled-button checkers + server 400). Integration test TestAudioPlaybackSpeed pins the full contract (unset → 1x, upsert resolution, range rejection, default fallback for row-less books, default updates re-satisfying unset books). Verified live: 9 Playwright checks.
321 lines
7.2 KiB
Go
321 lines
7.2 KiB
Go
package templates
|
||
|
||
import (
|
||
"time"
|
||
|
||
"github.com/a-h/templ"
|
||
)
|
||
|
||
type User struct {
|
||
ID string
|
||
Username string
|
||
Email string
|
||
Role string
|
||
Theme string
|
||
FirstName string
|
||
LastName string
|
||
CreatedAt time.Time
|
||
Token string
|
||
Timezone string
|
||
// User-level default audiobook playback speed (plan §8); 0 means
|
||
// "unset" and renders as the 1× option.
|
||
DefaultAudioSpeed float64
|
||
}
|
||
|
||
// SpeedOption is one entry of the default-playback-speed select — the
|
||
// server-side mirror of the web player's SPEEDS list (plan §5/§8).
|
||
var SpeedOptions = []float64{0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3}
|
||
|
||
type PageData struct {
|
||
User User
|
||
CurrentPage string
|
||
}
|
||
|
||
type CollectionData struct {
|
||
ID string
|
||
Name string
|
||
Description string
|
||
Color string
|
||
Icon string
|
||
IsSystem bool
|
||
// A2c §3.3: playlist toggles (static collections only). Hidden fields
|
||
// preserve the current values on submit; absent from the create form.
|
||
HideFinished bool
|
||
ShowUpNext bool
|
||
// IsSmart marks rule-based ("dynamic") collections: membership is
|
||
// evaluated live at render, not stored in collection_items.
|
||
IsSmart bool
|
||
// BookCount is the LIVE count for the card (rule matches for smart
|
||
// collections, stored links for static ones).
|
||
BookCount int
|
||
}
|
||
|
||
type LibraryData struct {
|
||
ID string
|
||
Name string
|
||
Description string
|
||
TypeName string
|
||
TypeValue string
|
||
MediaCount int64
|
||
FolderCount int
|
||
// Offline is true when none of the library's folders exist on disk
|
||
// (e.g. an unmounted external drive). Contents stay intact and reappear
|
||
// when the storage returns.
|
||
Offline bool
|
||
}
|
||
|
||
type FolderData struct {
|
||
FolderPath string
|
||
}
|
||
|
||
// ArchivedItem feeds the admin archived-items page: media hidden from
|
||
// libraries because their files vanished, with their pending fate. When an
|
||
// active item with identical content (same SHA-256) exists, MatchID and
|
||
// friends are set so the UI can offer merging the archived row's reading
|
||
// data into its active twin.
|
||
type ArchivedItem struct {
|
||
ID string
|
||
Title string
|
||
Author string
|
||
LibraryName string
|
||
FilePath string
|
||
MissingScans int32
|
||
ArchivedAt *time.Time
|
||
PurgeAt *time.Time
|
||
Status string
|
||
MatchID string
|
||
MatchTitle string
|
||
MatchLibraryName string
|
||
}
|
||
|
||
type DirEntry struct {
|
||
Name string
|
||
Path string
|
||
}
|
||
|
||
type UserVisibilityData struct {
|
||
UserID string
|
||
Username string
|
||
Email string
|
||
IsVisible bool
|
||
}
|
||
|
||
type AdminStats struct {
|
||
LibraryCount int
|
||
MediaCount int
|
||
UserCount int
|
||
DeviceCount int
|
||
}
|
||
|
||
type ScanSettingsData struct {
|
||
AutoScanEnabled bool
|
||
ScanPollIntervalSeconds int
|
||
}
|
||
|
||
// SettingEntry mirrors database.SettingEntry for the admin UI. Kept as a
|
||
// template-local type so the templates package does not import database.
|
||
type SettingEntry struct {
|
||
Key string
|
||
Value string
|
||
Type string
|
||
Min string
|
||
Max string
|
||
RequiresRestart bool
|
||
Category string
|
||
Group string
|
||
Description string
|
||
IsDefault bool
|
||
}
|
||
|
||
// SettingGroup is a labeled cluster of related settings rendered as a
|
||
// sub-section within a tunable-settings card.
|
||
type SettingGroup struct {
|
||
Name string
|
||
Entries []SettingEntry
|
||
}
|
||
|
||
type SeriesCardData struct {
|
||
Name string
|
||
LibraryID string
|
||
BookCount int64
|
||
TotalInSeries int
|
||
CoverPaths []string
|
||
}
|
||
|
||
type PendingRegistrationData struct {
|
||
RegistrationID string
|
||
DeviceName string
|
||
DeviceType string
|
||
ExpiresAt string
|
||
}
|
||
|
||
type ConflictData struct {
|
||
ID string
|
||
MediaItemID string
|
||
MediaItemTitle string
|
||
ConflictType string
|
||
ResolutionStatus string
|
||
CreatedAt string
|
||
ResolutionData map[string]interface{}
|
||
ResolvedBy string
|
||
ResolvedAt string
|
||
}
|
||
|
||
type QueueItemData struct {
|
||
ID string
|
||
DeviceID string
|
||
DeviceName string
|
||
DeviceType string
|
||
SyncType string
|
||
Status string
|
||
Priority int32
|
||
Attempts int32
|
||
CreatedAt string
|
||
}
|
||
|
||
type DeviceShelfMappingData struct {
|
||
ID string
|
||
CollectionID string
|
||
CollectionName string
|
||
DeviceShelfName string
|
||
SyncDirection string
|
||
CreatedAt string
|
||
}
|
||
|
||
type UnlinkedBookData struct {
|
||
ProgressID string
|
||
DeviceID string
|
||
DeviceType string
|
||
DeviceName string
|
||
FilePath string
|
||
SHA256 string
|
||
TitleFromDevice string
|
||
LastSyncTime string
|
||
ConfidenceScore float64
|
||
PotentialMatches []PotentialMatchData
|
||
}
|
||
|
||
type PotentialMatchData struct {
|
||
MediaItemID string
|
||
Title string
|
||
Author string
|
||
CoverImagePath string
|
||
MatchMethod string
|
||
Confidence float64
|
||
}
|
||
|
||
// Documentation types
|
||
type Navigation struct {
|
||
Sections []NavSection
|
||
}
|
||
|
||
type NavSection struct {
|
||
Title string
|
||
Items []NavItem
|
||
Collapsed bool
|
||
}
|
||
|
||
type NavItem struct {
|
||
Title string
|
||
URL string
|
||
Icon string
|
||
}
|
||
|
||
type Document struct {
|
||
Title string
|
||
Content string
|
||
TOC []TOCItem
|
||
Breadcrumb []BreadcrumbItem
|
||
Category string
|
||
SourceFile string
|
||
}
|
||
|
||
type TOCItem struct {
|
||
Level int
|
||
Title string
|
||
Anchor string
|
||
}
|
||
|
||
type BreadcrumbItem struct {
|
||
Title string
|
||
URL string
|
||
}
|
||
|
||
// UnsafeHTML is used for rendering raw HTML (like rendered markdown)
|
||
type UnsafeHTML string
|
||
|
||
// ToComponent converts UnsafeHTML to a templ Component
|
||
func (u UnsafeHTML) ToComponent() templ.Component {
|
||
return templ.Raw(string(u))
|
||
}
|
||
|
||
// Reader types
|
||
type ReaderMetadata struct {
|
||
MediaItemID string `json:"media_item_id"`
|
||
Title string `json:"title"`
|
||
Author string `json:"author"`
|
||
CoverImagePath string `json:"cover_image_path"`
|
||
LibraryType string `json:"library_type"`
|
||
MimeType string `json:"mime_type"`
|
||
FilePath string `json:"file_path"`
|
||
TotalPages int `json:"total_pages"`
|
||
ChapterCount int `json:"chapter_count"`
|
||
FormatGroup string `json:"format_group"`
|
||
MangaType string `json:"manga_type"`
|
||
ReadingDirection string `json:"reading_direction"`
|
||
FileURL string `json:"file_url"`
|
||
LibraryID string `json:"library_id"`
|
||
TotalCharacters int64 `json:"total_characters"`
|
||
EstimatedPages int `json:"estimated_pages"`
|
||
// TreatAsComic: fixed-layout item in a comics/manga library gets
|
||
// comic reader treatment (bookmarks only — no highlights, no search).
|
||
TreatAsComic bool `json:"treat_as_comic"`
|
||
}
|
||
|
||
type ReadingProgress struct {
|
||
ID string
|
||
MediaItemID string
|
||
UserID string
|
||
CurrentPage int
|
||
TotalPages int
|
||
Percentage float64
|
||
EpubCfi string
|
||
LastReadAt time.Time
|
||
Chapter int
|
||
ChapterProgress float64
|
||
FormatGroup string
|
||
}
|
||
|
||
type Bookmark struct {
|
||
ID string
|
||
MediaItemID string
|
||
UserID string
|
||
PageNumber *int
|
||
ChapterNumber *int
|
||
CfiPosition string
|
||
Title string
|
||
Position string
|
||
Notes string
|
||
CreatedAt time.Time
|
||
}
|
||
|
||
// ProcessingIssueData - Processing issue types
|
||
type ProcessingIssueData struct {
|
||
ID string
|
||
MediaItemID string
|
||
Title string
|
||
FilePath string
|
||
FormatGroup string
|
||
LibraryTypeName string
|
||
IssueType string
|
||
IssueDescription string
|
||
Severity string
|
||
CreatedAt time.Time
|
||
}
|
||
|
||
type IssueStats struct {
|
||
ErrorCount int64
|
||
WarningCount int64
|
||
InfoCount int64
|
||
}
|