mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
af8f55976f
Add a Style setting beside mode, accent, radius and background. Nostalgia restyles the app through tokens only (neutral surface palettes, system type, flat shell, top glow) and pins background, opacity and brightness. Add a top glow reach slider for both styles. Tidy the theme layer first: charts read live tokens, bare `rounded` follows the radius scale, one shared sign-in toolbar. Also carries the settings page header redesign.
73 lines
2.1 KiB
Vue
73 lines
2.1 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue'
|
|
import type { StoredBookRequestMediaKind } from '@bookorbit/types'
|
|
import { requestMediaIcon } from '../requestMedia'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
src: string | null
|
|
fallbackSources?: string[]
|
|
sourceKey?: string
|
|
mediaKind: StoredBookRequestMediaKind
|
|
/** Sizing lives with the caller, because a table thumbnail and a page header differ. */
|
|
class?: string
|
|
iconSize?: number
|
|
}>(),
|
|
{ class: '', iconSize: 18 },
|
|
)
|
|
|
|
/**
|
|
* Cover URLs come from metadata providers, so they point at hosts BookOrbit does not control and
|
|
* fail routinely. A bare `v-if="coverUrl"` only covers the null case: a URL that is present but
|
|
* dead renders as a transparent gap where the cover should be, which reads as a broken row.
|
|
*/
|
|
const failedIndex = ref(0)
|
|
|
|
const emit = defineEmits<{
|
|
sourceChange: [payload: { key: string | undefined; src: string | null }]
|
|
}>()
|
|
|
|
const sources = computed(() =>
|
|
[props.src, ...(props.fallbackSources ?? [])]
|
|
.filter((src): src is string => Boolean(src))
|
|
.filter((src, index, values) => values.indexOf(src) === index),
|
|
)
|
|
const sourceSignature = computed(() => sources.value.join('\u0000'))
|
|
|
|
const currentSrc = computed(() => sources.value[failedIndex.value] ?? null)
|
|
|
|
watch(sourceSignature, () => {
|
|
failedIndex.value = 0
|
|
})
|
|
|
|
watch(
|
|
currentSrc,
|
|
(src) => {
|
|
emit('sourceChange', { key: props.sourceKey, src })
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
const showImage = computed(() => currentSrc.value !== null)
|
|
const icon = computed(() => requestMediaIcon(props.mediaKind))
|
|
|
|
function handleError() {
|
|
failedIndex.value += 1
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<img
|
|
v-if="showImage"
|
|
:src="currentSrc ?? undefined"
|
|
alt=""
|
|
class="shrink-0 rounded-xs border border-border bg-muted object-cover"
|
|
:class="props.class"
|
|
loading="lazy"
|
|
@error="handleError"
|
|
/>
|
|
<div v-else class="flex shrink-0 items-center justify-center rounded-xs border border-border bg-muted" :class="props.class" aria-hidden="true">
|
|
<component :is="icon" :size="iconSize" class="text-muted-foreground" />
|
|
</div>
|
|
</template>
|