mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat: show provider id and source url in metadata diff
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user