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.
549 lines
25 KiB
Templ
549 lines
25 KiB
Templ
package templates
|
|
|
|
import (
|
|
"bookhoard/internal/handlers"
|
|
"fmt"
|
|
)
|
|
|
|
templ ProgressSyncModal(user User, book handlers.MediaDetail) {
|
|
<div
|
|
id="progress-sync-modal"
|
|
class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4"
|
|
style="background-color: var(--surface-overlay);"
|
|
>
|
|
<div
|
|
class="card w-full max-w-4xl my-8 p-6"
|
|
style="box-shadow: var(--shadow-pop);"
|
|
>
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div class="flex items-center gap-3">
|
|
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
|
@Icon("sync", "h-5 w-5")
|
|
</span>
|
|
<div>
|
|
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Sync Progress</h2>
|
|
<p class="text-sm" style="color: var(--text-secondary);">{ book.Title }</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
@click="hideProgressSyncModal()"
|
|
class="icon-btn"
|
|
aria-label="Close"
|
|
>
|
|
@Icon("close", "h-5 w-5")
|
|
</button>
|
|
</div>
|
|
if book.ActiveConflict != nil {
|
|
<div
|
|
class="mb-6 p-4 rounded-xl flex items-start gap-3"
|
|
style="background-color: color-mix(in srgb, var(--status-warning) 15%, transparent); border: 1px solid var(--status-warning);"
|
|
>
|
|
<span class="shrink-0 mt-0.5" style="color: var(--status-warning);">@Icon("alert", "h-5 w-5")</span>
|
|
<div>
|
|
<p class="font-semibold mb-1" style="color: var(--text-primary);">
|
|
Conflict Detected
|
|
</p>
|
|
<p class="text-sm" style="color: var(--text-secondary);">
|
|
Progress differs between devices. Choose which version to keep.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-4">
|
|
for source, data := range book.ActiveConflict.ConflictData {
|
|
<div class="card p-4">
|
|
<div class="flex justify-between items-start mb-3">
|
|
<div>
|
|
<span class="badge mb-2" style="background-color: var(--accent); color: #fff;">
|
|
{ data.Source }
|
|
</span>
|
|
<p class="text-xs" style="color: var(--text-secondary);">
|
|
{ FormatInTimezone(data.Timestamp, user.Timezone) }
|
|
</p>
|
|
</div>
|
|
<div class="text-right">
|
|
if data.Data["percentage"] != nil {
|
|
<div class="text-3xl font-bold" style="color: var(--accent);">
|
|
{ fmt.Sprintf("%.1f", data.Data["percentage"].(float64) * 100) }%
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
if data.Data["page"] != nil {
|
|
<div class="text-sm" style="color: var(--text-secondary);">
|
|
if data.Data["total_pages"] != nil {
|
|
Page: { fmt.Sprintf("%.0f", data.Data["page"].(float64)) } / { fmt.Sprintf("%.0f", data.Data["total_pages"].(float64)) }
|
|
} else {
|
|
Page: { fmt.Sprintf("%.0f", data.Data["page"].(float64)) }
|
|
}
|
|
</div>
|
|
}
|
|
if data.Data["epubcfi"] != nil {
|
|
<div class="text-xs mt-1" style="color: var(--text-secondary);">
|
|
CFI: { data.Data["epubcfi"].(string) }
|
|
</div>
|
|
}
|
|
<div class="mt-3">
|
|
<button
|
|
@click={"resolveConflict('" + book.ActiveConflict.ID + "', '" + source + "')"}
|
|
class="btn btn-primary"
|
|
>
|
|
Keep This
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
} else if book.ReadingProgress != nil {
|
|
<div class="mb-4" style="color: var(--text-secondary);">
|
|
<p>No conflicts detected. Current progress from <strong>{ book.ReadingProgress.LastSyncSource.String }</strong>:</p>
|
|
</div>
|
|
<div class="card p-6 text-center">
|
|
<div class="text-5xl font-bold mb-4" style="color: var(--accent);">
|
|
{ fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64 * 100) }%
|
|
</div>
|
|
<p class="capitalize text-lg mb-2" style="color: var(--text-primary);">
|
|
{ book.ReadingProgress.LastSyncSource.String }
|
|
</p>
|
|
if book.ReadingProgress.CurrentPage.Valid && book.ReadingProgress.TotalPages.Valid {
|
|
<p style="color: var(--text-secondary);">
|
|
Page { book.ReadingProgress.CurrentPage.Int32 } of { book.ReadingProgress.TotalPages.Int32 }
|
|
</p>
|
|
}
|
|
if book.ReadingProgress.LastReadAt.Valid {
|
|
<p class="text-sm mt-2" style="color: var(--text-secondary);">
|
|
Last read: { FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone) }
|
|
</p>
|
|
}
|
|
</div>
|
|
}
|
|
<div class="flex justify-end space-x-3 mt-6">
|
|
<button
|
|
@click="hideProgressSyncModal()"
|
|
class="btn btn-secondary"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ NotesHighlightsModal(book handlers.MediaDetail) {
|
|
<div
|
|
id="notes-modal"
|
|
class="hidden fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
style="background-color: var(--surface-overlay);"
|
|
>
|
|
<div
|
|
class="card w-full max-w-2xl p-8 text-center"
|
|
style="box-shadow: var(--shadow-pop);"
|
|
>
|
|
<span class="inline-grid place-items-center h-14 w-14 rounded-2xl mb-4" style="background-color: var(--accent-muted); color: var(--accent);">
|
|
@Icon("edit", "h-7 w-7")
|
|
</span>
|
|
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Notes & Highlights</h2>
|
|
<p class="mb-2" style="color: var(--text-secondary);">
|
|
This book has <strong>{ book.NotesCount }</strong> notes and <strong>{ book.HighlightsCount }</strong> highlights.
|
|
</p>
|
|
<p class="mb-6" style="color: var(--text-secondary);">Feature coming soon!</p>
|
|
<div>
|
|
<button
|
|
@click="hideNotesModal()"
|
|
class="btn btn-primary"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ MetadataEditorModal(book handlers.MediaDetail) {
|
|
<div
|
|
id="metadata-editor-modal"
|
|
class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4"
|
|
style="background-color: var(--surface-overlay);"
|
|
>
|
|
<div
|
|
class="card w-full max-w-5xl my-8 flex flex-col max-h-[90vh]"
|
|
style="box-shadow: var(--shadow-pop);"
|
|
>
|
|
<div class="flex justify-between items-center p-6 border-b flex-shrink-0" style="border-color: var(--border);">
|
|
<div class="flex items-center gap-3">
|
|
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
|
@Icon("edit", "h-5 w-5")
|
|
</span>
|
|
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Edit Metadata</h2>
|
|
</div>
|
|
<button
|
|
@click="hideMetadataEditor()"
|
|
class="icon-btn"
|
|
aria-label="Close"
|
|
>
|
|
@Icon("close", "h-5 w-5")
|
|
</button>
|
|
</div>
|
|
<div class="flex flex-col md:flex-row gap-8 p-6 flex-1 min-h-0 overflow-y-auto">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-64 h-96 rounded-xl overflow-hidden mb-3 cursor-pointer relative group"
|
|
style="background-color: var(--bg-primary);"
|
|
@click="document.getElementById('cover-upload-input').click()"
|
|
>
|
|
if book.CoverImagePath.Valid && book.CoverImagePath.String != "" {
|
|
<img id="metadata-cover-preview"
|
|
src={ book.CoverImagePath.String }
|
|
alt={ book.Title }
|
|
class="w-full h-full object-cover"
|
|
/>
|
|
} else {
|
|
<img id="metadata-cover-preview"
|
|
src="/static/placeholder-book.svg"
|
|
alt={ book.Title }
|
|
class="w-full h-full object-cover"
|
|
/>
|
|
}
|
|
<div class="absolute inset-0 bg-black bg-opacity-40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
|
<span class="text-white text-sm font-semibold">Click to upload</span>
|
|
</div>
|
|
</div>
|
|
<input type="file" id="cover-upload-input" accept="image/jpeg,image/png,image/webp" class="hidden"
|
|
@change="handleCoverUpload($event)" />
|
|
<div class="w-64 space-y-2">
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary w-full"
|
|
@click="generateCover()"
|
|
x-show="coverGenerating"
|
|
disabled
|
|
>
|
|
@Icon("refresh", "h-4 w-4")
|
|
Generating...
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary w-full"
|
|
@click="generateCover()"
|
|
x-show="!coverGenerating"
|
|
>
|
|
@Icon("refresh", "h-4 w-4")
|
|
Generate Cover
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost w-full"
|
|
@click="removeCover()"
|
|
x-show="hasExistingCover || newCoverPreview"
|
|
>
|
|
Remove Cover
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="flex-1 min-w-0 space-y-2">
|
|
<div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
|
|
style="color: var(--text-primary);"
|
|
@click="toggleSection('basic')">
|
|
<span class="font-semibold">Basic Info</span>
|
|
<span class="inline-flex">
|
|
<span x-show="openSections.basic">@Icon("chevron-down", "h-4 w-4")</span>
|
|
<span x-show="!openSections.basic" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
|
|
</span>
|
|
</button>
|
|
<div x-show="openSections.basic" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Title</label>
|
|
<input type="text" name="title" value={ book.Title } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Author</label>
|
|
<input type="text" name="author" value={ textToString(book.Author) } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Description</label>
|
|
<textarea name="description" rows="3" class="input"
|
|
>{ textToString(book.Description) }</textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Summary</label>
|
|
<textarea name="summary" rows="2" class="input"
|
|
>{ textToString(book.Summary) }</textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Tags</label>
|
|
<div class="flex flex-wrap gap-1.5 mb-2">
|
|
<template x-for="(tag, idx) in editorTags" :key="idx">
|
|
<span class="chip">
|
|
<span x-text="tag"></span>
|
|
<button type="button" class="hover:bg-surface-hover rounded leading-none" @click="removeEditorTag(idx)">@Icon("close", "h-3 w-3")</button>
|
|
</span>
|
|
</template>
|
|
</div>
|
|
for _, tag := range book.Tags {
|
|
<span data-editor-tag={ tag } class="hidden"></span>
|
|
}
|
|
<div>
|
|
<input type="text"
|
|
x-model="tagSearch"
|
|
@input.debounce.300ms="searchEditorTags()"
|
|
@keydown="onTagKeydown($event)"
|
|
@blur="hideEditorTagDropdown()"
|
|
placeholder="Add tag..."
|
|
class="input" />
|
|
<div
|
|
x-show="showTagDropdown"
|
|
x-transition
|
|
x-cloak
|
|
class="mt-1 w-full rounded-lg border max-h-48 overflow-y-auto"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); box-shadow: var(--shadow-card);"
|
|
>
|
|
<template x-for="sug in tagSuggestions" :key="sug.value">
|
|
<button
|
|
type="button"
|
|
class="w-full text-left px-3 py-2 text-sm flex justify-between items-center hover:bg-surface-hover"
|
|
:class="highlightedTagIndex === tagSuggestions.indexOf(sug) ? 'bg-surface-hover' : ''"
|
|
@click="selectEditorTagSuggestion(sug.value)"
|
|
>
|
|
<span x-text="sug.value"></span>
|
|
<span class="text-xs" style="color: var(--text-secondary);" x-text="sug.count + ' books'"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Community Rating (0-10)</label>
|
|
<input type="number" name="community_rating" min="0" max="10" step="0.1"
|
|
value={ fmt.Sprintf("%.1f", book.CommunityRating.Float64) }
|
|
class="input" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
|
|
style="color: var(--text-primary);"
|
|
@click="toggleSection('publication')">
|
|
<span class="font-semibold">Publication</span>
|
|
<span class="inline-flex">
|
|
<span x-show="openSections.publication">@Icon("chevron-down", "h-4 w-4")</span>
|
|
<span x-show="!openSections.publication" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
|
|
</span>
|
|
</button>
|
|
<div x-show="openSections.publication" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Publisher</label>
|
|
<input type="text" name="publisher" value={ textToString(book.Publisher) } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Date Published</label>
|
|
<input type="date" name="date_published"
|
|
value={ formatDateForInput(book.DatePublished) }
|
|
class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Edition</label>
|
|
<input type="text" name="edition" value={ textToString(book.Edition) } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Language</label>
|
|
<input type="text" name="language" value={ textToString(book.Language) } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Genre</label>
|
|
<input type="text" name="genre" value={ textToString(book.Genre) } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Copyright Year</label>
|
|
<input type="number" name="copyright_year"
|
|
value={ fmt.Sprintf("%d", book.CopyrightYear.Int32) }
|
|
class="input" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
|
|
style="color: var(--text-primary);"
|
|
@click="toggleSection('series')">
|
|
<span class="font-semibold">Series</span>
|
|
<span class="inline-flex">
|
|
<span x-show="openSections.series">@Icon("chevron-down", "h-4 w-4")</span>
|
|
<span x-show="!openSections.series" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
|
|
</span>
|
|
</button>
|
|
<div x-show="openSections.series" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Series</label>
|
|
<input type="text" name="series" value={ textToString(book.Series) } class="input" />
|
|
</div>
|
|
<div class="grid grid-cols-3 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Number</label>
|
|
<input type="number" name="series_number"
|
|
value={ fmt.Sprintf("%d", book.SeriesNumber.Int32) }
|
|
class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Count</label>
|
|
<input type="number" name="series_count"
|
|
value={ fmt.Sprintf("%d", book.SeriesCount.Int32) }
|
|
class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Volume</label>
|
|
<input type="number" name="volume"
|
|
value={ fmt.Sprintf("%d", book.Volume.Int32) }
|
|
class="input" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
|
|
style="color: var(--text-primary);"
|
|
@click="toggleSection('identifiers')">
|
|
<span class="font-semibold">Identifiers</span>
|
|
<span class="inline-flex">
|
|
<span x-show="openSections.identifiers">@Icon("chevron-down", "h-4 w-4")</span>
|
|
<span x-show="!openSections.identifiers" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
|
|
</span>
|
|
</button>
|
|
<div x-show="openSections.identifiers" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">ISBN</label>
|
|
<input type="text" name="isbn" value={ textToString(book.Isbn) } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">ASIN</label>
|
|
<input type="text" name="asin" value={ textToString(book.Asin) } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Goodreads ID</label>
|
|
<input type="text" name="goodreads_id" value={ textToString(book.GoodreadsID) } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">OpenLibrary ID</label>
|
|
<input type="text" name="openlibrary_id" value={ textToString(book.OpenlibraryID) } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Google Books ID</label>
|
|
<input type="text" name="google_books_id" value={ textToString(book.GoogleBooksID) } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Web URL</label>
|
|
<input type="url" name="web_url" value={ textToString(book.WebUrl) } class="input" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
|
|
style="color: var(--text-primary);"
|
|
@click="toggleSection('comic')">
|
|
<span class="font-semibold">Comic/Manga</span>
|
|
<span class="inline-flex">
|
|
<span x-show="openSections.comic">@Icon("chevron-down", "h-4 w-4")</span>
|
|
<span x-show="!openSections.comic" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
|
|
</span>
|
|
</button>
|
|
<div x-show="openSections.comic" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
|
|
<div>
|
|
<label for="manga_type" class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Manga Type</label>
|
|
<select id="manga_type" name="manga_type" class="input">
|
|
<option value="unknown" selected={ textToString(book.MangaType) == "unknown" }>Unknown</option>
|
|
<option value="no" selected={ textToString(book.MangaType) == "no" }>No</option>
|
|
<option value="yes" selected={ textToString(book.MangaType) == "yes" }>Yes</option>
|
|
<option value="yes_and_right_to_left" selected={ textToString(book.MangaType) == "yes_and_right_to_left" }>Yes (Right to Left)</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="reading_direction" class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Reading Direction</label>
|
|
<select id="reading_direction" name="reading_direction" class="input">
|
|
<option value="auto" selected={ textToString(book.ReadingDirection) == "auto" }>Auto</option>
|
|
<option value="ltr" selected={ textToString(book.ReadingDirection) == "ltr" }>Left to Right</option>
|
|
<option value="rtl" selected={ textToString(book.ReadingDirection) == "rtl" }>Right to Left</option>
|
|
<option value="vertical" selected={ textToString(book.ReadingDirection) == "vertical" }>Vertical</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Age Rating</label>
|
|
<input type="text" name="age_rating" value={ textToString(book.AgeRating) } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Story Arc</label>
|
|
<input type="text" name="story_arc" value={ textToString(book.StoryArc) } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Imprint</label>
|
|
<input type="text" name="imprint" value={ textToString(book.Imprint) } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Scan Information</label>
|
|
<input type="text" name="scan_information" value={ textToString(book.ScanInformation) } class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Metadata Notes</label>
|
|
<textarea name="metadata_notes" rows="2" class="input"
|
|
>{ textToString(book.MetadataNotes) }</textarea>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<input type="checkbox" name="is_black_and_white" id="is_black_and_white"
|
|
if book.IsBlackAndWhite.Bool { checked }
|
|
class="rounded" />
|
|
<label for="is_black_and_white" class="text-sm" style="color: var(--text-secondary);">Black & White</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
|
|
style="color: var(--text-primary);"
|
|
@click="toggleSection('technical')">
|
|
<span class="font-semibold">Technical</span>
|
|
<span class="inline-flex">
|
|
<span x-show="openSections.technical">@Icon("chevron-down", "h-4 w-4")</span>
|
|
<span x-show="!openSections.technical" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
|
|
</span>
|
|
</button>
|
|
<div x-show="openSections.technical" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Page Count</label>
|
|
<input type="number" name="page_count"
|
|
value={ fmt.Sprintf("%d", book.PageCount.Int32) }
|
|
class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Contributors (comma-separated)</label>
|
|
<input type="text" name="contributors" value={ stringSliceToString(book.Contributors) } class="input" />
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Format</label>
|
|
<input type="text" value={ textToString(book.MimeType) } readonly
|
|
class="input opacity-60" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">File Size</label>
|
|
<input type="text" value={ fmt.Sprintf("%.1f MB", float64(book.FileSize.Int64)/1024/1024) } readonly
|
|
class="input opacity-60" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end space-x-3 p-6 border-t flex-shrink-0" style="border-color: var(--border);">
|
|
<button
|
|
@click="hideMetadataEditor()"
|
|
class="btn btn-secondary"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
@click="saveMetadata()"
|
|
class="btn btn-primary"
|
|
>
|
|
@Icon("save", "h-4 w-4")
|
|
Save
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|