Apply the new design-system primitives across the remaining pages so the whole app shares one visual language: - Auth/entry: redesigned index, login, register (centered card layout, SVG icons, semantic tokens) - Admin: sidebar nav with icons, cohesive admin dashboard/users/library/ processing-issues/settings (tables use border-line + hover states; scan/websocket attributes preserved) - Secondary: analytics (stat cards), devices, progress, conflicts, queue, profile + form/modal all migrated to .card/.btn/.input primitives and SVG action icons - Docs + API explorer + setup wizard + collection/rules/modals + unlinked books + book-detail modals: consistent chrome, modal scrims use --surface-overlay, close buttons use icon-btn - reader.templ and error.templ intentionally left unchanged
545 lines
22 KiB
Templ
545 lines
22 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: var(--surface-overlay);"
|
|
>
|
|
<div
|
|
class="card p-6 w-full max-w-4xl mx-4 my-8"
|
|
>
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h2 class="text-2xl font-bold tracking-tight text-content">Sync Progress</h2>
|
|
<p class="text-sm text-content-muted">{ book.Title }</p>
|
|
</div>
|
|
<button
|
|
@click="hideProgressSyncModal()"
|
|
class="icon-btn"
|
|
>
|
|
@Icon("close", "h-5 w-5")
|
|
</button>
|
|
</div>
|
|
if book.ActiveConflict != nil {
|
|
<div
|
|
class="mb-6 p-4 rounded-lg border border-warning bg-warning/10"
|
|
>
|
|
<p class="font-semibold mb-2 flex items-center gap-2 text-content">
|
|
@Icon("alert", "h-4 w-4 text-warning")
|
|
Conflict Detected
|
|
</p>
|
|
<p class="text-sm text-content-muted">
|
|
Progress differs between devices. Choose which version to keep.
|
|
</p>
|
|
</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="inline-block px-2 py-1 rounded text-xs font-semibold capitalize mb-2 bg-brand text-white"
|
|
>
|
|
{ data.Source }
|
|
</span>
|
|
<p class="text-xs text-content-muted">
|
|
{ FormatInTimezone(data.Timestamp, user.Timezone) }
|
|
</p>
|
|
</div>
|
|
<div class="text-right">
|
|
if data.Data["percentage"] != nil {
|
|
<div class="text-3xl font-bold text-brand">
|
|
{ fmt.Sprintf("%.1f", data.Data["percentage"].(float64) * 100) }%
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
if data.Data["page"] != nil {
|
|
<div class="text-sm text-content-muted">
|
|
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 text-content-muted">
|
|
CFI: { data.Data["epubcfi"].(string) }
|
|
</div>
|
|
}
|
|
<div class="mt-3">
|
|
<button
|
|
@click={"resolveConflict('" + book.ActiveConflict.ID + "', '" + source + "')"}
|
|
class="btn btn-primary text-sm"
|
|
>
|
|
Keep This
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
} else if book.ReadingProgress != nil {
|
|
<div class="mb-4 text-content-muted">
|
|
<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 text-brand">
|
|
{ fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64 * 100) }%
|
|
</div>
|
|
<p class="capitalize text-lg mb-2 text-content">
|
|
{ book.ReadingProgress.LastSyncSource.String }
|
|
</p>
|
|
if book.ReadingProgress.CurrentPage.Valid && book.ReadingProgress.TotalPages.Valid {
|
|
<p class="text-content-muted">
|
|
Page { book.ReadingProgress.CurrentPage.Int32 } of { book.ReadingProgress.TotalPages.Int32 }
|
|
</p>
|
|
}
|
|
if book.ReadingProgress.LastReadAt.Valid {
|
|
<p class="text-sm mt-2 text-content-muted">
|
|
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"
|
|
style="background-color: var(--surface-overlay);"
|
|
>
|
|
<div
|
|
class="card p-8 w-full max-w-2xl mx-4 text-center"
|
|
>
|
|
<div class="mb-4 flex justify-center">
|
|
@Icon("edit", "h-12 w-12 text-content-muted")
|
|
</div>
|
|
<h2 class="text-2xl font-bold tracking-tight text-content mb-2">Notes & Highlights</h2>
|
|
<p class="mb-2 text-content-muted">
|
|
This book has <strong>{ book.NotesCount }</strong> notes and <strong>{ book.HighlightsCount }</strong> highlights.
|
|
</p>
|
|
<p class="mb-6 text-content-muted">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"
|
|
style="background-color: var(--surface-overlay);"
|
|
>
|
|
<div
|
|
class="card w-full max-w-5xl mx-4 my-8 flex flex-col max-h-[90vh]"
|
|
>
|
|
<div class="flex justify-between items-center p-6 border-b border-line flex-shrink-0">
|
|
<h2 class="text-2xl font-bold tracking-tight text-content">Edit Metadata</h2>
|
|
<button
|
|
@click="hideMetadataEditor()"
|
|
class="icon-btn"
|
|
>
|
|
@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">
|
|
<!-- 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 bg-surface"
|
|
@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 text-sm"
|
|
@click="generateCover()"
|
|
x-show="coverGenerating"
|
|
x-cloak
|
|
disabled
|
|
>
|
|
Generating...
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary w-full text-sm"
|
|
@click="generateCover()"
|
|
x-show="!coverGenerating"
|
|
x-cloak
|
|
>
|
|
Generate Cover
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary w-full text-sm"
|
|
@click="removeCover()"
|
|
x-show="hasExistingCover || newCoverPreview"
|
|
x-cloak
|
|
>
|
|
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 border-line">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center text-content bg-surface"
|
|
@click="toggleSection('basic')">
|
|
<span class="font-semibold">Basic Info</span>
|
|
<span x-text="openSections.basic ? '▾' : '▸'">▸</span>
|
|
</button>
|
|
<div x-show="openSections.basic" x-cloak x-transition class="p-4 space-y-3 bg-surface">
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Title</label>
|
|
<input type="text" name="title" value={ book.Title }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Author</label>
|
|
<input type="text" name="author" value={ textToString(book.Author) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Description</label>
|
|
<textarea name="description" rows="3"
|
|
class="input text-sm"
|
|
>{ textToString(book.Description) }</textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Summary</label>
|
|
<textarea name="summary" rows="2"
|
|
class="input text-sm"
|
|
>{ textToString(book.Summary) }</textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Tags</label>
|
|
<div class="flex flex-wrap gap-1.5 mb-2">
|
|
<template x-for="(tag, idx) in editorTags" :key="idx">
|
|
<span
|
|
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-brand text-white"
|
|
>
|
|
<span x-text="tag"></span>
|
|
<button type="button" class="hover:opacity-70 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 text-sm" />
|
|
<div
|
|
x-show="showTagDropdown"
|
|
x-cloak
|
|
x-transition
|
|
class="mt-1 w-full rounded-lg shadow-lg border border-line bg-surface max-h-48 overflow-y-auto"
|
|
>
|
|
<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"
|
|
:class="highlightedTagIndex === tagSuggestions.indexOf(sug) ? 'opacity-80' : ''"
|
|
:style="highlightedTagIndex === tagSuggestions.indexOf(sug) ? 'background-color: var(--bg-secondary); color: var(--text-primary);' : 'color: var(--text-primary);'"
|
|
@click="selectEditorTagSuggestion(sug.value)"
|
|
>
|
|
<span x-text="sug.value"></span>
|
|
<span class="text-xs text-content-muted" x-text="sug.count + ' books'"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">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 text-sm" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Publication -->
|
|
<div class="border rounded-lg border-line">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center text-content bg-surface"
|
|
@click="toggleSection('publication')">
|
|
<span class="font-semibold">Publication</span>
|
|
<span x-text="openSections.publication ? '▾' : '▸'">▸</span>
|
|
</button>
|
|
<div x-show="openSections.publication" x-cloak x-transition class="p-4 space-y-3 bg-surface">
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Publisher</label>
|
|
<input type="text" name="publisher" value={ textToString(book.Publisher) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Date Published</label>
|
|
<input type="date" name="date_published"
|
|
value={ formatDateForInput(book.DatePublished) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Edition</label>
|
|
<input type="text" name="edition" value={ textToString(book.Edition) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Language</label>
|
|
<input type="text" name="language" value={ textToString(book.Language) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Genre</label>
|
|
<input type="text" name="genre" value={ textToString(book.Genre) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Copyright Year</label>
|
|
<input type="number" name="copyright_year"
|
|
value={ fmt.Sprintf("%d", book.CopyrightYear.Int32) }
|
|
class="input text-sm" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Series -->
|
|
<div class="border rounded-lg border-line">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center text-content bg-surface"
|
|
@click="toggleSection('series')">
|
|
<span class="font-semibold">Series</span>
|
|
<span x-text="openSections.series ? '▾' : '▸'">▸</span>
|
|
</button>
|
|
<div x-show="openSections.series" x-cloak x-transition class="p-4 space-y-3 bg-surface">
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Series</label>
|
|
<input type="text" name="series" value={ textToString(book.Series) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div class="grid grid-cols-3 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Number</label>
|
|
<input type="number" name="series_number"
|
|
value={ fmt.Sprintf("%d", book.SeriesNumber.Int32) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Count</label>
|
|
<input type="number" name="series_count"
|
|
value={ fmt.Sprintf("%d", book.SeriesCount.Int32) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Volume</label>
|
|
<input type="number" name="volume"
|
|
value={ fmt.Sprintf("%d", book.Volume.Int32) }
|
|
class="input text-sm" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Identifiers -->
|
|
<div class="border rounded-lg border-line">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center text-content bg-surface"
|
|
@click="toggleSection('identifiers')">
|
|
<span class="font-semibold">Identifiers</span>
|
|
<span x-text="openSections.identifiers ? '▾' : '▸'">▸</span>
|
|
</button>
|
|
<div x-show="openSections.identifiers" x-cloak x-transition class="p-4 space-y-3 bg-surface">
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">ISBN</label>
|
|
<input type="text" name="isbn" value={ textToString(book.Isbn) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">ASIN</label>
|
|
<input type="text" name="asin" value={ textToString(book.Asin) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Goodreads ID</label>
|
|
<input type="text" name="goodreads_id" value={ textToString(book.GoodreadsID) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">OpenLibrary ID</label>
|
|
<input type="text" name="openlibrary_id" value={ textToString(book.OpenlibraryID) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Google Books ID</label>
|
|
<input type="text" name="google_books_id" value={ textToString(book.GoogleBooksID) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Web URL</label>
|
|
<input type="url" name="web_url" value={ textToString(book.WebUrl) }
|
|
class="input text-sm" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Comic/Manga -->
|
|
<div class="border rounded-lg border-line">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center text-content bg-surface"
|
|
@click="toggleSection('comic')">
|
|
<span class="font-semibold">Comic/Manga</span>
|
|
<span x-text="openSections.comic ? '▾' : '▸'">▸</span>
|
|
</button>
|
|
<div x-show="openSections.comic" x-cloak x-transition class="p-4 space-y-3 bg-surface">
|
|
<div>
|
|
<label for="manga_type" class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Manga Type</label>
|
|
<select id="manga_type" name="manga_type"
|
|
class="input text-sm">
|
|
<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 text-content-muted mb-1">Reading Direction</label>
|
|
<select id="reading_direction" name="reading_direction"
|
|
class="input text-sm">
|
|
<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 text-content-muted mb-1">Age Rating</label>
|
|
<input type="text" name="age_rating" value={ textToString(book.AgeRating) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Story Arc</label>
|
|
<input type="text" name="story_arc" value={ textToString(book.StoryArc) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Imprint</label>
|
|
<input type="text" name="imprint" value={ textToString(book.Imprint) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Scan Information</label>
|
|
<input type="text" name="scan_information" value={ textToString(book.ScanInformation) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Metadata Notes</label>
|
|
<textarea name="metadata_notes" rows="2"
|
|
class="input text-sm"
|
|
>{ 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 text-content-muted">Black & White</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Technical -->
|
|
<div class="border rounded-lg border-line">
|
|
<button type="button" class="w-full px-4 py-3 flex justify-between items-center text-content bg-surface"
|
|
@click="toggleSection('technical')">
|
|
<span class="font-semibold">Technical</span>
|
|
<span x-text="openSections.technical ? '▾' : '▸'">▸</span>
|
|
</button>
|
|
<div x-show="openSections.technical" x-cloak x-transition class="p-4 space-y-3 bg-surface">
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Page Count</label>
|
|
<input type="number" name="page_count"
|
|
value={ fmt.Sprintf("%d", book.PageCount.Int32) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Contributors (comma-separated)</label>
|
|
<input type="text" name="contributors" value={ stringSliceToString(book.Contributors) }
|
|
class="input text-sm" />
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Format</label>
|
|
<input type="text" value={ textToString(book.MimeType) } readonly
|
|
class="input text-sm opacity-60" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">File Size</label>
|
|
<input type="text" value={ fmt.Sprintf("%.1f MB", float64(book.FileSize.Int64)/1024/1024) } readonly
|
|
class="input text-sm opacity-60" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end space-x-3 p-6 border-t border-line flex-shrink-0">
|
|
<button
|
|
@click="hideMetadataEditor()"
|
|
class="btn btn-secondary"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
@click="saveMetadata()"
|
|
class="btn btn-primary"
|
|
>
|
|
Save
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|