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:
neon
2026-06-12 18:40:09 +00:00
parent 5d0b5143fc
commit 4f4189281c
3 changed files with 35 additions and 9 deletions
@@ -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) {