diff --git a/internal/router/frontend.go b/internal/router/frontend.go index b89306a..017a61f 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -12,6 +12,7 @@ import ( "strconv" "time" + "bookhoard/internal/config" "bookhoard/internal/database" "bookhoard/internal/handlers" "bookhoard/internal/services" @@ -295,6 +296,18 @@ func registerFrontendRoutes(cfg *Config) { libraryID := libRes.LibraryID 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 userUUID, _ := uuid.Parse(user.ID) var savedFilters []database.SavedFilters diff --git a/internal/router/helpers.go b/internal/router/helpers.go index 88441bf..c3b610e 100644 --- a/internal/router/helpers.go +++ b/internal/router/helpers.go @@ -4,6 +4,7 @@ import ( "context" "log" "net/url" + "os" "time" "bookhoard/internal/database" @@ -94,11 +95,11 @@ const selectedLibraryCookie = "selectedLibrary" const allLibrariesSentinel = "__all__" type LibraryResolution struct { - LibraryID string - IsAll bool - LibUUID pgtype.UUID - Libraries []templates.LibraryData - FirstID string + LibraryID string + IsAll bool + LibUUID pgtype.UUID + Libraries []templates.LibraryData + FirstID string } func getText(t pgtype.Text) string { @@ -132,12 +133,30 @@ func resolveLibrary(c *echo.Context, cfg *Config, userUUID string) LibraryResolu res.Libraries = make([]templates.LibraryData, len(libraries)) for i, lib := range libraries { 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{ ID: libUUID.String(), Name: lib.Name, Description: getText(lib.Description), TypeName: lib.TypeName, MediaCount: countMap[libUUID.String()], + Offline: offline, } } diff --git a/templates/bookshelf.templ b/templates/bookshelf.templ index 498f64f..46c4067 100644 --- a/templates/bookshelf.templ +++ b/templates/bookshelf.templ @@ -148,14 +148,20 @@ templ BookShelf( } else { } - for _, lib := range libraries { + for _, lib := range libraries { + if lib.Offline { if lib.ID == currentLibraryID { - + } else { - + } + } else if lib.ID == currentLibraryID { + + } else { + } } + }
diff --git a/templates/bookshelf_templ.go b/templates/bookshelf_templ.go index 5d08e46..1a2c378 100644 --- a/templates/bookshelf_templ.go +++ b/templates/bookshelf_templ.go @@ -203,98 +203,188 @@ func BookShelf( } } for _, lib := range libraries { - if lib.ID == currentLibraryID { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } else if lib.ID == currentLibraryID { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, ")") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, ")") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
= 2) fetchAuthorValues($el)\">
= 2) fetchTagValues($el)\" @keydown=\"onTagFilterKeydown($event)\" @blur=\"hideTagDropdown()\">
= 2) fetchSeriesValues($el)\">
= 2) fetchLanguageValues($el)\">
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
= 2) fetchAuthorValues($el)\">
= 2) fetchTagValues($el)\" @keydown=\"onTagFilterKeydown($event)\" @blur=\"hideTagDropdown()\">
= 2) fetchSeriesValues($el)\">
= 2) fetchLanguageValues($el)\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -303,53 +393,53 @@ func BookShelf( return templ_7745c5c3_Err } if errorMessage != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage) + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage) 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 { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if count > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " Page ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "Prev Page ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(offset/limit + 1) + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(offset/limit + 1) 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 { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

Save Filter

Save Filter

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/library_switcher.templ b/templates/library_switcher.templ index 0acfc38..bff69f7 100644 --- a/templates/library_switcher.templ +++ b/templates/library_switcher.templ @@ -13,14 +13,20 @@ templ LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions .. name="library_id" class="input w-auto py-1.5 pr-8 cursor-pointer" > - - for _, lib := range libData { + + for _, lib := range libData { + if lib.Offline { if lib.ID == currentLibraryID { - + } else { - + } + } else if lib.ID == currentLibraryID { + + } else { + } + } if len(actions) > 0 { diff --git a/templates/library_switcher_templ.go b/templates/library_switcher_templ.go index 10913cb..a14bfb2 100644 --- a/templates/library_switcher_templ.go +++ b/templates/library_switcher_templ.go @@ -36,7 +36,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ... var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(TotalMediaCount(libData)) 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)) if templ_7745c5c3_Err != nil { @@ -47,102 +47,192 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ... return templ_7745c5c3_Err } for _, lib := range libData { - if lib.ID == currentLibraryID { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } else if lib.ID == currentLibraryID { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, ")") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, ")") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(actions) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -152,12 +242,12 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ... return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -181,12 +271,12 @@ func DashboardActions() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var9 := templ.GetChildren(ctx) - if templ_7745c5c3_Var9 == nil { - templ_7745c5c3_Var9 = templ.NopComponent + templ_7745c5c3_Var15 := templ.GetChildren(ctx) + if templ_7745c5c3_Var15 == nil { + templ_7745c5c3_Var15 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/types.go b/templates/types.go index 9bde5a7..afdda90 100644 --- a/templates/types.go +++ b/templates/types.go @@ -41,22 +41,40 @@ type LibraryData struct { TypeValue string MediaCount int64 FolderCount int + // Offline is true when none of the library's folders exist on disk + // (e.g. an unmounted external drive). Contents stay intact and reappear + // when the storage returns. + Offline bool } type FolderData struct { FolderPath string } +// ArchivedItem feeds the admin archived-items page: media hidden from +// libraries because their files vanished, with their pending fate. +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 { Name string Path string } type UserVisibilityData struct { - UserID string - Username string - Email string - IsVisible bool + UserID string + Username string + Email string + IsVisible bool } type AdminStats struct { diff --git a/templates/utils.go b/templates/utils.go index 88c9add..7adf3b0 100644 --- a/templates/utils.go +++ b/templates/utils.go @@ -65,6 +65,12 @@ func isUserVisible(userID string, visibility []UserVisibilityData) bool { func TotalMediaCount(libs []LibraryData) int64 { var total int64 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 } return total