78 lines
2.6 KiB
JavaScript
78 lines
2.6 KiB
JavaScript
/**
|
|
* Resolving guide images.
|
|
*
|
|
* A guide image lives behind an authenticated route, and an <img src> is a
|
|
* plain browser request that carries no Authorization header - this app keeps
|
|
* its JWT in one. Pointing src at the image route therefore yields 401 and a
|
|
* broken-image icon for every reader, which is how guides first shipped.
|
|
*
|
|
* So the bytes are fetched through the api client, which attaches the token and
|
|
* resolves against its own baseURL - the latter also fixing a stored
|
|
* '/api/guides/images/N' string under a subpath mount, where the API is not at
|
|
* /api at all.
|
|
*
|
|
* Used by the renderer AND the editor: an author needs to see the screenshot
|
|
* they just pasted as much as a reader does.
|
|
*/
|
|
import { reactive, watch, onBeforeUnmount } from 'vue'
|
|
import api from '@/api'
|
|
|
|
/** The image id a block refers to, whichever way it was written.
|
|
*
|
|
* Blocks store imageid (written since this was fixed) or a url string (written
|
|
* before it). The url is never used as a src, only read for its id.
|
|
*/
|
|
export function imageIdOf (block) {
|
|
if (!block) return ''
|
|
if (block.imageid) return String(block.imageid)
|
|
const match = /\/guides\/images\/(\d+)/.exec(block.url || '')
|
|
return match ? match[1] : ''
|
|
}
|
|
|
|
/**
|
|
* Keeps object URLs for the image blocks in `blocksRef`, fetching each id once
|
|
* however many blocks use it, and revoking them as blocks go away.
|
|
*
|
|
* Returns `state(block)` -> { src, failed }.
|
|
*/
|
|
export function useGuideImages (blocksRef) {
|
|
const images = reactive({})
|
|
|
|
async function load (id) {
|
|
if (images[id]) return
|
|
images[id] = { src: '', failed: false }
|
|
try {
|
|
const response = await api.get(`/guides/images/${id}`, { responseType: 'blob' })
|
|
images[id].src = URL.createObjectURL(response.data)
|
|
} catch (err) {
|
|
images[id].failed = true
|
|
}
|
|
}
|
|
|
|
function release (id) {
|
|
if (images[id] && images[id].src) URL.revokeObjectURL(images[id].src)
|
|
delete images[id]
|
|
}
|
|
|
|
function state (block) {
|
|
const id = imageIdOf(block)
|
|
if (!id) return { src: '', failed: true }
|
|
return images[id] || { src: '', failed: false }
|
|
}
|
|
|
|
// Editing replaces the block list on every keystroke, so resolve only ids
|
|
// that are new and keep the ones already held. Without that, every keystroke
|
|
// would refetch every image in the guide.
|
|
watch(blocksRef, blocks => {
|
|
const wanted = new Set(
|
|
(blocks || []).filter(b => b && b.type === 'image')
|
|
.map(imageIdOf).filter(Boolean))
|
|
wanted.forEach(load)
|
|
Object.keys(images).forEach(id => { if (!wanted.has(id)) release(id) })
|
|
}, { immediate: true, deep: true })
|
|
|
|
onBeforeUnmount(() => { Object.keys(images).forEach(release) })
|
|
|
|
return { state }
|
|
}
|