feat: support attachment uploading
parent
de59800c2b
commit
d79011e39a
|
@ -2,7 +2,7 @@
|
|||
<div flex="~ col" items-center>
|
||||
<div i-ri:forbid-line text-10 mt10 mb2 />
|
||||
<div text-lg>
|
||||
<slot>Not found</slot>
|
||||
<slot>404 Not Found</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<script setup lang="ts">
|
||||
import type { Attachment } from 'masto'
|
||||
|
||||
withDefaults(defineProps<{
|
||||
attachment: Attachment
|
||||
alt?: string
|
||||
removable?: boolean
|
||||
}>(), {
|
||||
removable: true,
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
(evt: 'remove'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div relative group>
|
||||
<status-attachment :attachment="attachment" w-full />
|
||||
<div absolute right-2 top-2 hover:bg="gray/40" transition-100 p-1 rounded-5 cursor-pointer op-0 group-hover:op-100>
|
||||
<div v-if="removable" i-ri:close-line text-3 @click="$emit('remove')" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import type { CreateStatusParamsWithStatus } from 'masto'
|
||||
import type { Attachment, CreateStatusParams, CreateStatusParamsWithStatus } from 'masto'
|
||||
|
||||
const {
|
||||
draftKey,
|
||||
|
@ -19,13 +19,65 @@ function getDefaultStatus(): CreateStatusParamsWithStatus {
|
|||
inReplyToId,
|
||||
}
|
||||
}
|
||||
const draft = useLocalStorage<CreateStatusParamsWithStatus>(storageKey, getDefaultStatus())
|
||||
let draft = $(useLocalStorage<CreateStatusParamsWithStatus>(storageKey, getDefaultStatus()))
|
||||
let attachments = $(useLocalStorage<Attachment[]>(`${storageKey}-attachments`, []))
|
||||
const status = $computed(() => {
|
||||
return {
|
||||
...draft,
|
||||
mediaIds: attachments.map(a => a.id),
|
||||
} as CreateStatusParams
|
||||
})
|
||||
|
||||
let isUploading = $ref<boolean>(false)
|
||||
|
||||
async function handlePaste(evt: ClipboardEvent) {
|
||||
const files = evt.clipboardData?.files
|
||||
if (!files)
|
||||
return
|
||||
|
||||
await uploadAttachments(Array.from(files))
|
||||
}
|
||||
|
||||
async function pickAttachments() {
|
||||
if (!globalThis.showOpenFilePicker)
|
||||
// TODO: Safari don't support it.
|
||||
return
|
||||
|
||||
const handles = await showOpenFilePicker({
|
||||
multiple: true,
|
||||
// TODO: add more kinds of files: videos, audios
|
||||
types: [{
|
||||
description: 'Images',
|
||||
accept: {
|
||||
'image/*': ['.png', '.gif', '.jpeg', '.jpg', '.webp', '.avif', '.heic'],
|
||||
},
|
||||
}],
|
||||
})
|
||||
const files = await Promise.all(handles.map(handle => handle.getFile()))
|
||||
await uploadAttachments(files)
|
||||
}
|
||||
|
||||
async function uploadAttachments(files: File[]) {
|
||||
isUploading = true
|
||||
for (const file of files) {
|
||||
const attachment = await masto.mediaAttachments.create({
|
||||
file,
|
||||
})
|
||||
attachments.push(attachment)
|
||||
}
|
||||
isUploading = false
|
||||
}
|
||||
|
||||
async function removeAttachment(index: number) {
|
||||
attachments.splice(index, 1)
|
||||
}
|
||||
|
||||
async function publish() {
|
||||
try {
|
||||
isSending = true
|
||||
await masto.statuses.create(draft.value)
|
||||
draft.value = getDefaultStatus()
|
||||
await masto.statuses.create(status)
|
||||
draft = getDefaultStatus()
|
||||
attachments = []
|
||||
}
|
||||
finally {
|
||||
isSending = false
|
||||
|
@ -33,8 +85,9 @@ async function publish() {
|
|||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (!draft.value.status) {
|
||||
draft.value = undefined
|
||||
if (!draft.status) {
|
||||
// @ts-expect-error draft cannot be undefined
|
||||
draft = undefined
|
||||
nextTick(() => {
|
||||
localStorage.removeItem(storageKey)
|
||||
})
|
||||
|
@ -44,7 +97,7 @@ onUnmounted(() => {
|
|||
|
||||
<template>
|
||||
<div
|
||||
flex flex-col gap-4
|
||||
flex flex-col gap-3
|
||||
:class="isSending ? 'pointer-events-none' : ''"
|
||||
>
|
||||
<textarea
|
||||
|
@ -52,11 +105,32 @@ onUnmounted(() => {
|
|||
:placeholder="placeholder"
|
||||
p2 border-rounded w-full h-40
|
||||
bg-gray:10 outline-none border="~ base"
|
||||
@paste="handlePaste"
|
||||
/>
|
||||
|
||||
<div flex="~" gap-2>
|
||||
<button hover:bg-active p2 rounded-5 @click="pickAttachments">
|
||||
<div i-ri:upload-line />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div flex="~ col gap-2" max-h-50vh overflow-auto>
|
||||
<publish-attachment
|
||||
v-for="(att, idx) in attachments" :key="att.id"
|
||||
:attachment="att"
|
||||
@remove="removeAttachment(idx)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isUploading" flex gap-2 justify-end items-center>
|
||||
<div op50 i-ri:loader-2-fill animate-spin text-2xl />
|
||||
Uploading...
|
||||
</div>
|
||||
|
||||
<div flex justify-end>
|
||||
<button
|
||||
btn-solid
|
||||
:disabled="!draft.status"
|
||||
:disabled="isUploading || (attachments.length === 0 && !draft.status)"
|
||||
@click="publish"
|
||||
>
|
||||
Publish!
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"@pinia/nuxt": "^0.4.3",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/sanitize-html": "^2.6.2",
|
||||
"@types/wicg-file-system-access": "^2020.9.5",
|
||||
"@unocss/nuxt": "^0.46.5",
|
||||
"@vue-macros/nuxt": "^0.0.2",
|
||||
"@vueuse/nuxt": "^9.5.0",
|
||||
|
|
|
@ -9,6 +9,7 @@ specifiers:
|
|||
'@pinia/nuxt': ^0.4.3
|
||||
'@types/fs-extra': ^9.0.13
|
||||
'@types/sanitize-html': ^2.6.2
|
||||
'@types/wicg-file-system-access': ^2020.9.5
|
||||
'@unocss/nuxt': ^0.46.5
|
||||
'@vue-macros/nuxt': ^0.0.2
|
||||
'@vueuse/nuxt': ^9.5.0
|
||||
|
@ -36,6 +37,7 @@ devDependencies:
|
|||
'@pinia/nuxt': 0.4.3_typescript@4.9.3
|
||||
'@types/fs-extra': 9.0.13
|
||||
'@types/sanitize-html': 2.6.2
|
||||
'@types/wicg-file-system-access': 2020.9.5
|
||||
'@unocss/nuxt': 0.46.5
|
||||
'@vue-macros/nuxt': 0.0.2_nuxt@3.0.0
|
||||
'@vueuse/nuxt': 9.5.0_nuxt@3.0.0
|
||||
|
@ -1297,6 +1299,10 @@ packages:
|
|||
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
|
||||
dev: true
|
||||
|
||||
/@types/wicg-file-system-access/2020.9.5:
|
||||
resolution: {integrity: sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==}
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/eslint-plugin/5.42.1_a76fwnskzo2n3ynumjbqxmyj7i:
|
||||
resolution: {integrity: sha512-LyR6x784JCiJ1j6sH5Y0K6cdExqCCm8DJUTcwG5ThNXJj/G8o5E56u5EdG4SLy+bZAwZBswC+GYn3eGdttBVCg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="@types/wicg-file-system-access" />
|
Loading…
Reference in New Issue