diff --git a/client/src/composables/useViewDisplaySettings.ts b/client/src/composables/useViewDisplaySettings.ts new file mode 100644 index 000000000..7dd0fda31 --- /dev/null +++ b/client/src/composables/useViewDisplaySettings.ts @@ -0,0 +1,44 @@ +import { ref, watch, type Ref } from 'vue' +import { storage } from '@/services/storage' + +type ViewType = 'library' | 'lens' | 'collection' + +function coverSizeKey(type: ViewType, id: number) { + return `projectx:coverSize:${type}:${id}` +} + +function gridGapKey(type: ViewType, id: number) { + return `projectx:gridGap:${type}:${id}` +} + +export function useViewDisplaySettings(viewType: ViewType, viewId: Readonly>) { + const globalCoverSize = storage.get('coverSize', 140) + const globalGridGap = storage.get('gridGap', 20) + + const coverSize = ref(globalCoverSize) + const gridGap = ref(globalGridGap) + + function loadForView(id: number | null) { + if (id === null || !Number.isFinite(id)) { + coverSize.value = globalCoverSize + gridGap.value = globalGridGap + return + } + coverSize.value = storage.get(coverSizeKey(viewType, id), globalCoverSize) + gridGap.value = storage.get(gridGapKey(viewType, id), globalGridGap) + } + + watch(viewId, loadForView, { immediate: true }) + + watch(coverSize, (v) => { + const id = viewId.value + if (id !== null && Number.isFinite(id)) storage.set(coverSizeKey(viewType, id), v) + }) + + watch(gridGap, (v) => { + const id = viewId.value + if (id !== null && Number.isFinite(id)) storage.set(gridGapKey(viewType, id), v) + }) + + return { coverSize, gridGap } +} diff --git a/client/src/features/book/components/BookCoverCard.vue b/client/src/features/book/components/BookCoverCard.vue index 44ee76be1..a25f63985 100644 --- a/client/src/features/book/components/BookCoverCard.vue +++ b/client/src/features/book/components/BookCoverCard.vue @@ -3,7 +3,7 @@ import type { BookCard, BookFileRef } from '@projectx/types' import { FORMAT_TO_GROUP, READER_OPENABLE_FORMATS } from '@projectx/types' import { bookCoverStyle } from '../lib/book-cover' import { getFormatColor } from '../lib/format-colors' -import { computed, ref, watch } from 'vue' +import { computed, inject, ref, watch, type ComponentPublicInstance } from 'vue' import { useRouter } from 'vue-router' import { BookOpen, @@ -40,6 +40,7 @@ import { useRefreshMetadata } from '../composables/useRefreshMetadata' import { useRefreshingBooks } from '../composables/useRefreshingBooks' import { usePermissions } from '@/features/auth/composables/usePermissions' import { useDisplaySettings } from '@/composables/useDisplaySettings' +import { COVER_ASPECT_RATIO_KEY, DEFAULT_COVER_ASPECT_RATIO } from '../lib/cover-aspect-ratio' import { useBookDownload } from '@/features/book/composables/useBookDownload' import SendBookDialog from '@/features/email/components/SendBookDialog.vue' @@ -104,6 +105,7 @@ async function reExtractCover() { } const { hasPermission } = usePermissions() const { cardOverlays } = useDisplaySettings() +const coverAspectRatio = inject(COVER_ASPECT_RATIO_KEY, ref(DEFAULT_COVER_ASPECT_RATIO)) const showSendDialog = ref(false) const showSeriesOverlay = computed(() => cardOverlays.value.includes('series') && seriesLine.value != null) @@ -136,6 +138,11 @@ watch(coverSrc, () => { coverFailed.value = false }) +function onMainImgRef(el: Element | ComponentPublicInstance | null) { + const img = el as HTMLImageElement | null + if (img?.complete && img.naturalWidth > 0) coverLoaded.value = true +} + function openFile(file: BookFileRef) { router.push({ name: 'reader', @@ -153,6 +160,10 @@ function handleCardClick(event: MouseEvent) { if (primaryFile.value && !isMissing.value) openFile(primaryFile.value) } +function openQuickView() { + emit('action', 'quick-view') +} + function openAuthorBrowse() { if (!authorQuery.value) return void router.push({ name: 'authors', query: { q: authorQuery.value } }) @@ -202,16 +213,26 @@ async function handleSetStatus(status: ReadStatus) {
+ + +
- -
- -
+
+ -
+

{{ book.title ?? '-' }}

-
diff --git a/client/src/features/book/components/BookListRow.vue b/client/src/features/book/components/BookListRow.vue index 63b0dcea1..030c6694a 100644 --- a/client/src/features/book/components/BookListRow.vue +++ b/client/src/features/book/components/BookListRow.vue @@ -2,7 +2,7 @@ import type { BookCard, BookFileRef } from '@projectx/types' import { bookCoverStyle } from '../lib/book-cover' import { api } from '@/lib/api' -import { computed, ref, watch } from 'vue' +import { computed, inject, ref, watch } from 'vue' import { useRouter } from 'vue-router' import { BookOpen, @@ -22,6 +22,7 @@ import { import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useCoverVersions } from '../composables/useCoverVersions' +import { COVER_ASPECT_RATIO_KEY, DEFAULT_COVER_ASPECT_RATIO } from '../lib/cover-aspect-ratio' import { useRefreshMetadata } from '../composables/useRefreshMetadata' import { usePermissions } from '@/features/auth/composables/usePermissions' import SendBookDialog from '@/features/email/components/SendBookDialog.vue' @@ -93,6 +94,7 @@ watch(coverSrc, () => { }) const { refreshing, refreshWithFeedback } = useRefreshMetadata() +const coverAspectRatio = inject(COVER_ASPECT_RATIO_KEY, ref(DEFAULT_COVER_ASPECT_RATIO)) function openFile(file: BookFileRef) { router.push({ @@ -129,14 +131,23 @@ function openAuthorBrowse() {
+ + -import { computed, ref, watch } from 'vue' +import { computed, inject, ref, watch } from 'vue' import { useRouter } from 'vue-router' -import { BookOpen, ExternalLink, FolderPlus, Pencil, Star, Trash2, X } from 'lucide-vue-next' +import { BookOpen, ExternalLink, FolderPlus, Headphones, Pencil, Star, Trash2, X } from 'lucide-vue-next' import { usePermissions } from '@/features/auth/composables/usePermissions' import { Sheet, SheetContent, SheetTitle, SheetDescription } from '@/components/ui/sheet' import { Skeleton } from '@/components/ui/skeleton' @@ -12,6 +12,8 @@ import { useBookDetail } from '../composables/useBookDetail' import { useCoverVersions } from '../composables/useCoverVersions' import { bookCoverStyle } from '../lib/book-cover' import { getFormatColor } from '../lib/format-colors' +import { FORMAT_TO_GROUP } from '@projectx/types' +import { COVER_ASPECT_RATIO_KEY, DEFAULT_COVER_ASPECT_RATIO } from '../lib/cover-aspect-ratio' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' const props = defineProps<{ bookId: number | null; open: boolean }>() @@ -128,7 +130,13 @@ const providerLinks = computed(() => { const descriptionExpanded = ref(false) const coverLightboxOpen = ref(false) +const coverAspectRatio = inject(COVER_ASPECT_RATIO_KEY, ref(DEFAULT_COVER_ASPECT_RATIO)) + const primaryFile = computed(() => detail.value?.files.find((f) => f.role === 'primary') ?? detail.value?.files[0] ?? null) +const isPrimaryAudio = computed(() => primaryFile.value?.format != null && FORMAT_TO_GROUP[primaryFile.value.format] === 'audio') +const knownFormats = computed(() => [ + ...new Set((detail.value?.files ?? []).filter((f) => f.format && FORMAT_TO_GROUP[f.format]).map((f) => f.format!)), +]) function providerLinkStyle(provider: string) { const color = getProviderColor(provider) @@ -173,14 +181,14 @@ function openDetails() {