From 706be09dec7d12ee5f4ea465c3f5387c32d0c401 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 7 Aug 2026 09:35:39 -0400 Subject: [PATCH] feat(admin): redesign library management with HTMX expandable rows + stats dashboard Library management: - Redesign admin_library page with expandable rows that load detail panels via HTMX (LibraryList, LibraryPanel, FolderBrowserContent partials) - Add create/edit/delete library modals using data-* attributes - Add folder browser modal with inline add/remove via HTMX - Add user visibility checkboxes toggled via HTMX - New endpoints in admin_library.go: create, update, delete, panel, folders add/remove, browse, visibility - New template types: FolderData, DirEntry, UserVisibilityData, AdminStats; extend LibraryData with TypeValue and FolderCount Admin dashboard: - Rewrite admin.templ to show 4-stat grid (libraries, media, users, devices) - Add getAdminStats helper querying library/user/media/device counts - Pass AdminStats to template from both /admin and /admin/ handlers --- internal/router/admin_library.go | 380 ++++++++++ internal/router/frontend.go | 12 +- templates/admin.templ | 227 +++--- templates/admin_library.templ | 464 +++++++++---- templates/admin_library_templ.go | 1111 +++++++++++++++++++++++------- templates/admin_templ.go | 104 ++- templates/types.go | 25 + 7 files changed, 1813 insertions(+), 510 deletions(-) create mode 100644 internal/router/admin_library.go diff --git a/internal/router/admin_library.go b/internal/router/admin_library.go new file mode 100644 index 0000000..e1059ef --- /dev/null +++ b/internal/router/admin_library.go @@ -0,0 +1,380 @@ +package router + +import ( + "bytes" + "bookhoard/internal/database" + "bookhoard/internal/handlers" + "bookhoard/templates" + "context" + "log" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/labstack/echo/v5" +) + +func registerAdminLibraryRoutes(cfg *Config, frontendProtected *echo.Group) { + g := frontendProtected.Group("", handlers.AdminMiddleware) + + // HTMX: Create library + g.POST("/admin/library/create", func(c *echo.Context) error { + user := c.Get("user").(database.Users) + + name := c.FormValue("name") + desc := c.FormValue("description") + libType := c.FormValue("type") + if name == "" || libType == "" { + return c.HTML(http.StatusBadRequest, `
Name and type are required
`) + } + + _, err := cfg.LibraryService.CreateLibrary( + c.Request().Context(), + name, + desc, + libType, + user.ID, + ) + if err != nil { + return c.HTML(http.StatusInternalServerError, `
Failed to create library
`) + } + + return renderLibraryList(c, cfg) + }) + + // HTMX: Update library + g.PUT("/admin/library/:id", func(c *echo.Context) error { + libraryID, err := parseAdminUUID(c.Param("id")) + if err != nil { + return c.HTML(http.StatusBadRequest, `
Invalid library ID
`) + } + + name := c.FormValue("name") + desc := c.FormValue("description") + if name == "" { + return c.HTML(http.StatusBadRequest, `
Name is required
`) + } + + _, err = cfg.LibraryService.UpdateLibrary(c.Request().Context(), libraryID, name, desc) + if err != nil { + return c.HTML(http.StatusInternalServerError, `
Failed to update library
`) + } + + return renderLibraryList(c, cfg) + }) + + // HTMX: Delete library + g.DELETE("/admin/library/:id", func(c *echo.Context) error { + libraryID, err := parseAdminUUID(c.Param("id")) + if err != nil { + return c.HTML(http.StatusBadRequest, `
Invalid library ID
`) + } + + err = cfg.LibraryService.DeleteLibrary(c.Request().Context(), libraryID) + if err != nil { + return c.HTML(http.StatusInternalServerError, `
Failed to delete library
`) + } + + return renderLibraryList(c, cfg) + }) + + // HTMX: Library expanded panel + g.GET("/admin/library/:id/panel", func(c *echo.Context) error { + libraryID, err := parseAdminUUID(c.Param("id")) + if err != nil { + return c.HTML(http.StatusBadRequest, `
Invalid library ID
`) + } + + return renderLibraryPanel(c, cfg, libraryID) + }) + + // HTMX: Add folder + g.POST("/admin/library/:id/folders", func(c *echo.Context) error { + libraryID, err := parseAdminUUID(c.Param("id")) + if err != nil { + return c.HTML(http.StatusBadRequest, `
Invalid library ID
`) + } + + folderPath := c.FormValue("folder_path") + if folderPath == "" { + return renderLibraryPanel(c, cfg, libraryID) + } + + if strings.Contains(folderPath, "..") { + return renderLibraryPanelWithError(c, cfg, libraryID, "Path traversal not allowed") + } + + cleanPath := filepath.Clean(folderPath) + fileInfo, err := os.Stat(cleanPath) + if err != nil { + return renderLibraryPanelWithError(c, cfg, libraryID, "Folder path does not exist") + } + if !fileInfo.IsDir() { + return renderLibraryPanelWithError(c, cfg, libraryID, "Path must be a directory") + } + + _, err = cfg.LibraryService.AddLibraryFolder(c.Request().Context(), libraryID, cleanPath) + if err != nil { + return renderLibraryPanelWithError(c, cfg, libraryID, "Failed to add folder: "+err.Error()) + } + + return renderLibraryPanel(c, cfg, libraryID) + }) + + // HTMX: Remove folder + g.DELETE("/admin/library/:id/folders", func(c *echo.Context) error { + libraryID, err := parseAdminUUID(c.Param("id")) + if err != nil { + return c.HTML(http.StatusBadRequest, `
Invalid library ID
`) + } + + folderPath := c.FormValue("folder_path") + if folderPath == "" { + return renderLibraryPanel(c, cfg, libraryID) + } + + err = cfg.LibraryService.DeleteLibraryFolder(c.Request().Context(), libraryID, folderPath) + if err != nil { + return renderLibraryPanelWithError(c, cfg, libraryID, "Failed to remove folder") + } + + return renderLibraryPanel(c, cfg, libraryID) + }) + + // HTMX: Folder browser + g.GET("/admin/library/browse", func(c *echo.Context) error { + path := c.QueryParam("path") + if path == "" { + path = "/" + } + targetInput := c.QueryParam("target_input") + libraryID := c.QueryParam("library_id") + + dirs, currentPath, parentPath, err := cfg.LibraryService.BrowseDirectories(c.Request().Context(), path) + if err != nil { + return c.HTML(http.StatusBadRequest, `
Cannot browse: `+err.Error()+`
`) + } + + entries := make([]templates.DirEntry, len(dirs)) + for i, d := range dirs { + fullPath := filepath.Join(currentPath, d) + entries[i] = templates.DirEntry{Name: d, Path: fullPath} + } + + var buf bytes.Buffer + err = templates.FolderBrowserContent(currentPath, parentPath, entries, targetInput, libraryID).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) + }) + + // HTMX: Set user visibility for library + g.POST("/admin/library/:id/visibility", func(c *echo.Context) error { + libraryID, err := parseAdminUUID(c.Param("id")) + if err != nil { + return c.HTML(http.StatusBadRequest, `
Invalid library ID
`) + } + + userIDStr := c.FormValue("user_id") + isVisible := c.FormValue("is_visible") == "true" + + userID, err := parseAdminUUID(userIDStr) + if err != nil { + return c.HTML(http.StatusBadRequest, `
Invalid user ID
`) + } + + _, err = cfg.LibraryService.SetLibraryVisibility(c.Request().Context(), userID, libraryID, isVisible) + if err != nil { + return c.HTML(http.StatusInternalServerError, `
Failed to update visibility
`) + } + + return renderLibraryPanel(c, cfg, libraryID) + }) +} + +// renderLibraryList fetches all libraries + users and renders the LibraryList partial. +func renderLibraryList(c *echo.Context, cfg *Config) error { + libraries, err := cfg.LibraryHandler.ListLibrariesData(c.Request().Context()) + if err != nil { + return c.HTML(http.StatusInternalServerError, `
Failed to load libraries
`) + } + + libData := make([]templates.LibraryData, len(libraries)) + for i, lib := range libraries { + libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16]) + folderCount := getFolderCount(c.Request().Context(), cfg, lib.ID) + libData[i] = templates.LibraryData{ + ID: libUUID.String(), + Name: lib.Name, + Description: getText(lib.Description), + TypeName: lib.TypeName, + TypeValue: lib.TypeName, + FolderCount: folderCount, + } + } + + users, err := cfg.Queries.ListUsers(c.Request().Context()) + if err != nil { + log.Printf("ListUsers failed: %v", err) + users = []database.ListUsersRow{} + } + userData := make([]templates.User, len(users)) + for i, u := range users { + userUUID, _ := uuid.FromBytes(u.ID.Bytes[0:16]) + userData[i] = templates.User{ + ID: userUUID.String(), + Username: u.Username, + Email: u.Email, + Role: u.Role, + } + } + + var buf bytes.Buffer + err = templates.LibraryList(templates.User{}, libData, userData).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) +} + +// renderLibraryPanel fetches library details and renders the LibraryPanel partial. +func renderLibraryPanel(c *echo.Context, cfg *Config, libraryID pgtype.UUID) error { + return renderLibraryPanelWithError(c, cfg, libraryID, "") +} + +func renderLibraryPanelWithError(c *echo.Context, cfg *Config, libraryID pgtype.UUID, errMsg string) error { + ctx := c.Request().Context() + libraryIDStr := uuid.UUID(libraryID.Bytes).String() + + // Get library details + lib, err := cfg.LibraryService.GetLibrary(ctx, libraryID) + if err != nil { + return c.HTML(http.StatusNotFound, `
Library not found
`) + } + + libData := templates.LibraryData{ + ID: libraryIDStr, + Name: lib.Name, + Description: getText(lib.Description), + TypeName: lib.TypeName, + TypeValue: lib.TypeName, + } + + // Get folders + dbFolders, err := cfg.LibraryService.GetLibraryFolders(ctx, libraryID) + if err != nil { + log.Printf("GetLibraryFolders failed: %v", err) + } + folders := make([]templates.FolderData, len(dbFolders)) + for i, f := range dbFolders { + folders[i] = templates.FolderData{FolderPath: f.FolderPath} + } + libData.FolderCount = len(folders) + + // Get users + dbUsers, err := cfg.Queries.ListUsers(ctx) + if err != nil { + log.Printf("ListUsers failed: %v", err) + dbUsers = []database.ListUsersRow{} + } + userData := make([]templates.User, len(dbUsers)) + for i, u := range dbUsers { + userUUID, _ := uuid.FromBytes(u.ID.Bytes[0:16]) + userData[i] = templates.User{ + ID: userUUID.String(), + Username: u.Username, + Email: u.Email, + } + } + + // Get visibility for all users + visibility := make([]templates.UserVisibilityData, len(userData)) + for i, u := range userData { + userUUID, _ := parseAdminUUID(u.ID) + visibleLibs, err := cfg.LibraryService.GetUserVisibleLibraries(ctx, userUUID) + if err != nil { + log.Printf("GetUserVisibleLibraries failed: %v", err) + } + isVisible := false + for _, vl := range visibleLibs { + if vl.ID.Bytes == libraryID.Bytes { + isVisible = true + break + } + } + visibility[i] = templates.UserVisibilityData{ + UserID: u.ID, + Username: u.Username, + Email: u.Email, + IsVisible: isVisible, + } + } + + // Get issue count + issueStats, err := cfg.ProcessingIssuesHandler.GetProcessingIssueStatsData(ctx, libraryID) + if err != nil { + log.Printf("GetProcessingIssueStats failed: %v", err) + } + issueCount := issueStats.ErrorCount + issueStats.WarningCount + issueStats.InfoCount + + // Get current user for template + tmplUser := templates.User{} + if u, ok := c.Get("user").(database.Users); ok { + userUUID, _ := uuid.FromBytes(u.ID.Bytes[0:16]) + tmplUser = templates.User{ + ID: userUUID.String(), + Username: u.Username, + Role: u.Role, + } + } + + var buf bytes.Buffer + err = templates.LibraryPanel(tmplUser, libraryIDStr, libData, folders, userData, visibility, int(issueCount)).Render(ctx, &buf) + if err != nil { + return err + } + + html := buf.String() + if errMsg != "" { + html = `
` + errMsg + `
` + html + } + return c.HTML(http.StatusOK, html) +} + +func getFolderCount(ctx context.Context, cfg *Config, libraryID pgtype.UUID) int { + folders, err := cfg.LibraryService.GetLibraryFolders(ctx, libraryID) + if err != nil { + return 0 + } + return len(folders) +} + +func parseAdminUUID(s string) (pgtype.UUID, error) { + parsed, err := uuid.Parse(s) + if err != nil { + return pgtype.UUID{}, err + } + return pgtype.UUID{Bytes: parsed, Valid: true}, nil +} + +func getAdminStats(ctx context.Context, cfg *Config) templates.AdminStats { + stats := templates.AdminStats{} + + libs, _ := cfg.LibraryHandler.ListLibrariesData(ctx) + stats.LibraryCount = len(libs) + + users, _ := cfg.Queries.ListUsers(ctx) + stats.UserCount = len(users) + + if pool, ok := cfg.DBPool.(*pgxpool.Pool); ok { + _ = pool.QueryRow(ctx, "SELECT COUNT(*) FROM media_items").Scan(&stats.MediaCount) + _ = pool.QueryRow(ctx, "SELECT COUNT(*) FROM devices").Scan(&stats.DeviceCount) + } + + return stats +} diff --git a/internal/router/frontend.go b/internal/router/frontend.go index 1ee0774..39724d1 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -810,8 +810,9 @@ func registerFrontendRoutes(cfg *Config) { if err != nil { return renderErrorPage(c, "Error loading user", "user_load_error") } + stats := getAdminStats(c.Request().Context(), cfg) var buf bytes.Buffer - err = templates.Admin(user).Render(c.Request().Context(), &buf) + err = templates.Admin(user, stats).Render(c.Request().Context(), &buf) if err != nil { return err } @@ -823,8 +824,9 @@ func registerFrontendRoutes(cfg *Config) { if err != nil { return renderErrorPage(c, "Error loading user", "user_load_error") } + stats := getAdminStats(c.Request().Context(), cfg) var buf bytes.Buffer - err = templates.Admin(user).Render(c.Request().Context(), &buf) + err = templates.Admin(user, stats).Render(c.Request().Context(), &buf) if err != nil { return err } @@ -848,11 +850,14 @@ func registerFrontendRoutes(cfg *Config) { libData := make([]templates.LibraryData, len(libraries)) for i, lib := range libraries { libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16]) + folders, _ := cfg.LibraryService.GetLibraryFolders(c.Request().Context(), lib.ID) libData[i] = templates.LibraryData{ ID: libUUID.String(), Name: lib.Name, Description: getText(lib.Description), TypeName: lib.TypeName, + TypeValue: lib.TypeName, + FolderCount: len(folders), } } @@ -1046,6 +1051,9 @@ func registerFrontendRoutes(cfg *Config) { return c.HTML(http.StatusOK, buf.String()) })) + // Admin library HTMX endpoints + registerAdminLibraryRoutes(cfg, frontendProtected) + // ============================================================================ // LEGACY API ROUTES (for backward compatibility) // ============================================================================ diff --git a/templates/admin.templ b/templates/admin.templ index fb1d8a7..bb7bbe9 100644 --- a/templates/admin.templ +++ b/templates/admin.templ @@ -1,140 +1,135 @@ package templates -templ Admin(user User) { +templ Admin(user User, stats AdminStats) { Admin Dashboard - Bookhoard - - + @Header(user, "/admin") -
- @AdminSidebar(user, "/admin") -
-
-
-
- - @Icon("grid", "h-5 w-5") - -

Dashboard

-
-

Overview of your Bookhoard library and settings

+
+
+
+
+ + @Icon("grid", "h-5 w-5") + +

Dashboard

-
-
-
- - @Icon("library", "h-5 w-5") - -
-

Library

-

Manage your ebook collection

-
-
- - @Icon("arrow-right", "h-4 w-4") - View Library - -
-
-
- - @Icon("sync", "h-5 w-5") - -
-

Scan Watch Status

-

Auto-detecting new files

-
-
-
- - Watching 0 libraries -
+

Overview of your Bookhoard instance

+
+ +
+
+
+ @Icon("library", "h-4 w-4") + Libraries
+

{ stats.LibraryCount }

-
-

Quick Actions

-
- - - - @Icon("library", "h-5 w-5") - Manage Libraries - - Add or remove libraries and scan directories - +
+
+ @Icon("book", "h-4 w-4") + Books
+

{ stats.MediaCount }

- -
-
+ +
+

Quick Actions

+
+ + + + @Icon("library", "h-5 w-5") + Manage Libraries + + Add or remove libraries and folders + +
+
+ + +
+
} diff --git a/templates/admin_library.templ b/templates/admin_library.templ index 1872f7f..18bc24f 100644 --- a/templates/admin_library.templ +++ b/templates/admin_library.templ @@ -6,126 +6,53 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) { Library Management - Bookhoard + - + @Header(user, "/admin/library") -
- @AdminSidebar(user, "/admin/library") -
-
-
-
- - @Icon("arrow-left", "h-4 w-4") - Back to Dashboard - - -
-
- - @Icon("library", "h-5 w-5") - -

Library Management

-
-

Manage libraries and configure media scanning

-
-
- -
-
- @Icon("library", "h-5 w-5 shrink-0") -

Libraries

-
-

Manage media libraries and their folders

-
- if len(libraries) == 0 { -

- No libraries yet. Create your first library to get started. -

- } else { - for _, library := range libraries { -
-
-
-

{ library.Name }

- if library.Description != "" { -

{ library.Description }

- } - - @Icon("tag", "h-3 w-3") - { library.TypeName } - -
-
- - - -
-
- -
- } - } +
+
+
+
+
+
+ + @Icon("library", "h-5 w-5") + +

Libraries

+

Manage media libraries, folders, and scanning

- -
-
- @Icon("check-circle", "h-5 w-5 shrink-0") -

Library Visibility

-
-

Control which libraries are visible to users

-
- -
-
-
- -
-
- @Icon("users", "h-5 w-5 shrink-0") -

User Library Access

-
-

Manage individual user access to specific libraries

-
- -
-
- -
+
-
-
- +
+ @LibraryList(user, libraries, users) +
+
+
+ ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func LibraryList(user User, libraries []LibraryData, users []User) 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_Var2 := templ.GetChildren(ctx) + if templ_7745c5c3_Var2 == nil { + templ_7745c5c3_Var2 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if len(libraries) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Icon("library", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

No Libraries Yet

Create your first library to get started

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, library := range libraries { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + 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, 17, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(library.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 194, Col: 92} + } + _, 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, 18, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if library.Description != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(library.Description) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 196, Col: 95} + } + _, 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, 20, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Icon("tag", "h-3 w-3").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(library.TypeName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 201, Col: 26} + } + _, 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, 22, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if library.FolderCount > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Icon("folder", "h-3 w-3").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(library.FolderCount) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 206, Col: 30} + } + _, 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, 24, " folders") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func LibraryPanel(user User, libraryID string, library LibraryData, folders []FolderData, users []User, visibility []UserVisibilityData, issueCount 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_Var11 := templ.GetChildren(ctx) + if templ_7745c5c3_Var11 == nil { + templ_7745c5c3_Var11 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Icon("folder", "h-4 w-4 shrink-0").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "

Folders

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(folders) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "

No folders configured. Add a folder to enable scanning.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, folder := range folders { + 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(folder.FolderPath) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 254, Col: 99} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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, 44, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(users) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Icon("users", "h-4 w-4 shrink-0").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "

User Access

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, u := range users { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if issueCount > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Icon("alert", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if issueCount == 1 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "1 processing issue") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var27 string + templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(issueCount) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 331, Col: 24} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, " processing issues") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = Icon("chevron-right", "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, 70, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func FolderBrowserContent(currentPath string, parentPath string, entries []DirEntry, targetInput string, libraryID string) 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_Var33 := templ.GetChildren(ctx) + if templ_7745c5c3_Var33 == nil { + templ_7745c5c3_Var33 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Icon("folder", "h-4 w-4 shrink-0").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var34 string + templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(currentPath) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 366, Col: 89} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if parentPath != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, entry := range entries { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/admin_templ.go b/templates/admin_templ.go index 5351e6b..ed83f71 100644 --- a/templates/admin_templ.go +++ b/templates/admin_templ.go @@ -8,7 +8,7 @@ package templates import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -func Admin(user User) templ.Component { +func Admin(user User, stats AdminStats) 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 { @@ -29,7 +29,7 @@ func Admin(user User) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Admin Dashboard - Bookhoard") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Admin Dashboard - Bookhoard") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -37,15 +37,7 @@ func Admin(user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = AdminSidebar(user, "/admin").Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -53,23 +45,91 @@ func Admin(user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

Dashboard

Overview of your Bookhoard library and settings

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

Dashboard

Overview of your Bookhoard instance

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

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Icon("book", "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, 6, "Books

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.MediaCount) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 38, Col: 90} + } + _, 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, 7, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Icon("users", "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, 8, "Users

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.UserCount) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 45, Col: 89} + } + _, 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, 9, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Icon("device", "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, 10, "Devices

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(stats.DeviceCount) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 52, Col: 91} + } + _, 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, 11, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -77,7 +137,7 @@ func Admin(user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

Scan Watch Status

Auto-detecting new files

Watching 0 libraries

Quick Actions

Quick Actions

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "Scan All Libraries Re-scan existing files and detect new items ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -93,7 +153,7 @@ func Admin(user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Manage Libraries Add or remove libraries and scan directories
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "Scan Complete!
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/types.go b/templates/types.go index 84268d7..8201f58 100644 --- a/templates/types.go +++ b/templates/types.go @@ -38,7 +38,32 @@ type LibraryData struct { Name string Description string TypeName string + TypeValue string MediaCount int64 + FolderCount int +} + +type FolderData struct { + FolderPath string +} + +type DirEntry struct { + Name string + Path string +} + +type UserVisibilityData struct { + UserID string + Username string + Email string + IsVisible bool +} + +type AdminStats struct { + LibraryCount int + MediaCount int + UserCount int + DeviceCount int } type SeriesCardData struct {