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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e8ebe56d9e
commit
cddfe93c5c
@@ -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(() => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<rect width="200" height="200" fill="hsl(${hue}, 25%, 12%)"/>
|
||||
<rect x="2" y="2" width="196" height="196" rx="8" fill="none" stroke="hsl(${hue}, 30%, 22%)" stroke-width="2"/>
|
||||
<g fill="hsl(${hue}, 40%, 55%)" transform="translate(38 8) scale(1.5)">
|
||||
<rect width="200" height="200" fill="hsl(${hue}, 25%, 10%)"/>
|
||||
<rect x="3" y="3" width="194" height="194" rx="12" fill="none" stroke="hsl(${hue}, 30%, 18%)" stroke-width="1"/>
|
||||
<text x="100" y="78" text-anchor="middle" fill="hsl(${hue}, 30%, 25%)" font-family="system-ui,sans-serif" font-size="10" font-weight="500" letter-spacing="3">PODCAST</text>
|
||||
<g fill="hsl(${hue}, 35%, 40%)" transform="translate(58 82) scale(1.0)">
|
||||
<path d="${MICROPHONE_PATH}"/>
|
||||
</g>
|
||||
</svg>`
|
||||
@@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<rect width="200" height="200" fill="hsl(${hue}, 30%, 14%)"/>
|
||||
<rect x="2" y="2" width="196" height="196" rx="8" fill="none" stroke="hsl(${hue}, 40%, 25%)" stroke-width="2"/>
|
||||
<path d="M100 50 v100 M70 70 q30-20 60 0 M70 130 q30 20 60 0" fill="none" stroke="hsl(${hue}, 50%, 60%)" stroke-width="4" stroke-linecap="round"/>
|
||||
<text x="100" y="115" text-anchor="middle" fill="hsl(${hue}, 50%, 70%)" font-family="system-ui" font-size="14" font-weight="600">${escapeXml(title.length > 12 ? title.slice(0, 10) + '…' : title)}</text>
|
||||
${artist ? `<text x="100" y="135" text-anchor="middle" fill="hsl(${hue}, 30%, 50%)" font-family="system-ui" font-size="10">${escapeXml(artist.length > 14 ? artist.slice(0, 12) + '…' : artist)}</text>` : ''}
|
||||
<rect width="200" height="200" fill="hsl(${hue}, 25%, 10%)"/>
|
||||
<rect x="3" y="3" width="194" height="194" rx="12" fill="none" stroke="hsl(${hue}, 35%, 18%)" stroke-width="1"/>
|
||||
<text x="100" y="80" text-anchor="middle" fill="hsl(${hue}, 30%, 25%)" font-family="system-ui,sans-serif" font-size="10" font-weight="500" letter-spacing="3">MUSIC</text>
|
||||
<circle cx="100" cy="120" r="28" fill="none" stroke="hsl(${hue}, 40%, 30%)" stroke-width="1.5"/>
|
||||
<circle cx="100" cy="120" r="8" fill="hsl(${hue}, 40%, 30%)"/>
|
||||
<text x="100" y="170" text-anchor="middle" fill="hsl(${hue}, 40%, 55%)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">${escapeXml(title.length > 18 ? title.slice(0, 16) + '…' : title)}</text>
|
||||
${artist ? `<text x="100" y="185" text-anchor="middle" fill="hsl(${hue}, 25%, 40%)" font-family="system-ui,sans-serif" font-size="9">${escapeXml(artist.length > 20 ? artist.slice(0, 18) + '…' : artist)}</text>` : ''}
|
||||
</svg>`
|
||||
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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 342 513">
|
||||
<rect width="342" height="513" fill="hsl(${hue}, 30%, 15%)"/>
|
||||
<rect x="1" y="1" width="340" height="511" rx="8" fill="none" stroke="hsl(${hue}, 40%, 25%)" stroke-width="2"/>
|
||||
<text x="171" y="230" text-anchor="middle" fill="hsl(${hue}, 50%, 70%)" font-family="system-ui" font-size="20" font-weight="600">
|
||||
<rect width="342" height="513" fill="hsl(${hue}, 20%, 9%)"/>
|
||||
<rect x="3" y="3" width="336" height="507" rx="6" fill="none" stroke="hsl(${hue}, 25%, 16%)" stroke-width="1"/>
|
||||
<text x="171" y="210" text-anchor="middle" fill="hsl(${hue}, 20%, 22%)" font-family="'Helvetica Neue',Helvetica,Arial,sans-serif" font-size="13" font-weight="400" letter-spacing="5">FILM</text>
|
||||
<line x1="121" y1="225" x2="221" y2="225" stroke="hsl(${hue}, 25%, 18%)" stroke-width="1"/>
|
||||
<text x="171" y="265" text-anchor="middle" fill="hsl(${hue}, 40%, 55%)" font-family="'Helvetica Neue',Helvetica,Arial,sans-serif" font-size="18" font-weight="700">
|
||||
${escapeXml(title.length > 20 ? title.slice(0, 18) + '…' : title)}
|
||||
</text>
|
||||
${year ? `<text x="171" y="260" text-anchor="middle" fill="hsl(${hue}, 30%, 50%)" font-family="system-ui" font-size="14">${year}</text>` : ''}
|
||||
${year ? `<text x="171" y="295" text-anchor="middle" fill="hsl(${hue}, 25%, 38%)" font-family="'Helvetica Neue',Helvetica,Arial,sans-serif" font-size="14" font-weight="300">${year}</text>` : ''}
|
||||
</svg>`
|
||||
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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 342 513">
|
||||
<rect width="342" height="513" fill="hsl(${hue}, 18%, 9%)"/>
|
||||
<rect x="3" y="3" width="336" height="507" rx="6" fill="none" stroke="hsl(${hue}, 22%, 16%)" stroke-width="1"/>
|
||||
<text x="171" y="210" text-anchor="middle" fill="hsl(${hue}, 18%, 22%)" font-family="'SF Pro Display',system-ui,sans-serif" font-size="13" font-weight="400" letter-spacing="5">SERIES</text>
|
||||
<line x1="111" y1="225" x2="231" y2="225" stroke="hsl(${hue}, 22%, 18%)" stroke-width="1"/>
|
||||
<text x="171" y="265" text-anchor="middle" fill="hsl(${hue}, 35%, 55%)" font-family="'SF Pro Display',system-ui,sans-serif" font-size="18" font-weight="700">
|
||||
${escapeXml(title.length > 20 ? title.slice(0, 18) + '…' : title)}
|
||||
</text>
|
||||
${year ? `<text x="171" y="295" text-anchor="middle" fill="hsl(${hue}, 22%, 38%)" font-family="'SF Pro Display',system-ui,sans-serif" font-size="14" font-weight="300">${year}</text>` : ''}
|
||||
</svg>`
|
||||
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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 420">
|
||||
<rect width="280" height="420" fill="hsl(${hue}, 25%, 12%)"/>
|
||||
<rect x="2" y="2" width="276" height="416" rx="4" fill="none" stroke="hsl(${hue}, 35%, 22%)" stroke-width="2"/>
|
||||
<rect x="12" y="12" width="256" height="396" rx="2" fill="none" stroke="hsl(${hue}, 30%, 18%)" stroke-width="1"/>
|
||||
<line x1="28" y1="12" x2="28" y2="408" stroke="hsl(${hue}, 30%, 20%)" stroke-width="2"/>
|
||||
<text x="154" y="190" text-anchor="middle" fill="hsl(${hue}, 50%, 70%)" font-family="Georgia,serif" font-size="18" font-weight="700">
|
||||
<rect width="280" height="420" fill="hsl(${hue}, 20%, 9%)"/>
|
||||
<rect x="3" y="3" width="274" height="414" rx="3" fill="none" stroke="hsl(${hue}, 25%, 16%)" stroke-width="1"/>
|
||||
<line x1="22" y1="3" x2="22" y2="417" stroke="hsl(${hue}, 25%, 15%)" stroke-width="1.5"/>
|
||||
<text x="155" y="170" text-anchor="middle" fill="hsl(${hue}, 18%, 22%)" font-family="Georgia,'Times New Roman',serif" font-size="11" font-weight="400" letter-spacing="4">BOOK</text>
|
||||
<line x1="95" y1="182" x2="215" y2="182" stroke="hsl(${hue}, 22%, 17%)" stroke-width="0.8"/>
|
||||
<text x="155" y="215" text-anchor="middle" fill="hsl(${hue}, 35%, 55%)" font-family="Georgia,'Times New Roman',serif" font-size="17" font-weight="700" font-style="italic">
|
||||
${escapeXml(title.length > 22 ? title.slice(0, 20) + '…' : title)}
|
||||
</text>
|
||||
${author ? `<text x="154" y="220" text-anchor="middle" fill="hsl(${hue}, 30%, 50%)" font-family="system-ui" font-size="12">${escapeXml(author.length > 26 ? author.slice(0, 24) + '…' : author)}</text>` : ''}
|
||||
${author ? `<text x="155" y="248" text-anchor="middle" fill="hsl(${hue}, 22%, 40%)" font-family="Georgia,'Times New Roman',serif" font-size="11" font-weight="400">${escapeXml(author.length > 26 ? author.slice(0, 24) + '…' : author)}</text>` : ''}
|
||||
</svg>`
|
||||
return `data:image/svg+xml,${encodeURIComponent(svg)}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user