mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
feat(books): support details preference in cover card overlay
When the user preference for thumbnail clicks is set to 'details', the primary overlay button on book cards now routes to the book details page and displays a BookText icon instead of the reader icon. Also replaced the ExternalLink icon with BookText across dropdown menus and settings for better visual consistency.
This commit is contained in:
@@ -6,10 +6,10 @@ import { computed, inject, ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
BookOpen,
|
||||
BookText,
|
||||
Check,
|
||||
Download,
|
||||
Eye,
|
||||
ExternalLink,
|
||||
FolderPlus,
|
||||
Image,
|
||||
Lock,
|
||||
@@ -255,6 +255,21 @@ function handleSecondaryLabelClick(event: MouseEvent) {
|
||||
}
|
||||
|
||||
const isPrimaryClickAvailable = computed(() => thumbnailClickAction.value === 'details' || (primaryFile.value != null && !isMissing.value))
|
||||
const showPrimaryOverlayAction = computed(() => thumbnailClickAction.value === 'details' || (primaryFile.value != null && !isMissing.value))
|
||||
const primaryOverlayActionIcon = computed(() => {
|
||||
if (thumbnailClickAction.value === 'details') return BookText
|
||||
return isAudiobook.value ? Play : BookOpen
|
||||
})
|
||||
const primaryOverlayActionIconClass = computed(() => (thumbnailClickAction.value !== 'details' && isAudiobook.value ? 'ml-[2cqi]' : ''))
|
||||
|
||||
function handlePrimaryOverlayAction() {
|
||||
if (thumbnailClickAction.value === 'details') {
|
||||
openBookDetails()
|
||||
return
|
||||
}
|
||||
|
||||
if (primaryFile.value && !isMissing.value) openFile(primaryFile.value)
|
||||
}
|
||||
|
||||
function handleCardClick(event: MouseEvent) {
|
||||
const target = event.target
|
||||
@@ -509,15 +524,16 @@ const secondaryLabelText = computed(() => resolveBookLabel(gridCardSecondaryLabe
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Center: Play/Read button -->
|
||||
<!-- Center: primary thumbnail action -->
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<button
|
||||
v-if="primaryFile && !isMissing"
|
||||
v-if="showPrimaryOverlayAction"
|
||||
data-testid="grid-card-primary-action"
|
||||
class="size-[30cqi] flex items-center justify-center rounded-full bg-primary text-white shadow-2xl transition-all duration-300 scale-75 hover:scale-110 active:scale-90"
|
||||
:class="[showMobileOverlay || 'group-hover:scale-100', showMobileOverlay ? 'scale-100' : '']"
|
||||
@click.stop="openFile(primaryFile)"
|
||||
@click.stop="handlePrimaryOverlayAction"
|
||||
>
|
||||
<component :is="isAudiobook ? Play : BookOpen" class="size-[16cqi]" :class="{ 'ml-[2cqi]': isAudiobook }" />
|
||||
<component :is="primaryOverlayActionIcon" class="size-[16cqi]" :class="primaryOverlayActionIconClass" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -593,7 +609,7 @@ const secondaryLabelText = computed(() => resolveBookLabel(gridCardSecondaryLabe
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuItem @click="openBookDetails">
|
||||
<ExternalLink class="size-4 mr-2" />
|
||||
<BookText class="size-4 mr-2" />
|
||||
Book Details
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator v-if="hasPermission('library_edit_metadata')" />
|
||||
@@ -727,7 +743,7 @@ const secondaryLabelText = computed(() => resolveBookLabel(gridCardSecondaryLabe
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuItem @click="openBookDetails">
|
||||
<ExternalLink class="size-4 mr-2" />
|
||||
<BookText class="size-4 mr-2" />
|
||||
Book Details
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator v-if="hasPermission('library_edit_metadata')" />
|
||||
|
||||
@@ -203,6 +203,16 @@ describe('BookCoverCard — present state', () => {
|
||||
expect(routerPushMock).not.toHaveBeenCalledWith(expect.objectContaining({ name: 'reader' }))
|
||||
})
|
||||
|
||||
it('opens book details from the primary overlay action when thumbnail clicks prefer details', async () => {
|
||||
thumbnailClickAction.value = 'details'
|
||||
const wrapper = mountCard(presentBook)
|
||||
|
||||
await wrapper.get('[data-testid="grid-card-primary-action"]').trigger('click')
|
||||
|
||||
expect(routerPushMock).toHaveBeenCalledWith({ name: 'book-detail', params: { bookId: 2 } })
|
||||
expect(routerPushMock).not.toHaveBeenCalledWith(expect.objectContaining({ name: 'reader' }))
|
||||
})
|
||||
|
||||
it('opens book details for missing books when thumbnail clicks prefer details', async () => {
|
||||
thumbnailClickAction.value = 'details'
|
||||
const wrapper = mountCard(missingBook)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { BookOpen, ExternalLink } from 'lucide-vue-next'
|
||||
import { BookOpen, BookText } from 'lucide-vue-next'
|
||||
import ToggleSwitch from '@/components/ui/ToggleSwitch.vue'
|
||||
import { useDisplaySettings, type BookThumbnailClickAction } from '@/composables/useDisplaySettings'
|
||||
import { useSeriesCollapsePreference } from '@/features/book/composables/useSeriesCollapsePreference'
|
||||
@@ -12,7 +12,7 @@ const globalCollapseEnabled = computed(() => prefs.value?.global ?? false)
|
||||
|
||||
const THUMBNAIL_CLICK_OPTIONS: { id: BookThumbnailClickAction; label: string; hint: string; icon: typeof BookOpen }[] = [
|
||||
{ id: 'reader', label: 'Read first', hint: 'Open the reader when a readable file exists', icon: BookOpen },
|
||||
{ id: 'details', label: 'Open details', hint: 'Go to the book details page from grid and list thumbnails', icon: ExternalLink },
|
||||
{ id: 'details', label: 'Open details', hint: 'Go to the book details page from grid and list thumbnails', icon: BookText },
|
||||
]
|
||||
|
||||
async function handleGlobalCollapseToggle(value: boolean) {
|
||||
|
||||
Reference in New Issue
Block a user