/** * Resolving guide images. * * A guide image lives behind an authenticated route, and an 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 } }