Three fixes to the Delete Library flow on /admin/library:
1. Honest warning copy. The old text ("Media files will not be deleted")
read as reassurance while deleting a library actually cascade-destroys
every book record with it: reading progress, highlights, notes,
bookmarks, and ratings are gone with no archive window and no undo.
The modal now states this as a scannable list:
- Permanently removed, no archive or undo: the library, its folder
mappings, and all book records — with their reading progress,
highlights, notes, bookmarks, and ratings.
- Not touched: media files on disk.
Admins skim danger dialogs; the irreversible part now leads.
2. The dialog stayed open after confirming. The confirm button swaps
#libraries-container via htmx (so the deleted row vanished) but the
modal lives outside the swapped container and nothing closed it. An
htmx:afterRequest listener now hides the modal on successful deletes
and leaves it open on errors.
3. Latent ReferenceError in openFolderBrowser: the function parameter is
targetInputId but the body referenced an undefined targetInput, so
opening the folder browser threw and the picker never loaded.
459 lines
19 KiB
Templ
459 lines
19 KiB
Templ
package templates
|
|
|
|
templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCount int64) {
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<title>Library Management - 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/library")
|
|
<main class="p-8">
|
|
<div class="max-w-4xl">
|
|
<div class="mb-8">
|
|
<div class="flex items-center justify-between gap-4 flex-wrap mb-4">
|
|
<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)">Libraries</h1>
|
|
</div>
|
|
<p class="text-sm" style="color: var(--text-secondary)">Manage media libraries, folders, and scanning</p>
|
|
</div>
|
|
<button
|
|
onclick="document.getElementById('create-library-modal').classList.remove('hidden')"
|
|
class="btn btn-primary"
|
|
>
|
|
@Icon("plus", "h-4 w-4")
|
|
Create Library
|
|
</button>
|
|
</div>
|
|
</div>
|
|
if archivedCount > 0 {
|
|
<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: { archivedCount }
|
|
</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">
|
|
@Icon("trash", "h-4 w-4")
|
|
Purge Archived Now
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
<div id="libraries-container">
|
|
@LibraryList(user, libraries, users)
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<!-- Create / Edit Library Modal -->
|
|
<div id="create-library-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
|
<div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Library</h2>
|
|
<button type="button" onclick="document.getElementById('create-library-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
|
|
@Icon("close", "h-5 w-5")
|
|
</button>
|
|
</div>
|
|
<form
|
|
hx-post="/admin/library/create"
|
|
hx-target="#libraries-container"
|
|
hx-swap="innerHTML"
|
|
onsubmit="document.getElementById('create-library-modal').classList.add('hidden')"
|
|
>
|
|
<div class="mb-4">
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Library Name</label>
|
|
<input type="text" name="name" placeholder="My Ebook Library" class="input" required/>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Description</label>
|
|
<textarea name="description" placeholder="Optional description" rows="3" class="input"></textarea>
|
|
</div>
|
|
<div class="mb-6">
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Library Type</label>
|
|
<select name="type" class="input" required>
|
|
<option value="ebooks">Ebooks</option>
|
|
<option value="comics">Comics</option>
|
|
<option value="manga">Manga</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex justify-end space-x-3">
|
|
<button type="button" onclick="document.getElementById('create-library-modal').classList.add('hidden')" class="btn btn-secondary">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
@Icon("plus", "h-4 w-4")
|
|
Create
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<!-- Edit Library Modal -->
|
|
<div id="edit-library-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
|
<div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Edit Library</h2>
|
|
<button type="button" onclick="document.getElementById('edit-library-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
|
|
@Icon("close", "h-5 w-5")
|
|
</button>
|
|
</div>
|
|
<form id="edit-library-form" hx-target="#libraries-container" hx-swap="innerHTML" onsubmit="document.getElementById('edit-library-modal').classList.add('hidden')">
|
|
<div class="mb-4">
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Library Name</label>
|
|
<input type="text" name="name" id="edit-library-name" class="input" required/>
|
|
</div>
|
|
<div class="mb-6">
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Description</label>
|
|
<textarea name="description" id="edit-library-desc" rows="3" class="input"></textarea>
|
|
</div>
|
|
<div class="flex justify-end space-x-3">
|
|
<button type="button" onclick="document.getElementById('edit-library-modal').classList.add('hidden')" class="btn btn-secondary">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
@Icon("save", "h-4 w-4")
|
|
Save
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<!-- Folder Browser Modal -->
|
|
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
|
<div class="card p-6 w-full max-w-lg mx-4" style="box-shadow: var(--shadow-pop);">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Browse Folders</h2>
|
|
<button type="button" onclick="document.getElementById('folder-browser-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
|
|
@Icon("close", "h-5 w-5")
|
|
</button>
|
|
</div>
|
|
<div id="folder-browser-content"></div>
|
|
</div>
|
|
</div>
|
|
<!-- Delete Library Modal -->
|
|
<div id="delete-library-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
|
<div class="card p-6 w-full max-w-md mx-4" style="box-shadow: var(--shadow-pop);">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Delete Library</h2>
|
|
<button type="button" onclick="document.getElementById('delete-library-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
|
|
@Icon("close", "h-5 w-5")
|
|
</button>
|
|
</div>
|
|
<p class="mb-3" style="color: var(--text-primary)">
|
|
Are you sure you want to delete <strong id="delete-library-name"></strong>?
|
|
</p>
|
|
<ul class="mb-6 space-y-2 text-sm list-disc list-inside" style="color: var(--text-primary)">
|
|
<li>
|
|
<strong>Permanently removed, no archive or undo:</strong>
|
|
the library, its folder mappings, and all book records — with their reading progress, highlights, notes, bookmarks, and ratings.
|
|
</li>
|
|
<li>
|
|
<strong>Not touched:</strong> media files on disk.
|
|
</li>
|
|
</ul>
|
|
<div class="flex justify-end space-x-3">
|
|
<button type="button" onclick="document.getElementById('delete-library-modal').classList.add('hidden')" class="btn btn-secondary">Cancel</button>
|
|
<button type="button" id="delete-library-confirm" class="btn btn-danger">
|
|
@Icon("trash", "h-4 w-4")
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
function toggleLibraryPanel(btn) {
|
|
var libId = btn.dataset.libId;
|
|
var panel = document.getElementById('library-panel-' + libId);
|
|
if (panel.innerHTML.trim() !== '') {
|
|
panel.innerHTML = '';
|
|
} else {
|
|
htmx.ajax('GET', '/admin/library/' + libId + '/panel', {
|
|
target: '#library-panel-' + libId,
|
|
swap: 'innerHTML'
|
|
});
|
|
}
|
|
}
|
|
function openEditModal(id, name, description) {
|
|
document.getElementById('edit-library-form').setAttribute('hx-put', '/admin/library/' + id);
|
|
document.getElementById('edit-library-name').value = name;
|
|
document.getElementById('edit-library-desc').value = description;
|
|
htmx.process(document.getElementById('edit-library-form'));
|
|
document.getElementById('edit-library-modal').classList.remove('hidden');
|
|
}
|
|
function openDeleteModal(id, name) {
|
|
const btn = document.getElementById('delete-library-confirm');
|
|
btn.setAttribute('hx-delete', '/admin/library/' + id);
|
|
btn.setAttribute('hx-target', '#libraries-container');
|
|
btn.setAttribute('hx-swap', 'innerHTML');
|
|
document.getElementById('delete-library-name').textContent = name;
|
|
htmx.process(btn);
|
|
document.getElementById('delete-library-modal').classList.remove('hidden');
|
|
}
|
|
function openFolderBrowser(targetInputId, libraryId) {
|
|
const content = document.getElementById('folder-browser-content');
|
|
content.setAttribute('hx-get', '/admin/library/browse');
|
|
content.setAttribute('hx-vals', '{"target_input": "' + targetInputId + '", "library_id": "' + libraryId + '"}');
|
|
content.setAttribute('hx-trigger', 'load');
|
|
htmx.process(content);
|
|
document.getElementById('folder-browser-modal').classList.remove('hidden');
|
|
}
|
|
// Close the delete-library modal after a successful delete: the
|
|
// htmx swap already removes the deleted row from the libraries
|
|
// list, but the modal lives outside the swapped container and
|
|
// would otherwise stay open.
|
|
document.getElementById('delete-library-confirm').addEventListener('htmx:afterRequest', function(evt) {
|
|
if (evt.detail.successful) {
|
|
document.getElementById('delete-library-modal').classList.add('hidden');
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
}
|
|
|
|
templ LibraryList(user User, libraries []LibraryData, users []User) {
|
|
if len(libraries) == 0 {
|
|
<div class="card text-center py-16">
|
|
<span class="grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
|
@Icon("library", "h-7 w-7")
|
|
</span>
|
|
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Libraries Yet</h3>
|
|
<p class="mb-4 text-sm" style="color: var(--text-secondary)">Create your first library to get started</p>
|
|
<button onclick="document.getElementById('create-library-modal').classList.remove('hidden')" class="btn btn-primary">Create Your First Library</button>
|
|
</div>
|
|
} else {
|
|
<div class="space-y-4">
|
|
for _, library := range libraries {
|
|
<div class="card overflow-hidden">
|
|
<div class="flex items-center justify-between gap-4 p-5">
|
|
<div class="flex items-center gap-3 min-w-0 flex-1">
|
|
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
|
@Icon("library", "h-5 w-5")
|
|
</span>
|
|
<div class="min-w-0">
|
|
<h3 class="font-semibold truncate" style="color: var(--text-primary)">{ library.Name }</h3>
|
|
if library.Description != "" {
|
|
<p class="text-sm truncate" style="color: var(--text-secondary)">{ library.Description }</p>
|
|
}
|
|
</div>
|
|
<span class="chip shrink-0">
|
|
@Icon("tag", "h-3 w-3")
|
|
{ library.TypeName }
|
|
</span>
|
|
if library.FolderCount > 0 {
|
|
<span class="chip shrink-0">
|
|
@Icon("folder", "h-3 w-3")
|
|
{ library.FolderCount } folders
|
|
</span>
|
|
}
|
|
</div>
|
|
<div class="flex items-center gap-2 shrink-0">
|
|
<button
|
|
hx-post={ "/api/libraries/" + library.ID + "/scan" }
|
|
hx-vals='{"force": "true"}'
|
|
hx-target="#scan-indicator"
|
|
hx-swap="innerHTML"
|
|
class="btn btn-secondary text-xs px-3 py-1.5"
|
|
>
|
|
@Icon("refresh", "h-4 w-4")
|
|
Scan
|
|
</button>
|
|
<button
|
|
data-lib-id={ library.ID }
|
|
onclick="toggleLibraryPanel(this)"
|
|
class="btn btn-secondary text-xs px-3 py-1.5"
|
|
>
|
|
@Icon("chevron-down", "h-4 w-4")
|
|
Manage
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id={ "library-panel-" + library.ID }></div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
<div id="scan-indicator"></div>
|
|
}
|
|
|
|
templ LibraryPanel(user User, libraryID string, library LibraryData, folders []FolderData, users []User, visibility []UserVisibilityData, issueCount int) {
|
|
<div class="border-t p-5 space-y-6" style="border-color: var(--border);">
|
|
<!-- Folders -->
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-3">
|
|
@Icon("folder", "h-4 w-4 shrink-0")
|
|
<h4 class="text-sm font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Folders</h4>
|
|
</div>
|
|
if len(folders) == 0 {
|
|
<p class="text-sm mb-3" style="color: var(--text-secondary)">No folders configured. Add a folder to enable scanning.</p>
|
|
} else {
|
|
<div class="space-y-2 mb-3">
|
|
for _, folder := range folders {
|
|
<div class="flex items-center justify-between gap-2 p-2 rounded-lg" style="background-color: var(--bg-primary);">
|
|
<code class="text-xs flex-1 truncate" style="color: var(--text-primary)">{ folder.FolderPath }</code>
|
|
<button
|
|
class="icon-btn h-7 w-7 shrink-0"
|
|
hx-delete={ "/admin/library/" + libraryID + "/folders" }
|
|
hx-vals={ `{"folder_path": "` + folder.FolderPath + `"}` }
|
|
hx-target={ "#library-panel-" + libraryID }
|
|
hx-swap="innerHTML"
|
|
hx-confirm="Remove this folder from the library?"
|
|
>
|
|
@Icon("trash", "h-3.5 w-3.5")
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
<form class="flex gap-2" hx-post={ "/admin/library/" + libraryID + "/folders" } hx-target={ "#library-panel-" + libraryID } hx-swap="innerHTML">
|
|
<input
|
|
type="text"
|
|
name="folder_path"
|
|
id={ "folder-input-" + libraryID }
|
|
placeholder="/path/to/books"
|
|
class="input flex-1"
|
|
required
|
|
/>
|
|
<button
|
|
type="button"
|
|
data-library-id={ libraryID }
|
|
data-target-input={ "folder-input-" + libraryID }
|
|
onclick="openFolderBrowser(this.dataset.targetInput, this.dataset.libraryId)"
|
|
class="btn btn-secondary text-sm shrink-0"
|
|
>
|
|
@Icon("folder", "h-4 w-4")
|
|
Browse
|
|
</button>
|
|
<button type="submit" class="btn btn-primary text-sm shrink-0">
|
|
@Icon("plus", "h-4 w-4")
|
|
Add
|
|
</button>
|
|
</form>
|
|
</div>
|
|
<!-- User Visibility -->
|
|
if len(users) > 0 {
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-3">
|
|
@Icon("users", "h-4 w-4 shrink-0")
|
|
<h4 class="text-sm font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">User Access</h4>
|
|
</div>
|
|
<div class="space-y-1">
|
|
for _, u := range users {
|
|
<label class="flex items-center gap-3 cursor-pointer p-2 rounded-lg transition-colors hover:bg-surface-hover">
|
|
<input
|
|
type="checkbox"
|
|
class="w-4 h-4 rounded"
|
|
name="is_visible"
|
|
value="true"
|
|
checked?={ isUserVisible(u.ID, visibility) }
|
|
hx-post={ "/admin/library/" + libraryID + "/visibility" }
|
|
hx-vals={ `{"user_id": "` + u.ID + `"}` }
|
|
hx-trigger="change"
|
|
hx-target={ "#library-panel-" + libraryID }
|
|
hx-swap="innerHTML"
|
|
/>
|
|
<span class="text-sm" style="color: var(--text-primary)">{ u.Username }</span>
|
|
<span class="text-xs" style="color: var(--text-secondary)">{ u.Email }</span>
|
|
</label>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
<!-- Processing Issues -->
|
|
if issueCount > 0 {
|
|
<div>
|
|
<a href={ "/admin/libraries/" + libraryID + "/issues" } class="flex items-center gap-2 text-sm" style="color: var(--status-warning);">
|
|
@Icon("alert", "h-4 w-4")
|
|
if issueCount == 1 {
|
|
<span>1 processing issue</span>
|
|
} else {
|
|
<span>{ issueCount } processing issues</span>
|
|
}
|
|
@Icon("chevron-right", "h-4 w-4")
|
|
</a>
|
|
</div>
|
|
}
|
|
<!-- Actions -->
|
|
<div class="flex gap-2 pt-2 border-t" style="border-color: var(--border);">
|
|
<button
|
|
data-edit-id={ libraryID }
|
|
data-edit-name={ library.Name }
|
|
data-edit-desc={ library.Description }
|
|
onclick="openEditModal(this.dataset.editId, this.dataset.editName, this.dataset.editDesc)"
|
|
class="btn btn-secondary text-sm"
|
|
>
|
|
@Icon("edit", "h-4 w-4")
|
|
Edit Details
|
|
</button>
|
|
<button
|
|
data-delete-id={ libraryID }
|
|
data-delete-name={ library.Name }
|
|
onclick="openDeleteModal(this.dataset.deleteId, this.dataset.deleteName)"
|
|
class="btn btn-danger text-sm"
|
|
>
|
|
@Icon("trash", "h-4 w-4")
|
|
Delete Library
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ FolderBrowserContent(currentPath string, parentPath string, entries []DirEntry, targetInput string, libraryID string) {
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-3 p-2 rounded-lg" style="background-color: var(--bg-primary);">
|
|
@Icon("folder", "h-4 w-4 shrink-0")
|
|
<code class="text-xs flex-1 truncate" style="color: var(--text-primary)">{ currentPath }</code>
|
|
</div>
|
|
if parentPath != "" {
|
|
<button
|
|
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors hover:bg-surface-hover"
|
|
style="color: var(--text-secondary);"
|
|
hx-get={ "/admin/library/browse?path=" + parentPath + "&target_input=" + targetInput + "&library_id=" + libraryID }
|
|
hx-target="#folder-browser-content"
|
|
hx-swap="innerHTML"
|
|
>
|
|
@Icon("arrow-left", "h-4 w-4")
|
|
<span>..</span>
|
|
</button>
|
|
}
|
|
<div class="space-y-1 max-h-64 overflow-y-auto">
|
|
for _, entry := range entries {
|
|
<button
|
|
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors hover:bg-surface-hover"
|
|
style="color: var(--text-primary);"
|
|
hx-get={ "/admin/library/browse?path=" + entry.Path + "&target_input=" + targetInput + "&library_id=" + libraryID }
|
|
hx-target="#folder-browser-content"
|
|
hx-swap="innerHTML"
|
|
>
|
|
@Icon("folder", "h-4 w-4 shrink-0")
|
|
<span class="truncate">{ entry.Name }</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
<div class="mt-4 flex justify-end">
|
|
<button
|
|
type="button"
|
|
data-target-input={ targetInput }
|
|
data-current-path={ currentPath }
|
|
onclick="document.getElementById(this.dataset.targetInput).value = this.dataset.currentPath; document.getElementById('folder-browser-modal').classList.add('hidden')"
|
|
class="btn btn-primary text-sm"
|
|
>
|
|
@Icon("check", "h-4 w-4")
|
|
Select This Folder
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|