Files
bookhoard/templates/types.go
T
John O'Keefe 45e3a256e2 feat(collections): merge archived duplicates into their active copy
Covers the copy-then-delete-later workflow: a user copies books to a new
library, deletes the originals, and ends up with an active copy in the new
library plus an archived twin holding the real reading history. Until now
those twins could only be purged (destroying the history) or restored
(showing a permanently broken entry).

- POST /api/media-items/:id/merge (admin only, body {target_id}):
  validates the source is archived/missing, the target is active, and
  both share the same file_sha256; then re-parents every child row onto
  the target via the existing reparent_media_item_children function (the
  same machinery as hash-conflict resolution) and deletes the source row.
  Per-user collisions keep the active copy's data, mirroring that flow.
  Affected data: reading progress, speed, ratings, highlights, notes,
  bookmarks (including tombstoned deleted-annotation history), formats,
  collections, kobo shelves/entitlements, sync rows, panel data,
  processing issues, and device aliases.
- GET /admin/archived: ListHiddenMediaItems' match_* columns surface each
  row's best active twin; rows with a twin get a confirm-guarded "Merge"
  button (data-merge-source/-target) next to Restore/Delete, wired in
  web/src/admin.ts like the existing unarchive/delete handlers.
- templates.ArchivedItem gains MatchID/MatchTitle/MatchLibraryName;
  frontend.go populates them from the listing row.

After a merge the archived row is gone, so the retention purge can never
destroy the merged data. Deleted-annotation tombstones carry over and
remain restorable from the target book's "recently deleted" history.
2026-09-21 18:09:05 -04:00

303 lines
6.4 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
}
type PageData struct {
User User
CurrentPage string
}
type CollectionData struct {
ID string
Name string
Description string
Color string
Icon string
IsSystem bool
}
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
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
}