Files
FabledCurator/frontend/src/components/modal/ImageMetaBar.vue
T
bvandeusen 7b712920a4
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m17s
feat(explore): Explore view + tag-gap closing + modal meta/download (#94b–d, #4a/b)
Cluster C frontend, milestone #94.

#94b Explore walk: new /explore/:imageId route + ExploreView + explore store.
Anchor (reuse /api/gallery/image), neighbour grid (reuse /api/gallery/similar,
24), click a neighbour to re-anchor; in-memory breadcrumb that trims on
backtrack (route is the source of truth). Empty/loading/error + no-embedding
states.

#94c tag-gap closing: components/explore/TagGapPanel — fetches
/api/images/cluster/tag-gaps for the anchor+neighbours, a consensus-threshold
slider (default 60%), per gap shows present/total + the missing thumbnails +
'Apply to N missing' → /api/tags/images/bulk/tags (source manual) → re-fetch.

#94d entry points: 'Explore' button in the modal RelatedStrip; the TopNav entry
comes free from the route's meta.title.

#4a metadata HUD + #4b split Download: new modal ImageMetaBar (always-on, above
ProvenancePanel) shows dimensions/size/type and a split Download button
(default Download, chevron → Copy link via utils/clipboard — no clipboard-image,
rule 95).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XCUHUGQLrBrkgyk1t49kpX
2026-06-23 02:08:34 -04:00

97 lines
3.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<section v-if="img" class="fc-meta" aria-label="Image details">
<!-- #4a: dimensions / size / type already on the payload, shown nowhere
until now. -->
<dl class="fc-meta__grid">
<div v-if="img.width && img.height" class="fc-meta__item">
<dt>Dimensions</dt><dd>{{ img.width }} × {{ img.height }}</dd>
</div>
<div v-if="img.size_bytes" class="fc-meta__item">
<dt>Size</dt><dd>{{ humanSize(img.size_bytes) }}</dd>
</div>
<div v-if="img.mime" class="fc-meta__item">
<dt>Type</dt><dd>{{ shortType(img.mime) }}</dd>
</div>
</dl>
<!-- #4b: split Download default Download, chevron Copy link. Image-to-
clipboard is OFF the table on the plain-HTTP origin (rule 95), so the
secondary action copies the link TEXT, which works everywhere. -->
<v-btn-group divided density="comfortable" variant="tonal" color="accent" rounded="pill">
<v-btn prepend-icon="mdi-download" @click="download">Download</v-btn>
<v-menu location="bottom end">
<template #activator="{ props }">
<v-btn v-bind="props" icon="mdi-chevron-down" aria-label="More download options" />
</template>
<v-list density="compact">
<v-list-item prepend-icon="mdi-link-variant" title="Copy link" @click="copyLink" />
<v-list-item prepend-icon="mdi-download" title="Download" @click="download" />
</v-list>
</v-menu>
</v-btn-group>
</section>
</template>
<script setup>
import { computed } from 'vue'
import { useModalStore } from '../../stores/modal.js'
import { copyText } from '../../utils/clipboard.js'
import { toast } from '../../utils/toast.js'
const modal = useModalStore()
const img = computed(() => modal.current)
function humanSize (bytes) {
const units = ['B', 'KB', 'MB', 'GB']
let n = bytes
let i = 0
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++ }
return `${n < 10 && i > 0 ? n.toFixed(1) : Math.round(n)} ${units[i]}`
}
function shortType (mime) { return mime.split('/')[1]?.toUpperCase() || mime }
function absoluteUrl () {
const origin = typeof window !== 'undefined' ? window.location.origin : ''
return origin + img.value.image_url
}
function download () {
// Same-origin /images/* link — the download attribute lets the browser save
// the original (filename derived from the path) instead of navigating to it.
const a = document.createElement('a')
a.href = img.value.image_url
a.setAttribute('download', '')
document.body.appendChild(a)
a.click()
a.remove()
}
async function copyLink () {
try {
await copyText(absoluteUrl())
toast({ text: 'Link copied to clipboard', type: 'success' })
} catch {
toast({ text: 'Could not copy the link', type: 'error' })
}
}
</script>
<style scoped>
.fc-meta {
padding: 14px 16px 0;
display: flex; flex-direction: column; gap: 10px;
}
.fc-meta__grid {
display: flex; flex-wrap: wrap; gap: 4px 18px; margin: 0;
}
.fc-meta__item { display: flex; flex-direction: column; }
.fc-meta__item dt {
font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;
color: rgb(var(--v-theme-on-surface-variant));
}
.fc-meta__item dd {
margin: 0; font-size: 13px; font-variant-numeric: tabular-nums;
color: rgb(var(--v-theme-on-surface));
}
</style>