Apply the sidebar shell and bold primitives across the rest of the app so the whole experience shares one visual language: - Browse/organize: series (keeps stacked-covers/series-card CSS), collections (keeps wood-paneling + carousel classes), browse_detail. - Account/stats: profile + form + modal, progress, analytics (stat-card tiles), devices, conflicts (status semantics), queue (status/priority badges). - Admin: admin sidebar restyled with icons, dashboard/users/library/ processing-issues/settings migrated to .card/.btn/.input primitives. - Docs/setup/misc: docs (keeps prose/highlight.js), api_explorer, setup wizard, unlinked_books, custom_section builder. - Modals/fragments: collection_modal, collection_rules, restore_system_collection_modal, filter_item, book_detail_modals — all overlays use --surface-overlay + --shadow-pop. Every Alpine handler, HTMX attribute, id, name, and data-* is preserved; only presentation changes.
421 lines
18 KiB
Templ
421 lines
18 KiB
Templ
package templates
|
||
|
||
templ Setup() {
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>Setup - Bookhoard</title>
|
||
<script src="/static/htmx.min.js"></script>
|
||
<script type="module" src="/static/main.js"></script>
|
||
<link href="/static/style.css" rel="stylesheet"/>
|
||
</head>
|
||
<body class="theme-tokyo-night min-h-screen" x-data="setupWizard" x-init="initSetup()">
|
||
<div class="mx-auto container px-4 py-8 max-w-2xl">
|
||
<div class="text-center mb-8">
|
||
<span class="inline-grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4" style="background-color: var(--accent-muted); color: var(--accent);">
|
||
@Icon("library", "h-7 w-7")
|
||
</span>
|
||
<h1 class="text-4xl font-bold mb-2 tracking-tight" style="color: var(--text-primary)">Welcome to Bookhoard</h1>
|
||
<p class="text-lg" style="color: var(--text-secondary)">Let's get your library set up.</p>
|
||
</div>
|
||
|
||
<div class="flex items-center justify-center mb-8 gap-2">
|
||
<template x-for="n in 4" :key="n">
|
||
<div class="flex items-center gap-2">
|
||
<div
|
||
class="w-10 h-10 rounded-full grid place-items-center font-bold text-sm transition-all"
|
||
:class="currentStep >= n ? 'bg-[var(--accent)] text-[var(--bg-primary)]' : 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] border border-[var(--border)]'"
|
||
>
|
||
<template x-if="currentStep > n">@Icon("check", "h-5 w-5")</template>
|
||
<span x-show="currentStep <= n" x-text="n"></span>
|
||
</div>
|
||
<div
|
||
x-text="['Admin', 'Libraries', 'Folders', 'Scan'][n-1]"
|
||
class="text-sm hidden sm:inline"
|
||
:style="currentStep >= n ? 'color: var(--text-primary)' : 'color: var(--text-secondary)'"
|
||
></div>
|
||
<div x-show="n < 4" class="w-8 h-0.5 mx-1" :class="currentStep > n ? 'bg-[var(--accent)]' : 'bg-[var(--border)]'"></div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<div x-show="errorMsg" class="mb-4 card p-4 text-sm" style="border-color: var(--status-danger); background-color: color-mix(in srgb, var(--status-danger) 12%, transparent); color: var(--status-danger);">
|
||
<span x-text="errorMsg"></span>
|
||
</div>
|
||
|
||
<!-- ==================== STEP 1: Create Admin ==================== -->
|
||
<div x-show="currentStep === 1" class="card p-6">
|
||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Create Administrator Account</h2>
|
||
<p class="mb-6" style="color: var(--text-secondary)">This first account will have full admin privileges.</p>
|
||
|
||
<form @submit.prevent="submitAdmin()">
|
||
<div class="mb-6 card p-4" style="background-color: var(--bg-primary);">
|
||
<label for="base-url" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Server URL (Base URL)</label>
|
||
<input
|
||
type="url"
|
||
id="base-url"
|
||
x-model="admin.baseUrl"
|
||
placeholder="https://books.example.com"
|
||
class="input mt-2"
|
||
required
|
||
/>
|
||
<p class="text-sm mt-2" style="color: var(--text-secondary)">The public URL of your Bookhoard instance. Used for device sync, OPDS feed, and API endpoints.</p>
|
||
</div>
|
||
|
||
<div class="mb-4">
|
||
<label for="email" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Email</label>
|
||
<input
|
||
type="email"
|
||
id="email"
|
||
x-model="admin.email"
|
||
class="input mt-2"
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label for="username" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Username</label>
|
||
<input
|
||
type="text"
|
||
x-model="admin.username"
|
||
id="username"
|
||
class="input mt-2"
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||
<div>
|
||
<label for="first-name" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">First Name</label>
|
||
<input
|
||
type="text"
|
||
id="first-name"
|
||
x-model="admin.firstName"
|
||
placeholder="Optional"
|
||
class="input mt-2"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label for="last-name" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Last Name</label>
|
||
<input
|
||
type="text"
|
||
id="last-name"
|
||
x-model="admin.lastName"
|
||
placeholder="Optional"
|
||
class="input mt-2"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="password-requirements" class="mb-4 card p-4 text-sm" style="background-color: var(--bg-primary);">
|
||
<p class="font-semibold mb-2" style="color: var(--text-primary)">Password Requirements:</p>
|
||
<ul class="space-y-1" style="color: var(--text-secondary)">
|
||
<li id="req-length" class="flex items-center gap-2">
|
||
<span class="requirement-icon">○</span> At least 8 characters
|
||
</li>
|
||
<li id="req-upper" class="flex items-center gap-2">
|
||
<span class="requirement-icon">○</span> One uppercase letter
|
||
</li>
|
||
<li id="req-lower" class="flex items-center gap-2">
|
||
<span class="requirement-icon">○</span> One lowercase letter
|
||
</li>
|
||
<li id="req-number" class="flex items-center gap-2">
|
||
<span class="requirement-icon">○</span> One number
|
||
</li>
|
||
<li id="req-special" class="flex items-center gap-2">
|
||
<span class="requirement-icon">○</span> One special character
|
||
</li>
|
||
<li id="req-match" class="flex items-center gap-2">
|
||
<span class="requirement-icon">○</span> Passwords match
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label for="password" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Password</label>
|
||
<input
|
||
type="password"
|
||
x-model="admin.password"
|
||
id="password"
|
||
class="input mt-2"
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="mb-6">
|
||
<label for="confirm-password" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Confirm Password</label>
|
||
<input
|
||
type="password"
|
||
x-model="admin.confirmPassword"
|
||
id="confirm-password"
|
||
class="input mt-2"
|
||
required
|
||
/>
|
||
</div>
|
||
<button
|
||
type="submit"
|
||
id="register-btn"
|
||
class="btn btn-primary w-full"
|
||
:disabled="loading"
|
||
x-text="loading ? 'Creating…' : 'Create Admin Account'"
|
||
></button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- ==================== STEP 2: Create Libraries ==================== -->
|
||
<div x-show="currentStep === 2" class="card p-6">
|
||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Create Libraries</h2>
|
||
<p class="mb-6" style="color: var(--text-secondary)">Add one or more media libraries. You can always add more later.</p>
|
||
|
||
<div x-show="libraries.length > 0" class="mb-6 space-y-2">
|
||
<template x-for="(lib, idx) in libraries" :key="lib.id">
|
||
<div class="card p-3 flex items-center justify-between" style="background-color: var(--bg-primary);">
|
||
<div class="flex items-center gap-3">
|
||
<span class="grid place-items-center h-9 w-9 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||
<span class="inline-flex" x-show="lib.type === 'ebooks'">@Icon("book", "h-5 w-5")</span>
|
||
<span class="inline-flex" x-show="lib.type === 'comics'">@Icon("book-open", "h-5 w-5")</span>
|
||
<span class="inline-flex" x-show="lib.type === 'manga'">@Icon("library", "h-5 w-5")</span>
|
||
</span>
|
||
<div>
|
||
<p class="font-medium" style="color: var(--text-primary)" x-text="lib.name"></p>
|
||
<p class="text-xs capitalize" style="color: var(--text-secondary)" x-text="lib.type"></p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
@click="removeLibrary(idx)"
|
||
class="icon-btn"
|
||
style="color: var(--status-danger);"
|
||
aria-label="Remove library"
|
||
>@Icon("trash", "h-4 w-4")</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<form @submit.prevent="addLibrary()" class="space-y-4 card p-4" style="background-color: var(--bg-primary);">
|
||
<div>
|
||
<label for="lib-name" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Library Name</label>
|
||
<input
|
||
type="text"
|
||
id="lib-name"
|
||
x-model="newLibrary.name"
|
||
placeholder="My Ebook Library"
|
||
class="input mt-2"
|
||
required
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label for="lib-type" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Library Type</label>
|
||
<select
|
||
id="lib-type"
|
||
x-model="newLibrary.type"
|
||
class="input mt-2"
|
||
>
|
||
<option value="ebooks">Ebooks</option>
|
||
<option value="comics">Comics</option>
|
||
<option value="manga">Manga</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="lib-desc" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Description (optional)</label>
|
||
<input
|
||
type="text"
|
||
id="lib-desc"
|
||
x-model="newLibrary.description"
|
||
placeholder="Optional"
|
||
class="input mt-2"
|
||
/>
|
||
</div>
|
||
<button
|
||
type="submit"
|
||
class="btn btn-secondary w-full"
|
||
:disabled="loading || !newLibrary.name.trim()"
|
||
x-text="loading ? 'Creating…' : '+ Add This Library'"
|
||
></button>
|
||
</form>
|
||
|
||
<div class="flex justify-between mt-6">
|
||
<button type="button" @click="currentStep = 1" class="btn btn-secondary">
|
||
@Icon("arrow-left", "h-4 w-4")
|
||
Back
|
||
</button>
|
||
<button type="button" @click="currentStep = 3" class="btn btn-primary">
|
||
<span x-text="libraries.length > 0 ? 'Next: Configure Folders' : 'Skip – Continue'"></span>
|
||
@Icon("arrow-right", "h-4 w-4")
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ==================== STEP 3: Configure Folders ==================== -->
|
||
<div x-show="currentStep === 3" class="card p-6">
|
||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Configure Library Folders</h2>
|
||
<p class="mb-6" style="color: var(--text-secondary)">Add the filesystem folders where your media files are stored.</p>
|
||
|
||
<div x-show="libraries.length === 0" class="text-center py-8" style="color: var(--text-secondary)">
|
||
<p>No libraries created. You can add folders later from the admin panel.</p>
|
||
</div>
|
||
|
||
<div class="space-y-6">
|
||
<template x-for="lib in libraries" :key="lib.id">
|
||
<div class="card p-4" style="background-color: var(--bg-primary);">
|
||
<div class="flex items-center gap-2 mb-3">
|
||
<span class="grid place-items-center h-8 w-8 rounded-lg" style="background-color: var(--accent-muted); color: var(--accent);">
|
||
<span class="inline-flex" x-show="lib.type === 'ebooks'">@Icon("book", "h-4 w-4")</span>
|
||
<span class="inline-flex" x-show="lib.type === 'comics'">@Icon("book-open", "h-4 w-4")</span>
|
||
<span class="inline-flex" x-show="lib.type === 'manga'">@Icon("library", "h-4 w-4")</span>
|
||
</span>
|
||
<h4 class="font-semibold" style="color: var(--text-primary)" x-text="lib.name"></h4>
|
||
</div>
|
||
|
||
<div class="space-y-1 mb-3" x-show="lib.folders && lib.folders.length > 0">
|
||
<template x-for="(folder, fIdx) in (lib.folders || [])" :key="folder">
|
||
<div class="flex items-center justify-between p-2 rounded-lg text-sm" style="background-color: var(--bg-secondary);">
|
||
<span class="flex items-center gap-2 truncate" style="color: var(--text-primary)">
|
||
@Icon("folder", "h-4 w-4 shrink-0")
|
||
<span class="truncate" x-text="folder"></span>
|
||
</span>
|
||
<button type="button" @click="removeFolder(lib.id, folder, fIdx)" class="icon-btn shrink-0" style="color: var(--status-danger);" aria-label="Remove folder">@Icon("trash", "h-4 w-4")</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="flex gap-2">
|
||
<input
|
||
type="text"
|
||
:placeholder="'/path/to/' + lib.type"
|
||
:id="'folderInput-' + lib.id"
|
||
class="input flex-1 text-sm"
|
||
/>
|
||
<button
|
||
type="button"
|
||
@click="showFolderBrowser('folderInput-' + lib.id)"
|
||
class="btn btn-secondary px-3 text-xs"
|
||
>Browse</button>
|
||
<button
|
||
type="button"
|
||
@click="addFolderToLibrary(lib)"
|
||
class="btn btn-primary px-3 text-xs"
|
||
>Add</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="flex justify-between mt-6">
|
||
<button type="button" @click="currentStep = 2" class="btn btn-secondary">
|
||
@Icon("arrow-left", "h-4 w-4")
|
||
Back
|
||
</button>
|
||
<button type="button" @click="currentStep = 4" class="btn btn-primary">
|
||
Next: Scan
|
||
@Icon("arrow-right", "h-4 w-4")
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ==================== STEP 4: Scan & Finish ==================== -->
|
||
<div x-show="currentStep === 4" class="card p-6">
|
||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Scan & Finish</h2>
|
||
<p class="mb-6" style="color: var(--text-secondary)">Review your configuration and start the initial scan.</p>
|
||
|
||
<div class="space-y-3 mb-6">
|
||
<div class="card p-4" style="background-color: var(--bg-primary);">
|
||
<p class="text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Server URL</p>
|
||
<p style="color: var(--text-primary)" x-text="admin.baseUrl"></p>
|
||
</div>
|
||
<div class="card p-4" style="background-color: var(--bg-primary);">
|
||
<p class="text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Administrator</p>
|
||
<p style="color: var(--text-primary)" x-text="admin.username + ' (' + admin.email + ')'"></p>
|
||
</div>
|
||
<div class="card p-4" style="background-color: var(--bg-primary);">
|
||
<p class="text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">
|
||
Libraries (<span x-text="libraries.length"></span>)
|
||
</p>
|
||
<template x-for="lib in libraries" :key="lib.id">
|
||
<div class="flex items-start justify-between text-sm py-1">
|
||
<div class="flex items-center gap-2">
|
||
<span style="color: var(--text-primary)" x-text="lib.name"></span>
|
||
<span class="chip" x-text="lib.type"></span>
|
||
</div>
|
||
<div class="text-right" style="color: var(--text-secondary)">
|
||
<span x-text="(lib.folders || []).length + ' folder(s)'"></span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-show="scanStarted" class="mb-6 space-y-3">
|
||
<h3 class="font-semibold" style="color: var(--text-primary)">Scan Progress</h3>
|
||
<div id="library-progress-list" class="space-y-3">
|
||
<template x-for="job in scanJobs" :key="job.jobId">
|
||
<div class="card p-3" style="background-color: var(--bg-primary);">
|
||
<div class="flex justify-between items-center mb-2">
|
||
<span class="font-medium" style="color: var(--text-primary)" x-text="job.libraryName"></span>
|
||
<span class="text-sm" style="color: var(--text-secondary)" x-text="job.statusText"></span>
|
||
</div>
|
||
<div class="w-full rounded-full h-2" style="background-color: var(--bg-secondary)">
|
||
<div
|
||
class="h-2 rounded-full transition-all duration-500"
|
||
:style="'width: ' + job.progress + '%; background-color: var(--accent);'"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div x-show="scanComplete" class="card p-4 text-sm" style="border-color: var(--status-success); background-color: color-mix(in srgb, var(--status-success) 12%, transparent); color: var(--status-success);">
|
||
Initial scan complete! Your libraries are ready to use.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex justify-between mt-6">
|
||
<button type="button" @click="currentStep = 3" class="btn btn-secondary" :disabled="loading">
|
||
@Icon("arrow-left", "h-4 w-4")
|
||
Back
|
||
</button>
|
||
|
||
<div class="flex gap-3">
|
||
<button
|
||
x-show="!scanStarted"
|
||
type="button"
|
||
@click="startScan()"
|
||
class="btn btn-primary"
|
||
:disabled="loading"
|
||
>
|
||
@Icon("sync", "h-4 w-4")
|
||
Start Scan
|
||
</button>
|
||
|
||
<button
|
||
x-show="scanStarted && !scanComplete"
|
||
type="button"
|
||
@click="finishSetup()"
|
||
class="btn btn-secondary"
|
||
:disabled="loading"
|
||
>Skip to Dashboard</button>
|
||
|
||
<button
|
||
x-show="scanComplete"
|
||
type="button"
|
||
@click="finishSetup()"
|
||
class="btn btn-primary"
|
||
:disabled="loading"
|
||
x-text="loading ? 'Finishing…' : 'Go to Dashboard'"
|
||
></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<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-md mx-4">
|
||
<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" @click="hideFolderBrowser()" class="icon-btn" aria-label="Close">@Icon("close", "h-5 w-5")</button>
|
||
</div>
|
||
<div id="folder-browser-content">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
}
|