feat: media picker and image removal
This commit is contained in:
1 parent
008b0dfbb9
commit
b3793d5dbb
54 files changed
+3769
-1412
No files matched your search
@@ -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"
|
||||
/>
|
||||
|
||||
|
||||
Reference in new issue
Block a user