The daily briefing needs AND: today's rules engine was OR-only. The
match semantics become a per-collection column
(collections.rule_match_mode, 'any' legacy default | 'all'), flipped on
its own endpoint (PUT /api/collections/:id/match-mode, owner-only —
nothing else on the row changes) and surfaced in the rules page as an
ANY/ALL toggle with matching copy.
Episode rule fields join the vocabulary (plan item 2.4): feed (the
multi-select rides the new `in` operator — comma-separated feed ids),
played (the canonical finished predicate — viewer progress ≥ 0.999,
same as hide_finished), downloaded, published_within (N days), plus
title/description contains. The rules candidate query now carries the
viewer's progress (LEFT JOIN reading_progress) so played/unplayed is
per-user, and TestRules/preview speak the SAME evaluator: the handler's
duplicated field switch is deleted and delegates to
CollectionService.FieldMatches — one vocabulary, one semantics.
Legitimate semantics note: in ANY mode `played:false` matches unplayed
BOOKS too (everything with no progress row) — correct per-rule
behavior, and exactly why the briefing mode exists.
Backward compatibility: every legacy row defaults to 'any' (the old
OR), UpdateCollection keeps the stored mode when the form doesn't send
one, and the evaluator treats absent data honestly (no date → no
match; no progress → unplayed).
Pinned by TestPodcastSmartCollections: the AND-mode briefing
(feed∈{A} ∧ unplayed ∧ <7d = exactly the new episode), OR-mode
widening, owner-only flips, the preview honoring the mode, and
downloaded/contains rules. Also fixes a latent bug the new query
surfaced: UpdateCollectionViewSettings referenced a nonexistent
updated_at column (never exercised until now — the new sibling query
was written without it).
397 lines
10 KiB
Go
397 lines
10 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
|
||
// Whether the web reader/player should hold a Screen Wake Lock.
|
||
KeepScreenAwake bool
|
||
// Sidebar /podcasts entry gate (plan D13): the user can see >=1
|
||
// podcasts-type library. Resolved centrally in
|
||
// getTemplateUserWithTheme so every page's Header is correct.
|
||
ShowPodcastsNav bool
|
||
}
|
||
|
||
// 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
|
||
// MatchMode is the smart ruleset's semantics (item 2.4): "any" (OR)
|
||
// or "all" (AND). Static collections ignore it.
|
||
MatchMode string
|
||
// 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
|
||
// Viewer's finished books in this series (drives the card's
|
||
// progress bar / "X of N read" line).
|
||
ReadCount int64
|
||
}
|
||
|
||
// SeriesPageState is the /series page's sort/filter state, echoed back
|
||
// into the toolbar controls and the pagination URLs (the URL is the
|
||
// single source of truth on this plain-GET page). Sort holds the BASE
|
||
// toolbar key (name/author/…); Dir is the effective direction.
|
||
type SeriesPageState struct {
|
||
Sort string
|
||
Dir string
|
||
Query string
|
||
Status 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
|
||
}
|
||
|
||
// NavSection is a top-level documentation category. Items are documents that
|
||
// live directly in the section; Groups are subcategories (subdirectories).
|
||
type NavSection struct {
|
||
Title string
|
||
Icon string
|
||
Items []NavItem
|
||
Groups []NavGroup
|
||
Collapsed bool
|
||
}
|
||
|
||
// NavGroup is a subcategory inside a NavSection (a subdirectory of docs).
|
||
type NavGroup struct {
|
||
Title string
|
||
Icon string
|
||
Items []NavItem
|
||
Collapsed bool
|
||
}
|
||
|
||
type NavItem struct {
|
||
Title string
|
||
URL string
|
||
}
|
||
|
||
// SearchResultItem is one entry in the docs search results partial. The
|
||
// server renders it (templ); the client only swaps the markup in.
|
||
type SearchResultItem struct {
|
||
Title string
|
||
Category string
|
||
URL 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"`
|
||
// KeepScreenAwake: user preference — the reader holds a Screen Wake
|
||
// Lock while open when true.
|
||
KeepScreenAwake bool `json:"keep_screen_awake"`
|
||
// PodcastSequence (plan D7/Phase 2): set ONLY for podcast episodes —
|
||
// the neighboring downloaded episodes in the caller's effective
|
||
// order. Powers chain-by-default playback (auto-advance across
|
||
// episodes) and the player's episode prev/next. nil for books.
|
||
PodcastSequence *PodcastSequenceContext `json:"podcast_sequence,omitempty"`
|
||
}
|
||
|
||
// PodcastSequenceContext is the episode-level chain payload rendered
|
||
// into the audio reader's config JSON.
|
||
type PodcastSequenceContext struct {
|
||
FeedURL string `json:"feed_url,omitempty"`
|
||
FeedTitle string `json:"feed_title,omitempty"`
|
||
PrevURL string `json:"prev_url,omitempty"`
|
||
NextURL string `json:"next_url,omitempty"`
|
||
NextTitle string `json:"next_title,omitempty"`
|
||
// Source is the queue's owner (§10 R2: every producer emits the
|
||
// same shape): "feed" = the feed's effective order (per-user
|
||
// override applied), "collection" = an ordered Up Next collection
|
||
// (2.3). The player shows/hides controls by source — the order
|
||
// control is FEED-mode only, because a collection's order is
|
||
// curated content, not a listening preference.
|
||
Source string `json:"source,omitempty"`
|
||
// Feed-mode fields: FeedID powers the player's order control (it
|
||
// PUTs /api/podcasts/feeds/:id/order — the same per-user override
|
||
// the feed page writes); Order is the resolved current value that
|
||
// the dropdown reflects. Empty in collection mode.
|
||
FeedID string `json:"feed_id,omitempty"`
|
||
Order string `json:"order,omitempty"`
|
||
// EpisodeIndex/IndexTotal render the "Ep N of M" indicator (D7).
|
||
EpisodeIndex int `json:"episode_index,omitempty"`
|
||
IndexTotal int `json:"index_total,omitempty"`
|
||
}
|
||
|
||
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
|
||
}
|