From cddfe93c5c219ca7d09d4ff26585a5ac286af9b6 Mon Sep 17 00:00:00 2001 From: Dorian Date: Tue, 3 Mar 2026 07:21:45 +0000 Subject: [PATCH] style(content): add distinct placeholder covers per content type Each content type now has a unique fallback cover with fitting typography: - Books: Georgia serif italic with spine detail - Films: Helvetica Neue with separator line - TV Series: SF Pro Display (new dedicated generator) - Music: system-ui with vinyl record motif - Podcasts: system-ui with microphone icon Co-Authored-By: Claude Opus 4.6 --- .../src/components/content/TVSeriesCard.vue | 4 +- .../src/components/content/TVSeriesDetail.vue | 4 +- .../src/components/content/TVSeriesGrid.vue | 4 +- .../app/src/composables/useImageFallback.ts | 64 +++++++++++++------ 4 files changed, 50 insertions(+), 26 deletions(-) diff --git a/packages/app/src/components/content/TVSeriesCard.vue b/packages/app/src/components/content/TVSeriesCard.vue index eb6260d1..5e445053 100644 --- a/packages/app/src/components/content/TVSeriesCard.vue +++ b/packages/app/src/components/content/TVSeriesCard.vue @@ -64,7 +64,7 @@ import { ref, computed, onMounted } from 'vue' import type { TVSeries } from '@aiui/core/types/content' import { useTheme } from '@/composables/useTheme' -import { generatePosterFallback, fetchTmdbTVPoster } from '@/composables/useImageFallback' +import { generateTVSeriesFallback, fetchTmdbTVPoster } from '@/composables/useImageFallback' const props = defineProps<{ series: TVSeries }>() defineEmits<{ select: [series: TVSeries] }>() @@ -79,7 +79,7 @@ const posterSrc = computed(() => { }) const fallbackPoster = computed(() => - generatePosterFallback(props.series.title, props.series.year) + generateTVSeriesFallback(props.series.title, props.series.year) ) const yearDisplay = computed(() => { diff --git a/packages/app/src/components/content/TVSeriesDetail.vue b/packages/app/src/components/content/TVSeriesDetail.vue index 185d1ee5..9e92d174 100644 --- a/packages/app/src/components/content/TVSeriesDetail.vue +++ b/packages/app/src/components/content/TVSeriesDetail.vue @@ -127,7 +127,7 @@ import { ref, computed, onMounted } from 'vue' import type { TVSeries } from '@aiui/core/types/content' import { useTheme } from '@/composables/useTheme' -import { generatePosterFallback, fetchTmdbTVPoster } from '@/composables/useImageFallback' +import { generateTVSeriesFallback, fetchTmdbTVPoster } from '@/composables/useImageFallback' const props = defineProps<{ series: TVSeries }>() defineEmits<{ back: [] }>() @@ -142,7 +142,7 @@ const coverSrc = computed(() => { }) const fallbackCover = computed(() => - generatePosterFallback(props.series.title, props.series.year) + generateTVSeriesFallback(props.series.title, props.series.year) ) const yearDisplay = computed(() => { diff --git a/packages/app/src/components/content/TVSeriesGrid.vue b/packages/app/src/components/content/TVSeriesGrid.vue index 619eff14..d1790c53 100644 --- a/packages/app/src/components/content/TVSeriesGrid.vue +++ b/packages/app/src/components/content/TVSeriesGrid.vue @@ -116,7 +116,7 @@ import { ref, computed, reactive, onMounted, watch } from 'vue' import type { TVSeries } from '@aiui/core/types/content' import { useTheme } from '@/composables/useTheme' -import { generatePosterFallback, fetchTmdbTVPoster } from '@/composables/useImageFallback' +import { generateTVSeriesFallback, fetchTmdbTVPoster } from '@/composables/useImageFallback' const props = withDefaults(defineProps<{ series: TVSeries[] @@ -139,7 +139,7 @@ function coverSrc(s: TVSeries): string | null { } function fallbackFor(s: TVSeries): string { - return generatePosterFallback(s.title, s.year) + return generateTVSeriesFallback(s.title, s.year) } function onCoverError(s: TVSeries) { diff --git a/packages/app/src/composables/useImageFallback.ts b/packages/app/src/composables/useImageFallback.ts index 1e216612..0cb69f0c 100644 --- a/packages/app/src/composables/useImageFallback.ts +++ b/packages/app/src/composables/useImageFallback.ts @@ -79,12 +79,13 @@ loadPodcastCache() const MICROPHONE_PATH = 'M45.04,95.45v24.11c0,1.83-1.49,3.32-3.32,3.32c-1.83,0-3.32-1.49-3.32-3.32V95.45c-10.16-0.81-19.32-5.3-26.14-12.12C4.69,75.77,0,65.34,0,53.87c0-1.83,1.49-3.32,3.32-3.32s3.32,1.49,3.32,3.32c0,9.64,3.95,18.41,10.31,24.77c6.36,6.36,15.13,10.31,24.77,10.31h0c9.64,0,18.41-3.95,24.77-10.31c6.36-6.36,10.31-15.13,10.31-24.77c0-1.83,1.49-3.32,3.32-3.32s3.32,1.49,3.32,3.32c0,11.48-4.69,21.91-12.25,29.47C64.36,90.16,55.2,94.64,45.04,95.45z M41.94,0c6.38,0,12.18,2.61,16.38,6.81c4.2,4.2,6.81,10,6.81,16.38v30c0,6.38-2.61,12.18-6.81,16.38c-4.2,4.2-10,6.81-16.38,6.81s-12.18-2.61-16.38-6.81c-4.2-4.2-6.81-10-6.81-16.38v-30c0-6.38,2.61-12.18,6.81-16.38C29.76,2.61,35.56,0,41.94,0z M53.62,11.51c-3-3-7.14-4.86-11.68-4.86c-4.55,0-8.68,1.86-11.68,4.86c-3,3-4.86,7.14-4.86,11.68v30c0,4.55,1.86,8.68,4.86,11.68c3,3,7.14,4.86,11.68,4.86c4.55,0,8.68-1.86,11.68-4.86c3-3,4.86-7.14,4.86-11.68v-30C58.49,18.64,56.62,14.51,53.62,11.51z' -export function generatePodcastCoverFallback(_title: string, _host?: string): string { - const hue = 220 +export function generatePodcastCoverFallback(title: string, _host?: string): string { + const hue = [...title].reduce((acc, c) => acc + c.charCodeAt(0), 0) % 360 const svg = ` - - - + + + PODCAST + ` @@ -94,24 +95,45 @@ export function generatePodcastCoverFallback(_title: string, _host?: string): st export function generateSongCoverFallback(title: string, artist?: string): string { const hue = [...(title + (artist ?? ''))].reduce((acc, c) => acc + c.charCodeAt(0), 0) % 360 const svg = ` - - - - ${escapeXml(title.length > 12 ? title.slice(0, 10) + '…' : title)} - ${artist ? `${escapeXml(artist.length > 14 ? artist.slice(0, 12) + '…' : artist)}` : ''} + + + MUSIC + + + ${escapeXml(title.length > 18 ? title.slice(0, 16) + '…' : title)} + ${artist ? `${escapeXml(artist.length > 20 ? artist.slice(0, 18) + '…' : artist)}` : ''} ` return `data:image/svg+xml,${encodeURIComponent(svg)}` } +/** Film poster fallback — cinematic sans-serif */ export function generatePosterFallback(title: string, year?: number): string { const hue = [...title].reduce((acc, c) => acc + c.charCodeAt(0), 0) % 360 const svg = ` - - - + + + FILM + + ${escapeXml(title.length > 20 ? title.slice(0, 18) + '…' : title)} - ${year ? `${year}` : ''} + ${year ? `${year}` : ''} + ` + return `data:image/svg+xml,${encodeURIComponent(svg)}` +} + +/** TV Series poster fallback — modern grotesque */ +export function generateTVSeriesFallback(title: string, year?: number): string { + const hue = [...title].reduce((acc, c) => acc + c.charCodeAt(0), 0) % 360 + const svg = ` + + + SERIES + + + ${escapeXml(title.length > 20 ? title.slice(0, 18) + '…' : title)} + + ${year ? `${year}` : ''} ` return `data:image/svg+xml,${encodeURIComponent(svg)}` } @@ -255,17 +277,19 @@ function saveBookCache(): void { loadBookCache() +/** Book cover fallback — elegant serif with spine detail */ export function generateBookCoverFallback(title: string, author?: string): string { const hue = [...(title + (author ?? ''))].reduce((acc, c) => acc + c.charCodeAt(0), 0) % 360 const svg = ` - - - - - + + + + BOOK + + ${escapeXml(title.length > 22 ? title.slice(0, 20) + '…' : title)} - ${author ? `${escapeXml(author.length > 26 ? author.slice(0, 24) + '…' : author)}` : ''} + ${author ? `${escapeXml(author.length > 26 ? author.slice(0, 24) + '…' : author)}` : ''} ` return `data:image/svg+xml,${encodeURIComponent(svg)}` }