feat(attachments): downloadable attachments in ProvenancePanel + PostInfoHeader
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,11 @@
|
|||||||
v-if="info.data.post.url" :href="info.data.post.url"
|
v-if="info.data.post.url" :href="info.data.post.url"
|
||||||
target="_blank" rel="noopener noreferrer"
|
target="_blank" rel="noopener noreferrer"
|
||||||
>↗ original post</a>
|
>↗ original post</a>
|
||||||
|
<a
|
||||||
|
v-for="at in (info.data.attachments || [])" :key="at.id"
|
||||||
|
class="fc-postinfo__attach"
|
||||||
|
:href="at.download_url" :download="at.original_filename"
|
||||||
|
>⬇ {{ at.original_filename }}</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<span v-else-if="info && info.error" class="fc-postinfo__title">
|
<span v-else-if="info && info.error" class="fc-postinfo__title">
|
||||||
@@ -90,4 +95,8 @@ function clear() {
|
|||||||
font-size: 13px; color: rgb(var(--v-theme-on-surface-variant));
|
font-size: 13px; color: rgb(var(--v-theme-on-surface-variant));
|
||||||
}
|
}
|
||||||
.fc-postinfo__main a { color: rgb(var(--v-theme-accent)); text-decoration: none; }
|
.fc-postinfo__main a { color: rgb(var(--v-theme-accent)); text-decoration: none; }
|
||||||
|
.fc-postinfo__attach {
|
||||||
|
font-size: 13px; color: rgb(var(--v-theme-accent));
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -44,6 +44,17 @@
|
|||||||
/>
|
/>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<div v-if="attachments.length" class="fc-prov__attach">
|
||||||
|
<h4 class="fc-prov__attach-title">Attachments</h4>
|
||||||
|
<a
|
||||||
|
v-for="at in attachments" :key="at.id"
|
||||||
|
class="fc-prov__attach-row"
|
||||||
|
:href="at.download_url" :download="at.original_filename"
|
||||||
|
>⬇ {{ at.original_filename }}
|
||||||
|
<span class="fc-prov__attach-size">({{ at.size_bytes }} B)</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -77,6 +88,8 @@ const show = computed(() => {
|
|||||||
return Array.isArray(st.entries) && st.entries.length > 0
|
return Array.isArray(st.entries) && st.entries.length > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const attachments = computed(() => state.value?.attachments || [])
|
||||||
|
|
||||||
function postDate(e) { return formatPostDate(e.post.date) }
|
function postDate(e) { return formatPostDate(e.post.date) }
|
||||||
|
|
||||||
function openPost(postId) {
|
function openPost(postId) {
|
||||||
@@ -124,4 +137,14 @@ function openPost(postId) {
|
|||||||
color: rgb(var(--v-theme-on-surface-variant));
|
color: rgb(var(--v-theme-on-surface-variant));
|
||||||
}
|
}
|
||||||
.fc-prov__desc :deep(a) { color: rgb(var(--v-theme-accent)); }
|
.fc-prov__desc :deep(a) { color: rgb(var(--v-theme-accent)); }
|
||||||
|
.fc-prov__attach { padding: 0 0 12px; }
|
||||||
|
.fc-prov__attach-title {
|
||||||
|
font-size: 13px; color: rgb(var(--v-theme-on-surface-variant));
|
||||||
|
margin: 8px 0 4px;
|
||||||
|
}
|
||||||
|
.fc-prov__attach-row {
|
||||||
|
display: block; font-size: 13px; text-decoration: none;
|
||||||
|
color: rgb(var(--v-theme-accent)); padding: 2px 0;
|
||||||
|
}
|
||||||
|
.fc-prov__attach-size { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ export const useProvenanceStore = defineStore('provenance', () => {
|
|||||||
imageCache.value[id] = { loading: true, error: null, entries: null }
|
imageCache.value[id] = { loading: true, error: null, entries: null }
|
||||||
try {
|
try {
|
||||||
const body = await api.get(`/api/provenance/image/${id}`)
|
const body = await api.get(`/api/provenance/image/${id}`)
|
||||||
imageCache.value[id] =
|
imageCache.value[id] = {
|
||||||
{ loading: false, error: null, entries: body.provenance }
|
loading: false, error: null,
|
||||||
|
entries: body.provenance,
|
||||||
|
attachments: body.attachments || [],
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
imageCache.value[id] =
|
imageCache.value[id] =
|
||||||
{ loading: false, error: e.message, entries: null }
|
{ loading: false, error: e.message, entries: null }
|
||||||
|
|||||||
@@ -59,4 +59,18 @@ describe('provenance store', () => {
|
|||||||
expect(globalThis.fetch.mock.calls.length).toBe(1)
|
expect(globalThis.fetch.mock.calls.length).toBe(1)
|
||||||
expect(store.postInfo(3).data.artist.slug).toBe('a')
|
expect(store.postInfo(3).data.artist.slug).toBe('a')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('loadForImage keeps attachments from the payload', async () => {
|
||||||
|
const store = useProvenanceStore()
|
||||||
|
stubFetch(() => ({
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
image_id: 1, provenance: [],
|
||||||
|
attachments: [{ id: 9, original_filename: 'p.zip', size_bytes: 5,
|
||||||
|
ext: '.zip', download_url: '/api/attachments/9/download' }]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
await store.loadForImage(1)
|
||||||
|
expect(store.imageProv(1).attachments[0].id).toBe(9)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user