Release / build-and-push (push) Successful in 2m44s
# Conflicts: # templates/admin_library.templ # templates/admin_library_templ.go # templates/conflicts_templ.go # templates/unlinked_books_templ.go
420 lines
17 KiB
Templ
420 lines
17 KiB
Templ
package templates
|
|
|
|
templ AdminLibrary(user User, libraries []LibraryData, users []User) {
|
|
<!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>
|
|
<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-6" style="color: var(--text-primary)">
|
|
Are you sure you want to delete <strong id="delete-library-name"></strong>?
|
|
This will remove the library and all its folder mappings. Media files will not be deleted.
|
|
</p>
|
|
<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');
|
|
}
|
|
</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>
|
|
}
|