From 9bd5f887de6515f93c7db48d7d1370898b2d7b78 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 7 Jun 2025 07:47:43 +0900 Subject: enhance(frontend): 投稿フォームにアップローダーを埋め込み (#16173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * Update MkPostForm.vue * wip * wip * Update MkPostForm.vue * wip * wip * add tip * Update tips.ts * Update MkPostForm.vue --- packages/frontend/src/components/MkPostForm.vue | 97 ++- .../frontend/src/components/MkPostFormDialog.vue | 15 +- .../frontend/src/components/MkUploaderDialog.vue | 676 ++------------------- .../frontend/src/components/MkUploaderItems.vue | 196 ++++++ 4 files changed, 323 insertions(+), 661 deletions(-) create mode 100644 packages/frontend/src/components/MkUploaderItems.vue (limited to 'packages/frontend/src/components') diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index cd4fabea02..46893a0752 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -72,24 +72,29 @@ SPDX-License-Identifier: AGPL-3.0-only +
+ + {{ i18n.ts._postForm.uploaderTip }} + + +
- + + - - - + +
- - +
@@ -105,10 +110,12 @@ import * as Misskey from 'misskey-js'; import insertTextAtCursor from 'insert-text-at-cursor'; import { toASCII } from 'punycode.js'; import { host, url } from '@@/js/config.js'; +import MkUploaderItems from './MkUploaderItems.vue'; import type { ShallowRef } from 'vue'; import type { PostFormProps } from '@/types/post-form.js'; import type { MenuItem } from '@/types/menu.js'; import type { PollEditorModelValue } from '@/components/MkPollEditor.vue'; +import type { UploaderItem } from '@/composables/use-uploader.js'; import MkNotePreview from '@/components/MkNotePreview.vue'; import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; import XTextCounter from '@/components/MkPostForm.TextCounter.vue'; @@ -120,7 +127,7 @@ import { formatTimeString } from '@/utility/format-time-string.js'; import { Autocomplete } from '@/utility/autocomplete.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; -import { selectFile } from '@/utility/drive.js'; +import { chooseDriveFile } from '@/utility/drive.js'; import { store } from '@/store.js'; import MkInfo from '@/components/MkInfo.vue'; import { i18n } from '@/i18n.js'; @@ -138,6 +145,7 @@ import { getPluginHandlers } from '@/plugin.js'; import { DI } from '@/di.js'; import { globalEvents } from '@/events.js'; import { checkDragDataType, getDragData } from '@/drag-and-drop.js'; +import { useUploader } from '@/composables/use-uploader.js'; const $i = ensureSignin(); @@ -201,6 +209,15 @@ const justEndedComposition = ref(false); const renoteTargetNote: ShallowRef = shallowRef(props.renote); const postFormActions = getPluginHandlers('post_form_action'); +const uploader = useUploader({ + multiple: true, +}); + +uploader.events.on('itemUploaded', ctx => { + files.value.push(ctx.item.uploaded!); + uploader.removeItem(ctx.item); +}); + const draftKey = computed((): string => { let key = props.channel ? `channel:${props.channel.id}` : ''; @@ -258,10 +275,11 @@ const cwTextLength = computed((): number => { const maxCwTextLength = 100; const canPost = computed((): boolean => { - return !props.mock && !posting.value && !posted.value && + return !props.mock && !posting.value && !posted.value && !uploader.uploading.value && (uploader.items.value.length === 0 || uploader.readyForUpload.value) && ( 1 <= textLength.value || 1 <= files.value.length || + 1 <= uploader.items.value.length || poll.value != null || renoteTargetNote.value != null || quoteId.value != null @@ -434,17 +452,20 @@ function focus() { } } -function chooseFileFrom(ev) { +function chooseFileFromPc(ev: MouseEvent) { if (props.mock) return; - selectFile({ - anchorElement: ev.currentTarget ?? ev.target, - multiple: true, - label: i18n.ts.attachFile, - }).then(files_ => { - for (const file of files_) { - files.value.push(file); - } + os.chooseFileFromPc({ multiple: true }).then(files => { + if (files.length === 0) return; + uploader.addFiles(files); + }); +} + +function chooseFileFromDrive(ev: MouseEvent) { + if (props.mock) return; + + chooseDriveFile({ multiple: true }).then(driveFiles => { + files.value.push(...driveFiles); }); } @@ -571,6 +592,10 @@ function showOtherSettings() { toggleReactionAcceptance(); }, }, { type: 'divider' }, { + type: 'switch', + text: i18n.ts.preview, + ref: showPreview, + }, { icon: 'ti ti-trash', text: i18n.ts.reset, danger: true, @@ -797,6 +822,15 @@ function isAnnoying(text: string): boolean { text.includes('$[position'); } +async function uploadFiles() { + await uploader.upload(); + + for (const uploadedItem of uploader.items.value.filter(x => x.uploaded != null)) { + files.value.push(uploadedItem.uploaded!); + uploader.removeItem(uploadedItem); + } +} + async function post(ev?: MouseEvent) { if (ev) { const el = (ev.currentTarget ?? ev.target) as HTMLElement | null; @@ -840,6 +874,10 @@ async function post(ev?: MouseEvent) { } } + if (uploader.items.value.some(x => x.uploaded == null)) { + await uploadFiles(); + } + let postData = { text: text.value === '' ? null : text.value, fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined, @@ -1043,6 +1081,16 @@ function openAccountMenu(ev: MouseEvent) { }, ev); } +function showPerUploadItemMenu(item: UploaderItem, ev: MouseEvent) { + const menu = uploader.getMenu(item); + os.popupMenu(menu, ev.currentTarget ?? ev.target); +} + +function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) { + const menu = uploader.getMenu(item); + os.contextMenu(menu, ev); +} + onMounted(() => { if (props.autofocus) { focus(); @@ -1111,8 +1159,23 @@ onMounted(() => { }); }); +async function canClose() { + if (!uploader.allItemsUploaded.value) { + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.ts._postForm.quitInspiteOfThereAreUnuploadedFilesConfirm, + okText: i18n.ts.yes, + cancelText: i18n.ts.no, + }); + if (canceled) return false; + } + + return true; +} + defineExpose({ clear, + canClose, }); diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue index c467e29df6..0a655bab99 100644 --- a/packages/frontend/src/components/MkPostFormDialog.vue +++ b/packages/frontend/src/components/MkPostFormDialog.vue @@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -43,6 +43,7 @@ const emit = defineEmits<{ }>(); const modal = useTemplateRef('modal'); +const form = useTemplateRef('form'); function onPosted() { modal.value?.close({ @@ -50,6 +51,12 @@ function onPosted() { }); } +async function _close() { + const canClose = await form.value?.canClose(); + if (!canClose) return; + modal.value?.close(); +} + function onModalClosed() { emit('closed'); } diff --git a/packages/frontend/src/components/MkUploaderDialog.vue b/packages/frontend/src/components/MkUploaderDialog.vue index 6a0909bded..ce098d71e4 100644 --- a/packages/frontend/src/components/MkUploaderDialog.vue +++ b/packages/frontend/src/components/MkUploaderDialog.vue @@ -23,37 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._uploader.tip }} -
-
-
-
- -
-
-
-
{{ ctx.name }}
-
- {{ ctx.file.type }} - ({{ i18n.tsx._uploader.compressedToX({ x: bytes(ctx.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - ctx.compressedSize / ctx.file.size) * 100) }) }}) - {{ bytes(ctx.file.size) }} -
-
-
-
-
- - - -
-
-
-
+
@@ -69,8 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/packages/frontend/src/components/MkUploaderItems.vue b/packages/frontend/src/components/MkUploaderItems.vue new file mode 100644 index 0000000000..2d624cf344 --- /dev/null +++ b/packages/frontend/src/components/MkUploaderItems.vue @@ -0,0 +1,196 @@ + + + + + + + -- cgit v1.2.3-freya