Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ab294bf81 | ||
|
|
e944f11415 | ||
|
|
708598687e | ||
|
|
aac7c72900 | ||
|
|
fe5ab9e5f8 | ||
|
|
249b1dfe93 | ||
|
|
17216e9cc7 | ||
|
|
841db91a29 |
@@ -347,6 +347,10 @@ type Querier interface {
|
|||||||
ListDeletedAnnotationsForBook(ctx context.Context, arg ListDeletedAnnotationsForBookParams) ([]ListDeletedAnnotationsForBookRow, error)
|
ListDeletedAnnotationsForBook(ctx context.Context, arg ListDeletedAnnotationsForBookParams) ([]ListDeletedAnnotationsForBookRow, error)
|
||||||
ListDevicesByType(ctx context.Context, deviceType string) ([]Devices, error)
|
ListDevicesByType(ctx context.Context, deviceType string) ([]Devices, error)
|
||||||
ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([]Devices, error)
|
ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([]Devices, error)
|
||||||
|
// Items hidden from libraries: either missing from disk (waiting on the
|
||||||
|
// second scan that archives them) or already archived and pending purge.
|
||||||
|
// Feeds the admin archived-items page.
|
||||||
|
ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaItemsRow, error)
|
||||||
ListLibraries(ctx context.Context) ([]ListLibrariesRow, error)
|
ListLibraries(ctx context.Context) ([]ListLibrariesRow, error)
|
||||||
ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error)
|
ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error)
|
||||||
ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryRow, error)
|
ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryRow, error)
|
||||||
@@ -366,6 +370,10 @@ type Querier interface {
|
|||||||
ListUsers(ctx context.Context) ([]ListUsersRow, error)
|
ListUsers(ctx context.Context) ([]ListUsersRow, error)
|
||||||
// First consecutive scan that cannot find the file on disk.
|
// First consecutive scan that cannot find the file on disk.
|
||||||
MarkMediaItemMissing(ctx context.Context, id pgtype.UUID) error
|
MarkMediaItemMissing(ctx context.Context, id pgtype.UUID) error
|
||||||
|
// Content reappeared at a new location while the old path vanished: treat as
|
||||||
|
// a move. Repoints the row (and refreshes file-level fields) so reading
|
||||||
|
// history follows the book; archive/missing state is cleared separately.
|
||||||
|
MoveMediaItemFilePath(ctx context.Context, arg MoveMediaItemFilePathParams) error
|
||||||
// Manual bulk purge from the library admin page.
|
// Manual bulk purge from the library admin page.
|
||||||
PurgeAllArchivedMediaItems(ctx context.Context) ([]PurgeAllArchivedMediaItemsRow, error)
|
PurgeAllArchivedMediaItems(ctx context.Context) ([]PurgeAllArchivedMediaItemsRow, error)
|
||||||
// Retention sweep at library-scan time: hard-delete archived items older
|
// Retention sweep at library-scan time: hard-delete archived items older
|
||||||
|
|||||||
@@ -3097,6 +3097,7 @@ next_books AS (
|
|||||||
FROM media_items mi
|
FROM media_items mi
|
||||||
JOIN user_series_progress usp ON mi.series = usp.series
|
JOIN user_series_progress usp ON mi.series = usp.series
|
||||||
WHERE mi.archived_at IS NULL
|
WHERE mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND ($3::uuid IS NULL OR mi.library_id = $3::uuid)
|
AND ($3::uuid IS NULL OR mi.library_id = $3::uuid)
|
||||||
AND (mi.series_number > usp.max_read_number OR usp.max_read_number IS NULL)
|
AND (mi.series_number > usp.max_read_number OR usp.max_read_number IS NULL)
|
||||||
ORDER BY mi.series, mi.series_number ASC NULLS LAST
|
ORDER BY mi.series, mi.series_number ASC NULLS LAST
|
||||||
@@ -7884,6 +7885,8 @@ SELECT l.id, COUNT(mi.id) as media_count
|
|||||||
FROM libraries l
|
FROM libraries l
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
||||||
LEFT JOIN media_items mi ON mi.library_id = l.id AND mi.archived_at IS NULL
|
LEFT JOIN media_items mi ON mi.library_id = l.id AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
GROUP BY l.id
|
GROUP BY l.id
|
||||||
`
|
`
|
||||||
@@ -8415,6 +8418,174 @@ func (q *Queries) ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([]
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ListHiddenMediaItems = `-- name: ListHiddenMediaItems :many
|
||||||
|
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name
|
||||||
|
FROM media_items mi
|
||||||
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
|
WHERE mi.archived_at IS NOT NULL OR mi.missing_scan_count > 0
|
||||||
|
ORDER BY mi.archived_at NULLS LAST, mi.updated_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListHiddenMediaItemsRow struct {
|
||||||
|
ID pgtype.UUID `db:"id" json:"id"`
|
||||||
|
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||||
|
Title string `db:"title" json:"title"`
|
||||||
|
Author pgtype.Text `db:"author" json:"author"`
|
||||||
|
Isbn pgtype.Text `db:"isbn" json:"isbn"`
|
||||||
|
Description pgtype.Text `db:"description" json:"description"`
|
||||||
|
FilePath string `db:"file_path" json:"file_path"`
|
||||||
|
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
|
||||||
|
MimeType pgtype.Text `db:"mime_type" json:"mime_type"`
|
||||||
|
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
|
||||||
|
Series pgtype.Text `db:"series" json:"series"`
|
||||||
|
SeriesNumber pgtype.Int4 `db:"series_number" json:"series_number"`
|
||||||
|
Tags []string `db:"tags" json:"tags"`
|
||||||
|
Asin pgtype.Text `db:"asin" json:"asin"`
|
||||||
|
DatePublished pgtype.Date `db:"date_published" json:"date_published"`
|
||||||
|
Publisher pgtype.Text `db:"publisher" json:"publisher"`
|
||||||
|
Contributors []string `db:"contributors" json:"contributors"`
|
||||||
|
Language pgtype.Text `db:"language" json:"language"`
|
||||||
|
Edition pgtype.Text `db:"edition" json:"edition"`
|
||||||
|
PageCount pgtype.Int4 `db:"page_count" json:"page_count"`
|
||||||
|
Genre pgtype.Text `db:"genre" json:"genre"`
|
||||||
|
CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"`
|
||||||
|
GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"`
|
||||||
|
OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"`
|
||||||
|
GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"`
|
||||||
|
AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"`
|
||||||
|
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||||
|
ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||||
|
FormatGroup string `db:"format_group" json:"format_group"`
|
||||||
|
FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"`
|
||||||
|
IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"`
|
||||||
|
HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"`
|
||||||
|
TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"`
|
||||||
|
ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"`
|
||||||
|
EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"`
|
||||||
|
RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"`
|
||||||
|
KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"`
|
||||||
|
KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"`
|
||||||
|
MangaType pgtype.Text `db:"manga_type" json:"manga_type"`
|
||||||
|
ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"`
|
||||||
|
SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"`
|
||||||
|
Volume pgtype.Int4 `db:"volume" json:"volume"`
|
||||||
|
Imprint pgtype.Text `db:"imprint" json:"imprint"`
|
||||||
|
AgeRating pgtype.Text `db:"age_rating" json:"age_rating"`
|
||||||
|
WebUrl pgtype.Text `db:"web_url" json:"web_url"`
|
||||||
|
StoryArc pgtype.Text `db:"story_arc" json:"story_arc"`
|
||||||
|
IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"`
|
||||||
|
MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"`
|
||||||
|
CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"`
|
||||||
|
AlternateInfo []byte `db:"alternate_info" json:"alternate_info"`
|
||||||
|
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
||||||
|
Summary pgtype.Text `db:"summary" json:"summary"`
|
||||||
|
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
||||||
|
MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"`
|
||||||
|
ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"`
|
||||||
|
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
||||||
|
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
||||||
|
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
||||||
|
ContributorsSearch []string `db:"contributors_search" json:"contributors_search"`
|
||||||
|
FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"`
|
||||||
|
OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"`
|
||||||
|
OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"`
|
||||||
|
HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"`
|
||||||
|
LibraryName string `db:"library_name" json:"library_name"`
|
||||||
|
LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Items hidden from libraries: either missing from disk (waiting on the
|
||||||
|
// second scan that archives them) or already archived and pending purge.
|
||||||
|
// Feeds the admin archived-items page.
|
||||||
|
func (q *Queries) ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaItemsRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListHiddenMediaItems)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListHiddenMediaItemsRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListHiddenMediaItemsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.LibraryID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Author,
|
||||||
|
&i.Isbn,
|
||||||
|
&i.Description,
|
||||||
|
&i.FilePath,
|
||||||
|
&i.FileSize,
|
||||||
|
&i.MimeType,
|
||||||
|
&i.CoverImagePath,
|
||||||
|
&i.Series,
|
||||||
|
&i.SeriesNumber,
|
||||||
|
&i.Tags,
|
||||||
|
&i.Asin,
|
||||||
|
&i.DatePublished,
|
||||||
|
&i.Publisher,
|
||||||
|
&i.Contributors,
|
||||||
|
&i.Language,
|
||||||
|
&i.Edition,
|
||||||
|
&i.PageCount,
|
||||||
|
&i.Genre,
|
||||||
|
&i.CopyrightYear,
|
||||||
|
&i.GoodreadsID,
|
||||||
|
&i.OpenlibraryID,
|
||||||
|
&i.GoogleBooksID,
|
||||||
|
&i.AddedByAdminID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.ImportedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.FormatGroup,
|
||||||
|
&i.FormatMimetype,
|
||||||
|
&i.IsReflowable,
|
||||||
|
&i.HasFixedLayout,
|
||||||
|
&i.TotalCharacters,
|
||||||
|
&i.ChapterCount,
|
||||||
|
&i.EntitlementID,
|
||||||
|
&i.RevisionNumber,
|
||||||
|
&i.KoboContentID,
|
||||||
|
&i.KoboMetadata,
|
||||||
|
&i.MangaType,
|
||||||
|
&i.ReadingDirection,
|
||||||
|
&i.SeriesCount,
|
||||||
|
&i.Volume,
|
||||||
|
&i.Imprint,
|
||||||
|
&i.AgeRating,
|
||||||
|
&i.WebUrl,
|
||||||
|
&i.StoryArc,
|
||||||
|
&i.IsBlackAndWhite,
|
||||||
|
&i.MetadataNotes,
|
||||||
|
&i.CommunityRating,
|
||||||
|
&i.AlternateInfo,
|
||||||
|
&i.ScanInformation,
|
||||||
|
&i.Summary,
|
||||||
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
|
&i.ChapterMetadata,
|
||||||
|
&i.LibraryTypeName,
|
||||||
|
&i.TagsSearch,
|
||||||
|
&i.ContributorsSearch,
|
||||||
|
&i.FileSha256,
|
||||||
|
&i.OpfIdentifier,
|
||||||
|
&i.OpfUuid,
|
||||||
|
&i.HashConfidence,
|
||||||
|
&i.LibraryName,
|
||||||
|
&i.LibraryTypeName_2,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const ListLibraries = `-- name: ListLibraries :many
|
const ListLibraries = `-- name: ListLibraries :many
|
||||||
SELECT l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, lt.name as type_name, lt.description as type_description
|
SELECT l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, lt.name as type_name, lt.description as type_description
|
||||||
FROM libraries l
|
FROM libraries l
|
||||||
@@ -8470,6 +8641,7 @@ FROM media_items mi
|
|||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
WHERE mi.archived_at IS NULL
|
WHERE mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2
|
ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -8641,6 +8813,7 @@ JOIN libraries l ON mi.library_id = l.id
|
|||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
WHERE mi.library_id = $1
|
WHERE mi.library_id = $1
|
||||||
AND mi.archived_at IS NULL
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
ORDER BY mi.created_at DESC
|
ORDER BY mi.created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -9156,6 +9329,7 @@ FROM media_items mi
|
|||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
WHERE mi.archived_at IS NULL
|
WHERE mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND mi.library_id = $1
|
AND mi.library_id = $1
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE
|
CASE
|
||||||
@@ -9805,6 +9979,26 @@ func (q *Queries) MarkMediaItemMissing(ctx context.Context, id pgtype.UUID) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MoveMediaItemFilePath = `-- name: MoveMediaItemFilePath :exec
|
||||||
|
UPDATE media_items SET file_path = $2, file_size = $3, missing_scan_count = 0,
|
||||||
|
archived_at = NULL, updated_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type MoveMediaItemFilePathParams struct {
|
||||||
|
ID pgtype.UUID `db:"id" json:"id"`
|
||||||
|
FilePath string `db:"file_path" json:"file_path"`
|
||||||
|
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content reappeared at a new location while the old path vanished: treat as
|
||||||
|
// a move. Repoints the row (and refreshes file-level fields) so reading
|
||||||
|
// history follows the book; archive/missing state is cleared separately.
|
||||||
|
func (q *Queries) MoveMediaItemFilePath(ctx context.Context, arg MoveMediaItemFilePathParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, MoveMediaItemFilePath, arg.ID, arg.FilePath, arg.FileSize)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const PurgeAllArchivedMediaItems = `-- name: PurgeAllArchivedMediaItems :many
|
const PurgeAllArchivedMediaItems = `-- name: PurgeAllArchivedMediaItems :many
|
||||||
DELETE FROM media_items
|
DELETE FROM media_items
|
||||||
WHERE archived_at IS NOT NULL
|
WHERE archived_at IS NOT NULL
|
||||||
@@ -10407,6 +10601,7 @@ JOIN libraries l ON mi.library_id = l.id
|
|||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
AND mi.archived_at IS NULL
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND mi.library_id = $3
|
AND mi.library_id = $3
|
||||||
AND word_similarity($1, COALESCE(mi.author, '')) > 0.3
|
AND word_similarity($1, COALESCE(mi.author, '')) > 0.3
|
||||||
AND mi.author IS NOT NULL
|
AND mi.author IS NOT NULL
|
||||||
@@ -10466,6 +10661,7 @@ JOIN libraries l ON mi.library_id = l.id
|
|||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
AND mi.archived_at IS NULL
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND mi.library_id = $3
|
AND mi.library_id = $3
|
||||||
AND word_similarity($1, COALESCE(mi.genre, '')) > 0.3
|
AND word_similarity($1, COALESCE(mi.genre, '')) > 0.3
|
||||||
AND mi.genre IS NOT NULL
|
AND mi.genre IS NOT NULL
|
||||||
@@ -10525,6 +10721,7 @@ JOIN libraries l ON mi.library_id = l.id
|
|||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
AND mi.archived_at IS NULL
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND mi.library_id = $3
|
AND mi.library_id = $3
|
||||||
AND word_similarity($1, COALESCE(mi.language, '')) > 0.3
|
AND word_similarity($1, COALESCE(mi.language, '')) > 0.3
|
||||||
AND mi.language IS NOT NULL
|
AND mi.language IS NOT NULL
|
||||||
@@ -10582,6 +10779,7 @@ JOIN library_types lt ON l.library_type_id = lt.id
|
|||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
AND mi.archived_at IS NULL
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
|
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
|
||||||
AND (
|
AND (
|
||||||
mi.title ILIKE $3 OR
|
mi.title ILIKE $3 OR
|
||||||
@@ -10827,6 +11025,7 @@ JOIN library_types lt ON l.library_type_id = lt.id
|
|||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
AND mi.archived_at IS NULL
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND ($2::uuid IS NULL
|
AND ($2::uuid IS NULL
|
||||||
OR mi.library_id = $2::uuid)
|
OR mi.library_id = $2::uuid)
|
||||||
-- Fuzzy author filter
|
-- Fuzzy author filter
|
||||||
@@ -11112,6 +11311,7 @@ JOIN libraries l ON mi.library_id = l.id
|
|||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
AND mi.archived_at IS NULL
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND mi.library_id = $3
|
AND mi.library_id = $3
|
||||||
AND word_similarity($1, COALESCE(mi.series, '')) > 0.3
|
AND word_similarity($1, COALESCE(mi.series, '')) > 0.3
|
||||||
AND mi.series IS NOT NULL
|
AND mi.series IS NOT NULL
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ SELECT l.id, COUNT(mi.id) as media_count
|
|||||||
FROM libraries l
|
FROM libraries l
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
||||||
LEFT JOIN media_items mi ON mi.library_id = l.id AND mi.archived_at IS NULL
|
LEFT JOIN media_items mi ON mi.library_id = l.id AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
GROUP BY l.id;
|
GROUP BY l.id;
|
||||||
|
|
||||||
@@ -161,6 +163,7 @@ FROM media_items mi
|
|||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
WHERE mi.archived_at IS NULL
|
WHERE mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2;
|
ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2;
|
||||||
|
|
||||||
-- name: ListMediaItemsByLibrary :many
|
-- name: ListMediaItemsByLibrary :many
|
||||||
@@ -170,6 +173,7 @@ JOIN libraries l ON mi.library_id = l.id
|
|||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
WHERE mi.library_id = $1
|
WHERE mi.library_id = $1
|
||||||
AND mi.archived_at IS NULL
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
ORDER BY mi.created_at DESC;
|
ORDER BY mi.created_at DESC;
|
||||||
|
|
||||||
-- name: ListMediaItemsByLibraryIncludingArchived :many
|
-- name: ListMediaItemsByLibraryIncludingArchived :many
|
||||||
@@ -186,6 +190,7 @@ FROM media_items mi
|
|||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
WHERE mi.archived_at IS NULL
|
WHERE mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND mi.library_id = sqlc.narg('library_id')
|
AND mi.library_id = sqlc.narg('library_id')
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE
|
CASE
|
||||||
@@ -309,6 +314,14 @@ WHERE id = $1;
|
|||||||
UPDATE media_items SET missing_scan_count = missing_scan_count + 1, updated_at = NOW()
|
UPDATE media_items SET missing_scan_count = missing_scan_count + 1, updated_at = NOW()
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: MoveMediaItemFilePath :exec
|
||||||
|
-- Content reappeared at a new location while the old path vanished: treat as
|
||||||
|
-- a move. Repoints the row (and refreshes file-level fields) so reading
|
||||||
|
-- history follows the book; archive/missing state is cleared separately.
|
||||||
|
UPDATE media_items SET file_path = $2, file_size = $3, missing_scan_count = 0,
|
||||||
|
archived_at = NULL, updated_at = NOW()
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
-- name: ArchiveMediaItem :exec
|
-- name: ArchiveMediaItem :exec
|
||||||
-- Second consecutive missing scan: hide the item from all browsing while
|
-- Second consecutive missing scan: hide the item from all browsing while
|
||||||
-- preserving reading history in case the file returns.
|
-- preserving reading history in case the file returns.
|
||||||
@@ -337,6 +350,17 @@ RETURNING id, title;
|
|||||||
-- name: CountArchivedMediaItems :one
|
-- name: CountArchivedMediaItems :one
|
||||||
SELECT COUNT(*) FROM media_items WHERE archived_at IS NOT NULL;
|
SELECT COUNT(*) FROM media_items WHERE archived_at IS NOT NULL;
|
||||||
|
|
||||||
|
-- name: ListHiddenMediaItems :many
|
||||||
|
-- Items hidden from libraries: either missing from disk (waiting on the
|
||||||
|
-- second scan that archives them) or already archived and pending purge.
|
||||||
|
-- Feeds the admin archived-items page.
|
||||||
|
SELECT mi.*, l.name as library_name, lt.name as library_type_name
|
||||||
|
FROM media_items mi
|
||||||
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
|
WHERE mi.archived_at IS NOT NULL OR mi.missing_scan_count > 0
|
||||||
|
ORDER BY mi.archived_at NULLS LAST, mi.updated_at DESC;
|
||||||
|
|
||||||
-- name: DeleteMediaItem :exec
|
-- name: DeleteMediaItem :exec
|
||||||
DELETE FROM media_items WHERE id = $1;
|
DELETE FROM media_items WHERE id = $1;
|
||||||
|
|
||||||
@@ -486,6 +510,7 @@ JOIN library_types lt ON l.library_type_id = lt.id
|
|||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
AND mi.archived_at IS NULL
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||||
AND (
|
AND (
|
||||||
mi.title ILIKE sqlc.narg('search_pattern') OR
|
mi.title ILIKE sqlc.narg('search_pattern') OR
|
||||||
@@ -559,6 +584,7 @@ JOIN library_types lt ON l.library_type_id = lt.id
|
|||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
AND mi.archived_at IS NULL
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND (sqlc.narg('library_id')::uuid IS NULL
|
AND (sqlc.narg('library_id')::uuid IS NULL
|
||||||
OR mi.library_id = sqlc.narg('library_id')::uuid)
|
OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||||
-- Fuzzy author filter
|
-- Fuzzy author filter
|
||||||
@@ -693,6 +719,7 @@ JOIN libraries l ON mi.library_id = l.id
|
|||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
AND mi.archived_at IS NULL
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND mi.library_id = sqlc.narg('library_id')
|
AND mi.library_id = sqlc.narg('library_id')
|
||||||
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, '')) > 0.3
|
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, '')) > 0.3
|
||||||
AND mi.author IS NOT NULL
|
AND mi.author IS NOT NULL
|
||||||
@@ -711,6 +738,7 @@ JOIN libraries l ON mi.library_id = l.id
|
|||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
AND mi.archived_at IS NULL
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND mi.library_id = sqlc.narg('library_id')
|
AND mi.library_id = sqlc.narg('library_id')
|
||||||
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.genre, '')) > 0.3
|
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.genre, '')) > 0.3
|
||||||
AND mi.genre IS NOT NULL
|
AND mi.genre IS NOT NULL
|
||||||
@@ -746,6 +774,7 @@ JOIN libraries l ON mi.library_id = l.id
|
|||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
AND mi.archived_at IS NULL
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND mi.library_id = sqlc.narg('library_id')
|
AND mi.library_id = sqlc.narg('library_id')
|
||||||
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, '')) > 0.3
|
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, '')) > 0.3
|
||||||
AND mi.series IS NOT NULL
|
AND mi.series IS NOT NULL
|
||||||
@@ -764,6 +793,7 @@ JOIN libraries l ON mi.library_id = l.id
|
|||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
AND mi.archived_at IS NULL
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND mi.library_id = sqlc.narg('library_id')
|
AND mi.library_id = sqlc.narg('library_id')
|
||||||
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.language, '')) > 0.3
|
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.language, '')) > 0.3
|
||||||
AND mi.language IS NOT NULL
|
AND mi.language IS NOT NULL
|
||||||
@@ -2512,6 +2542,7 @@ next_books AS (
|
|||||||
FROM media_items mi
|
FROM media_items mi
|
||||||
JOIN user_series_progress usp ON mi.series = usp.series
|
JOIN user_series_progress usp ON mi.series = usp.series
|
||||||
WHERE mi.archived_at IS NULL
|
WHERE mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||||
AND (mi.series_number > usp.max_read_number OR usp.max_read_number IS NULL)
|
AND (mi.series_number > usp.max_read_number OR usp.max_read_number IS NULL)
|
||||||
ORDER BY mi.series, mi.series_number ASC NULLS LAST
|
ORDER BY mi.series, mi.series_number ASC NULLS LAST
|
||||||
|
|||||||
@@ -1330,6 +1330,30 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error {
|
|||||||
return c.JSON(http.StatusOK, item)
|
return c.JSON(http.StatusOK, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnarchiveMediaItem handles POST /api/media-items/:id/unarchive (admin
|
||||||
|
// only). Manually restores a hidden/archived item to library visibility
|
||||||
|
// without waiting for its file to return. If the file is still gone, the
|
||||||
|
// next scan hides it again.
|
||||||
|
func (mh *MediaHandler) UnarchiveMediaItem(c *echo.Context) error {
|
||||||
|
user := MustGetAuthenticatedUser(c)
|
||||||
|
|
||||||
|
if user.Role != "admin" {
|
||||||
|
return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID := c.Param("id")
|
||||||
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mh.db.ClearMediaItemArchive(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true}); err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
// PurgeArchivedMediaItems handles POST /api/media-items/purge-archived
|
// PurgeArchivedMediaItems handles POST /api/media-items/purge-archived
|
||||||
// (admin only). Hard-deletes every archived item (files missing from disk for
|
// (admin only). Hard-deletes every archived item (files missing from disk for
|
||||||
// 2+ scans) together with its reading history. The archive retention window
|
// 2+ scans) together with its reading history. The archive retention window
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"bookhoard/internal/config"
|
||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
"bookhoard/internal/handlers"
|
"bookhoard/internal/handlers"
|
||||||
"bookhoard/internal/services"
|
"bookhoard/internal/services"
|
||||||
@@ -295,6 +296,18 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
libraryID := libRes.LibraryID
|
libraryID := libRes.LibraryID
|
||||||
libData := libRes.Libraries
|
libData := libRes.Libraries
|
||||||
|
|
||||||
|
// Steam Deck behavior: an offline library (all folders missing, e.g.
|
||||||
|
// an unmounted external drive) shows an empty shelf with a notice
|
||||||
|
// instead of querying items. Nothing is marked or purged.
|
||||||
|
if !libRes.IsAll {
|
||||||
|
for _, l := range libData {
|
||||||
|
if l.ID == libraryID && l.Offline {
|
||||||
|
errorMsg = l.Name + " is currently unavailable - its storage is not connected. Your books are safe and will return when the drive is reattached."
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch saved filters for SSR
|
// Fetch saved filters for SSR
|
||||||
userUUID, _ := uuid.Parse(user.ID)
|
userUUID, _ := uuid.Parse(user.ID)
|
||||||
var savedFilters []database.SavedFilters
|
var savedFilters []database.SavedFilters
|
||||||
@@ -836,6 +849,54 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
return c.HTML(http.StatusOK, buf.String())
|
return c.HTML(http.StatusOK, buf.String())
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
frontendProtected.GET("/admin/archived", handlers.AdminMiddleware(func(c *echo.Context) error {
|
||||||
|
user, err := getTemplateUserWithTheme(c, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := cfg.Queries.ListHiddenMediaItems(c.Request().Context())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ListHiddenMediaItems failed: %v", err)
|
||||||
|
rows = []database.ListHiddenMediaItemsRow{}
|
||||||
|
}
|
||||||
|
|
||||||
|
retentionDays := config.ArchiveRetentionDays()
|
||||||
|
items := make([]templates.ArchivedItem, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
itemUUID, _ := uuid.FromBytes(row.ID.Bytes[0:16])
|
||||||
|
item := templates.ArchivedItem{
|
||||||
|
ID: itemUUID.String(),
|
||||||
|
Title: row.Title,
|
||||||
|
Author: getText(row.Author),
|
||||||
|
LibraryName: row.LibraryName,
|
||||||
|
FilePath: row.FilePath,
|
||||||
|
MissingScans: row.MissingScanCount,
|
||||||
|
}
|
||||||
|
if row.ArchivedAt.Valid {
|
||||||
|
archived := row.ArchivedAt.Time
|
||||||
|
item.ArchivedAt = &archived
|
||||||
|
switch {
|
||||||
|
case retentionDays <= 0:
|
||||||
|
item.Status = "Archived - kept until manually purged (retention disabled)"
|
||||||
|
default:
|
||||||
|
purge := archived.AddDate(0, 0, retentionDays)
|
||||||
|
item.PurgeAt = &purge
|
||||||
|
item.Status = "Archived - permanently deleted " + purge.Format("Jan 2, 2006")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.Status = fmt.Sprintf("Missing from disk - archives on the next scan (%d/2)", row.MissingScanCount)
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := templates.AdminArchived(user, items, retentionDays).Render(c.Request().Context(), &buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.HTML(http.StatusOK, buf.String())
|
||||||
|
}))
|
||||||
|
|
||||||
frontendProtected.GET("/admin/library", handlers.AdminMiddleware(func(c *echo.Context) error {
|
frontendProtected.GET("/admin/library", handlers.AdminMiddleware(func(c *echo.Context) error {
|
||||||
user, err := getTemplateUserWithTheme(c, cfg)
|
user, err := getTemplateUserWithTheme(c, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
@@ -132,12 +133,30 @@ func resolveLibrary(c *echo.Context, cfg *Config, userUUID string) LibraryResolu
|
|||||||
res.Libraries = make([]templates.LibraryData, len(libraries))
|
res.Libraries = make([]templates.LibraryData, len(libraries))
|
||||||
for i, lib := range libraries {
|
for i, lib := range libraries {
|
||||||
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
|
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
|
||||||
|
// Steam Deck SD-card behavior: a library whose folders are all
|
||||||
|
// missing (unmounted drive) renders offline - hidden contents, no
|
||||||
|
// marking, no purging - and returns when the storage does.
|
||||||
|
offline := false
|
||||||
|
folders, folderErr := cfg.Queries.GetLibraryFolders(c.Request().Context(), lib.ID)
|
||||||
|
if folderErr != nil || len(folders) == 0 {
|
||||||
|
offline = true
|
||||||
|
} else {
|
||||||
|
anyFolderExists := false
|
||||||
|
for _, folder := range folders {
|
||||||
|
if _, statErr := os.Stat(folder.FolderPath); statErr == nil {
|
||||||
|
anyFolderExists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offline = !anyFolderExists
|
||||||
|
}
|
||||||
res.Libraries[i] = templates.LibraryData{
|
res.Libraries[i] = templates.LibraryData{
|
||||||
ID: libUUID.String(),
|
ID: libUUID.String(),
|
||||||
Name: lib.Name,
|
Name: lib.Name,
|
||||||
Description: getText(lib.Description),
|
Description: getText(lib.Description),
|
||||||
TypeName: lib.TypeName,
|
TypeName: lib.TypeName,
|
||||||
MediaCount: countMap[libUUID.String()],
|
MediaCount: countMap[libUUID.String()],
|
||||||
|
Offline: offline,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ func registerMediaRoutes(cfg *Config) {
|
|||||||
admin.POST("/media-items/:id/rescan", cfg.MediaHandler.RescanMediaItem)
|
admin.POST("/media-items/:id/rescan", cfg.MediaHandler.RescanMediaItem)
|
||||||
admin.DELETE("/media-items/:id", cfg.MediaHandler.DeleteMediaItem)
|
admin.DELETE("/media-items/:id", cfg.MediaHandler.DeleteMediaItem)
|
||||||
admin.POST("/media-items/purge-archived", cfg.MediaHandler.PurgeArchivedMediaItems)
|
admin.POST("/media-items/purge-archived", cfg.MediaHandler.PurgeArchivedMediaItems)
|
||||||
|
admin.POST("/media-items/:id/unarchive", cfg.MediaHandler.UnarchiveMediaItem)
|
||||||
|
|
||||||
// Shelf management (protected)
|
// Shelf management (protected)
|
||||||
protected.POST("/devices/:id/shelves", cfg.MediaHandler.AddToShelf)
|
protected.POST("/devices/:id/shelves", cfg.MediaHandler.AddToShelf)
|
||||||
|
|||||||
@@ -795,6 +795,39 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool,
|
|||||||
LibraryID: libraryID,
|
LibraryID: libraryID,
|
||||||
})
|
})
|
||||||
if err == nil && existingByHash.ID.Valid {
|
if err == nil && existingByHash.ID.Valid {
|
||||||
|
// Same content at a different path. If the old location is gone,
|
||||||
|
// the book was MOVED: repoint the row so history follows it and
|
||||||
|
// the archive pass doesn't cycle it into missing/archived. Only
|
||||||
|
// treat it as a duplicate copy when the old path still exists.
|
||||||
|
oldPathStillExists := false
|
||||||
|
for _, folder := range s.folders {
|
||||||
|
if _, statErr := os.Stat(filepath.Join(folder, existingByHash.FilePath)); statErr == nil {
|
||||||
|
oldPathStillExists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !oldPathStillExists {
|
||||||
|
if err := s.db.MoveMediaItemFilePath(ctx, database.MoveMediaItemFilePathParams{
|
||||||
|
ID: existingByHash.ID,
|
||||||
|
FilePath: s.getRelativePath(path),
|
||||||
|
FileSize: pgtype.Int8{Int64: info.Size(), Valid: true},
|
||||||
|
}); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to repoint moved media item %s -> %s: %v\n", existingByHash.FilePath, path, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("[MOVE] Item content moved: %q -> %s (history preserved)\n", existingByHash.FilePath, path)
|
||||||
|
s.logger.LogDelete(fmt.Sprintf("[MOVE] Repointed item '%s' from %q to %s", existingByHash.Title, existingByHash.FilePath, path))
|
||||||
|
}
|
||||||
|
if s.forceRescan {
|
||||||
|
moved := existingByHash
|
||||||
|
moved.FilePath = s.getRelativePath(path)
|
||||||
|
moved.ArchivedAt = pgtype.Timestamptz{}
|
||||||
|
moved.MissingScanCount = 0
|
||||||
|
_ = s.updateMediaItem(ctx, moved, path)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("Media item with same SHA-256 already exists in library (path %q), skipping duplicate: %s\n",
|
fmt.Printf("Media item with same SHA-256 already exists in library (path %q), skipping duplicate: %s\n",
|
||||||
existingByHash.FilePath, path)
|
existingByHash.FilePath, path)
|
||||||
// Content returned (possibly at a new path): restore archived rows.
|
// Content returned (possibly at a new path): restore archived rows.
|
||||||
@@ -1007,6 +1040,27 @@ func (s *MediaScanner) extractCalibreSidecar(path string) *MediaMetadata {
|
|||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// applyEmbeddedCoverFallback extracts a cover from the media file itself when
|
||||||
|
// a metadata sidecar (metadata.opf / metadata.json) supplied the metadata but
|
||||||
|
// no sidecar cover (cover.jpg etc.) exists. Without this, a full rescan would
|
||||||
|
// clear cover_image_path for sidecar-managed PDFs and EPUBs - mergeMetadata
|
||||||
|
// has no PDF/EPUB cover logic of its own.
|
||||||
|
func (s *MediaScanner) applyEmbeddedCoverFallback(path string, metadata *MediaMetadata) {
|
||||||
|
if metadata.CoverPath != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch strings.ToLower(filepath.Ext(path)) {
|
||||||
|
case ".pdf":
|
||||||
|
if coverPath, err := s.extractPDFCover(path); err == nil && coverPath != "" {
|
||||||
|
metadata.CoverPath = s.getRelativePath(coverPath)
|
||||||
|
}
|
||||||
|
case ".epub", ".kepub":
|
||||||
|
if coverPath, err := s.extractEPUBCover(path); err == nil && coverPath != "" {
|
||||||
|
metadata.CoverPath = s.getRelativePath(coverPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// extractAudiobookshelfSidecar checks for and parses an Audiobookshelf-style
|
// extractAudiobookshelfSidecar checks for and parses an Audiobookshelf-style
|
||||||
// metadata.json sidecar next to the media file. Only fields with a matching
|
// metadata.json sidecar next to the media file. Only fields with a matching
|
||||||
// media_items column are mapped; narrators, subtitle, explicit, abridged and
|
// media_items column are mapped; narrators, subtitle, explicit, abridged and
|
||||||
@@ -1107,7 +1161,7 @@ func (s *MediaScanner) mergeMetadata(path string, calibreMetadata *MediaMetadata
|
|||||||
ext := strings.ToLower(filepath.Ext(path))
|
ext := strings.ToLower(filepath.Ext(path))
|
||||||
|
|
||||||
// For EPUB files
|
// For EPUB files
|
||||||
if ext == ".epub" {
|
if ext == ".epub" || ext == ".kepub" {
|
||||||
book, err := epub.ReadBook(path)
|
book, err := epub.ReadBook(path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
genreTags := extractGenreTagsFromEPUB(book)
|
genreTags := extractGenreTagsFromEPUB(book)
|
||||||
@@ -1123,6 +1177,76 @@ func (s *MediaScanner) mergeMetadata(path string, calibreMetadata *MediaMetadata
|
|||||||
metadata.PageCount = int32(pageCount)
|
metadata.PageCount = int32(pageCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gap-fill: sidecar-sourced metadata wins, but blanks are filled from
|
||||||
|
// the book's own OPF so a sparse metadata.opf/metadata.json doesn't
|
||||||
|
// hide data the file carries. Never overwrites sidecar values.
|
||||||
|
if embedded, err := s.extractEPUBMetadata(path); err == nil && embedded != nil {
|
||||||
|
if metadata.Title == "" && embedded.Title != "" {
|
||||||
|
metadata.Title = embedded.Title
|
||||||
|
}
|
||||||
|
if metadata.Author == "" && embedded.Author != "" {
|
||||||
|
metadata.Author = embedded.Author
|
||||||
|
}
|
||||||
|
if metadata.Description == "" && embedded.Description != "" {
|
||||||
|
metadata.Description = embedded.Description
|
||||||
|
}
|
||||||
|
if metadata.Publisher == "" && embedded.Publisher != "" {
|
||||||
|
metadata.Publisher = embedded.Publisher
|
||||||
|
}
|
||||||
|
if metadata.Language == "" && embedded.Language != "" {
|
||||||
|
metadata.Language = embedded.Language
|
||||||
|
}
|
||||||
|
if metadata.ISBN == "" && embedded.ISBN != "" {
|
||||||
|
metadata.ISBN = embedded.ISBN
|
||||||
|
}
|
||||||
|
if metadata.ASIN == "" && embedded.ASIN != "" {
|
||||||
|
metadata.ASIN = embedded.ASIN
|
||||||
|
}
|
||||||
|
if metadata.Series == "" && embedded.Series != "" {
|
||||||
|
metadata.Series = embedded.Series
|
||||||
|
}
|
||||||
|
if metadata.SeriesNumber == 0 && embedded.SeriesNumber != 0 {
|
||||||
|
metadata.SeriesNumber = embedded.SeriesNumber
|
||||||
|
}
|
||||||
|
if metadata.ReadingDirection == "" && embedded.ReadingDirection != "" {
|
||||||
|
metadata.ReadingDirection = embedded.ReadingDirection
|
||||||
|
}
|
||||||
|
if metadata.PublishDate.IsZero() && !embedded.PublishDate.IsZero() {
|
||||||
|
metadata.PublishDate = embedded.PublishDate
|
||||||
|
}
|
||||||
|
if len(metadata.Tags) == 0 && len(embedded.Tags) > 0 {
|
||||||
|
metadata.Tags = embedded.Tags
|
||||||
|
}
|
||||||
|
if len(metadata.Contributors) == 0 && len(embedded.Contributors) > 0 {
|
||||||
|
metadata.Contributors = embedded.Contributors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For PDFs: fill blanks from the embedded Info dictionary. Same rule as
|
||||||
|
// EPUBs - sidecar values win, the file only fills gaps.
|
||||||
|
if ext == ".pdf" {
|
||||||
|
if embedded, err := s.readPDFInfoDict(path); err == nil && embedded != nil {
|
||||||
|
if metadata.Title == "" && embedded.Title != "" {
|
||||||
|
metadata.Title = embedded.Title
|
||||||
|
}
|
||||||
|
if metadata.Author == "" && embedded.Author != "" {
|
||||||
|
metadata.Author = embedded.Author
|
||||||
|
}
|
||||||
|
if metadata.Description == "" && embedded.Description != "" {
|
||||||
|
metadata.Description = embedded.Description
|
||||||
|
}
|
||||||
|
if metadata.Publisher == "" && embedded.Publisher != "" {
|
||||||
|
metadata.Publisher = embedded.Publisher
|
||||||
|
}
|
||||||
|
if len(metadata.Tags) == 0 && len(embedded.Tags) > 0 {
|
||||||
|
metadata.Tags = embedded.Tags
|
||||||
|
}
|
||||||
|
if metadata.PageCount == 0 && embedded.PageCount > 0 {
|
||||||
|
metadata.PageCount = embedded.PageCount
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For comic archives, try to extract ComicInfo.xml
|
// For comic archives, try to extract ComicInfo.xml
|
||||||
@@ -1414,6 +1538,7 @@ func (s *MediaScanner) extractMetadata(path string) (*MediaMetadata, error) {
|
|||||||
if coverPath != "" {
|
if coverPath != "" {
|
||||||
calibreMetadata.CoverPath = s.getRelativePath(coverPath)
|
calibreMetadata.CoverPath = s.getRelativePath(coverPath)
|
||||||
}
|
}
|
||||||
|
s.applyEmbeddedCoverFallback(path, calibreMetadata)
|
||||||
|
|
||||||
return s.mergeMetadata(path, calibreMetadata)
|
return s.mergeMetadata(path, calibreMetadata)
|
||||||
}
|
}
|
||||||
@@ -1428,6 +1553,7 @@ func (s *MediaScanner) extractMetadata(path string) (*MediaMetadata, error) {
|
|||||||
if coverPath != "" {
|
if coverPath != "" {
|
||||||
abMetadata.CoverPath = s.getRelativePath(coverPath)
|
abMetadata.CoverPath = s.getRelativePath(coverPath)
|
||||||
}
|
}
|
||||||
|
s.applyEmbeddedCoverFallback(path, abMetadata)
|
||||||
|
|
||||||
return s.mergeMetadata(path, abMetadata)
|
return s.mergeMetadata(path, abMetadata)
|
||||||
}
|
}
|
||||||
@@ -1743,6 +1869,12 @@ func parseOPFContent(content []byte) (*MediaMetadata, error) {
|
|||||||
if title := opf.selectTitle(); title != "" {
|
if title := opf.selectTitle(); title != "" {
|
||||||
metadata.Title = title
|
metadata.Title = title
|
||||||
}
|
}
|
||||||
|
// Reading direction from the OPF spine's page-progression-direction.
|
||||||
|
// Sidecar OPFs are metadata-only documents without a spine, so this is a
|
||||||
|
// no-op for them and only fires on real books.
|
||||||
|
if dir := opf.pageProgressionDirection(); dir != "" {
|
||||||
|
metadata.ReadingDirection = dir
|
||||||
|
}
|
||||||
// Author (first creator)
|
// Author (first creator)
|
||||||
if len(md.Creators) > 0 {
|
if len(md.Creators) > 0 {
|
||||||
metadata.Author = md.Creators[0]
|
metadata.Author = md.Creators[0]
|
||||||
@@ -2170,6 +2302,51 @@ func findSidecarCover(mediaPath string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readPDFInfoDict reads a PDF's embedded Info dictionary into MediaMetadata,
|
||||||
|
// using the same field conventions as extractPDFMetadata (creator falls back
|
||||||
|
// to author, subject maps to description, producer to publisher, keywords to
|
||||||
|
// tags). No cover or filename logic - purely the document's own metadata, for
|
||||||
|
// filling sidecar gaps in mergeMetadata.
|
||||||
|
func (s *MediaScanner) readPDFInfoDict(path string) (*MediaMetadata, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to close PDF file %s: %v\n", path, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
pdfInfo, err := pdfcpuapi.PDFInfo(f, filepath.Base(path), nil, false, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := &MediaMetadata{}
|
||||||
|
if pdfInfo.Title != "" {
|
||||||
|
metadata.Title = pdfInfo.Title
|
||||||
|
}
|
||||||
|
if pdfInfo.Author != "" {
|
||||||
|
metadata.Author = pdfInfo.Author
|
||||||
|
} else if pdfInfo.Creator != "" {
|
||||||
|
metadata.Author = pdfInfo.Creator
|
||||||
|
}
|
||||||
|
if pdfInfo.Subject != "" {
|
||||||
|
metadata.Description = pdfInfo.Subject
|
||||||
|
}
|
||||||
|
if pdfInfo.Producer != "" {
|
||||||
|
metadata.Publisher = pdfInfo.Producer
|
||||||
|
}
|
||||||
|
if len(pdfInfo.Keywords) > 0 {
|
||||||
|
metadata.Tags = utils.NormalizeTags(pdfInfo.Keywords)
|
||||||
|
}
|
||||||
|
if pdfInfo.PageCount > 0 {
|
||||||
|
metadata.PageCount = int32(pdfInfo.PageCount)
|
||||||
|
}
|
||||||
|
return metadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *MediaScanner) extractPDFMetadata(path string) (*MediaMetadata, error) {
|
func (s *MediaScanner) extractPDFMetadata(path string) (*MediaMetadata, error) {
|
||||||
metadata := &MediaMetadata{}
|
metadata := &MediaMetadata{}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package services
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"os"
|
"os"
|
||||||
@@ -230,3 +231,180 @@ func TestExtractAudiobookshelfSidecar(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createTestPDFWithImage builds a minimal one-page PDF embedding a JPEG
|
||||||
|
// XObject, so pdfcpu-based cover extraction has an image to find.
|
||||||
|
func createTestPDFWithImage() []byte {
|
||||||
|
jpegData := []byte(createTestJPEGBytes())
|
||||||
|
content := "q 100 0 0 100 0 0 cm /Im0 Do Q"
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
offsets := []int{0} // object numbers are 1-based
|
||||||
|
buf.WriteString("%PDF-1.4\n")
|
||||||
|
|
||||||
|
writeObj := func(n int, body func(w *bytes.Buffer)) {
|
||||||
|
offsets = append(offsets, buf.Len())
|
||||||
|
fmt.Fprintf(&buf, "%d 0 obj\n", n)
|
||||||
|
body(&buf)
|
||||||
|
buf.WriteString("endobj\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
writeObj(1, func(w *bytes.Buffer) { w.WriteString("<< /Type /Catalog /Pages 2 0 R >>\n") })
|
||||||
|
writeObj(2, func(w *bytes.Buffer) { w.WriteString("<< /Type /Pages /Kids [3 0 R] /Count 1 >>\n") })
|
||||||
|
writeObj(3, func(w *bytes.Buffer) {
|
||||||
|
w.WriteString("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources << /XObject << /Im0 4 0 R >> >> /Contents 5 0 R >>\n")
|
||||||
|
})
|
||||||
|
writeObj(4, func(w *bytes.Buffer) {
|
||||||
|
fmt.Fprintf(w, "<< /Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length %d >>\nstream\n", len(jpegData))
|
||||||
|
w.Write(jpegData)
|
||||||
|
w.WriteString("\nendstream\n")
|
||||||
|
})
|
||||||
|
writeObj(5, func(w *bytes.Buffer) {
|
||||||
|
fmt.Fprintf(w, "<< /Length %d >>\nstream\n%s\nendstream\n", len(content), content)
|
||||||
|
})
|
||||||
|
writeObj(6, func(w *bytes.Buffer) {
|
||||||
|
// Info dictionary deliberately richer than the sidecars in gap-fill
|
||||||
|
// tests: gap-fill must use these, never overwrite with them.
|
||||||
|
w.WriteString("<< /Title (Embedded Title) /Author (Embedded Author) /Subject (Embedded Subject) /Producer (Embedded Producer) /Keywords (embedded-kw) >>\n")
|
||||||
|
})
|
||||||
|
|
||||||
|
xrefStart := buf.Len()
|
||||||
|
fmt.Fprintf(&buf, "xref\n0 %d\n0000000000 65535 f \n", len(offsets))
|
||||||
|
for _, off := range offsets[1:] {
|
||||||
|
fmt.Fprintf(&buf, "%010d 00000 n \n", off)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&buf, "trailer\n<< /Size %d /Root 1 0 R /Info 6 0 R >>\nstartxref\n%d\n%%%%EOF\n", len(offsets), xrefStart)
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSidecarCoverFallback guards the regression where a metadata.json
|
||||||
|
// sidecar (no cover.jpg next to the book) made extractMetadata return an
|
||||||
|
// empty CoverPath for PDFs - mergeMetadata has no PDF/EPUB cover logic, so a
|
||||||
|
// full rescan wiped cover_image_path for sidecar-managed books.
|
||||||
|
func TestSidecarCoverFallback(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
pdfPath := filepath.Join(dir, "book.pdf")
|
||||||
|
if err := os.WriteFile(pdfPath, createTestPDFWithImage(), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "metadata.json"), []byte(`{"title":"Sidecar Book","authors":["A"]}`), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := NewMediaScanner(nil)
|
||||||
|
s.folders = []string{dir} // SetFolders requires a live DB; tests only need path relativization
|
||||||
|
metadata, err := s.extractMetadata(pdfPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extractMetadata() error: %v", err)
|
||||||
|
}
|
||||||
|
if metadata.Title != "Sidecar Book" {
|
||||||
|
t.Errorf("Title = %q, want sidecar title", metadata.Title)
|
||||||
|
}
|
||||||
|
if metadata.CoverPath == "" {
|
||||||
|
t.Fatal("CoverPath empty - sidecar branch skipped embedded cover extraction")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, metadata.CoverPath)); err != nil {
|
||||||
|
t.Errorf("extracted cover not on disk: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMergeMetadataEPUBGapFill verifies that a sparse sidecar (metadata.opf
|
||||||
|
// with only a title) gets its blanks filled from the book's own embedded OPF
|
||||||
|
// while sidecar-set fields are never overwritten.
|
||||||
|
func TestMergeMetadataEPUBGapFill(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
epubPath := filepath.Join(dir, "gappy.epub")
|
||||||
|
f, err := os.Create(epubPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
zipWriter := zip.NewWriter(f)
|
||||||
|
mimetype, _ := zipWriter.CreateHeader(&zip.FileHeader{Name: "mimetype", Method: zip.Store})
|
||||||
|
mimetype.Write([]byte("application/epub+zip"))
|
||||||
|
epubFiles := map[string]string{
|
||||||
|
"META-INF/container.xml": `<?xml version="1.0"?><container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"><rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles></container>`,
|
||||||
|
"OEBPS/content.opf": `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="u">
|
||||||
|
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<dc:title>Embedded Title</dc:title>
|
||||||
|
<dc:creator>Embedded Author</dc:creator>
|
||||||
|
<dc:description>Embedded description from the book.</dc:description>
|
||||||
|
<dc:publisher>Embedded Publisher</dc:publisher>
|
||||||
|
<dc:language>en</dc:language>
|
||||||
|
<dc:date>2021-06-01</dc:date>
|
||||||
|
</metadata>
|
||||||
|
<manifest/>
|
||||||
|
<spine/>
|
||||||
|
</package>`,
|
||||||
|
}
|
||||||
|
for name, content := range epubFiles {
|
||||||
|
w, err := zipWriter.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
w.Write([]byte(content))
|
||||||
|
}
|
||||||
|
if err := zipWriter.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
sparse := &MediaMetadata{Title: "Sidecar Title", Author: "Sidecar Author"}
|
||||||
|
s := NewMediaScanner(nil)
|
||||||
|
merged, err := s.mergeMetadata(epubPath, sparse)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mergeMetadata() error: %v", err)
|
||||||
|
}
|
||||||
|
if merged.Title != "Sidecar Title" || merged.Author != "Sidecar Author" {
|
||||||
|
t.Errorf("sidecar values overwritten: title=%q author=%q", merged.Title, merged.Author)
|
||||||
|
}
|
||||||
|
if merged.Description != "Embedded description from the book." {
|
||||||
|
t.Errorf("Description = %q, want embedded fill", merged.Description)
|
||||||
|
}
|
||||||
|
if merged.Publisher != "Embedded Publisher" {
|
||||||
|
t.Errorf("Publisher = %q, want embedded fill", merged.Publisher)
|
||||||
|
}
|
||||||
|
if merged.Language != "en" {
|
||||||
|
t.Errorf("Language = %q, want embedded fill", merged.Language)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMergeMetadataPDFGapFill verifies the same for PDFs: a sparse sidecar
|
||||||
|
// keeps its title while author/description/publisher/tags/pagecount fill in
|
||||||
|
// from the embedded Info dictionary.
|
||||||
|
func TestMergeMetadataPDFGapFill(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
pdfPath := filepath.Join(dir, "gappy.pdf")
|
||||||
|
if err := os.WriteFile(pdfPath, createTestPDFWithImage(), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sparse := &MediaMetadata{Title: "Kept Title"}
|
||||||
|
s := NewMediaScanner(nil)
|
||||||
|
merged, err := s.mergeMetadata(pdfPath, sparse)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mergeMetadata() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checks := []struct {
|
||||||
|
name string
|
||||||
|
got string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"Title (sidecar wins)", merged.Title, "Kept Title"},
|
||||||
|
{"Author", merged.Author, "Embedded Author"},
|
||||||
|
{"Description", merged.Description, "Embedded Subject"},
|
||||||
|
{"Publisher", merged.Publisher, "Embedded Producer"},
|
||||||
|
}
|
||||||
|
for _, c := range checks {
|
||||||
|
if c.got != c.want {
|
||||||
|
t.Errorf("%s = %q, want %q", c.name, c.got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(merged.Tags) == 0 {
|
||||||
|
t.Error("Tags empty, want keywords from Info dict")
|
||||||
|
}
|
||||||
|
if merged.PageCount != 1 {
|
||||||
|
t.Errorf("PageCount = %d, want 1 from Info dict", merged.PageCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ type opfDocument struct {
|
|||||||
Items []opfItem `xml:"item"`
|
Items []opfItem `xml:"item"`
|
||||||
} `xml:"manifest"`
|
} `xml:"manifest"`
|
||||||
Spine struct {
|
Spine struct {
|
||||||
|
PageProgressionDirection string `xml:"page-progression-direction,attr"`
|
||||||
Itemrefs []struct {
|
Itemrefs []struct {
|
||||||
IDRef string `xml:"idref,attr"`
|
IDRef string `xml:"idref,attr"`
|
||||||
} `xml:"itemref"`
|
} `xml:"itemref"`
|
||||||
@@ -184,6 +185,22 @@ func (d *opfDocument) readSeries() (series string, index float64) {
|
|||||||
return series, index
|
return series, index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pageProgressionDirection returns the OPF spine's reading direction as
|
||||||
|
// "rtl" or "ltr", or "" when the file declares none (callers treat that as
|
||||||
|
// unknown, not as left-to-right). EPUB2/3 declare this on <spine>; it is
|
||||||
|
// what foliate reads client-side, and the DB column feeds clients (and the
|
||||||
|
// web reader's fixed-layout override) that need it up front.
|
||||||
|
func (d *opfDocument) pageProgressionDirection() string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(d.Spine.PageProgressionDirection)) {
|
||||||
|
case "rtl", "right-to-left":
|
||||||
|
return "rtl"
|
||||||
|
case "ltr", "left-to-right", "default":
|
||||||
|
return "ltr"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// itemByID returns manifest items with id, href and media-type, keyed by id.
|
// itemByID returns manifest items with id, href and media-type, keyed by id.
|
||||||
func (d *opfDocument) itemByID() map[string]opfItem {
|
func (d *opfDocument) itemByID() map[string]opfItem {
|
||||||
m := make(map[string]opfItem, len(d.Manifest.Items))
|
m := make(map[string]opfItem, len(d.Manifest.Items))
|
||||||
|
|||||||
@@ -221,3 +221,77 @@ func TestResolveOPFPath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestParseOPFContentPageProgressionDirection verifies the EPUB spine's
|
||||||
|
// page-progression-direction feeds ReadingDirection, and that an undeclared
|
||||||
|
// direction stays empty rather than forcing left-to-right.
|
||||||
|
func TestParseOPFContentPageProgressionDirection(t *testing.T) {
|
||||||
|
makeOPF := func(spineAttrs string) string {
|
||||||
|
return `<?xml version="1.0"?>
|
||||||
|
<package xmlns="http://www.idpf.org/2007/opf" version="3.0">
|
||||||
|
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>Dir Test</dc:title></metadata>
|
||||||
|
<manifest><item id="p1" href="p1.xhtml" media-type="application/xhtml+xml"/></manifest>
|
||||||
|
<spine` + spineAttrs + `><itemref idref="p1"/></spine>
|
||||||
|
</package>`
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
spineAttrs string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"rtl declared lowercase", ` page-progression-direction="rtl"`, "rtl"},
|
||||||
|
{"rtl declared uppercase", ` page-progression-direction="RTL"`, "rtl"},
|
||||||
|
{"ltr declared", ` page-progression-direction="ltr"`, "ltr"},
|
||||||
|
{"undeclared stays empty", ``, ""},
|
||||||
|
{"unknown value stays empty", ` page-progression-direction="sideways"`, ""},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
metadata, err := parseOPFContent([]byte(makeOPF(tt.spineAttrs)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseOPFContent() error: %v", err)
|
||||||
|
}
|
||||||
|
if metadata.ReadingDirection != tt.want {
|
||||||
|
t.Errorf("ReadingDirection = %q, want %q", metadata.ReadingDirection, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMergeMetadataReadingDirectionGapFill verifies the sidecar wins when it
|
||||||
|
// declares a direction, while an embedded-only direction fills the blank.
|
||||||
|
func TestMergeMetadataReadingDirectionGapFill(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
epubPath := filepath.Join(dir, "dir.epub")
|
||||||
|
files := map[string]string{
|
||||||
|
"META-INF/container.xml": containerXML,
|
||||||
|
"OEBPS/package.opf": `<?xml version="1.0"?>
|
||||||
|
<package xmlns="http://www.idpf.org/2007/opf" version="3.0">
|
||||||
|
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>RTL Book</dc:title></metadata>
|
||||||
|
<manifest><item id="p1" href="p1.xhtml" media-type="application/xhtml+xml"/></manifest>
|
||||||
|
<spine page-progression-direction="rtl"><itemref idref="p1"/></spine>
|
||||||
|
</package>`,
|
||||||
|
"OEBPS/p1.xhtml": `<html><body><p>hi</p></body></html>`,
|
||||||
|
}
|
||||||
|
writeEPUB(t, epubPath, files)
|
||||||
|
|
||||||
|
s := NewMediaScanner(nil)
|
||||||
|
|
||||||
|
// Sidecar blank -> embedded rtl fills it
|
||||||
|
merged, err := s.mergeMetadata(epubPath, &MediaMetadata{Title: "Sidecar"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mergeMetadata() error: %v", err)
|
||||||
|
}
|
||||||
|
if merged.ReadingDirection != "rtl" {
|
||||||
|
t.Errorf("ReadingDirection = %q, want embedded rtl fill", merged.ReadingDirection)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sidecar ltr wins over embedded rtl
|
||||||
|
merged, err = s.mergeMetadata(epubPath, &MediaMetadata{Title: "Sidecar", ReadingDirection: "ltr"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mergeMetadata() error: %v", err)
|
||||||
|
}
|
||||||
|
if merged.ReadingDirection != "ltr" {
|
||||||
|
t.Errorf("ReadingDirection = %q, want sidecar ltr preserved", merged.ReadingDirection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
templ AdminArchived(user User, items []ArchivedItem, retentionDays int) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<title>Archived Items - Bookhoard</title>
|
||||||
|
<script src="/static/htmx.min.js"></script>
|
||||||
|
<link href="/static/style.css" rel="stylesheet"/>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||||
|
</head>
|
||||||
|
<body class="theme-{ user.Theme }">
|
||||||
|
@Header(user, "/admin/archived")
|
||||||
|
<main class="p-8">
|
||||||
|
<div class="max-w-5xl">
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between gap-4 flex-wrap">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-3 mb-1">
|
||||||
|
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||||
|
@Icon("library", "h-5 w-5")
|
||||||
|
</span>
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Archived Items</h1>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm" style="color: var(--text-secondary)">
|
||||||
|
Books hidden because their files are missing from disk. Reading history is
|
||||||
|
kept in case a file returns; moves are detected automatically.
|
||||||
|
if retentionDays > 0 {
|
||||||
|
Archived items are permanently deleted after { retentionDays } days
|
||||||
|
(ARCHIVE_RETENTION_DAYS).
|
||||||
|
} else {
|
||||||
|
Archived items are kept until manually purged (retention disabled).
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
if len(items) > 0 {
|
||||||
|
<button type="button" onclick="purgeArchivedItems()" class="btn btn-secondary">
|
||||||
|
@Icon("trash", "h-4 w-4")
|
||||||
|
Purge All Archived
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
if len(items) == 0 {
|
||||||
|
<div class="card p-8 text-center">
|
||||||
|
<p style="color: var(--text-secondary)">Nothing is archived or missing. All clear.</p>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="card overflow-hidden">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b text-left" style="border-color: var(--border); color: var(--text-secondary);">
|
||||||
|
<th class="px-4 py-3 font-semibold">Book</th>
|
||||||
|
<th class="px-4 py-3 font-semibold">Library</th>
|
||||||
|
<th class="px-4 py-3 font-semibold">Status</th>
|
||||||
|
<th class="px-4 py-3 font-semibold text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
for _, item := range items {
|
||||||
|
<tr class="border-b" style="border-color: color-mix(in srgb, var(--text-primary) 5%, transparent);">
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<p class="font-medium" style="color: var(--text-primary)">{ item.Title }</p>
|
||||||
|
if item.Author != "" {
|
||||||
|
<p style="color: var(--text-secondary)">{ item.Author }</p>
|
||||||
|
}
|
||||||
|
<p class="font-mono text-xs break-all" style="color: var(--text-secondary)">{ item.FilePath }</p>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3" style="color: var(--text-primary)">{ item.LibraryName }</td>
|
||||||
|
<td class="px-4 py-3" style="color: var(--text-secondary)">{ item.Status }</td>
|
||||||
|
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-unarchive={ item.ID }
|
||||||
|
class="btn btn-ghost text-xs"
|
||||||
|
title="Restore to libraries now; if the file is still gone the next scan hides it again"
|
||||||
|
>
|
||||||
|
@Icon("refresh", "h-3.5 w-3.5")
|
||||||
|
Restore
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-delete-archived={ item.ID }
|
||||||
|
class="btn btn-ghost text-xs"
|
||||||
|
title="Delete this item and its reading history permanently"
|
||||||
|
>
|
||||||
|
@Icon("trash", "h-3.5 w-3.5")
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1020
|
||||||
|
package templates
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Archived Items - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-{ user.Theme }\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = Header(user, "/admin/archived").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"p-8\"><div class=\"max-w-5xl\"><div class=\"mb-8\"><div class=\"flex items-center justify-between gap-4 flex-wrap\"><div><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = Icon("library", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Archived Items</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Books hidden because their files are missing from disk. Reading history is kept in case a file returns; moves are detected automatically. ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if retentionDays > 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Archived items are permanently deleted after ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(retentionDays)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 30, Col: 70}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " days (ARCHIVE_RETENTION_DAYS).")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Archived items are kept until manually purged (retention disabled).")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(items) > 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<button type=\"button\" onclick=\"purgeArchivedItems()\" class=\"btn btn-secondary\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Purge All Archived</button>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"card p-8 text-center\"><p style=\"color: var(--text-secondary)\">Nothing is archived or missing. All clear.</p></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"card overflow-hidden\"><table class=\"w-full text-sm\"><thead><tr class=\"border-b text-left\" style=\"border-color: var(--border); color: var(--text-secondary);\"><th class=\"px-4 py-3 font-semibold\">Book</th><th class=\"px-4 py-3 font-semibold\">Library</th><th class=\"px-4 py-3 font-semibold\">Status</th><th class=\"px-4 py-3 font-semibold text-right\">Actions</th></tr></thead> <tbody>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<tr class=\"border-b\" style=\"border-color: color-mix(in srgb, var(--text-primary) 5%, transparent);\"><td class=\"px-4 py-3\"><p class=\"font-medium\" style=\"color: var(--text-primary)\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 65, Col: 82}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if item.Author != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<p style=\"color: var(--text-secondary)\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 67, Col: 66}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<p class=\"font-mono text-xs break-all\" style=\"color: var(--text-secondary)\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.FilePath)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 69, Col: 103}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</p></td><td class=\"px-4 py-3\" style=\"color: var(--text-primary)\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.LibraryName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 71, Col: 86}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td><td class=\"px-4 py-3\" style=\"color: var(--text-secondary)\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.Status)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 72, Col: 83}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td><td class=\"px-4 py-3 text-right whitespace-nowrap\"><button type=\"button\" data-unarchive=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 76, Col: 37}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" class=\"btn btn-ghost text-xs\" title=\"Restore to libraries now; if the file is still gone the next scan hides it again\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = Icon("refresh", "h-3.5 w-3.5").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "Restore</button> <button type=\"button\" data-delete-archived=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 85, Col: 43}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" class=\"btn btn-ghost text-xs\" title=\"Delete this item and its reading history permanently\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = Icon("trash", "h-3.5 w-3.5").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Delete</button></td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</tbody></table></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div></main></body></html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -38,18 +38,23 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
|
|||||||
<div class="card p-4 mb-6 flex items-center justify-between gap-4 flex-wrap">
|
<div class="card p-4 mb-6 flex items-center justify-between gap-4 flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold" style="color: var(--text-primary)">
|
<p class="font-semibold" style="color: var(--text-primary)">
|
||||||
|
<a href="/admin/archived" class="hover:underline" style="color: var(--text-primary); text-decoration: none;">
|
||||||
Archived items: { archivedCount }
|
Archived items: { archivedCount }
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm" style="color: var(--text-secondary)">
|
<p class="text-sm" style="color: var(--text-secondary)">
|
||||||
Files missing from disk for two consecutive scans. Reading history is kept
|
Files missing from disk for two consecutive scans. Reading history is kept
|
||||||
unless purged; items return automatically if their files come back.
|
unless purged; items return automatically if their files come back.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<a href="/admin/archived" class="btn btn-secondary">View Archived</a>
|
||||||
<button type="button" onclick="purgeArchivedItems()" class="btn btn-secondary">
|
<button type="button" onclick="purgeArchivedItems()" class="btn btn-secondary">
|
||||||
@Icon("trash", "h-4 w-4")
|
@Icon("trash", "h-4 w-4")
|
||||||
Purge Archived Now
|
Purge Archived Now
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
<div id="libraries-container">
|
<div id="libraries-container">
|
||||||
@LibraryList(user, libraries, users)
|
@LibraryList(user, libraries, users)
|
||||||
|
|||||||
@@ -58,20 +58,20 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if archivedCount > 0 {
|
if archivedCount > 0 {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"card p-4 mb-6 flex items-center justify-between gap-4 flex-wrap\"><div><p class=\"font-semibold\" style=\"color: var(--text-primary)\">Archived items: ")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"card p-4 mb-6 flex items-center justify-between gap-4 flex-wrap\"><div><p class=\"font-semibold\" style=\"color: var(--text-primary)\"><a href=\"/admin/archived\" class=\"hover:underline\" style=\"color: var(--text-primary); text-decoration: none;\">Archived items: ")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(archivedCount)
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(archivedCount)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 41, Col: 40}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 42, Col: 41}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</p><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Files missing from disk for two consecutive scans. Reading history is kept unless purged; items return automatically if their files come back.</p></div><button type=\"button\" onclick=\"purgeArchivedItems()\" class=\"btn btn-secondary\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</a></p><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Files missing from disk for two consecutive scans. Reading history is kept unless purged; items return automatically if their files come back.</p></div><div class=\"flex items-center gap-2\"><a href=\"/admin/archived\" class=\"btn btn-secondary\">View Archived</a> <button type=\"button\" onclick=\"purgeArchivedItems()\" class=\"btn btn-secondary\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "Purge Archived Now</button></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "Purge Archived Now</button></div></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -211,7 +211,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
|||||||
var templ_7745c5c3_Var4 string
|
var templ_7745c5c3_Var4 string
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(library.Name)
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(library.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 224, Col: 92}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 229, Col: 92}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -229,7 +229,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
|||||||
var templ_7745c5c3_Var5 string
|
var templ_7745c5c3_Var5 string
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(library.Description)
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(library.Description)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 226, Col: 95}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 231, Col: 95}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -251,7 +251,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
|||||||
var templ_7745c5c3_Var6 string
|
var templ_7745c5c3_Var6 string
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(library.TypeName)
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(library.TypeName)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 231, Col: 26}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 236, Col: 26}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -273,7 +273,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
|||||||
var templ_7745c5c3_Var7 string
|
var templ_7745c5c3_Var7 string
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(library.FolderCount)
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(library.FolderCount)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 236, Col: 30}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 241, Col: 30}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -291,7 +291,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
|||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var8 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/libraries/" + library.ID + "/scan")
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/libraries/" + library.ID + "/scan")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 242, Col: 58}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 247, Col: 58}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -312,7 +312,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
|||||||
var templ_7745c5c3_Var9 string
|
var templ_7745c5c3_Var9 string
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.ID)
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.ID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 252, Col: 32}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 257, Col: 32}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -333,7 +333,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
|||||||
var templ_7745c5c3_Var10 string
|
var templ_7745c5c3_Var10 string
|
||||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("library-panel-" + library.ID)
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("library-panel-" + library.ID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 261, Col: 44}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 266, Col: 44}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -408,7 +408,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var12 string
|
var templ_7745c5c3_Var12 string
|
||||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(folder.FolderPath)
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(folder.FolderPath)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 283, Col: 99}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 288, Col: 99}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -421,7 +421,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var13 string
|
var templ_7745c5c3_Var13 string
|
||||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/folders")
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/folders")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 286, Col: 62}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 291, Col: 62}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -434,7 +434,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var14 string
|
var templ_7745c5c3_Var14 string
|
||||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(`{"folder_path": "` + folder.FolderPath + `"}`)
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(`{"folder_path": "` + folder.FolderPath + `"}`)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 287, Col: 64}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 292, Col: 64}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -447,7 +447,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var15 string
|
var templ_7745c5c3_Var15 string
|
||||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 288, Col: 49}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 293, Col: 49}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -478,7 +478,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var16 string
|
var templ_7745c5c3_Var16 string
|
||||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/folders")
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/folders")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 298, Col: 80}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 303, Col: 80}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -491,7 +491,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var17 string
|
var templ_7745c5c3_Var17 string
|
||||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 298, Col: 124}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 303, Col: 124}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -504,7 +504,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var18 string
|
var templ_7745c5c3_Var18 string
|
||||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue("folder-input-" + libraryID)
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue("folder-input-" + libraryID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 302, Col: 37}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 307, Col: 37}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -517,7 +517,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var19 string
|
var templ_7745c5c3_Var19 string
|
||||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
|
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 309, Col: 32}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 314, Col: 32}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -530,7 +530,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var20 string
|
var templ_7745c5c3_Var20 string
|
||||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue("folder-input-" + libraryID)
|
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue("folder-input-" + libraryID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 310, Col: 52}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 315, Col: 52}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -587,7 +587,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var21 string
|
var templ_7745c5c3_Var21 string
|
||||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/visibility")
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/visibility")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 339, Col: 63}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 344, Col: 63}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -600,7 +600,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var22 string
|
var templ_7745c5c3_Var22 string
|
||||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(`{"user_id": "` + u.ID + `"}`)
|
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(`{"user_id": "` + u.ID + `"}`)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 340, Col: 47}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 345, Col: 47}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -613,7 +613,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var23 string
|
var templ_7745c5c3_Var23 string
|
||||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
|
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 342, Col: 49}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 347, Col: 49}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -626,7 +626,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var24 string
|
var templ_7745c5c3_Var24 string
|
||||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(u.Username)
|
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(u.Username)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 345, Col: 76}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 350, Col: 76}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -639,7 +639,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var25 string
|
var templ_7745c5c3_Var25 string
|
||||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(u.Email)
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(u.Email)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 346, Col: 75}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 351, Col: 75}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -667,7 +667,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var26 templ.SafeURL
|
var templ_7745c5c3_Var26 templ.SafeURL
|
||||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs("/admin/libraries/" + libraryID + "/issues")
|
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs("/admin/libraries/" + libraryID + "/issues")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 355, Col: 57}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 360, Col: 57}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -694,7 +694,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var27 string
|
var templ_7745c5c3_Var27 string
|
||||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(issueCount)
|
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(issueCount)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 360, Col: 24}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 365, Col: 24}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -721,7 +721,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var28 string
|
var templ_7745c5c3_Var28 string
|
||||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
|
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 369, Col: 28}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 374, Col: 28}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -734,7 +734,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var29 string
|
var templ_7745c5c3_Var29 string
|
||||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Name)
|
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 370, Col: 33}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 375, Col: 33}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -747,7 +747,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var30 string
|
var templ_7745c5c3_Var30 string
|
||||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Description)
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Description)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 371, Col: 40}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 376, Col: 40}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -768,7 +768,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var31 string
|
var templ_7745c5c3_Var31 string
|
||||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
|
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 379, Col: 30}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 384, Col: 30}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -781,7 +781,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
|||||||
var templ_7745c5c3_Var32 string
|
var templ_7745c5c3_Var32 string
|
||||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Name)
|
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 380, Col: 35}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 385, Col: 35}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -839,7 +839,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
|
|||||||
var templ_7745c5c3_Var34 string
|
var templ_7745c5c3_Var34 string
|
||||||
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(currentPath)
|
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(currentPath)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 395, Col: 89}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 400, Col: 89}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -857,7 +857,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
|
|||||||
var templ_7745c5c3_Var35 string
|
var templ_7745c5c3_Var35 string
|
||||||
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/browse?path=" + parentPath + "&target_input=" + targetInput + "&library_id=" + libraryID)
|
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/browse?path=" + parentPath + "&target_input=" + targetInput + "&library_id=" + libraryID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 401, Col: 117}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 406, Col: 117}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -888,7 +888,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
|
|||||||
var templ_7745c5c3_Var36 string
|
var templ_7745c5c3_Var36 string
|
||||||
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/browse?path=" + entry.Path + "&target_input=" + targetInput + "&library_id=" + libraryID)
|
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/browse?path=" + entry.Path + "&target_input=" + targetInput + "&library_id=" + libraryID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 414, Col: 118}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 419, Col: 118}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -909,7 +909,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
|
|||||||
var templ_7745c5c3_Var37 string
|
var templ_7745c5c3_Var37 string
|
||||||
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Name)
|
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 419, Col: 40}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 424, Col: 40}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -927,7 +927,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
|
|||||||
var templ_7745c5c3_Var38 string
|
var templ_7745c5c3_Var38 string
|
||||||
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(targetInput)
|
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(targetInput)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 426, Col: 35}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 431, Col: 35}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -940,7 +940,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
|
|||||||
var templ_7745c5c3_Var39 string
|
var templ_7745c5c3_Var39 string
|
||||||
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(currentPath)
|
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(currentPath)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 427, Col: 35}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 432, Col: 35}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
@@ -149,7 +149,13 @@ templ BookShelf(
|
|||||||
<option value="">All Books ({ TotalMediaCount(libraries) })</option>
|
<option value="">All Books ({ TotalMediaCount(libraries) })</option>
|
||||||
}
|
}
|
||||||
for _, lib := range libraries {
|
for _, lib := range libraries {
|
||||||
|
if lib.Offline {
|
||||||
if lib.ID == currentLibraryID {
|
if lib.ID == currentLibraryID {
|
||||||
|
<option value={ lib.ID } selected>{ lib.Name } ({ lib.MediaCount }) — storage offline</option>
|
||||||
|
} else {
|
||||||
|
<option value={ lib.ID }>{ lib.Name } ({ lib.MediaCount }) — storage offline</option>
|
||||||
|
}
|
||||||
|
} else if lib.ID == currentLibraryID {
|
||||||
<option value={ lib.ID } selected>{ lib.Name } ({ lib.MediaCount })</option>
|
<option value={ lib.ID } selected>{ lib.Name } ({ lib.MediaCount })</option>
|
||||||
} else {
|
} else {
|
||||||
<option value={ lib.ID }>{ lib.Name } ({ lib.MediaCount })</option>
|
<option value={ lib.ID }>{ lib.Name } ({ lib.MediaCount })</option>
|
||||||
|
|||||||
+130
-40
@@ -203,6 +203,7 @@ func BookShelf(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, lib := range libraries {
|
for _, lib := range libraries {
|
||||||
|
if lib.Offline {
|
||||||
if lib.ID == currentLibraryID {
|
if lib.ID == currentLibraryID {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<option value=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<option value=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -211,7 +212,7 @@ func BookShelf(
|
|||||||
var templ_7745c5c3_Var6 string
|
var templ_7745c5c3_Var6 string
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 153, Col: 35}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 154, Col: 35}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -224,7 +225,7 @@ func BookShelf(
|
|||||||
var templ_7745c5c3_Var7 string
|
var templ_7745c5c3_Var7 string
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 153, Col: 57}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 154, Col: 57}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -237,13 +238,13 @@ func BookShelf(
|
|||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var8 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 153, Col: 77}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 154, Col: 77}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, ")</option>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, ") — storage offline</option>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -255,7 +256,7 @@ func BookShelf(
|
|||||||
var templ_7745c5c3_Var9 string
|
var templ_7745c5c3_Var9 string
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 155, Col: 35}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 156, Col: 35}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -268,7 +269,7 @@ func BookShelf(
|
|||||||
var templ_7745c5c3_Var10 string
|
var templ_7745c5c3_Var10 string
|
||||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 155, Col: 48}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 156, Col: 48}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -281,20 +282,109 @@ func BookShelf(
|
|||||||
var templ_7745c5c3_Var11 string
|
var templ_7745c5c3_Var11 string
|
||||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 155, Col: 68}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 156, Col: 68}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, ")</option>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, ") — storage offline</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if lib.ID == currentLibraryID {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<option value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 159, Col: 34}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" selected>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 159, Col: 56}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " (")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 159, Col: 76}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, ")</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<option value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 161, Col: 34}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 string
|
||||||
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 161, Col: 47}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " (")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var17 string
|
||||||
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 161, Col: 67}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, ")</option>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</select></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Author</label> <input type=\"text\" name=\"author_filter\" placeholder=\"Filter by author\" class=\"input\" list=\"author-datalist\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchAuthorValues($el)\"> <datalist id=\"author-datalist\"></datalist></div><div class=\"relative\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Tags</label> <input type=\"text\" name=\"tags_filter\" placeholder=\"Filter by tags\" class=\"input\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchTagValues($el)\" @keydown=\"onTagFilterKeydown($event)\" @blur=\"hideTagDropdown()\"><div x-show=\"showTagDropdown\" x-transition x-cloak class=\"absolute z-50 mt-1 w-full rounded-lg shadow-lg max-h-48 overflow-y-auto\" style=\"background-color: var(--bg-primary); border: 1px solid var(--border);\"><template x-for=\"sug in tagSuggestions\" :key=\"sug.value\"><button type=\"button\" class=\"w-full text-left px-3 py-2 text-sm flex justify-between items-center\" :class=\"tagHighlightIndex === tagSuggestions.indexOf(sug) ? 'opacity-80' : ''\" :style=\"tagHighlightIndex === tagSuggestions.indexOf(sug) ? 'background-color: var(--bg-secondary); color: var(--text-primary);' : 'color: var(--text-primary);'\" @click=\"selectTagSuggestion(sug.value)\"><span x-text=\"sug.value\"></span> <span class=\"text-xs\" style=\"color: var(--text-secondary);\" x-text=\"sug.count + ' books'\"></span></button></template></div></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Series</label> <input type=\"text\" name=\"series_filter\" placeholder=\"Filter by series\" class=\"input\" list=\"series-datalist\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchSeriesValues($el)\"> <datalist id=\"series-datalist\"></datalist></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Language</label> <input type=\"text\" name=\"language_filter\" placeholder=\"Filter by language\" class=\"input\" list=\"language-datalist\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchLanguageValues($el)\"> <datalist id=\"language-datalist\"></datalist></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Year Range</label><div class=\"flex gap-2\"><input type=\"number\" name=\"year_min\" placeholder=\"From\" class=\"input\"> <input type=\"number\" name=\"year_max\" placeholder=\"To\" class=\"input\"></div></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Cover</label> <input type=\"button\" id=\"has-cover-tristate\" class=\"tristate-btn w-full transition-all duration-300 cursor-pointer flex items-center justify-center gap-2 px-4 py-2 rounded-lg border text-sm\" :class=\"{\n\t\t\t\t\t\t\t\t\t\t\t'state-null': hasCoverState === null,\n\t\t\t\t\t\t\t\t\t\t\t'state-true': hasCoverState === true,\n\t\t\t\t\t\t\t\t\t\t\t'state-false': hasCoverState === false\n\t\t\t\t\t\t\t\t\t\t}\" :value=\"hasCoverState === null ? '○ Any cover' : hasCoverState === true ? '✓ Has cover' : '✗ No cover'\" @click=\"cycleHasCover()\"><template x-if=\"hasCoverState !== null\"><input type=\"hidden\" name=\"has_cover\" :value=\"hasCoverState ? 'true' : 'false'\"></template></div></div><div class=\"sticky bottom-0 px-6 py-4 border-t\" style=\"background-color: var(--bg-secondary); border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);\"><button type=\"submit\" @click=\"filtersOpen = false\" class=\"btn btn-primary w-full\">Apply Filters</button></div></div></div></form><div id=\"books-grid\" class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</select></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Author</label> <input type=\"text\" name=\"author_filter\" placeholder=\"Filter by author\" class=\"input\" list=\"author-datalist\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchAuthorValues($el)\"> <datalist id=\"author-datalist\"></datalist></div><div class=\"relative\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Tags</label> <input type=\"text\" name=\"tags_filter\" placeholder=\"Filter by tags\" class=\"input\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchTagValues($el)\" @keydown=\"onTagFilterKeydown($event)\" @blur=\"hideTagDropdown()\"><div x-show=\"showTagDropdown\" x-transition x-cloak class=\"absolute z-50 mt-1 w-full rounded-lg shadow-lg max-h-48 overflow-y-auto\" style=\"background-color: var(--bg-primary); border: 1px solid var(--border);\"><template x-for=\"sug in tagSuggestions\" :key=\"sug.value\"><button type=\"button\" class=\"w-full text-left px-3 py-2 text-sm flex justify-between items-center\" :class=\"tagHighlightIndex === tagSuggestions.indexOf(sug) ? 'opacity-80' : ''\" :style=\"tagHighlightIndex === tagSuggestions.indexOf(sug) ? 'background-color: var(--bg-secondary); color: var(--text-primary);' : 'color: var(--text-primary);'\" @click=\"selectTagSuggestion(sug.value)\"><span x-text=\"sug.value\"></span> <span class=\"text-xs\" style=\"color: var(--text-secondary);\" x-text=\"sug.count + ' books'\"></span></button></template></div></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Series</label> <input type=\"text\" name=\"series_filter\" placeholder=\"Filter by series\" class=\"input\" list=\"series-datalist\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchSeriesValues($el)\"> <datalist id=\"series-datalist\"></datalist></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Language</label> <input type=\"text\" name=\"language_filter\" placeholder=\"Filter by language\" class=\"input\" list=\"language-datalist\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchLanguageValues($el)\"> <datalist id=\"language-datalist\"></datalist></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Year Range</label><div class=\"flex gap-2\"><input type=\"number\" name=\"year_min\" placeholder=\"From\" class=\"input\"> <input type=\"number\" name=\"year_max\" placeholder=\"To\" class=\"input\"></div></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Cover</label> <input type=\"button\" id=\"has-cover-tristate\" class=\"tristate-btn w-full transition-all duration-300 cursor-pointer flex items-center justify-center gap-2 px-4 py-2 rounded-lg border text-sm\" :class=\"{\n\t\t\t\t\t\t\t\t\t\t\t'state-null': hasCoverState === null,\n\t\t\t\t\t\t\t\t\t\t\t'state-true': hasCoverState === true,\n\t\t\t\t\t\t\t\t\t\t\t'state-false': hasCoverState === false\n\t\t\t\t\t\t\t\t\t\t}\" :value=\"hasCoverState === null ? '○ Any cover' : hasCoverState === true ? '✓ Has cover' : '✗ No cover'\" @click=\"cycleHasCover()\"><template x-if=\"hasCoverState !== null\"><input type=\"hidden\" name=\"has_cover\" :value=\"hasCoverState ? 'true' : 'false'\"></template></div></div><div class=\"sticky bottom-0 px-6 py-4 border-t\" style=\"background-color: var(--bg-secondary); border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);\"><button type=\"submit\" @click=\"filtersOpen = false\" class=\"btn btn-primary w-full\">Apply Filters</button></div></div></div></form><div id=\"books-grid\" class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -303,53 +393,53 @@ func BookShelf(
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if errorMessage != "" {
|
if errorMessage != "" {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<div class=\"col-span-full mt-2 p-4 rounded-xl border\" style=\"background-color: color-mix(in srgb, var(--status-danger) 10%, transparent); border-color: var(--status-danger);\"><p style=\"color: var(--text-primary)\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div class=\"col-span-full mt-2 p-4 rounded-xl border\" style=\"background-color: color-mix(in srgb, var(--status-danger) 10%, transparent); border-color: var(--status-danger);\"><p style=\"color: var(--text-primary)\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var12 string
|
var templ_7745c5c3_Var18 string
|
||||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 269, Col: 59}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 275, Col: 59}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</p></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</p></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div><div id=\"pagination\" class=\"mt-6 flex justify-center items-center gap-2\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div><div id=\"pagination\" class=\"mt-6 flex justify-center items-center gap-2\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<button type=\"button\" class=\"btn btn-secondary disabled:opacity-40\" hx-get=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<button type=\"button\" class=\"btn btn-secondary disabled:opacity-40\" hx-get=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var13 string
|
var templ_7745c5c3_Var19 string
|
||||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/api/media-items/search?library_id=%s&limit=%d&offset=%d", currentLibraryID, limit, offset-limit))
|
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/api/media-items/search?library_id=%s&limit=%d&offset=%d", currentLibraryID, limit, offset-limit))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 278, Col: 126}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 284, Col: 126}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" hx-target=\"#books-grid\" hx-include=\"#filter-form\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\" hx-target=\"#books-grid\" hx-include=\"#filter-form\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if offset <= 0 {
|
if offset <= 0 {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " disabled")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, " disabled")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, ">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, ">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -357,43 +447,43 @@ func BookShelf(
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<span>Prev</span></button> <span class=\"px-4 py-2 text-sm\" style=\"color: var(--text-secondary);\">Page ")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<span>Prev</span></button> <span class=\"px-4 py-2 text-sm\" style=\"color: var(--text-secondary);\">Page ")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var14 string
|
var templ_7745c5c3_Var20 string
|
||||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(offset/limit + 1)
|
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(offset/limit + 1)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 287, Col: 32}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 293, Col: 32}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</span> <button type=\"button\" class=\"btn btn-secondary disabled:opacity-40\" hx-get=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</span> <button type=\"button\" class=\"btn btn-secondary disabled:opacity-40\" hx-get=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var15 string
|
var templ_7745c5c3_Var21 string
|
||||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/api/media-items/search?library_id=%s&limit=%d&offset=%d", currentLibraryID, limit, offset+limit))
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/api/media-items/search?library_id=%s&limit=%d&offset=%d", currentLibraryID, limit, offset+limit))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 292, Col: 126}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 298, Col: 126}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" hx-target=\"#books-grid\" hx-include=\"#filter-form\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" hx-target=\"#books-grid\" hx-include=\"#filter-form\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if offset+limit >= count {
|
if offset+limit >= count {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " disabled")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " disabled")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "><span>Next</span>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "><span>Next</span>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -401,12 +491,12 @@ func BookShelf(
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</button>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</button>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</div></div><div x-show=\"showSaveModal\" @click.self=\"showSaveModal = false\" x-transition x-cloak class=\"fixed inset-0 z-[70] flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div @click.stop class=\"card rounded-2xl p-6 w-full max-w-md\"><div class=\"flex justify-between items-center mb-4\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Save Filter</h2><button @click=\"showSaveModal = false\" class=\"icon-btn\" aria-label=\"Close\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</div></div><div x-show=\"showSaveModal\" @click.self=\"showSaveModal = false\" x-transition x-cloak class=\"fixed inset-0 z-[70] flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div @click.stop class=\"card rounded-2xl p-6 w-full max-w-md\"><div class=\"flex justify-between items-center mb-4\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Save Filter</h2><button @click=\"showSaveModal = false\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -414,7 +504,7 @@ func BookShelf(
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</button></div><form hx-post=\"/api/saved-filters\" hx-target=\"#saved-filters-list\" hx-swap=\"beforeend\" hx-include=\"#filter-form\" @htmx:afterRequest=\"if(event.detail.xhr.status < 400) { afterFilterSave() }\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Filter Name</label> <input type=\"text\" name=\"filter_name\" placeholder=\"My Custom Filter\" class=\"input\" required><div id=\"filter-save-error\" style=\"display: none; color: var(--status-danger); padding: 0.75rem; border-radius: 0.5rem; margin-top: 0.75rem;\"></div><input type=\"hidden\" name=\"resource_type\" value=\"media-items\"></div><div class=\"flex justify-end gap-2\"><button type=\"button\" @click=\"showSaveModal = false\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">Save</button></div><input type=\"hidden\" name=\"limit\" value=\"50\"> <input type=\"hidden\" name=\"offset\" value=\"0\"></form></div></div></body></html>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</button></div><form hx-post=\"/api/saved-filters\" hx-target=\"#saved-filters-list\" hx-swap=\"beforeend\" hx-include=\"#filter-form\" @htmx:afterRequest=\"if(event.detail.xhr.status < 400) { afterFilterSave() }\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Filter Name</label> <input type=\"text\" name=\"filter_name\" placeholder=\"My Custom Filter\" class=\"input\" required><div id=\"filter-save-error\" style=\"display: none; color: var(--status-danger); padding: 0.75rem; border-radius: 0.5rem; margin-top: 0.75rem;\"></div><input type=\"hidden\" name=\"resource_type\" value=\"media-items\"></div><div class=\"flex justify-end gap-2\"><button type=\"button\" @click=\"showSaveModal = false\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">Save</button></div><input type=\"hidden\" name=\"limit\" value=\"50\"> <input type=\"hidden\" name=\"offset\" value=\"0\"></form></div></div></body></html>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,13 @@ templ LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ..
|
|||||||
>
|
>
|
||||||
<option value="">All Libraries ({ TotalMediaCount(libData) })</option>
|
<option value="">All Libraries ({ TotalMediaCount(libData) })</option>
|
||||||
for _, lib := range libData {
|
for _, lib := range libData {
|
||||||
|
if lib.Offline {
|
||||||
if lib.ID == currentLibraryID {
|
if lib.ID == currentLibraryID {
|
||||||
|
<option value={ lib.ID } selected>{ lib.Name } ({ lib.MediaCount }) — storage offline</option>
|
||||||
|
} else {
|
||||||
|
<option value={ lib.ID }>{ lib.Name } ({ lib.MediaCount }) — storage offline</option>
|
||||||
|
}
|
||||||
|
} else if lib.ID == currentLibraryID {
|
||||||
<option value={ lib.ID } selected>{ lib.Name } ({ lib.MediaCount })</option>
|
<option value={ lib.ID } selected>{ lib.Name } ({ lib.MediaCount })</option>
|
||||||
} else {
|
} else {
|
||||||
<option value={ lib.ID }>{ lib.Name } ({ lib.MediaCount })</option>
|
<option value={ lib.ID }>{ lib.Name } ({ lib.MediaCount })</option>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
|||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(TotalMediaCount(libData))
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(TotalMediaCount(libData))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 16, Col: 63}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 16, Col: 62}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -47,6 +47,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
for _, lib := range libData {
|
for _, lib := range libData {
|
||||||
|
if lib.Offline {
|
||||||
if lib.ID == currentLibraryID {
|
if lib.ID == currentLibraryID {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<option value=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<option value=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -55,7 +56,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
|||||||
var templ_7745c5c3_Var3 string
|
var templ_7745c5c3_Var3 string
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 29}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 20, Col: 29}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -68,7 +69,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
|||||||
var templ_7745c5c3_Var4 string
|
var templ_7745c5c3_Var4 string
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 51}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 20, Col: 51}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -81,13 +82,13 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
|||||||
var templ_7745c5c3_Var5 string
|
var templ_7745c5c3_Var5 string
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 71}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 20, Col: 71}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, ")</option>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, ") — storage offline</option>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -99,7 +100,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
|||||||
var templ_7745c5c3_Var6 string
|
var templ_7745c5c3_Var6 string
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 21, Col: 29}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 22, Col: 29}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -112,7 +113,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
|||||||
var templ_7745c5c3_Var7 string
|
var templ_7745c5c3_Var7 string
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 21, Col: 42}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 22, Col: 42}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -125,24 +126,113 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
|||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var8 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 21, Col: 62}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 22, Col: 62}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, ")</option>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, ") — storage offline</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if lib.ID == currentLibraryID {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<option value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 25, Col: 28}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" selected>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 25, Col: 50}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " (")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 string
|
||||||
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 25, Col: 70}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, ")</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<option value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 27, Col: 28}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 27, Col: 41}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " (")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 27, Col: 61}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, ")</option>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</select></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</select></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if len(actions) > 0 {
|
if len(actions) > 0 {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"flex items-center gap-2\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div class=\"flex items-center gap-2\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -152,12 +242,12 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div><div id=\"loading-spinner\" class=\"hidden fixed inset-0 bg-opacity-50 flex items-center justify-center z-50\" style=\"background-color: var(--bg-primary);\"><div class=\"animate-spin rounded-full h-12 w-12 border-b-2\" style=\"border-color: var(--accent);\"></div></div></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div><div id=\"loading-spinner\" class=\"hidden fixed inset-0 bg-opacity-50 flex items-center justify-center z-50\" style=\"background-color: var(--bg-primary);\"><div class=\"animate-spin rounded-full h-12 w-12 border-b-2\" style=\"border-color: var(--accent);\"></div></div></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -181,12 +271,12 @@ func DashboardActions() templ.Component {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
|
templ_7745c5c3_Var15 := templ.GetChildren(ctx)
|
||||||
if templ_7745c5c3_Var9 == nil {
|
if templ_7745c5c3_Var15 == nil {
|
||||||
templ_7745c5c3_Var9 = templ.NopComponent
|
templ_7745c5c3_Var15 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<button data-action=\"open-dashboard-settings\" class=\"icon-btn\" title=\"Customize Dashboard\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<button data-action=\"open-dashboard-settings\" class=\"icon-btn\" title=\"Customize Dashboard\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -194,7 +284,7 @@ func DashboardActions() templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</button> <button data-action=\"reload-page\" class=\"icon-btn\" title=\"Refresh\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</button> <button data-action=\"reload-page\" class=\"icon-btn\" title=\"Refresh\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -202,7 +292,7 @@ func DashboardActions() templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</button>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</button>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,12 +41,30 @@ type LibraryData struct {
|
|||||||
TypeValue string
|
TypeValue string
|
||||||
MediaCount int64
|
MediaCount int64
|
||||||
FolderCount int
|
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 {
|
type FolderData struct {
|
||||||
FolderPath string
|
FolderPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ArchivedItem feeds the admin archived-items page: media hidden from
|
||||||
|
// libraries because their files vanished, with their pending fate.
|
||||||
|
type ArchivedItem struct {
|
||||||
|
ID string
|
||||||
|
Title string
|
||||||
|
Author string
|
||||||
|
LibraryName string
|
||||||
|
FilePath string
|
||||||
|
MissingScans int32
|
||||||
|
ArchivedAt *time.Time
|
||||||
|
PurgeAt *time.Time
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
type DirEntry struct {
|
type DirEntry struct {
|
||||||
Name string
|
Name string
|
||||||
Path string
|
Path string
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ func isUserVisible(userID string, visibility []UserVisibilityData) bool {
|
|||||||
func TotalMediaCount(libs []LibraryData) int64 {
|
func TotalMediaCount(libs []LibraryData) int64 {
|
||||||
var total int64
|
var total int64
|
||||||
for _, l := range libs {
|
for _, l := range libs {
|
||||||
|
// Unmounted storage contributes nothing visible: an offline
|
||||||
|
// library's holdings stay in the database but out of the UI
|
||||||
|
// until its folders return.
|
||||||
|
if l.Offline {
|
||||||
|
continue
|
||||||
|
}
|
||||||
total += l.MediaCount
|
total += l.MediaCount
|
||||||
}
|
}
|
||||||
return total
|
return total
|
||||||
|
|||||||
@@ -289,6 +289,67 @@ async function purgeArchivedItems(): Promise<void> {
|
|||||||
|
|
||||||
(window as any).purgeArchivedItems = purgeArchivedItems;
|
(window as any).purgeArchivedItems = purgeArchivedItems;
|
||||||
|
|
||||||
|
// Restore a hidden/archived item to library visibility without waiting for
|
||||||
|
// its file to return (POST /api/media-items/:id/unarchive). If the file is
|
||||||
|
// still gone the next scan hides it again.
|
||||||
|
async function unarchiveItem(id: string): Promise<void> {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (!token) return;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/media-items/${id}/unarchive`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
const err = await resp.json().catch(() => ({}));
|
||||||
|
throw new Error(err.error || "Failed to restore item");
|
||||||
|
}
|
||||||
|
showToast("Item restored to libraries", "success");
|
||||||
|
setTimeout(() => window.location.reload(), 500);
|
||||||
|
} catch (e) {
|
||||||
|
showToast(e instanceof Error ? e.message : "Failed to restore item", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permanently delete one archived item and its reading history
|
||||||
|
// (DELETE /api/media-items/:id).
|
||||||
|
async function deleteArchivedItem(id: string): Promise<void> {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (!token) return;
|
||||||
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
"Permanently delete this item? Its reading progress, notes, and highlights will be lost.",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/media-items/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
const err = await resp.json().catch(() => ({}));
|
||||||
|
throw new Error(err.error || "Failed to delete item");
|
||||||
|
}
|
||||||
|
showToast("Item deleted", "success");
|
||||||
|
setTimeout(() => window.location.reload(), 500);
|
||||||
|
} catch (e) {
|
||||||
|
showToast(e instanceof Error ? e.message : "Failed to delete item", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The archived-items page renders per-row buttons with data attributes
|
||||||
|
// (escaping-safe); bind them here. Module scripts run after DOM parse.
|
||||||
|
document.querySelectorAll<HTMLElement>("[data-unarchive]").forEach((el) => {
|
||||||
|
el.addEventListener("click", () => unarchiveItem(el.dataset.unarchive || ""));
|
||||||
|
});
|
||||||
|
document.querySelectorAll<HTMLElement>("[data-delete-archived]").forEach((el) => {
|
||||||
|
el.addEventListener("click", () =>
|
||||||
|
deleteArchivedItem(el.dataset.deleteArchived || ""),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
hideScanProgress,
|
hideScanProgress,
|
||||||
loadWatchStatus,
|
loadWatchStatus,
|
||||||
|
|||||||
Reference in New Issue
Block a user