feat: show provider id and source url in metadata diff

This commit is contained in:
neon
2026-03-06 06:07:56 -06:00
parent ac180e521f
commit e435629e9e
4 changed files with 115 additions and 31 deletions
@@ -12,6 +12,7 @@ const props = defineProps<{
providers: MetadataProviderInfo[]
backLabel?: string
currentCoverUrl?: string
currentProviderId?: string | null
fieldSources?: Record<string, string>
}>()
@@ -22,7 +23,12 @@ const emit = defineEmits<{
const providerLabel = computed(() => getProviderLabel(props.candidate.provider, props.providers))
const { fields, toggleField, copyAll, copyMissing, buildPatch, hasCopied } = useMetadataDiff(props.current, props.candidate, props.currentCoverUrl)
const { fields, toggleField, copyAll, copyMissing, buildPatch, hasCopied } = useMetadataDiff(
props.current,
props.candidate,
props.currentCoverUrl,
props.currentProviderId,
)
function apply() {
emit('apply', buildPatch())
@@ -36,7 +36,7 @@ const lightboxSrc = ref<string | null>(null)
<!-- Toggle -->
<div class="flex justify-center">
<button
v-if="field.candidateDisplay"
v-if="field.isCopyable && field.candidateDisplay"
class="size-8 rounded-full flex items-center justify-center transition-all active:scale-90 shadow-sm"
:class="
field.isCopied
@@ -90,7 +90,7 @@ const lightboxSrc = ref<string | null>(null)
<!-- Toggle button -->
<div class="flex items-center justify-center">
<button
v-if="field.hasDiff"
v-if="field.isCopyable && field.hasDiff"
class="size-8 rounded-full flex items-center justify-center transition-all active:scale-90 shadow-sm shrink-0"
:class="
field.isCopied
@@ -102,9 +102,10 @@ const lightboxSrc = ref<string | null>(null)
<RotateCcw v-if="field.isCopied" class="size-3.5" />
<ArrowLeft v-else class="size-3.5" />
</button>
<div v-else class="size-8 flex items-center justify-center">
<div v-else-if="field.isCopyable" class="size-8 flex items-center justify-center">
<CheckCircle2 class="size-3.5 text-muted-foreground/25" />
</div>
<div v-else class="size-8" />
</div>
<!-- New value: muted by default, highlights when selected -->
@@ -121,7 +122,16 @@ const lightboxSrc = ref<string | null>(null)
{{ source }}
</span>
</div>
<p class="wrap-break-word leading-snug text-sm w-full" :class="field.isCopied ? 'text-primary font-medium' : 'text-muted-foreground'">
<a
v-if="field.key === 'sourceUrl' && field.candidateDisplay"
:href="field.candidateDisplay"
target="_blank"
rel="noopener noreferrer"
class="wrap-break-word leading-snug text-sm w-full text-primary hover:underline"
>
{{ field.candidateDisplay }}
</a>
<p v-else class="wrap-break-word leading-snug text-sm w-full" :class="field.isCopied ? 'text-primary font-medium' : 'text-muted-foreground'">
{{ field.candidateDisplay }}
</p>
</div>
@@ -52,6 +52,9 @@ const {
const view = ref<'search' | 'diff'>('search')
const selectedCandidate = ref<MetadataCandidate | null>(null)
const currentProviderId = computed(() =>
selectedCandidate.value ? (props.book.providerIds[selectedCandidate.value.provider] ?? null) : null,
)
onMounted(() => {
loadProviders()
@@ -138,6 +141,7 @@ function handleApply(patch: { formPatch: MetadataPatch; coverUrl?: string }) {
:candidate="selectedCandidate"
:providers="providers"
:current-cover-url="bookCoverUrl"
:current-provider-id="currentProviderId"
@back="backToSearch"
@apply="handleApply"
/>
@@ -16,6 +16,8 @@ export type DiffFieldKey =
| 'isbn10'
| 'genres'
| 'coverUrl'
| 'providerId'
| 'sourceUrl'
export interface DiffField {
key: DiffFieldKey
@@ -26,6 +28,7 @@ export interface DiffField {
hasDiff: boolean
isCopied: boolean
isCover: boolean
isCopyable: boolean
}
export interface MetadataPatch {
@@ -66,7 +69,9 @@ const FIELD_DEFS: { key: DiffFieldKey; label: string }[] = [
{ key: 'genres', label: 'Genres' },
]
const PROVIDER_ID_FIELD: Record<MetadataProviderKey, keyof MetadataPatch | undefined> = {
type ProviderIdPatchField = 'googleBooksId' | 'goodreadsId' | 'amazonId' | 'hardcoverId' | 'openLibraryId'
const PROVIDER_ID_FIELD: Record<MetadataProviderKey, ProviderIdPatchField | undefined> = {
google: 'googleBooksId',
goodreads: 'goodreadsId',
amazon: 'amazonId',
@@ -74,7 +79,20 @@ const PROVIDER_ID_FIELD: Record<MetadataProviderKey, keyof MetadataPatch | undef
openLibrary: 'openLibraryId',
}
export function useMetadataDiff(current: MetadataSource, candidate: MetadataCandidate, currentCoverUrl?: string) {
const PROVIDER_ID_LABEL: Record<MetadataProviderKey, string> = {
google: 'Google Books ID',
goodreads: 'Goodreads ID',
amazon: 'Amazon ID',
hardcover: 'Hardcover ID',
openLibrary: 'Open Library ID',
}
export function useMetadataDiff(
current: MetadataSource,
candidate: MetadataCandidate,
currentCoverUrl?: string,
currentProviderId?: string | null,
) {
const copiedFields = reactive(new Set<DiffFieldKey>())
function getBookValue(key: DiffFieldKey): string {
@@ -94,28 +112,66 @@ export function useMetadataDiff(current: MetadataSource, candidate: MetadataCand
}
const fields = computed<DiffField[]>(() =>
FIELD_DEFS.flatMap((def) => {
const candidateVal = getCandidateValue(def.key)
const bookVal = getBookValue(def.key)
if (def.key === 'coverUrl') {
if (!candidateVal && !bookVal) return []
} else {
if (!candidateVal) return []
}
const isCopied = copiedFields.has(def.key)
return [
{
key: def.key,
label: def.label,
bookValue: bookVal,
currentDisplay: isCopied ? candidateVal : bookVal,
candidateDisplay: candidateVal,
hasDiff: bookVal !== candidateVal,
isCopied,
isCover: def.key === 'coverUrl',
},
]
}),
[
...FIELD_DEFS.flatMap((def) => {
const candidateVal = getCandidateValue(def.key)
const bookVal = getBookValue(def.key)
if (def.key === 'coverUrl') {
if (!candidateVal && !bookVal) return []
} else {
if (!candidateVal) return []
}
const isCopied = copiedFields.has(def.key)
return [
{
key: def.key,
label: def.label,
bookValue: bookVal,
currentDisplay: isCopied ? candidateVal : bookVal,
candidateDisplay: candidateVal,
hasDiff: bookVal !== candidateVal,
isCopied,
isCover: def.key === 'coverUrl',
isCopyable: true,
},
]
}),
...(candidate.providerId || currentProviderId
? [
(() => {
const providerIsCopied = copiedFields.has('providerId')
const candidateProviderId = candidate.providerId ?? ''
const existingProviderId = currentProviderId ?? ''
return {
key: 'providerId' as const,
label: PROVIDER_ID_LABEL[candidate.provider] ?? 'Provider ID',
bookValue: existingProviderId,
currentDisplay: providerIsCopied ? candidateProviderId : existingProviderId,
candidateDisplay: candidateProviderId,
hasDiff: existingProviderId !== candidateProviderId,
isCopied: providerIsCopied,
isCover: false,
isCopyable: true,
}
})(),
]
: []),
...(candidate.sourceUrl
? [
{
key: 'sourceUrl' as const,
label: 'Source URL',
bookValue: '',
currentDisplay: '',
candidateDisplay: candidate.sourceUrl,
hasDiff: true,
isCopied: false,
isCover: false,
isCopyable: false,
},
]
: []),
] as DiffField[],
)
function toggleField(key: DiffFieldKey) {
@@ -124,12 +180,14 @@ export function useMetadataDiff(current: MetadataSource, candidate: MetadataCand
}
function copyAll() {
for (const f of fields.value) copiedFields.add(f.key)
for (const f of fields.value) {
if (f.isCopyable) copiedFields.add(f.key)
}
}
function copyMissing() {
for (const f of fields.value) {
if (!f.bookValue) copiedFields.add(f.key)
if (f.isCopyable && !f.bookValue) copiedFields.add(f.key)
}
}
@@ -162,6 +220,12 @@ export function useMetadataDiff(current: MetadataSource, candidate: MetadataCand
formPatch.seriesIndex = candidate.seriesIndex ?? null
continue
}
if (key === 'providerId') {
const idField = PROVIDER_ID_FIELD[candidate.provider]
if (idField) formPatch[idField] = candidate.providerId
continue
}
if (key === 'sourceUrl') continue
const val = candidate[key as keyof MetadataCandidate]
;(formPatch as Record<string, unknown>)[key] = val != null ? String(val) : null
}