New MetadataEditorModal component with: - Cover section (w-64 h-96, matching book detail page layout): click-to-upload, Generate Cover button, Remove Cover button - Accordion sections: Basic Info, Publication, Series, Identifiers, Comic/Manga, Technical — covering all 34 editable metadata fields - Modal capped at 90vh with scrollable content area - Cover upload via hidden file input with hover overlay - Select dropdowns for MangaType and ReadingDirection - Read-only display for Format and File Size
549 lines
27 KiB
Templ
549 lines
27 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"
|
|
style="background-color: rgba(0, 0, 0, 0.7);"
|
|
>
|
|
<div
|
|
class="card rounded-lg p-6 w-full max-w-4xl mx-4 my-8"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
|
>
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Sync Progress</h2>
|
|
<p class="text-sm" style="color: var(--text-secondary);">{ book.Title }</p>
|
|
</div>
|
|
<button
|
|
@click="hideProgressSyncModal()"
|
|
class="p-2 hover:opacity-80 rounded-lg"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
if book.ActiveConflict != nil {
|
|
<div
|
|
class="mb-6 p-4 rounded-lg border"
|
|
style="background-color: #f59e0b20; border-color: #f59e0b;"
|
|
>
|
|
<p class="font-semibold mb-2" style="color: var(--text-primary);">
|
|
⚠️ Conflict Detected
|
|
</p>
|
|
<p class="text-sm" style="color: var(--text-secondary);">
|
|
Progress differs between devices. Review the options below and manually resolve via the Conflicts page.
|
|
</p>
|
|
<a
|
|
href="/conflicts"
|
|
class="inline-block mt-3 px-4 py-2 rounded-lg text-sm font-semibold"
|
|
style="background-color: #f59e0b; color: white; text-decoration: none;"
|
|
>
|
|
Go to Conflicts Page →
|
|
</a>
|
|
</div>
|
|
<div class="space-y-4">
|
|
for source, data := range book.ActiveConflict.ConflictData {
|
|
<div
|
|
class="card p-4 rounded-lg border"
|
|
style="background-color: var(--bg-primary); border-color: var(--border);"
|
|
>
|
|
<div class="flex justify-between items-start mb-3">
|
|
<div>
|
|
<span
|
|
class="inline-block px-2 py-1 rounded text-xs font-semibold capitalize mb-2"
|
|
style="background-color: var(--accent); color: white;"
|
|
>
|
|
{ source }
|
|
</span>
|
|
<p class="text-xs" style="color: var(--text-secondary);">
|
|
{ FormatInTimezone(data.Timestamp, user.Timezone) }
|
|
</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<div class="text-3xl font-bold" style="color: var(--accent);">
|
|
{ fmt.Sprintf("%.1f", data.Data["percentage"].(float64)) }%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
if data.Data["current_page"] != nil {
|
|
<div class="text-sm" style="color: var(--text-secondary);">
|
|
Page: { fmt.Sprintf("%.0f", data.Data["current_page"].(float64)) } / { fmt.Sprintf("%.0f", data.Data["total_pages"].(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>
|
|
}
|
|
</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 rounded-lg border text-center"
|
|
style="background-color: var(--bg-primary); border-color: var(--border);"
|
|
>
|
|
<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="px-4 py-2 rounded-lg"
|
|
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
|
|
>
|
|
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"
|
|
style="background-color: rgba(0, 0, 0, 0.7);"
|
|
>
|
|
<div
|
|
class="card rounded-lg p-8 w-full max-w-2xl mx-4 text-center"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
|
>
|
|
<div class="text-6xl mb-4">📝</div>
|
|
<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="px-6 py-2 rounded-lg font-semibold"
|
|
style="background-color: var(--accent); color: white;"
|
|
>
|
|
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"
|
|
style="background-color: rgba(0, 0, 0, 0.7);"
|
|
>
|
|
<div
|
|
class="rounded-lg w-full max-w-5xl mx-4 my-8 flex flex-col max-h-[90vh]"
|
|
style="background-color: var(--bg-secondary); border: 1px solid var(--border);"
|
|
>
|
|
<div class="flex justify-between items-center p-6 border-b flex-shrink-0" style="border-color: var(--border);">
|
|
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Edit Metadata</h2>
|
|
<button
|
|
@click="hideMetadataEditor()"
|
|
class="p-2 hover:opacity-80 rounded-lg"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
<div class="flex flex-col md:flex-row gap-8 p-6 flex-1 min-h-0 overflow-y-auto">
|
|
<!-- Cover Section (Left) -->
|
|
<div class="flex-shrink-0">
|
|
<div class="w-64 h-96 rounded-lg overflow-hidden shadow-lg 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="w-full px-3 py-2 text-sm rounded-lg font-semibold"
|
|
style="background-color: var(--accent); color: white;"
|
|
@click="generateCover()"
|
|
x-show="coverGenerating"
|
|
disabled
|
|
>
|
|
Generating...
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="w-full px-3 py-2 text-sm rounded-lg font-semibold"
|
|
style="background-color: var(--accent); color: white;"
|
|
@click="generateCover()"
|
|
x-show="!coverGenerating"
|
|
>
|
|
Generate Cover
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="w-full px-3 py-2 text-sm rounded-lg"
|
|
style="background-color: var(--bg-primary); color: var(--text-secondary); border: 1px solid var(--border);"
|
|
@click="removeCover()"
|
|
x-show="hasExistingCover || newCoverPreview"
|
|
>
|
|
Remove Cover
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- Accordion Fields (Right) -->
|
|
<div class="flex-1 min-w-0 space-y-2">
|
|
<!-- Basic Info -->
|
|
<div class="border rounded-lg" style="border-color: var(--border);">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
@click="toggleSection('basic')">
|
|
<span class="font-semibold">Basic Info</span>
|
|
<span x-text="openSections.basic ? '▾' : '▸'">▸</span>
|
|
</button>
|
|
<div x-show="openSections.basic" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Title</label>
|
|
<input type="text" name="title" value={ book.Title }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Author</label>
|
|
<input type="text" name="author" value={ textToString(book.Author) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Description</label>
|
|
<textarea name="description" rows="3"
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);"
|
|
>{ textToString(book.Description) }</textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Summary</label>
|
|
<textarea name="summary" rows="2"
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);"
|
|
>{ textToString(book.Summary) }</textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Tags (comma-separated)</label>
|
|
<input type="text" name="tags" value={ tagSliceToString(book.Tags) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium 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="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Publication -->
|
|
<div class="border rounded-lg" style="border-color: var(--border);">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
@click="toggleSection('publication')">
|
|
<span class="font-semibold">Publication</span>
|
|
<span x-text="openSections.publication ? '▾' : '▸'">▸</span>
|
|
</button>
|
|
<div x-show="openSections.publication" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Publisher</label>
|
|
<input type="text" name="publisher" value={ textToString(book.Publisher) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Date Published</label>
|
|
<input type="date" name="date_published"
|
|
value={ formatDateForInput(book.DatePublished) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Edition</label>
|
|
<input type="text" name="edition" value={ textToString(book.Edition) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Language</label>
|
|
<input type="text" name="language" value={ textToString(book.Language) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Genre</label>
|
|
<input type="text" name="genre" value={ textToString(book.Genre) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Copyright Year</label>
|
|
<input type="number" name="copyright_year"
|
|
value={ fmt.Sprintf("%d", book.CopyrightYear.Int32) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Series -->
|
|
<div class="border rounded-lg" style="border-color: var(--border);">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
@click="toggleSection('series')">
|
|
<span class="font-semibold">Series</span>
|
|
<span x-text="openSections.series ? '▾' : '▸'">▸</span>
|
|
</button>
|
|
<div x-show="openSections.series" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Series</label>
|
|
<input type="text" name="series" value={ textToString(book.Series) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div class="grid grid-cols-3 gap-3">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Number</label>
|
|
<input type="number" name="series_number"
|
|
value={ fmt.Sprintf("%d", book.SeriesNumber.Int32) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Count</label>
|
|
<input type="number" name="series_count"
|
|
value={ fmt.Sprintf("%d", book.SeriesCount.Int32) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Volume</label>
|
|
<input type="number" name="volume"
|
|
value={ fmt.Sprintf("%d", book.Volume.Int32) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Identifiers -->
|
|
<div class="border rounded-lg" style="border-color: var(--border);">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
@click="toggleSection('identifiers')">
|
|
<span class="font-semibold">Identifiers</span>
|
|
<span x-text="openSections.identifiers ? '▾' : '▸'">▸</span>
|
|
</button>
|
|
<div x-show="openSections.identifiers" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">ISBN</label>
|
|
<input type="text" name="isbn" value={ textToString(book.Isbn) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">ASIN</label>
|
|
<input type="text" name="asin" value={ textToString(book.Asin) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Goodreads ID</label>
|
|
<input type="text" name="goodreads_id" value={ textToString(book.GoodreadsID) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">OpenLibrary ID</label>
|
|
<input type="text" name="openlibrary_id" value={ textToString(book.OpenlibraryID) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Google Books ID</label>
|
|
<input type="text" name="google_books_id" value={ textToString(book.GoogleBooksID) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Web URL</label>
|
|
<input type="url" name="web_url" value={ textToString(book.WebUrl) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Comic/Manga -->
|
|
<div class="border rounded-lg" style="border-color: var(--border);">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
@click="toggleSection('comic')">
|
|
<span class="font-semibold">Comic/Manga</span>
|
|
<span x-text="openSections.comic ? '▾' : '▸'">▸</span>
|
|
</button>
|
|
<div x-show="openSections.comic" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Manga Type</label>
|
|
<select name="manga_type"
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);">
|
|
<option value="unknown" if textToString(book.MangaType) == "unknown" { selected } >Unknown</option>
|
|
<option value="no" if textToString(book.MangaType) == "no" { selected }>No</option>
|
|
<option value="yes" if textToString(book.MangaType) == "yes" { selected }>Yes</option>
|
|
<option value="yes_and_right_to_left" if textToString(book.MangaType) == "yes_and_right_to_left" { selected }>Yes (Right to Left)</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Reading Direction</label>
|
|
<select name="reading_direction"
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);">
|
|
<option value="auto" if textToString(book.ReadingDirection) == "auto" { selected }>Auto</option>
|
|
<option value="ltr" if textToString(book.ReadingDirection) == "ltr" { selected }>Left to Right</option>
|
|
<option value="rtl" if textToString(book.ReadingDirection) == "rtl" { selected }>Right to Left</option>
|
|
<option value="vertical" if textToString(book.ReadingDirection) == "vertical" { selected }>Vertical</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Age Rating</label>
|
|
<input type="text" name="age_rating" value={ textToString(book.AgeRating) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Story Arc</label>
|
|
<input type="text" name="story_arc" value={ textToString(book.StoryArc) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Imprint</label>
|
|
<input type="text" name="imprint" value={ textToString(book.Imprint) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Scan Information</label>
|
|
<input type="text" name="scan_information" value={ textToString(book.ScanInformation) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Metadata Notes</label>
|
|
<textarea name="metadata_notes" rows="2"
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);"
|
|
>{ 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>
|
|
<!-- Technical -->
|
|
<div class="border rounded-lg" style="border-color: var(--border);">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
@click="toggleSection('technical')">
|
|
<span class="font-semibold">Technical</span>
|
|
<span x-text="openSections.technical ? '▾' : '▸'">▸</span>
|
|
</button>
|
|
<div x-show="openSections.technical" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Page Count</label>
|
|
<input type="number" name="page_count"
|
|
value={ fmt.Sprintf("%d", book.PageCount.Int32) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Contributors (comma-separated)</label>
|
|
<input type="text" name="contributors" value={ stringSliceToString(book.Contributors) }
|
|
class="w-full px-3 py-2 rounded-lg border text-sm"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Format</label>
|
|
<input type="text" value={ textToString(book.MimeType) } readonly
|
|
class="w-full px-3 py-2 rounded-lg border text-sm opacity-60"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium 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="w-full px-3 py-2 rounded-lg border text-sm opacity-60"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
|
</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="px-4 py-2 rounded-lg"
|
|
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
@click="saveMetadata()"
|
|
class="px-6 py-2 rounded-lg font-semibold"
|
|
style="background-color: var(--accent); color: white;"
|
|
>
|
|
Save
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|