feat: media picker and image removal

This commit is contained in:
fsylvain committed 2026-10-01 05:04:58 +02:00
1 parent 008b0dfbb9
commit b3793d5dbb
54 files changed
+3769 -1412

No files matched your search

+38 -13
View File
@@ -15,6 +15,10 @@ import ImageUploader from '@/components/ImageUploader.vue'
import { ApiError, authRedirectPending, useApi } from '@/composables/useApi'
import type { Article } from '@/composables/useArticles'
import { useArticleDraft } from '@/composables/useArticleDraft'
import type { MediaItem } from '@/composables/useMedia'
import { bodyImageCount, bodyImageIds } from '@/lib/bodyImageIds'
import { formatSize } from '@/lib/mediaFormat'
import { useSeenImagesStore } from '@/stores/seenImages'
const SKELETON_DELAY_MS = 150
const BODY_MAX_LENGTH = 10000
@@ -29,6 +33,7 @@ const toast = useToast()
const articleId = Number(route.params.id)
const draft = useArticleDraft(articleId)
const seenImages = useSeenImagesStore()
const loadState = ref<LoadState>('loading')
const showSkeleton = ref(false)
@@ -39,6 +44,7 @@ const publishPending = ref<string | undefined>(undefined)
const editorRef = ref<InstanceType<typeof ArticleEditor> | null>(null)
const imageUploaderOpen = ref(false)
const imageUploaderFile = ref<File | null>(null)
const imageIdsInArticle = ref<number[]>([])
let skeletonTimer: ReturnType<typeof setTimeout> | null = null
function clearSkeletonTimer() {
@@ -159,7 +165,7 @@ function handleUnpublish() {
}
function deleteDescription() {
const count = article.value?.images.length ?? 0
const count = bodyImageCount(article.value?.body ?? '')
const imagesPhrase =
count === 0 ? '' : count === 1 ? ' and its image' : ` and its ${count} images`
return `"${article.value?.title ?? ''}"${imagesPhrase} will be deleted. This can't be undone.`
@@ -206,6 +212,7 @@ async function handleDelete() {
function openImageUploader(file: File | null) {
imageUploaderFile.value = file
imageIdsInArticle.value = bodyImageIds(draft.body.value)
imageUploaderOpen.value = true
}
@@ -221,8 +228,7 @@ function onEditorDrop(event: DragEvent) {
openImageUploader(file)
}
function onImageUploaded(location: string, imageId: number) {
editorRef.value?.insertImage(location, '', imageId)
function announceImageAdded() {
toast.add({
color: 'success',
icon: 'i-lucide-circle-check',
@@ -231,31 +237,49 @@ function onImageUploaded(location: string, imageId: number) {
})
}
function onImageUploaded(item: MediaItem) {
seenImages.remember([item])
editorRef.value?.insertImage(item.url, '', item.id)
announceImageAdded()
}
function onImageInserted(item: MediaItem, alt: string) {
seenImages.remember([item])
editorRef.value?.insertImage(item.url, alt, item.id)
announceImageAdded()
}
async function performRemoveImage(imageId: number) {
try {
await request(`/v1/article/${articleId}/image/${imageId}`, { method: 'DELETE' })
} catch {
throw new Error("Couldn't remove the image, so it's still there. Try again in a moment.")
}
editorRef.value?.removeImage(imageId)
toast.add({
color: 'success',
icon: 'i-lucide-circle-check',
title: 'Image removed',
description: 'Save to keep this change.',
description: "It's still in your library.",
})
}
function removalPreview(imageId: number) {
const seen = seenImages.find(imageId)
return {
src: editorRef.value?.imageSrc(imageId) ?? '',
filename: seen?.filename,
size: seen ? formatSize(seen.size) : undefined,
}
}
async function onRequestRemoveImage(imageId: number) {
await overlay
.create(ConfirmDialog, {
props: {
title: 'Remove this image?',
description: "This image will be removed from the article. This can't be undone.",
title: 'Remove this image from the article?',
description:
"It's removed from this article only. The file stays in your library and in any other article that uses it.",
confirmLabel: 'Remove image',
cancelLabel: 'Cancel',
pendingLabel: 'Removing…',
tone: 'danger',
tone: 'neutral',
preview: removalPreview(imageId),
action: () => performRemoveImage(imageId),
},
})
@@ -476,9 +500,10 @@ onBeforeRouteLeave(async () => {
<ImageUploader
v-model:open="imageUploaderOpen"
:article-id="articleId"
:initial-file="imageUploaderFile"
:in-article-ids="imageIdsInArticle"
@uploaded="onImageUploaded"
@inserted="onImageInserted"
@close="onImageUploaderClosed"
/>