mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
fix(books): fix rating persistence and overhaul BookListRow UX
This commit is contained in:
@@ -38,7 +38,6 @@ const secondaryFiles = computed(() => props.book.files.filter((f) => f !== prima
|
||||
const metaLine = computed(() => {
|
||||
const parts: string[] = []
|
||||
if (props.book.publishedYear) parts.push(String(props.book.publishedYear))
|
||||
if (props.book.pageCount) parts.push(`${props.book.pageCount}p`)
|
||||
if (props.book.language) parts.push(props.book.language.toUpperCase())
|
||||
return parts.length > 0 ? parts.join(' · ') : null
|
||||
})
|
||||
@@ -56,7 +55,7 @@ async function setRating(star: number) {
|
||||
const newRating = localRating.value === star ? null : star
|
||||
localRating.value = newRating
|
||||
emit('rating-change', newRating)
|
||||
await api(`/api/books/${props.book.id}/rating`, { method: 'PATCH', body: JSON.stringify({ rating: newRating }) })
|
||||
await api(`/api/books/${props.book.id}/rating`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rating: newRating }) })
|
||||
}
|
||||
|
||||
const { coverUrl } = useCoverVersions()
|
||||
@@ -73,13 +72,12 @@ function openFile(file: BookFileRef) {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center gap-3 py-2 px-2 rounded-md transition-colors"
|
||||
class="flex items-center gap-3 py-3 px-2 rounded-md transition-colors cursor-pointer hover:bg-muted/50"
|
||||
:class="[
|
||||
selectionMode || (primaryFile && !isMissing) ? 'cursor-pointer hover:bg-muted/50' : 'cursor-default',
|
||||
isMissing ? 'opacity-60 grayscale' : '',
|
||||
selectionMode ? 'cursor-pointer' : '',
|
||||
selected ? 'bg-primary/8 ring-1 ring-primary/30' : '',
|
||||
]"
|
||||
@click="selectionMode ? emit('select') : primaryFile && !isMissing && openFile(primaryFile)"
|
||||
@click="selectionMode ? emit('select') : emit('action', 'quick-view')"
|
||||
>
|
||||
<!-- Selection checkbox -->
|
||||
<div
|
||||
@@ -91,7 +89,11 @@ function openFile(file: BookFileRef) {
|
||||
</div>
|
||||
|
||||
<!-- Cover -->
|
||||
<div class="h-16 w-12 rounded shrink-0 overflow-hidden relative" :style="coverLoaded ? {} : coverStyle">
|
||||
<div
|
||||
class="h-24 w-16 rounded shrink-0 overflow-hidden relative"
|
||||
:class="isMissing ? 'opacity-50 grayscale' : ''"
|
||||
:style="coverLoaded ? {} : coverStyle"
|
||||
>
|
||||
<img
|
||||
v-if="!coverFailed"
|
||||
:src="coverSrc"
|
||||
@@ -106,22 +108,18 @@ function openFile(file: BookFileRef) {
|
||||
|
||||
<!-- Main info -->
|
||||
<div class="flex flex-col min-w-0 flex-1 gap-0.5">
|
||||
<span class="text-sm font-medium text-foreground truncate leading-snug">{{ book.title ?? '-' }}</span>
|
||||
<span class="text-sm font-medium text-foreground truncate leading-snug" :class="isMissing ? 'opacity-60' : ''">{{ book.title ?? '-' }}</span>
|
||||
<span v-if="authorLine" class="text-xs text-muted-foreground truncate">{{ authorLine }}</span>
|
||||
<span v-if="seriesLine" class="text-xs text-muted-foreground/70 truncate italic">{{ seriesLine }}</span>
|
||||
<span v-if="metaLine" class="text-xs text-muted-foreground/60 truncate">{{ metaLine }}</span>
|
||||
<!-- Tags -->
|
||||
<span v-if="seriesLine" class="text-xs text-muted-foreground truncate italic">{{ seriesLine }}</span>
|
||||
<span v-if="metaLine" class="text-xs text-muted-foreground truncate">{{ metaLine }}</span>
|
||||
<div v-if="visibleTags.length > 0" class="flex items-center gap-1 flex-wrap">
|
||||
<span v-for="tag in visibleTags" :key="tag" class="text-[10px] px-1.5 py-0 rounded-full bg-muted text-muted-foreground leading-5">{{
|
||||
tag
|
||||
}}</span>
|
||||
<span v-for="tag in visibleTags" :key="tag" class="text-[11px] px-1.5 py-0 rounded-full bg-muted text-muted-foreground leading-5">{{ tag }}</span>
|
||||
</div>
|
||||
<!-- Reading progress -->
|
||||
<div v-if="book.readingProgress != null && book.readingProgress > 0" class="flex items-center gap-1.5 mt-0.5">
|
||||
<div class="h-1 flex-1 rounded-full bg-muted overflow-hidden max-w-24">
|
||||
<div v-if="book.readingProgress != null && book.readingProgress > 0" class="mt-1">
|
||||
<div class="h-1 w-24 rounded-full bg-muted overflow-hidden">
|
||||
<div class="h-full rounded-full bg-primary/60 transition-all" :style="{ width: `${book.readingProgress}%` }" />
|
||||
</div>
|
||||
<span class="text-[10px] text-muted-foreground/70 tabular-nums">{{ Math.round(book.readingProgress) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -169,25 +167,6 @@ function openFile(file: BookFileRef) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Open button -->
|
||||
<button
|
||||
v-if="primaryFile && !isMissing"
|
||||
class="p-1 rounded hover:bg-muted transition-colors text-muted-foreground hover:text-foreground"
|
||||
title="Open"
|
||||
@click="openFile(primaryFile)"
|
||||
>
|
||||
<BookOpen class="size-4" />
|
||||
</button>
|
||||
|
||||
<!-- Quick view (hidden on mobile) -->
|
||||
<button
|
||||
class="hidden sm:flex p-1 rounded hover:bg-muted transition-colors text-muted-foreground hover:text-foreground"
|
||||
title="Quick view"
|
||||
@click="emit('action', 'quick-view')"
|
||||
>
|
||||
<PanelRight class="size-4" />
|
||||
</button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<button class="p-1 rounded hover:bg-muted transition-colors text-muted-foreground hover:text-foreground">
|
||||
@@ -195,10 +174,14 @@ function openFile(file: BookFileRef) {
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem @click="primaryFile && openFile(primaryFile)">
|
||||
<DropdownMenuItem :disabled="!primaryFile || isMissing" @click="primaryFile && !isMissing && openFile(primaryFile)">
|
||||
<BookOpen class="size-4 mr-2" />
|
||||
Open
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="emit('action', 'quick-view')">
|
||||
<PanelRight class="size-4 mr-2" />
|
||||
Quick View
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="router.push({ name: 'book-detail', params: { bookId: book.id } })">
|
||||
<ExternalLink class="size-4 mr-2" />
|
||||
Book Details
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { Settings2, Trash2 } from 'lucide-vue-next'
|
||||
import BookCoverCard from '@/features/book/components/BookCoverCard.vue'
|
||||
import BookListRow from '@/features/book/components/BookListRow.vue'
|
||||
import BookQuickView from '@/features/book/components/BookQuickView.vue'
|
||||
import AppHeader from '@/components/AppHeader.vue'
|
||||
import ViewHeader from '@/components/ViewHeader.vue'
|
||||
import AppSidebar from '@/components/AppSidebar.vue'
|
||||
@@ -17,7 +18,7 @@ import { useDisplaySettings } from '@/composables/useDisplaySettings'
|
||||
import { useBookSelection } from '@/features/book/composables/useBookSelection'
|
||||
import { BACKGROUND_OPTIONS, useThemeStore } from '@/stores/theme'
|
||||
import { FIELD_LABELS, ruleToLabel } from '@/features/book/lib/filter-labels'
|
||||
import type { GroupRule } from '@projectx/types'
|
||||
import type { BookCard, GroupRule } from '@projectx/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -65,6 +66,27 @@ function toggleSelectionMode() {
|
||||
|
||||
const addToCollectionOpen = ref(false)
|
||||
|
||||
type BookActionType = 'quick-view' | 'edit-metadata' | 'add-to-collection' | 'delete'
|
||||
|
||||
const quickViewBookId = ref<number | null>(null)
|
||||
const quickViewOpen = ref(false)
|
||||
|
||||
function handleBookAction(book: BookCard, action: BookActionType) {
|
||||
if (action === 'quick-view') {
|
||||
quickViewBookId.value = book.id
|
||||
quickViewOpen.value = true
|
||||
return
|
||||
}
|
||||
if (action === 'add-to-collection') {
|
||||
if (!selectionMode.value) {
|
||||
enterSelectionMode()
|
||||
toggleBook(book.id)
|
||||
}
|
||||
addToCollectionOpen.value = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const editorOpen = ref(false)
|
||||
const confirmDelete = ref(false)
|
||||
const deleting = ref(false)
|
||||
@@ -125,6 +147,13 @@ watch(loading, (isLoading) => {
|
||||
<template>
|
||||
<LensEditorPanel :open="editorOpen" :lens="lens" @close="editorOpen = false" @saved="onSaved" />
|
||||
|
||||
<BookQuickView
|
||||
:book-id="quickViewBookId"
|
||||
:open="quickViewOpen"
|
||||
@update:open="quickViewOpen = $event"
|
||||
@action="quickViewBookId !== null && handleBookAction({ id: quickViewBookId } as BookCard, $event)"
|
||||
/>
|
||||
|
||||
<SelectionActionBar
|
||||
:visible="selectionMode"
|
||||
:count="selectedCount"
|
||||
@@ -252,6 +281,7 @@ watch(loading, (isLoading) => {
|
||||
:selection-mode="selectionMode"
|
||||
:selected="isSelected(book.id)"
|
||||
@select="toggleBook(book.id)"
|
||||
@action="handleBookAction(book, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ export type BookCard = {
|
||||
seriesIndex: number | null;
|
||||
files: BookFileRef[];
|
||||
publishedYear: number | null;
|
||||
pageCount: number | null;
|
||||
language: string | null;
|
||||
tags: string[];
|
||||
rating: number | null;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1770373997889,
|
||||
"when": 1770383082812,
|
||||
"tag": "0000_add_rating_and_book_card_fields",
|
||||
"breakpoints": true
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ export class BookRepository {
|
||||
seriesName: bookMetadata.seriesName,
|
||||
seriesIndex: bookMetadata.seriesIndex,
|
||||
publishedYear: bookMetadata.publishedYear,
|
||||
pageCount: bookMetadata.pageCount,
|
||||
language: bookMetadata.language,
|
||||
rating: bookMetadata.rating,
|
||||
})
|
||||
|
||||
@@ -51,7 +51,6 @@ export class BookService {
|
||||
seriesName: string | null;
|
||||
seriesIndex: number | null;
|
||||
publishedYear: number | null;
|
||||
pageCount: number | null;
|
||||
language: string | null;
|
||||
rating: number | null;
|
||||
}[],
|
||||
@@ -100,7 +99,6 @@ export class BookService {
|
||||
authors: authorsByBook.get(row.id) ?? [],
|
||||
files,
|
||||
publishedYear: row.publishedYear ?? null,
|
||||
pageCount: row.pageCount ?? null,
|
||||
language: row.language ?? null,
|
||||
tags: tagsByBook.get(row.id) ?? [],
|
||||
rating: row.rating ?? null,
|
||||
|
||||
@@ -13,7 +13,6 @@ export class BookCardDto {
|
||||
seriesIndex: number | null;
|
||||
files: BookFileRefDto[];
|
||||
publishedYear: number | null;
|
||||
pageCount: number | null;
|
||||
language: string | null;
|
||||
tags: string[];
|
||||
rating: number | null;
|
||||
|
||||
Reference in New Issue
Block a user