feat: support attachment uploading
This commit is contained in:
parent
de59800c2b
commit
d79011e39a
|
@ -2,7 +2,7 @@
|
||||||
<div flex="~ col" items-center>
|
<div flex="~ col" items-center>
|
||||||
<div i-ri:forbid-line text-10 mt10 mb2 />
|
<div i-ri:forbid-line text-10 mt10 mb2 />
|
||||||
<div text-lg>
|
<div text-lg>
|
||||||
<slot>Not found</slot>
|
<slot>404 Not Found</slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
24
components/publish/PublishAttachment.vue
Normal file
24
components/publish/PublishAttachment.vue
Normal file
|
@ -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">
|
<script setup lang="ts">
|
||||||
import type { CreateStatusParamsWithStatus } from 'masto'
|
import type { Attachment, CreateStatusParams, CreateStatusParamsWithStatus } from 'masto'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
draftKey,
|
draftKey,
|
||||||
|
@ -19,13 +19,65 @@ function getDefaultStatus(): CreateStatusParamsWithStatus {
|
||||||
inReplyToId,
|
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() {
|
async function publish() {
|
||||||
try {
|
try {
|
||||||
isSending = true
|
isSending = true
|
||||||
await masto.statuses.create(draft.value)
|
await masto.statuses.create(status)
|
||||||
draft.value = getDefaultStatus()
|
draft = getDefaultStatus()
|
||||||
|
attachments = []
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
isSending = false
|
isSending = false
|
||||||
|
@ -33,8 +85,9 @@ async function publish() {
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (!draft.value.status) {
|
if (!draft.status) {
|
||||||
draft.value = undefined
|
// @ts-expect-error draft cannot be undefined
|
||||||
|
draft = undefined
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
localStorage.removeItem(storageKey)
|
localStorage.removeItem(storageKey)
|
||||||
})
|
})
|
||||||
|
@ -44,7 +97,7 @@ onUnmounted(() => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
flex flex-col gap-4
|
flex flex-col gap-3
|
||||||
:class="isSending ? 'pointer-events-none' : ''"
|
:class="isSending ? 'pointer-events-none' : ''"
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
|
@ -52,11 +105,32 @@ onUnmounted(() => {
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
p2 border-rounded w-full h-40
|
p2 border-rounded w-full h-40
|
||||||
bg-gray:10 outline-none border="~ base"
|
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>
|
<div flex justify-end>
|
||||||
<button
|
<button
|
||||||
btn-solid
|
btn-solid
|
||||||
:disabled="!draft.status"
|
:disabled="isUploading || (attachments.length === 0 && !draft.status)"
|
||||||
@click="publish"
|
@click="publish"
|
||||||
>
|
>
|
||||||
Publish!
|
Publish!
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
"@pinia/nuxt": "^0.4.3",
|
"@pinia/nuxt": "^0.4.3",
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/fs-extra": "^9.0.13",
|
||||||
"@types/sanitize-html": "^2.6.2",
|
"@types/sanitize-html": "^2.6.2",
|
||||||
|
"@types/wicg-file-system-access": "^2020.9.5",
|
||||||
"@unocss/nuxt": "^0.46.5",
|
"@unocss/nuxt": "^0.46.5",
|
||||||
"@vue-macros/nuxt": "^0.0.2",
|
"@vue-macros/nuxt": "^0.0.2",
|
||||||
"@vueuse/nuxt": "^9.5.0",
|
"@vueuse/nuxt": "^9.5.0",
|
||||||
|
|
|
@ -9,6 +9,7 @@ specifiers:
|
||||||
'@pinia/nuxt': ^0.4.3
|
'@pinia/nuxt': ^0.4.3
|
||||||
'@types/fs-extra': ^9.0.13
|
'@types/fs-extra': ^9.0.13
|
||||||
'@types/sanitize-html': ^2.6.2
|
'@types/sanitize-html': ^2.6.2
|
||||||
|
'@types/wicg-file-system-access': ^2020.9.5
|
||||||
'@unocss/nuxt': ^0.46.5
|
'@unocss/nuxt': ^0.46.5
|
||||||
'@vue-macros/nuxt': ^0.0.2
|
'@vue-macros/nuxt': ^0.0.2
|
||||||
'@vueuse/nuxt': ^9.5.0
|
'@vueuse/nuxt': ^9.5.0
|
||||||
|
@ -36,6 +37,7 @@ devDependencies:
|
||||||
'@pinia/nuxt': 0.4.3_typescript@4.9.3
|
'@pinia/nuxt': 0.4.3_typescript@4.9.3
|
||||||
'@types/fs-extra': 9.0.13
|
'@types/fs-extra': 9.0.13
|
||||||
'@types/sanitize-html': 2.6.2
|
'@types/sanitize-html': 2.6.2
|
||||||
|
'@types/wicg-file-system-access': 2020.9.5
|
||||||
'@unocss/nuxt': 0.46.5
|
'@unocss/nuxt': 0.46.5
|
||||||
'@vue-macros/nuxt': 0.0.2_nuxt@3.0.0
|
'@vue-macros/nuxt': 0.0.2_nuxt@3.0.0
|
||||||
'@vueuse/nuxt': 9.5.0_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==}
|
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/wicg-file-system-access/2020.9.5:
|
||||||
|
resolution: {integrity: sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@typescript-eslint/eslint-plugin/5.42.1_a76fwnskzo2n3ynumjbqxmyj7i:
|
/@typescript-eslint/eslint-plugin/5.42.1_a76fwnskzo2n3ynumjbqxmyj7i:
|
||||||
resolution: {integrity: sha512-LyR6x784JCiJ1j6sH5Y0K6cdExqCCm8DJUTcwG5ThNXJj/G8o5E56u5EdG4SLy+bZAwZBswC+GYn3eGdttBVCg==}
|
resolution: {integrity: sha512-LyR6x784JCiJ1j6sH5Y0K6cdExqCCm8DJUTcwG5ThNXJj/G8o5E56u5EdG4SLy+bZAwZBswC+GYn3eGdttBVCg==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
|
1
shims.d.ts
vendored
Normal file
1
shims.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="@types/wicg-file-system-access" />
|
Loading…
Reference in a new issue