feat(app): add real images to all seed content + universal SVG text fallbacks
- Songs: 19/21 now have iTunes album art URLs (2 niche Bitcoin artists use fallback) - Podcasts: 21/21 now have iTunes artwork URLs - News: 21/21 now have Unsplash topic images - Books: Fixed The Network State cover URL - Places: 21/21 now have photos (Unsplash + Wikimedia) - Added generateNewsFallback() and generateImageFallback() SVG generators - Updated NewsCard, NewsGrid, ImageCard to use SVG text fallback instead of emoji - Added error handling to PlaceCard and PlaceGrid for failed photo loads Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
982a0c6aa6
commit
c88e837cc9
@@ -18,13 +18,9 @@
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full flex items-center justify-center"
|
||||
:class="isDark ? 'bg-white/5' : 'bg-black/5'"
|
||||
>
|
||||
<svg class="w-5 h-5" :class="isDark ? 'text-white/20' : 'text-gray-300'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
class="w-full h-full bg-cover bg-center"
|
||||
:style="{ backgroundImage: `url(${fallbackImg})` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 py-0.5">
|
||||
@@ -45,13 +41,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { ImageItem } from '@aiui/core/types/content'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { generateImageFallback } from '@/composables/useImageFallback'
|
||||
|
||||
defineProps<{ image: ImageItem }>()
|
||||
const props = defineProps<{ image: ImageItem }>()
|
||||
defineEmits<{ select: [image: ImageItem] }>()
|
||||
|
||||
const { isDark } = useTheme()
|
||||
const imgFailed = ref(false)
|
||||
|
||||
const fallbackImg = computed(() =>
|
||||
generateImageFallback(props.image.title || props.image.alt || 'Image')
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -17,13 +17,9 @@
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full rounded-[6px] bg-cover bg-center flex items-center justify-center"
|
||||
:style="{ backgroundImage: faviconUrl ? `url(${faviconUrl})` : undefined }"
|
||||
>
|
||||
<span v-if="!faviconUrl"
|
||||
class="text-lg opacity-40"
|
||||
:class="isDark ? 'text-white' : 'text-gray-600'">📰</span>
|
||||
</div>
|
||||
class="w-full h-full rounded-[6px] bg-cover bg-center"
|
||||
:style="{ backgroundImage: `url(${fallbackImg})` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1 py-0.5">
|
||||
@@ -52,6 +48,7 @@ import { ref, computed } from 'vue'
|
||||
import type { WebSearchResult } from '@aiui/core/types/message'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { isSafeUrl, formatDomain } from '@/utils/html'
|
||||
import { generateNewsFallback } from '@/composables/useImageFallback'
|
||||
|
||||
const props = defineProps<{ article: WebSearchResult }>()
|
||||
defineEmits<{ 'select-article': [article: WebSearchResult] }>()
|
||||
@@ -65,15 +62,8 @@ const imgSrc = computed(() => {
|
||||
return isSafeUrl(u) ? u : null
|
||||
})
|
||||
|
||||
const faviconUrl = computed(() => {
|
||||
if (imgSrc.value) return null
|
||||
try {
|
||||
const u = new URL(props.article.url)
|
||||
if (!/^https?:\/\//i.test(props.article.url)) return null
|
||||
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(u.hostname)}&sz=64`
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
const fallbackImg = computed(() =>
|
||||
generateNewsFallback(props.article.title, formatDomain(props.article.url))
|
||||
)
|
||||
|
||||
</script>
|
||||
|
||||
@@ -49,37 +49,9 @@
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
:class="isDark ? 'bg-white/5' : 'bg-black/[0.04]'"
|
||||
>
|
||||
<!-- Websites: glassmorphic circle -->
|
||||
<div
|
||||
v-if="variant === 'websites'"
|
||||
class="w-14 h-14 rounded-full flex items-center justify-center path-glass-icon shrink-0"
|
||||
>
|
||||
<img
|
||||
v-if="faviconUrl(article.url)"
|
||||
:src="faviconUrl(article.url)!"
|
||||
:alt="formatDomain(article.url)"
|
||||
class="w-7 h-7 object-contain"
|
||||
/>
|
||||
<span v-else
|
||||
class="text-xl opacity-70"
|
||||
:class="isDark ? 'text-white' : 'text-gray-600'">🌐</span>
|
||||
</div>
|
||||
<!-- News: icon -->
|
||||
<template v-else>
|
||||
<img
|
||||
v-if="faviconUrl(article.url)"
|
||||
:src="faviconUrl(article.url)!"
|
||||
:alt="formatDomain(article.url)"
|
||||
class="w-8 h-8 object-contain opacity-70"
|
||||
/>
|
||||
<span v-else
|
||||
class="text-2xl opacity-40"
|
||||
:class="isDark ? 'text-white' : 'text-gray-600'">📰</span>
|
||||
</template>
|
||||
</div>
|
||||
class="absolute inset-0 bg-cover bg-center"
|
||||
:style="{ backgroundImage: `url(${newsFallback(article)})` }"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent pointer-events-none" />
|
||||
</div>
|
||||
|
||||
@@ -122,6 +94,7 @@ import { ref, computed } from 'vue'
|
||||
import type { WebSearchResult } from '@aiui/core/types/message'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { useContentPanel } from '@/composables/useContentPanel'
|
||||
import { generateNewsFallback } from '@/composables/useImageFallback'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
articles: WebSearchResult[]
|
||||
@@ -158,14 +131,8 @@ function formatDomain(url: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function faviconUrl(url: string): string | null {
|
||||
try {
|
||||
if (!/^https?:\/\//i.test(url)) return null
|
||||
const u = new URL(url)
|
||||
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(u.hostname)}&sz=64`
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
function newsFallback(article: WebSearchResult): string {
|
||||
return generateNewsFallback(article.title, formatDomain(article.url))
|
||||
}
|
||||
|
||||
function openArticle(article: WebSearchResult) {
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
>
|
||||
<div class="shrink-0 w-12 h-12 rounded-lg overflow-hidden">
|
||||
<img
|
||||
v-if="place.photoUrl"
|
||||
:src="place.photoUrl"
|
||||
v-if="photoSrc"
|
||||
:src="photoSrc"
|
||||
:alt="place.name"
|
||||
class="w-full h-full object-cover rounded-[6px] transition-transform duration-300 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
@error="photoFailed = true"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
@@ -45,7 +46,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Place } from '@aiui/core/types/content'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { generatePlaceFallback } from '@/composables/useImageFallback'
|
||||
@@ -54,6 +55,12 @@ const props = defineProps<{ place: Place }>()
|
||||
defineEmits<{ select: [place: Place] }>()
|
||||
|
||||
const { isDark } = useTheme()
|
||||
const photoFailed = ref(false)
|
||||
|
||||
const photoSrc = computed(() => {
|
||||
if (photoFailed.value) return null
|
||||
return props.place.photoUrl || null
|
||||
})
|
||||
|
||||
const fallbackPhoto = computed(() =>
|
||||
generatePlaceFallback(props.place.name, props.place.cuisine || props.place.category)
|
||||
|
||||
@@ -47,14 +47,15 @@
|
||||
>
|
||||
<div class="aspect-[4/3] relative w-full overflow-hidden rounded-t-[10px]">
|
||||
<img
|
||||
v-if="place.photoUrl"
|
||||
v-if="place.photoUrl && !failedPhotos.has(place.id)"
|
||||
:src="place.photoUrl"
|
||||
:alt="place.name"
|
||||
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
|
||||
loading="lazy"
|
||||
@error="onPhotoError(place.id)"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-if="!place.photoUrl || failedPhotos.has(place.id)"
|
||||
class="w-full h-full bg-cover bg-center"
|
||||
:style="{ backgroundImage: `url(${fallbackFor(place)})` }"
|
||||
/>
|
||||
@@ -103,6 +104,11 @@ defineEmits<{ selectPlace: [place: Place] }>()
|
||||
const { isDark } = useTheme()
|
||||
const search = ref('')
|
||||
const activeCategory = ref<string | null>(null)
|
||||
const failedPhotos = ref<Set<string>>(new Set())
|
||||
|
||||
function onPhotoError(id: string) {
|
||||
failedPhotos.value = new Set([...failedPhotos.value, id])
|
||||
}
|
||||
|
||||
function fallbackFor(place: Place): string {
|
||||
return generatePlaceFallback(place.name, place.cuisine || place.category)
|
||||
|
||||
Reference in New Issue
Block a user