From a7339c7b7b1b4ac299addac7f590dd12f0c4a670 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 28 Mar 2025 10:12:18 +0900 Subject: [PATCH 01/62] wip --- packages/backend/src/core/DriveService.ts | 17 +++++++- .../backend/src/server/api/endpoint-list.ts | 1 + .../api/endpoints/drive/files/move-bulk.ts | 43 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 1550fe3d3c..097dfaea87 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -8,7 +8,7 @@ import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import sharp from 'sharp'; import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; -import { IsNull } from 'typeorm'; +import { In, IsNull } from 'typeorm'; import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js'; @@ -713,6 +713,21 @@ export class DriveService { return fileObj; } + @bindThis + public async moveFiles(fileIds: MiDriveFile['id'][], folderId: MiDriveFolder['id'] | null, userId: MiUser['id']) { + const folder = folderId ? await this.driveFoldersRepository.findOneByOrFail({ + id: folderId, + userId: userId, + }) : null; + + await this.driveFilesRepository.update({ + id: In(fileIds), + userId: userId, + }, { + folderId: folder ? folder.id : null, + }); + } + @bindThis public async deleteFile(file: MiDriveFile, isExpired = false, deleter?: MiUser) { if (file.storedInternal) { diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index 34aaef3cc7..eda7a4446d 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -169,6 +169,7 @@ export * as 'drive/files/find' from './endpoints/drive/files/find.js'; export * as 'drive/files/find-by-hash' from './endpoints/drive/files/find-by-hash.js'; export * as 'drive/files/show' from './endpoints/drive/files/show.js'; export * as 'drive/files/update' from './endpoints/drive/files/update.js'; +export * as 'drive/files/move-bulk' from './endpoints/drive/files/move-bulk.js'; export * as 'drive/files/upload-from-url' from './endpoints/drive/files/upload-from-url.js'; export * as 'drive/folders' from './endpoints/drive/folders.js'; export * as 'drive/folders/create' from './endpoints/drive/folders/create.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts b/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts new file mode 100644 index 0000000000..79e505575b --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { DriveService } from '@/core/DriveService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['drive'], + + requireCredential: true, + + kind: 'write:drive', + + errors: { + }, + + res: {}, +} as const; + +export const paramDef = { + type: 'object', + properties: { + fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 100, items: { type: 'string', format: 'misskey:id' } }, + folderId: { type: 'string', format: 'misskey:id', nullable: true }, + }, + required: ['fileIds'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private driveService: DriveService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.driveService.moveFiles(ps.fileIds, ps.folderId ?? null, me.id); + }); + } +} From 8ce2f43722f05eba925f391117fc334fcae62dd6 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 10 May 2025 12:56:19 +0900 Subject: [PATCH 02/62] wip --- packages/frontend/src/components/MkDrive.vue | 168 ++++++------------- 1 file changed, 53 insertions(+), 115 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index a1f76ac563..d7430439f8 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -68,23 +68,27 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.loadMore }} -
- - -
- {{ i18n.ts.loadMore }} + +
+
+
+ {{ getSeparatorInfo(filesPaginator.items.value[i -1].createdAt, file.createdAt).prevText }} + + {{ getSeparatorInfo(filesPaginator.items.value[i -1].createdAt, file.createdAt).nextText }} +
+ +
+ {{ i18n.ts.loadMore }}
{{ i18n.ts['empty-draghover'] }}
@@ -99,7 +103,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/composables/use-pagination.ts b/packages/frontend/src/composables/use-pagination.ts index 8dd3b888db..9325caeadc 100644 --- a/packages/frontend/src/composables/use-pagination.ts +++ b/packages/frontend/src/composables/use-pagination.ts @@ -39,6 +39,7 @@ export type PagingCtx(props: { ctx: PagingCtx; + autoInit?: boolean; useShallowRef?: boolean; }) { const items = props.useShallowRef ? shallowRef([]) : ref([]); @@ -232,9 +233,11 @@ export function usePagination { - init(); - }); + if (props.autoInit !== false) { + onMounted(() => { + init(); + }); + } return { items: items as DeepReadonly>, From 293aa9512cc5bd13fc2e3d4e28f97ba27088b50e Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 10 May 2025 14:59:50 +0900 Subject: [PATCH 05/62] Update MkDrive.vue --- packages/frontend/src/components/MkDrive.vue | 45 +++++++++++--------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index d861f40f42..23a90eab52 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -142,6 +142,23 @@ const emit = defineEmits<{ (ev: 'open-folder', v: Misskey.entities.DriveFolder): void; }>(); +const folder = ref(null); +const hierarchyFolders = ref([]); +const selectedFiles = ref([]); +const selectedFolders = ref([]); +const uploadings = uploads; + +// ドロップされようとしているか +const draghover = ref(false); + +// 自身の所有するアイテムがドラッグをスタートさせたか +// (自分自身の階層にドロップできないようにするためのフラグ) +const isDragSource = ref(false); + +const fetching = ref(true); + +const sortModeSelect = ref>('+createdAt'); + const filesPaginator = usePagination({ ctx: { endpoint: 'drive/files', @@ -166,23 +183,6 @@ const foldersPaginator = usePagination({ autoInit: false, }); -const folder = ref(null); -const hierarchyFolders = ref([]); -const selectedFiles = ref([]); -const selectedFolders = ref([]); -const uploadings = uploads; - -// ドロップされようとしているか -const draghover = ref(false); - -// 自身の所有するアイテムがドラッグをスタートさせたか -// (自分自身の階層にドロップできないようにするためのフラグ) -const isDragSource = ref(false); - -const fetching = ref(true); - -const sortModeSelect = ref>('+createdAt'); - watch(folder, () => emit('cd', folder.value)); watch(sortModeSelect, () => { initialize(); @@ -627,8 +627,13 @@ function closeTip() { store.set('readDriveTip', true); } +let connection: Misskey.ChannelConnection | null = null; + onMounted(() => { - connection.on('fileCreated', onStreamDriveFileCreated); + if (store.s.realtimeMode) { + connection = useStream().useChannel('drive'); + connection.on('fileCreated', onStreamDriveFileCreated); + } if (props.initialFolder) { move(props.initialFolder); @@ -641,7 +646,9 @@ onActivated(() => { }); onBeforeUnmount(() => { - connection.dispose(); + if (connection != null) { + connection.dispose(); + } }); From f25bc9bd95bdf437f9e4535d9c0d9416b03bd506 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 10 May 2025 15:03:09 +0900 Subject: [PATCH 06/62] Update MkDrive.vue --- packages/frontend/src/components/MkDrive.vue | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 23a90eab52..03b1f17c78 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -263,7 +263,7 @@ function onDrop(ev: DragEvent) { const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); - if (files.value.some(f => f.id === file.id)) return; + if (filesPaginator.items.value.some(f => f.id === file.id)) return; removeFile(file.id); misskeyApi('drive/files/update', { fileId: file.id, @@ -279,7 +279,7 @@ function onDrop(ev: DragEvent) { // 移動先が自分自身ならreject if (folder.value && droppedFolder.id === folder.value.id) return false; - if (folders.value.some(f => f.id === droppedFolder.id)) return false; + if (foldersPaginator.items.value.some(f => f.id === droppedFolder.id)) return false; removeFolder(droppedFolder.id); misskeyApi('drive/folders/update', { folderId: droppedFolder.id, @@ -463,16 +463,16 @@ function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) { const current = folder.value ? folder.value.id : null; if (current !== folderToAdd.parentId) return; - if (folders.value.some(f => f.id === folderToAdd.id)) { - const exist = folders.value.map(f => f.id).indexOf(folderToAdd.id); - folders.value[exist] = folderToAdd; + if (foldersPaginator.items.value.some(f => f.id === folderToAdd.id)) { + const exist = foldersPaginator.items.value.map(f => f.id).indexOf(folderToAdd.id); + foldersPaginator.items.value[exist] = folderToAdd; return; } if (unshift) { - folders.value.unshift(folderToAdd); + foldersPaginator.items.value.unshift(folderToAdd); } else { - folders.value.push(folderToAdd); + foldersPaginator.items.value.push(folderToAdd); } } @@ -480,27 +480,27 @@ function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) { const current = folder.value ? folder.value.id : null; if (current !== fileToAdd.folderId) return; - if (files.value.some(f => f.id === fileToAdd.id)) { - const exist = files.value.map(f => f.id).indexOf(fileToAdd.id); - files.value[exist] = fileToAdd; + if (filesPaginator.items.value.some(f => f.id === fileToAdd.id)) { + const exist = filesPaginator.items.value.map(f => f.id).indexOf(fileToAdd.id); + filesPaginator.items.value[exist] = fileToAdd; return; } if (unshift) { - files.value.unshift(fileToAdd); + filesPaginator.items.value.unshift(fileToAdd); } else { - files.value.push(fileToAdd); + filesPaginator.items.value.push(fileToAdd); } } function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) { const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove; - folders.value = folders.value.filter(f => f.id !== folderIdToRemove); + foldersPaginator.items.value = foldersPaginator.items.value.filter(f => f.id !== folderIdToRemove); } function removeFile(file: Misskey.entities.DriveFile | string) { const fileId = typeof file === 'object' ? file.id : file; - files.value = files.value.filter(f => f.id !== fileId); + filesPaginator.items.value = filesPaginator.items.value.filter(f => f.id !== fileId); } function appendFile(file: Misskey.entities.DriveFile) { From c98d72e6e1a439e2f00421cb6854d851c8856bcc Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 10 May 2025 15:28:06 +0900 Subject: [PATCH 07/62] wip --- packages/frontend/src/components/MkDrive.vue | 62 ++++++++----------- .../src/utility/timeline-date-separate.ts | 62 ++++++++++++++----- 2 files changed, 74 insertions(+), 50 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 03b1f17c78..81f25f115f 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -69,25 +69,28 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.loadMore }}
-
-
-
- {{ getSeparatorInfo(filesPaginator.items.value[i -1].createdAt, file.createdAt).prevText }} - - {{ getSeparatorInfo(filesPaginator.items.value[i -1].createdAt, file.createdAt).nextText }} +
+ + + +
+
- -
+ {{ i18n.ts.loadMore }}
@@ -121,7 +124,7 @@ import { claimAchievement } from '@/utility/achievements.js'; import { prefer } from '@/preferences.js'; import { chooseFileFromPc } from '@/utility/select-file.js'; import { store } from '@/store.js'; -import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js'; +import { isSeparatorNeeded, getSeparatorInfo, makeDateGroupedTimelineComputedRef } from '@/utility/timeline-date-separate.js'; import { usePagination } from '@/composables/use-pagination.js'; const props = withDefaults(defineProps<{ @@ -183,6 +186,8 @@ const foldersPaginator = usePagination({ autoInit: false, }); +const filesTimeline = makeDateGroupedTimelineComputedRef(filesPaginator.items, 'month'); + watch(folder, () => emit('cd', folder.value)); watch(sortModeSelect, () => { initialize(); @@ -725,22 +730,9 @@ onBeforeUnmount(() => { .folders, .files { - display: flex; - flex-wrap: wrap; -} - -.folder, -.file { - flex-grow: 1; - width: 128px; - margin: 4px; - box-sizing: border-box; -} - -.padding { - flex-grow: 1; - pointer-events: none; - width: 128px + 8px; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + grid-gap: 12px; } .empty { diff --git a/packages/frontend/src/utility/timeline-date-separate.ts b/packages/frontend/src/utility/timeline-date-separate.ts index 1071a80962..33ddea048b 100644 --- a/packages/frontend/src/utility/timeline-date-separate.ts +++ b/packages/frontend/src/utility/timeline-date-separate.ts @@ -4,7 +4,7 @@ */ import { computed } from 'vue'; -import type { Ref } from 'vue'; +import type { Ref, ShallowRef } from 'vue'; export function getDateText(dateInstance: Date) { const date = dateInstance.getDate(); @@ -12,19 +12,6 @@ export function getDateText(dateInstance: Date) { return `${month.toString()}/${date.toString()}`; } -export type DateSeparetedTimelineItem = { - id: string; - type: 'item'; - data: T; -} | { - id: string; - type: 'date'; - prev: Date; - prevText: string; - next: Date; - nextText: string; -}; - // TODO: いちいちDateインスタンス作成するのは無駄感あるから文字列のまま解析したい export function isSeparatorNeeded( prev: string | null, @@ -56,7 +43,20 @@ export function getSeparatorInfo( }; } -export function makeDateSeparatedTimelineComputedRef(items: Ref) { +export type DateSeparetedTimelineItem = { + id: string; + type: 'item'; + data: T; +} | { + id: string; + type: 'date'; + prev: Date; + prevText: string; + next: Date; + nextText: string; +}; + +export function makeDateSeparatedTimelineComputedRef(items: Ref | ShallowRef) { return computed[]>(() => { const tl: DateSeparetedTimelineItem[] = []; for (let i = 0; i < items.value.length; i++) { @@ -92,3 +92,35 @@ export function makeDateSeparatedTimelineComputedRef = { + date: Date; + items: T[]; +}; + +export function makeDateGroupedTimelineComputedRef(items: Ref | ShallowRef, span: 'day' | 'month' = 'day') { + return computed[]>(() => { + const tl: DateGroupedTimelineItem[] = []; + for (let i = 0; i < items.value.length; i++) { + const item = items.value[i]; + const date = new Date(item.createdAt); + const nextDate = items.value[i + 1] ? new Date(items.value[i + 1].createdAt) : null; + + if (tl.length === 0 || ( + span === 'day' && tl[tl.length - 1].date.getTime() !== date.getTime() + ) || ( + span === 'month' && ( + tl[tl.length - 1].date.getFullYear() !== date.getFullYear() || + tl[tl.length - 1].date.getMonth() !== date.getMonth() + ) + )) { + tl.push({ + date, + items: [], + }); + } + tl[tl.length - 1].items.push(item); + } + return tl; + }); +} From 796b0c3706a765fe062c87ea30506e279bd13739 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 10 May 2025 15:35:41 +0900 Subject: [PATCH 08/62] Update MkDrive.vue --- packages/frontend/src/components/MkDrive.vue | 122 ++++++++++--------- 1 file changed, 63 insertions(+), 59 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 81f25f115f..115f753aab 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -44,64 +44,62 @@ SPDX-License-Identifier: AGPL-3.0-only @drop.prevent.stop="onDrop" @contextmenu.stop="onContextmenu" > -
-
-
- - -
- {{ i18n.ts.loadMore }} -
- -
- - - -
- -
-
- {{ i18n.ts.loadMore }} -
- -
-
{{ i18n.ts['empty-draghover'] }}
-
{{ i18n.ts.emptyDrive }}
{{ i18n.ts['empty-drive-description'] }}
-
{{ i18n.ts.emptyFolder }}
-
+
+
+ + +
+ {{ i18n.ts.loadMore }} +
+ +
+ + + +
+ +
+
+ {{ i18n.ts.loadMore }} +
+ +
+
{{ i18n.ts['empty-draghover'] }}
+
{{ i18n.ts.emptyDrive }}
{{ i18n.ts['empty-drive-description'] }}
+
{{ i18n.ts.emptyFolder }}
-
+
@@ -712,9 +710,6 @@ onBeforeUnmount(() => { } .main { - flex: 1; - overflow: auto; - padding: var(--MI-margin); user-select: none; &.fetching { @@ -733,6 +728,15 @@ onBeforeUnmount(() => { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-gap: 12px; + padding: var(--MI-margin); +} + +.date { + padding: 8px 16px; + font-size: 90%; + -webkit-backdrop-filter: var(--MI-blur, blur(8px)); + backdrop-filter: var(--MI-blur, blur(8px)); + background-color: color(from var(--MI_THEME-bg) srgb r g b / 0.85); } .empty { From 9d6eb954b7feb2fd87d00178f500833448f9233e Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 10 May 2025 15:40:46 +0900 Subject: [PATCH 09/62] Update MkDrive.vue --- packages/frontend/src/components/MkDrive.vue | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 115f753aab..35aed7c202 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -45,30 +45,30 @@ SPDX-License-Identifier: AGPL-3.0-only @contextmenu.stop="onContextmenu" >
-
- - -
- {{ i18n.ts.loadMore }} +
+
+ +
+ {{ i18n.ts.loadMore }}
-
+
-
-
-
-
- -
- {{ i18n.ts.loadMore }} -
- -
- - - -
- +
+
+
+
+
- - {{ i18n.ts.loadMore }} -
+ {{ i18n.ts.loadMore }} +
-
-
{{ i18n.ts['empty-draghover'] }}
-
{{ i18n.ts.emptyDrive }}
{{ i18n.ts['empty-drive-description'] }}
-
{{ i18n.ts.emptyFolder }}
+
+ + + +
+ +
+
+ {{ i18n.ts.loadMore }} +
+ +
+
{{ i18n.ts['empty-draghover'] }}
+
{{ i18n.ts.emptyDrive }}
{{ i18n.ts['empty-drive-description'] }}
+
{{ i18n.ts.emptyFolder }}
+
+ +
- -
+ +
@@ -156,6 +164,8 @@ const draghover = ref(false); // (自分自身の階層にドロップできないようにするためのフラグ) const isDragSource = ref(false); +const isEditMode = ref(false); + const fetching = ref(true); const sortModeSelect = ref>('+createdAt'); @@ -171,6 +181,7 @@ const filesPaginator = usePagination({ })), }, autoInit: false, + autoReInit: false, }); const foldersPaginator = usePagination({ @@ -182,6 +193,7 @@ const foldersPaginator = usePagination({ })), }, autoInit: false, + autoReInit: false, }); const filesTimeline = makeDateGroupedTimelineComputedRef(filesPaginator.items, 'month'); @@ -613,6 +625,11 @@ function getMenu() { text: i18n.ts.createFolder, icon: 'ti ti-folder-plus', action: () => { createFolder(); }, + }, { type: 'divider' }, { + type: 'switch', + text: i18n.ts.edit, + icon: 'ti ti-pointer', + ref: isEditMode, }); return menu; @@ -739,6 +756,14 @@ onBeforeUnmount(() => { background-color: color(from var(--MI_THEME-bg) srgb r g b / 0.85); } +.footer { + padding: 8px 16px; + font-size: 90%; + -webkit-backdrop-filter: var(--MI-blur, blur(8px)); + backdrop-filter: var(--MI-blur, blur(8px)); + background-color: color(from var(--MI_THEME-bg) srgb r g b / 0.85); +} + .empty { padding: 16px; text-align: center; diff --git a/packages/frontend/src/composables/use-pagination.ts b/packages/frontend/src/composables/use-pagination.ts index 9325caeadc..cf8543c16a 100644 --- a/packages/frontend/src/composables/use-pagination.ts +++ b/packages/frontend/src/composables/use-pagination.ts @@ -40,6 +40,7 @@ export type PagingCtx(props: { ctx: PagingCtx; autoInit?: boolean; + autoReInit?: boolean; useShallowRef?: boolean; }) { const items = props.useShallowRef ? shallowRef([]) : ref([]); @@ -50,8 +51,9 @@ export function usePagination [props.ctx.endpoint, props.ctx.params], init, { deep: true }); + if (props.autoReInit !== false) { + watch(() => [props.ctx.endpoint, props.ctx.params], init, { deep: true }); + } function getNewestId(): string | null | undefined { // 様々な要因により並び順は保証されないのでソートが必要 From 5149e1c28535885dccd26d74ffb2b120ad78e591 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 10 May 2025 17:08:25 +0900 Subject: [PATCH 11/62] Update MkDrive.vue --- packages/frontend/src/components/MkDrive.vue | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 3407805660..7c618110ad 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -83,7 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only :class="$style.file" :file="file" :folder="folder" - :selectMode="select === 'file'" + :selectMode="select === 'file' || isEditMode" :isSelected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile" @dragstart="isDragSource = true" @@ -405,6 +405,16 @@ function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null function chooseFile(file: Misskey.entities.DriveFile) { const isAlreadySelected = selectedFiles.value.some(f => f.id === file.id); + + if (isEditMode.value) { + if (isAlreadySelected) { + selectedFiles.value = selectedFiles.value.filter(f => f.id !== file.id); + } else { + selectedFiles.value.push(file); + } + return; + } + if (props.multiple) { if (isAlreadySelected) { selectedFiles.value = selectedFiles.value.filter(f => f.id !== file.id); From 6d5e9506e4483b99a850fbab9874f8525dd47c4d Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 10 May 2025 17:53:54 +0900 Subject: [PATCH 12/62] wip --- .../frontend/src/components/MkDrive.file.vue | 16 ++++++++-------- packages/frontend/src/components/MkDrive.vue | 13 ++++++++++++- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index 388b8b56d7..0bb97c40ed 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -114,7 +114,7 @@ function onDragend() { &:hover { background: rgba(#000, 0.05); - > .label { + .label { &::before, &::after { background: #0b65a5; @@ -132,7 +132,7 @@ function onDragend() { &:active { background: rgba(#000, 0.1); - > .label { + .label { &::before, &::after { background: #0b588c; @@ -158,19 +158,19 @@ function onDragend() { background: hsl(from var(--MI_THEME-accent) h s calc(l - 10)); } - > .label { + .label { &::before, &::after { display: none; } } - > .name { - color: #fff; + .name { + color: var(--MI_THEME-fgOnAccent); } - > .thumbnail { - color: #fff; + .thumbnail { + color: var(--MI_THEME-fgOnAccent); } } } @@ -241,7 +241,7 @@ function onDragend() { .name { display: block; margin: 8px 0 0 0; - font-size: 85%; + font-size: 82%; text-align: center; word-break: break-all; color: var(--MI_THEME-fg); diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 7c618110ad..7a283120da 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -106,7 +106,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -484,6 +484,17 @@ function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFold }); } +async function moveFilesBulk() { + if (selectedFiles.value.length === 0) return; + + const toFolder = await os.selectDriveFolder(false); + + os.apiWithDialog('drive/files/move-bulk', { + fileIds: selectedFiles.value.map(f => f.id), + folderId: toFolder[0] ? toFolder[0].id : null, + }); +} + function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) { const current = folder.value ? folder.value.id : null; if (current !== folderToAdd.parentId) return; From 90ac02931736598083c30067219c45b4f461599b Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 10 May 2025 18:18:05 +0900 Subject: [PATCH 13/62] wip --- .../src/components/MkDrive.folder.vue | 2 +- packages/frontend/src/components/MkDrive.vue | 54 +++++++++++----- ...> MkDriveFileSelectDialog.stories.impl.ts} | 2 +- ...Dialog.vue => MkDriveFileSelectDialog.vue} | 20 +++--- .../components/MkDriveFolderSelectDialog.vue | 63 +++++++++++++++++++ packages/frontend/src/os.ts | 10 ++- .../frontend/src/pages/drive.file.info.vue | 2 +- .../frontend/src/pages/settings/drive.vue | 2 +- .../src/utility/get-drive-file-menu.ts | 2 +- .../frontend/src/widgets/WidgetSlideshow.vue | 2 +- 10 files changed, 121 insertions(+), 38 deletions(-) rename packages/frontend/src/components/{MkDriveSelectDialog.stories.impl.ts => MkDriveFileSelectDialog.stories.impl.ts} (66%) rename packages/frontend/src/components/{MkDriveSelectDialog.vue => MkDriveFileSelectDialog.vue} (54%) create mode 100644 packages/frontend/src/components/MkDriveFolderSelectDialog.vue diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 9c72691d21..37181f65b6 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -230,7 +230,7 @@ function rename() { } function move() { - os.selectDriveFolder(false).then(folder => { + os.selectDriveFolder().then(folder => { if (folder[0] && folder[0].id === props.folder.id) return; misskeyApi('drive/folders/update', { diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 7a283120da..f215457a78 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -36,6 +36,25 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ + +
+
@@ -134,7 +153,7 @@ import { isSeparatorNeeded, getSeparatorInfo, makeDateGroupedTimelineComputedRef import { usePagination } from '@/composables/use-pagination.js'; const props = withDefaults(defineProps<{ - initialFolder?: Misskey.entities.DriveFolder; + initialFolder?: Misskey.entities.DriveFolder['id'] | null; type?: string; multiple?: boolean; select?: 'file' | 'folder' | null; @@ -144,8 +163,8 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void; - (ev: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void; + (ev: 'changeSelectedFiles', v: Misskey.entities.DriveFile[]): void; + (ev: 'changeSelectedFolders', v: (Misskey.entities.DriveFolder | null)[]): void; (ev: 'move-root'): void; (ev: 'cd', v: Misskey.entities.DriveFolder | null): void; (ev: 'open-folder', v: Misskey.entities.DriveFolder): void; @@ -153,8 +172,6 @@ const emit = defineEmits<{ const folder = ref(null); const hierarchyFolders = ref([]); -const selectedFiles = ref([]); -const selectedFolders = ref([]); const uploadings = uploads; // ドロップされようとしているか @@ -166,6 +183,18 @@ const isDragSource = ref(false); const isEditMode = ref(false); +const selectedFiles = ref([]); +const selectedFolders = ref([]); +const isRootSelected = ref(false); + +watch(selectedFiles, () => { + emit('changeSelectedFiles', selectedFiles.value); +}); + +watch([selectedFolders, isRootSelected], () => { + emit('changeSelectedFolders', isRootSelected.value ? [null, ...selectedFolders.value] : selectedFolders.value); +}); + const fetching = ref(true); const sortModeSelect = ref>('+createdAt'); @@ -403,7 +432,7 @@ function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null }); } -function chooseFile(file: Misskey.entities.DriveFile) { +function onChooseFile(file: Misskey.entities.DriveFile) { const isAlreadySelected = selectedFiles.value.some(f => f.id === file.id); if (isEditMode.value) { @@ -421,13 +450,11 @@ function chooseFile(file: Misskey.entities.DriveFile) { } else { selectedFiles.value.push(file); } - emit('change-selection', selectedFiles.value); } else { if (isAlreadySelected) { - emit('selected', file); + //emit('selected', file); } else { selectedFiles.value = [file]; - emit('change-selection', [file]); } } } @@ -440,20 +467,17 @@ function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) { } else { selectedFolders.value.push(folderToChoose); } - emit('change-selection', selectedFolders.value); } else { if (isAlreadySelected) { - emit('selected', folderToChoose); + //emit('selected', folderToChoose); } else { selectedFolders.value = [folderToChoose]; - emit('change-selection', [folderToChoose]); } } } function unchoseFolder(folderToUnchose: Misskey.entities.DriveFolder) { selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToUnchose.id); - emit('change-selection', selectedFolders.value); } function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id' | 'parentId']) { @@ -487,7 +511,7 @@ function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFold async function moveFilesBulk() { if (selectedFiles.value.length === 0) return; - const toFolder = await os.selectDriveFolder(false); + const toFolder = await os.selectDriveFolder(folder.value ? folder.value.id : null); os.apiWithDialog('drive/files/move-bulk', { fileIds: selectedFiles.value.map(f => f.id), diff --git a/packages/frontend/src/components/MkDriveSelectDialog.stories.impl.ts b/packages/frontend/src/components/MkDriveFileSelectDialog.stories.impl.ts similarity index 66% rename from packages/frontend/src/components/MkDriveSelectDialog.stories.impl.ts rename to packages/frontend/src/components/MkDriveFileSelectDialog.stories.impl.ts index fe8f705165..a5073337cd 100644 --- a/packages/frontend/src/components/MkDriveSelectDialog.stories.impl.ts +++ b/packages/frontend/src/components/MkDriveFileSelectDialog.stories.impl.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import MkDriveSelectDialog from './MkDriveSelectDialog.vue'; +import MkDriveSelectDialog from './MkDriveFileSelectDialog.vue'; void MkDriveSelectDialog; diff --git a/packages/frontend/src/components/MkDriveSelectDialog.vue b/packages/frontend/src/components/MkDriveFileSelectDialog.vue similarity index 54% rename from packages/frontend/src/components/MkDriveSelectDialog.vue rename to packages/frontend/src/components/MkDriveFileSelectDialog.vue index 1b9455e3f3..9cccd4ab5e 100644 --- a/packages/frontend/src/components/MkDriveSelectDialog.vue +++ b/packages/frontend/src/components/MkDriveFileSelectDialog.vue @@ -9,43 +9,41 @@ SPDX-License-Identifier: AGPL-3.0-only :width="800" :height="500" :withOkButton="true" - :okButtonDisabled="(type === 'file') && (selected.length === 0)" + :okButtonDisabled="selected.length === 0" @click="cancel()" @close="cancel()" @ok="ok()" @closed="emit('closed')" > - + diff --git a/packages/frontend/src/components/MkDriveFolderSelectDialog.vue b/packages/frontend/src/components/MkDriveFolderSelectDialog.vue new file mode 100644 index 0000000000..adc85c49e1 --- /dev/null +++ b/packages/frontend/src/components/MkDriveFolderSelectDialog.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index d891525782..9d26242c3c 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -594,8 +594,7 @@ export async function selectUser(opts: { includeSelf?: boolean; localOnly?: bool export async function selectDriveFile(multiple: boolean): Promise { return new Promise(resolve => { - const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), { - type: 'file', + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveFileSelectDialog.vue')), { multiple, }, { done: files => { @@ -608,11 +607,10 @@ export async function selectDriveFile(multiple: boolean): Promise { +export async function selectDriveFolder(initialFolder: Misskey.entities.DriveFolder['id'] | null): Promise { return new Promise(resolve => { - const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), { - type: 'folder', - multiple, + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveFolderSelectDialog.vue')), { + initialFolder, }, { done: folders => { if (folders) { diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue index 21be0b18a9..fec260be72 100644 --- a/packages/frontend/src/pages/drive.file.info.vue +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -139,7 +139,7 @@ function crop() { function move() { if (!file.value) return; - os.selectDriveFolder(false).then(folder => { + os.selectDriveFolder().then(folder => { misskeyApi('drive/files/update', { fileId: file.value.id, folderId: folder[0] ? folder[0].id : null, diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index 2130cbc868..c4619c354d 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -138,7 +138,7 @@ if (prefer.s.uploadFolder) { } function chooseUploadFolder() { - os.selectDriveFolder(false).then(async folder => { + os.selectDriveFolder().then(async folder => { prefer.commit('uploadFolder', folder[0] ? folder[0].id : null); os.success(); if (prefer.s.uploadFolder) { diff --git a/packages/frontend/src/utility/get-drive-file-menu.ts b/packages/frontend/src/utility/get-drive-file-menu.ts index 3c6cbba002..a5595cb0cb 100644 --- a/packages/frontend/src/utility/get-drive-file-menu.ts +++ b/packages/frontend/src/utility/get-drive-file-menu.ts @@ -42,7 +42,7 @@ function describe(file: Misskey.entities.DriveFile) { } function move(file: Misskey.entities.DriveFile) { - os.selectDriveFolder(false).then(folder => { + os.selectDriveFolder().then(folder => { misskeyApi('drive/files/update', { fileId: file.id, folderId: folder[0] ? folder[0].id : null, diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue index 2ccbb7a28f..a83e2754f6 100644 --- a/packages/frontend/src/widgets/WidgetSlideshow.vue +++ b/packages/frontend/src/widgets/WidgetSlideshow.vue @@ -93,7 +93,7 @@ const fetch = () => { }; const choose = () => { - os.selectDriveFolder(false).then(folder => { + os.selectDriveFolder().then(folder => { if (folder[0] == null) { return; } From ade38bfe2f480432be2d35af1d283f63cffbad59 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 10 May 2025 18:28:26 +0900 Subject: [PATCH 14/62] wip --- packages/frontend/src/components/MkDrive.vue | 6 ++- .../src/composables/use-pagination.ts | 44 +++++++++++++------ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index f215457a78..fdf231cd9d 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -85,7 +85,7 @@ SPDX-License-Identifier: AGPL-3.0-only @dragend="isDragSource = false" />
- {{ i18n.ts.loadMore }} + {{ i18n.ts.loadMore }}
@@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only />
- {{ i18n.ts.loadMore }} + {{ i18n.ts.loadMore }}
@@ -203,6 +203,7 @@ const filesPaginator = usePagination({ ctx: { endpoint: 'drive/files', limit: 30, + canFetchDetection: 'limit', params: computed(() => ({ folderId: folder.value ? folder.value.id : null, type: props.type, @@ -217,6 +218,7 @@ const foldersPaginator = usePagination({ ctx: { endpoint: 'drive/folders', limit: 30, + canFetchDetection: 'limit', params: computed(() => ({ folderId: folder.value ? folder.value.id : null, })), diff --git a/packages/frontend/src/composables/use-pagination.ts b/packages/frontend/src/composables/use-pagination.ts index cf8543c16a..b0be8255ef 100644 --- a/packages/frontend/src/composables/use-pagination.ts +++ b/packages/frontend/src/composables/use-pagination.ts @@ -35,6 +35,9 @@ export type PagingCtx(props: { @@ -95,12 +98,20 @@ export function usePagination { fetchingOlder.value = false; From 40c5cd0013c60c5581d77fa5fa77127c30487ddf Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 10 May 2025 20:15:03 +0900 Subject: [PATCH 15/62] wip --- packages/frontend/src/components/MkDrive.vue | 9 ++++++++- packages/frontend/src/events.ts | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index fdf231cd9d..e4caf9aef9 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -151,6 +151,7 @@ import { chooseFileFromPc } from '@/utility/select-file.js'; import { store } from '@/store.js'; import { isSeparatorNeeded, getSeparatorInfo, makeDateGroupedTimelineComputedRef } from '@/utility/timeline-date-separate.js'; import { usePagination } from '@/composables/use-pagination.js'; +import { globalEvents, useGlobalEvent } from '@/events.js'; const props = withDefaults(defineProps<{ initialFolder?: Misskey.entities.DriveFolder['id'] | null; @@ -515,10 +516,12 @@ async function moveFilesBulk() { const toFolder = await os.selectDriveFolder(folder.value ? folder.value.id : null); - os.apiWithDialog('drive/files/move-bulk', { + await os.apiWithDialog('drive/files/move-bulk', { fileIds: selectedFiles.value.map(f => f.id), folderId: toFolder[0] ? toFolder[0].id : null, }); + + globalEvents.emit('driveFilesMoved', selectedFiles.value, toFolder[0]); } function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) { @@ -694,6 +697,10 @@ function closeTip() { store.set('readDriveTip', true); } +useGlobalEvent('driveFilesMoved', (files, to) => { + // TODO +}); + let connection: Misskey.ChannelConnection | null = null; onMounted(() => { diff --git a/packages/frontend/src/events.ts b/packages/frontend/src/events.ts index 26b1881d15..83412b365a 100644 --- a/packages/frontend/src/events.ts +++ b/packages/frontend/src/events.ts @@ -13,6 +13,7 @@ type Events = { clientNotification: (notification: Misskey.entities.Notification) => void; notePosted: (note: Misskey.entities.Note) => void; noteDeleted: (noteId: Misskey.entities.Note['id']) => void; + driveFilesMoved: (files: Misskey.entities.DriveFile[], to: Misskey.entities.DriveFolder | null) => void; }; export const globalEvents = new EventEmitter(); From 4d185b49cd5d10356e87c865aa219c7b8442690c Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 11 May 2025 09:16:16 +0900 Subject: [PATCH 16/62] Update MkDrive.vue --- packages/frontend/src/components/MkDrive.vue | 129 ++++++------------- 1 file changed, 41 insertions(+), 88 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index e4caf9aef9..72d6ae32f5 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -245,7 +245,9 @@ async function initialize() { } function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) { - addFile(file, true); + if (file.folderId === (folder.value?.id ?? null)) { + filesPaginator.prepend(file); + } } function onDragover(ev: DragEvent) { @@ -354,55 +356,55 @@ function onDrop(ev: DragEvent) { //#endregion } -function urlUpload() { - os.inputText({ +async function urlUpload() { + const { canceled, result: url } = await os.inputText({ title: i18n.ts.uploadFromUrl, type: 'url', placeholder: i18n.ts.uploadFromUrlDescription, - }).then(({ canceled, result: url }) => { - if (canceled || !url) return; - misskeyApi('drive/files/upload-from-url', { - url: url, - folderId: folder.value ? folder.value.id : undefined, - }); + }); + if (canceled || !url) return; - os.alert({ - title: i18n.ts.uploadFromUrlRequested, - text: i18n.ts.uploadFromUrlMayTakeTime, - }); + await os.apiWithDialog('drive/files/upload-from-url', { + url: url, + folderId: folder.value ? folder.value.id : undefined, + }); + + os.alert({ + title: i18n.ts.uploadFromUrlRequested, + text: i18n.ts.uploadFromUrlMayTakeTime, }); } -function createFolder() { - os.inputText({ +async function createFolder() { + const { canceled, result: name } = await os.inputText({ title: i18n.ts.createFolder, placeholder: i18n.ts.folderName, - }).then(({ canceled, result: name }) => { - if (canceled || name == null) return; - misskeyApi('drive/folders/create', { - name: name, - parentId: folder.value ? folder.value.id : undefined, - }).then(createdFolder => { - addFolder(createdFolder, true); - }); }); + if (canceled || name == null) return; + + const createdFolder = await os.apiWithDialog('drive/folders/create', { + name: name, + parentId: folder.value ? folder.value.id : undefined, + }); + + foldersPaginator.prepend(createdFolder); } -function renameFolder(folderToRename: Misskey.entities.DriveFolder) { - os.inputText({ +async function renameFolder(folderToRename: Misskey.entities.DriveFolder) { + const { canceled, result: name } = await os.inputText({ title: i18n.ts.renameFolder, placeholder: i18n.ts.inputNewFolderName, default: folderToRename.name, - }).then(({ canceled, result: name }) => { - if (canceled) return; - misskeyApi('drive/folders/update', { - folderId: folderToRename.id, - name: name, - }).then(updatedFolder => { - // FIXME: 画面を更新するために自分自身に移動 - move(updatedFolder); - }); }); + if (canceled) return; + + const updatedFolder = await os.apiWithDialog('drive/folders/update', { + folderId: folderToRename.id, + name: name, + }); + + // FIXME: 画面を更新するために自分自身に移動 + move(updatedFolder); } function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) { @@ -431,7 +433,9 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) { function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null, keepOriginal?: boolean) { uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal).then(res => { - addFile(res, true); + if (res.folderId === (folder.value?.id ?? null)) { + filesPaginator.prepend(res); + } }); } @@ -524,67 +528,16 @@ async function moveFilesBulk() { globalEvents.emit('driveFilesMoved', selectedFiles.value, toFolder[0]); } -function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) { - const current = folder.value ? folder.value.id : null; - if (current !== folderToAdd.parentId) return; - - if (foldersPaginator.items.value.some(f => f.id === folderToAdd.id)) { - const exist = foldersPaginator.items.value.map(f => f.id).indexOf(folderToAdd.id); - foldersPaginator.items.value[exist] = folderToAdd; - return; - } - - if (unshift) { - foldersPaginator.items.value.unshift(folderToAdd); - } else { - foldersPaginator.items.value.push(folderToAdd); - } -} - -function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) { - const current = folder.value ? folder.value.id : null; - if (current !== fileToAdd.folderId) return; - - if (filesPaginator.items.value.some(f => f.id === fileToAdd.id)) { - const exist = filesPaginator.items.value.map(f => f.id).indexOf(fileToAdd.id); - filesPaginator.items.value[exist] = fileToAdd; - return; - } - - if (unshift) { - filesPaginator.items.value.unshift(fileToAdd); - } else { - filesPaginator.items.value.push(fileToAdd); - } -} - function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) { const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove; - foldersPaginator.items.value = foldersPaginator.items.value.filter(f => f.id !== folderIdToRemove); + foldersPaginator.removeItem(folderIdToRemove); } function removeFile(file: Misskey.entities.DriveFile | string) { const fileId = typeof file === 'object' ? file.id : file; - filesPaginator.items.value = filesPaginator.items.value.filter(f => f.id !== fileId); + filesPaginator.removeItem(fileId); } -function appendFile(file: Misskey.entities.DriveFile) { - addFile(file); -} - -function appendFolder(folderToAppend: Misskey.entities.DriveFolder) { - addFolder(folderToAppend); -} - -/* -function prependFile(file: Misskey.entities.DriveFile) { - addFile(file, true); -} - -function prependFolder(folderToPrepend: Misskey.entities.DriveFolder) { - addFolder(folderToPrepend, true); -} -*/ function goRoot() { // 既にrootにいるなら何もしない if (folder.value == null) return; From db09744ce87362f16907234f7598a00934e87f7d Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 11 May 2025 09:41:15 +0900 Subject: [PATCH 17/62] wip --- packages/frontend-embed/@types/global.d.ts | 3 -- packages/frontend-embed/eslint.config.js | 3 -- packages/frontend-shared/@types/global.d.ts | 3 -- packages/frontend-shared/eslint.config.js | 3 -- packages/frontend/@types/global.d.ts | 3 -- packages/frontend/eslint.config.js | 3 -- .../frontend/src/components/MkDrive.file.vue | 7 +++-- .../src/components/MkDrive.folder.vue | 11 ++++---- .../src/components/MkDrive.navFolder.vue | 9 +++--- packages/frontend/src/components/MkDrive.vue | 28 +++++++++++++++---- .../frontend/src/components/MkPostForm.vue | 5 ++-- packages/frontend/src/consts.ts | 10 +++++++ .../frontend/src/pages/chat/room.form.vue | 5 ++-- packages/frontend/src/ui/deck/column.vue | 7 +++-- packages/frontend/vite.config.ts | 3 -- 15 files changed, 57 insertions(+), 46 deletions(-) create mode 100644 packages/frontend/src/consts.ts diff --git a/packages/frontend-embed/@types/global.d.ts b/packages/frontend-embed/@types/global.d.ts index 1025d1bedb..8a067a78ec 100644 --- a/packages/frontend-embed/@types/global.d.ts +++ b/packages/frontend-embed/@types/global.d.ts @@ -10,9 +10,6 @@ declare const _VERSION_: string; declare const _ENV_: string; declare const _DEV_: boolean; declare const _PERF_PREFIX_: string; -declare const _DATA_TRANSFER_DRIVE_FILE_: string; -declare const _DATA_TRANSFER_DRIVE_FOLDER_: string; -declare const _DATA_TRANSFER_DECK_COLUMN_: string; // for dev-mode declare const _LANGS_FULL_: string[][]; diff --git a/packages/frontend-embed/eslint.config.js b/packages/frontend-embed/eslint.config.js index 7805256fd4..2aef311e2e 100644 --- a/packages/frontend-embed/eslint.config.js +++ b/packages/frontend-embed/eslint.config.js @@ -30,9 +30,6 @@ export default [ _VERSION_: false, _ENV_: false, _PERF_PREFIX_: false, - _DATA_TRANSFER_DRIVE_FILE_: false, - _DATA_TRANSFER_DRIVE_FOLDER_: false, - _DATA_TRANSFER_DECK_COLUMN_: false, }, parser, parserOptions: { diff --git a/packages/frontend-shared/@types/global.d.ts b/packages/frontend-shared/@types/global.d.ts index 4b8d679e75..52081d07b3 100644 --- a/packages/frontend-shared/@types/global.d.ts +++ b/packages/frontend-shared/@types/global.d.ts @@ -11,9 +11,6 @@ declare const _VERSION_: string; declare const _ENV_: string; declare const _DEV_: boolean; declare const _PERF_PREFIX_: string; -declare const _DATA_TRANSFER_DRIVE_FILE_: string; -declare const _DATA_TRANSFER_DRIVE_FOLDER_: string; -declare const _DATA_TRANSFER_DECK_COLUMN_: string; // for dev-mode declare const _LANGS_FULL_: string[][]; diff --git a/packages/frontend-shared/eslint.config.js b/packages/frontend-shared/eslint.config.js index ac5c67d0b6..f6fd64153c 100644 --- a/packages/frontend-shared/eslint.config.js +++ b/packages/frontend-shared/eslint.config.js @@ -35,9 +35,6 @@ export default [ _VERSION_: false, _ENV_: false, _PERF_PREFIX_: false, - _DATA_TRANSFER_DRIVE_FILE_: false, - _DATA_TRANSFER_DRIVE_FOLDER_: false, - _DATA_TRANSFER_DECK_COLUMN_: false, }, parser, parserOptions: { diff --git a/packages/frontend/@types/global.d.ts b/packages/frontend/@types/global.d.ts index 1025d1bedb..8a067a78ec 100644 --- a/packages/frontend/@types/global.d.ts +++ b/packages/frontend/@types/global.d.ts @@ -10,9 +10,6 @@ declare const _VERSION_: string; declare const _ENV_: string; declare const _DEV_: boolean; declare const _PERF_PREFIX_: string; -declare const _DATA_TRANSFER_DRIVE_FILE_: string; -declare const _DATA_TRANSFER_DRIVE_FOLDER_: string; -declare const _DATA_TRANSFER_DECK_COLUMN_: string; // for dev-mode declare const _LANGS_FULL_: string[][]; diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js index 1b9a9b68c0..8f835975a8 100644 --- a/packages/frontend/eslint.config.js +++ b/packages/frontend/eslint.config.js @@ -30,9 +30,6 @@ export default [ _VERSION_: false, _ENV_: false, _PERF_PREFIX_: false, - _DATA_TRANSFER_DRIVE_FILE_: false, - _DATA_TRANSFER_DRIVE_FOLDER_: false, - _DATA_TRANSFER_DECK_COLUMN_: false, }, parser, parserOptions: { diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index 0bb97c40ed..8cd28375c4 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -48,6 +48,7 @@ import { $i } from '@/i.js'; import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js'; import { deviceKind } from '@/utility/device-kind.js'; import { useRouter } from '@/router.js'; +import { DATA_TRANSFER_DRIVE_FILE } from '@/consts.js'; const router = useRouter(); @@ -63,7 +64,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ (ev: 'chosen', r: Misskey.entities.DriveFile): void; - (ev: 'dragstart'): void; + (ev: 'dragstart', dragEvent: DragEvent): void; (ev: 'dragend'): void; }>(); @@ -90,11 +91,11 @@ function onContextmenu(ev: MouseEvent) { function onDragstart(ev: DragEvent) { if (ev.dataTransfer) { ev.dataTransfer.effectAllowed = 'move'; - ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file)); + ev.dataTransfer.setData(DATA_TRANSFER_DRIVE_FILE, JSON.stringify(props.file)); } isDragging.value = true; - emit('dragstart'); + emit('dragstart', ev); } function onDragend() { diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 37181f65b6..e4791258c8 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -43,6 +43,7 @@ import { i18n } from '@/i18n.js'; import { claimAchievement } from '@/utility/achievements.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { prefer } from '@/preferences.js'; +import { DATA_TRANSFER_DRIVE_FILE, DATA_TRANSFER_DRIVE_FOLDER } from '@/consts.js'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; @@ -101,8 +102,8 @@ function onDragover(ev: DragEvent) { } const isFile = ev.dataTransfer.items[0].kind === 'file'; - const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; - const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_; + const isDriveFile = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FILE; + const isDriveFolder = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FOLDER; if (isFile || isDriveFile || isDriveFolder) { switch (ev.dataTransfer.effectAllowed) { @@ -148,7 +149,7 @@ function onDrop(ev: DragEvent) { } //#region ドライブのファイル - const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); + const driveFile = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILE); if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); emit('removeFile', file.id); @@ -160,7 +161,7 @@ function onDrop(ev: DragEvent) { //#endregion //#region ドライブのフォルダ - const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); + const driveFolder = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FOLDER); if (driveFolder != null && driveFolder !== '') { const folder = JSON.parse(driveFolder); @@ -198,7 +199,7 @@ function onDragstart(ev: DragEvent) { if (!ev.dataTransfer) return; ev.dataTransfer.effectAllowed = 'move'; - ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(props.folder)); + ev.dataTransfer.setData(DATA_TRANSFER_DRIVE_FOLDER, JSON.stringify(props.folder)); isDragging.value = true; // 親ブラウザに対して、ドラッグが開始されたフラグを立てる diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue index 7433aea061..176aceee5a 100644 --- a/packages/frontend/src/components/MkDrive.navFolder.vue +++ b/packages/frontend/src/components/MkDrive.navFolder.vue @@ -22,6 +22,7 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; +import { DATA_TRANSFER_DRIVE_FILE, DATA_TRANSFER_DRIVE_FOLDER } from '@/consts.js'; const props = defineProps<{ folder?: Misskey.entities.DriveFolder; @@ -59,8 +60,8 @@ function onDragover(ev: DragEvent) { } const isFile = ev.dataTransfer.items[0].kind === 'file'; - const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; - const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_; + const isDriveFile = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FILE; + const isDriveFolder = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FOLDER; if (isFile || isDriveFile || isDriveFolder) { switch (ev.dataTransfer.effectAllowed) { @@ -108,7 +109,7 @@ function onDrop(ev: DragEvent) { } //#region ドライブのファイル - const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); + const driveFile = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILE); if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); emit('removeFile', file.id); @@ -120,7 +121,7 @@ function onDrop(ev: DragEvent) { //#endregion //#region ドライブのフォルダ - const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); + const driveFolder = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FOLDER); if (driveFolder != null && driveFolder !== '') { const folder = JSON.parse(driveFolder); // 移動先が自分自身ならreject diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 72d6ae32f5..ffbe293045 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -105,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only :selectMode="select === 'file' || isEditMode" :isSelected="selectedFiles.some(x => x.id === file.id)" @chosen="onChooseFile" - @dragstart="isDragSource = true" + @dragstart="onFileDragstart(file, $event)" @dragend="isDragSource = false" />
@@ -152,6 +152,7 @@ import { store } from '@/store.js'; import { isSeparatorNeeded, getSeparatorInfo, makeDateGroupedTimelineComputedRef } from '@/utility/timeline-date-separate.js'; import { usePagination } from '@/composables/use-pagination.js'; import { globalEvents, useGlobalEvent } from '@/events.js'; +import { DATA_TRANSFER_DRIVE_FILE, DATA_TRANSFER_DRIVE_FILES, DATA_TRANSFER_DRIVE_FOLDER } from '@/consts.js'; const props = withDefaults(defineProps<{ initialFolder?: Misskey.entities.DriveFolder['id'] | null; @@ -250,6 +251,19 @@ function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) { } } +function onFileDragstart(file: Misskey.entities.DriveFile, ev: DragEvent) { + if (isEditMode.value) { + if (!selectedFiles.value.some(f => f.id === file.id)) { + selectedFiles.value.push(file); + } + + ev.dataTransfer.effectAllowed = 'move'; + ev.dataTransfer.setData(DATA_TRANSFER_DRIVE_FILES, JSON.stringify(selectedFiles.value)); + } + + isDragSource.value = true; +} + function onDragover(ev: DragEvent) { if (!ev.dataTransfer) return; @@ -261,8 +275,8 @@ function onDragover(ev: DragEvent) { } const isFile = ev.dataTransfer.items[0].kind === 'file'; - const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; - const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_; + const isDriveFile = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FILE; + const isDriveFolder = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FOLDER; if (isFile || isDriveFile || isDriveFolder) { switch (ev.dataTransfer.effectAllowed) { case 'all': @@ -309,7 +323,7 @@ function onDrop(ev: DragEvent) { } //#region ドライブのファイル - const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); + const driveFile = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILE); if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); if (filesPaginator.items.value.some(f => f.id === file.id)) return; @@ -322,7 +336,7 @@ function onDrop(ev: DragEvent) { //#endregion //#region ドライブのフォルダ - const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); + const driveFolder = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FOLDER); if (driveFolder != null && driveFolder !== '') { const droppedFolder = JSON.parse(driveFolder); @@ -651,7 +665,9 @@ function closeTip() { } useGlobalEvent('driveFilesMoved', (files, to) => { - // TODO + if ((to?.id ?? null) === (folder.value?.id ?? null)) { + filesPaginator.unshiftItems(files); + } }); let connection: Misskey.ChannelConnection | null = null; diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 5114e98494..7678d18e58 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -138,6 +138,7 @@ import { prefer } from '@/preferences.js'; import { getPluginHandlers } from '@/plugin.js'; import { DI } from '@/di.js'; import { globalEvents } from '@/events.js'; +import { DATA_TRANSFER_DRIVE_FILE } from '@/consts.js'; const $i = ensureSignin(); @@ -701,7 +702,7 @@ async function onPaste(ev: ClipboardEvent) { function onDragover(ev) { if (!ev.dataTransfer.items[0]) return; const isFile = ev.dataTransfer.items[0].kind === 'file'; - const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; + const isDriveFile = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FILE; if (isFile || isDriveFile) { ev.preventDefault(); draghover.value = true; @@ -743,7 +744,7 @@ function onDrop(ev: DragEvent): void { } //#region ドライブのファイル - const driveFile = ev.dataTransfer?.getData(_DATA_TRANSFER_DRIVE_FILE_); + const driveFile = ev.dataTransfer?.getData(DATA_TRANSFER_DRIVE_FILE); if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); files.value.push(file); diff --git a/packages/frontend/src/consts.ts b/packages/frontend/src/consts.ts new file mode 100644 index 0000000000..cbc3e28556 --- /dev/null +++ b/packages/frontend/src/consts.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const DATA_TRANSFER_DRIVE_FILE = 'misskey-drive-file'; +export const DATA_TRANSFER_DRIVE_FILES = 'misskey-drive-files'; +export const DATA_TRANSFER_DRIVE_FOLDER = 'misskey-drive-folder'; +export const DATA_TRANSFER_DRIVE_FOLDERS = 'misskey-drive-folders'; +export const DATA_TRANSFER_DECK_COLUMN = 'misskey-deck-column'; diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue index 9389b16ce7..ea15a126a8 100644 --- a/packages/frontend/src/pages/chat/room.form.vue +++ b/packages/frontend/src/pages/chat/room.form.vue @@ -47,6 +47,7 @@ import { misskeyApi } from '@/utility/misskey-api.js'; import { prefer } from '@/preferences.js'; import { Autocomplete } from '@/utility/autocomplete.js'; import { emojiPicker } from '@/utility/emoji-picker.js'; +import { DATA_TRANSFER_DRIVE_FILE } from '@/consts.js'; const props = defineProps<{ user?: Misskey.entities.UserDetailed | null; @@ -101,7 +102,7 @@ function onDragover(ev: DragEvent) { if (!ev.dataTransfer) return; const isFile = ev.dataTransfer.items[0].kind === 'file'; - const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; + const isDriveFile = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FILE; if (isFile || isDriveFile) { ev.preventDefault(); switch (ev.dataTransfer.effectAllowed) { @@ -141,7 +142,7 @@ function onDrop(ev: DragEvent): void { } //#region ドライブのファイル - const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); + const driveFile = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILE); if (driveFile != null && driveFile !== '') { file.value = JSON.parse(driveFile); ev.preventDefault(); diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index 2085c73e03..d86d1f3d08 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -51,6 +51,7 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { prefer } from '@/preferences.js'; import { DI } from '@/di.js'; +import { DATA_TRANSFER_DECK_COLUMN } from '@/consts.js'; provide('shouldHeaderThin', true); provide('shouldOmitHeaderTitle', true); @@ -262,7 +263,7 @@ function goTop() { function onDragstart(ev) { ev.dataTransfer.effectAllowed = 'move'; - ev.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, props.column.id); + ev.dataTransfer.setData(DATA_TRANSFER_DECK_COLUMN, props.column.id); // Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう // SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately @@ -281,7 +282,7 @@ function onDragover(ev) { // 自分自身にはドロップさせない ev.dataTransfer.dropEffect = 'none'; } else { - const isDeckColumn = ev.dataTransfer.types[0] === _DATA_TRANSFER_DECK_COLUMN_; + const isDeckColumn = ev.dataTransfer.types[0] === DATA_TRANSFER_DECK_COLUMN; ev.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none'; @@ -297,7 +298,7 @@ function onDrop(ev) { draghover.value = false; os.deckGlobalEvents.emit('column.dragEnd'); - const id = ev.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_); + const id = ev.dataTransfer.getData(DATA_TRANSFER_DECK_COLUMN); if (id != null && id !== '') { swapColumn(props.column.id, id); } diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index aa7bf24174..71c133acc8 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -148,9 +148,6 @@ export function getConfig(): UserConfig { _ENV_: JSON.stringify(process.env.NODE_ENV), _DEV_: process.env.NODE_ENV !== 'production', _PERF_PREFIX_: JSON.stringify('Misskey:'), - _DATA_TRANSFER_DRIVE_FILE_: JSON.stringify('mk_drive_file'), - _DATA_TRANSFER_DRIVE_FOLDER_: JSON.stringify('mk_drive_folder'), - _DATA_TRANSFER_DECK_COLUMN_: JSON.stringify('mk_deck_column'), __VUE_OPTIONS_API__: true, __VUE_PROD_DEVTOOLS__: false, }, From 4ff3cdde873217a055c5a98f63752b00334995ae Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 11 May 2025 09:46:52 +0900 Subject: [PATCH 18/62] wip --- .../src/components/MkDrive.folder.vue | 10 ++++---- .../src/components/MkDrive.navFolder.vue | 4 ---- packages/frontend/src/components/MkDrive.vue | 24 ++++--------------- 3 files changed, 8 insertions(+), 30 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index e4791258c8..667369072c 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -43,7 +43,7 @@ import { i18n } from '@/i18n.js'; import { claimAchievement } from '@/utility/achievements.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { prefer } from '@/preferences.js'; -import { DATA_TRANSFER_DRIVE_FILE, DATA_TRANSFER_DRIVE_FOLDER } from '@/consts.js'; +import { DATA_TRANSFER_DRIVE_FILE, DATA_TRANSFER_DRIVE_FILES, DATA_TRANSFER_DRIVE_FOLDER, DATA_TRANSFER_DRIVE_FOLDERS } from '@/consts.js'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; @@ -59,8 +59,6 @@ const emit = defineEmits<{ (ev: 'unchose', v: Misskey.entities.DriveFolder): void; (ev: 'move', v: Misskey.entities.DriveFolder): void; (ev: 'upload', file: File, folder: Misskey.entities.DriveFolder); - (ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void; - (ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void; (ev: 'dragstart'): void; (ev: 'dragend'): void; }>(); @@ -103,9 +101,11 @@ function onDragover(ev: DragEvent) { const isFile = ev.dataTransfer.items[0].kind === 'file'; const isDriveFile = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FILE; + const isDriveFiles = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FILES; const isDriveFolder = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FOLDER; + const isDriveFolders = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FOLDERS; - if (isFile || isDriveFile || isDriveFolder) { + if (isFile || isDriveFile || isDriveFolder || isDriveFiles || isDriveFolders) { switch (ev.dataTransfer.effectAllowed) { case 'all': case 'uninitialized': @@ -152,7 +152,6 @@ function onDrop(ev: DragEvent) { const driveFile = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILE); if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); - emit('removeFile', file.id); misskeyApi('drive/files/update', { fileId: file.id, folderId: props.folder.id, @@ -168,7 +167,6 @@ function onDrop(ev: DragEvent) { // 移動先が自分自身ならreject if (folder.id === props.folder.id) return; - emit('removeFolder', folder.id); misskeyApi('drive/folders/update', { folderId: folder.id, parentId: props.folder.id, diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue index 176aceee5a..8eb2083a01 100644 --- a/packages/frontend/src/components/MkDrive.navFolder.vue +++ b/packages/frontend/src/components/MkDrive.navFolder.vue @@ -32,8 +32,6 @@ const props = defineProps<{ const emit = defineEmits<{ (ev: 'move', v?: Misskey.entities.DriveFolder): void; (ev: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void; - (ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void; - (ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void; }>(); const hover = ref(false); @@ -112,7 +110,6 @@ function onDrop(ev: DragEvent) { const driveFile = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILE); if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); - emit('removeFile', file.id); misskeyApi('drive/files/update', { fileId: file.id, folderId: props.folder ? props.folder.id : null, @@ -126,7 +123,6 @@ function onDrop(ev: DragEvent) { const folder = JSON.parse(driveFolder); // 移動先が自分自身ならreject if (props.folder && folder.id === props.folder.id) return; - emit('removeFolder', folder.id); misskeyApi('drive/folders/update', { folderId: folder.id, parentId: props.folder ? props.folder.id : null, diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index ffbe293045..5648a1dc4c 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -13,8 +13,6 @@ SPDX-License-Identifier: AGPL-3.0-only :parentFolder="folder" @move="move" @upload="upload" - @removeFile="removeFile" - @removeFolder="removeFolder" /> @@ -79,8 +75,6 @@ SPDX-License-Identifier: AGPL-3.0-only @unchose="unchoseFolder" @move="move" @upload="upload" - @removeFile="removeFile" - @removeFolder="removeFolder" @dragstart="isDragSource = true" @dragend="isDragSource = false" /> @@ -257,8 +251,10 @@ function onFileDragstart(file: Misskey.entities.DriveFile, ev: DragEvent) { selectedFiles.value.push(file); } - ev.dataTransfer.effectAllowed = 'move'; - ev.dataTransfer.setData(DATA_TRANSFER_DRIVE_FILES, JSON.stringify(selectedFiles.value)); + if (ev.dataTransfer) { + ev.dataTransfer.effectAllowed = 'move'; + ev.dataTransfer.setData(DATA_TRANSFER_DRIVE_FILES, JSON.stringify(selectedFiles.value)); + } } isDragSource.value = true; @@ -327,7 +323,6 @@ function onDrop(ev: DragEvent) { if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); if (filesPaginator.items.value.some(f => f.id === file.id)) return; - removeFile(file.id); misskeyApi('drive/files/update', { fileId: file.id, folderId: folder.value ? folder.value.id : null, @@ -343,7 +338,6 @@ function onDrop(ev: DragEvent) { // 移動先が自分自身ならreject if (folder.value && droppedFolder.id === folder.value.id) return false; if (foldersPaginator.items.value.some(f => f.id === droppedFolder.id)) return false; - removeFolder(droppedFolder.id); misskeyApi('drive/folders/update', { folderId: droppedFolder.id, parentId: folder.value ? folder.value.id : null, @@ -542,16 +536,6 @@ async function moveFilesBulk() { globalEvents.emit('driveFilesMoved', selectedFiles.value, toFolder[0]); } -function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) { - const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove; - foldersPaginator.removeItem(folderIdToRemove); -} - -function removeFile(file: Misskey.entities.DriveFile | string) { - const fileId = typeof file === 'object' ? file.id : file; - filesPaginator.removeItem(fileId); -} - function goRoot() { // 既にrootにいるなら何もしない if (folder.value == null) return; From 9bf9b10ba4e758ff3219e7ea56ec5203693415fc Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 11 May 2025 09:54:59 +0900 Subject: [PATCH 19/62] wip --- .../frontend/src/components/MkDrive.folder.vue | 6 ------ .../src/components/MkDrive.navFolder.vue | 15 --------------- packages/frontend/src/components/MkDrive.vue | 18 +++++++++--------- 3 files changed, 9 insertions(+), 30 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 667369072c..42746aa0ec 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -8,7 +8,6 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[$style.root, { [$style.draghover]: draghover }]" draggable="true" :title="title" - @click="onClick" @contextmenu.stop="onContextmenu" @mouseover="onMouseover" @mouseout="onMouseout" @@ -57,7 +56,6 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ (ev: 'chosen', v: Misskey.entities.DriveFolder): void; (ev: 'unchose', v: Misskey.entities.DriveFolder): void; - (ev: 'move', v: Misskey.entities.DriveFolder): void; (ev: 'upload', file: File, folder: Misskey.entities.DriveFolder); (ev: 'dragstart'): void; (ev: 'dragend'): void; @@ -77,10 +75,6 @@ function checkboxClicked() { } } -function onClick() { - emit('move', props.folder); -} - function onMouseover() { hover.value = true; } diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue index 8eb2083a01..d44cbb6e17 100644 --- a/packages/frontend/src/components/MkDrive.navFolder.vue +++ b/packages/frontend/src/components/MkDrive.navFolder.vue @@ -6,7 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -73,7 +73,7 @@ SPDX-License-Identifier: AGPL-3.0-only :isSelected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder" @unchose="unchoseFolder" - @move="move" + @click="cd(f)" @upload="upload" @dragstart="isDragSource = true" @dragend="isDragSource = false" @@ -412,7 +412,7 @@ async function renameFolder(folderToRename: Misskey.entities.DriveFolder) { }); // FIXME: 画面を更新するために自分自身に移動 - move(updatedFolder); + cd(updatedFolder); } function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) { @@ -420,7 +420,7 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) { folderId: folderToDelete.id, }).then(() => { // 削除時に親フォルダに移動 - move(folderToDelete.parentId); + cd(folderToDelete.parentId); }).catch(err => { switch (err.id) { case 'b0fc8a17-963c-405d-bfbc-859a487295e1': @@ -495,7 +495,7 @@ function unchoseFolder(folderToUnchose: Misskey.entities.DriveFolder) { selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToUnchose.id); } -function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id' | 'parentId']) { +function cd(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id' | 'parentId']) { if (!target) { goRoot(); return; @@ -663,7 +663,7 @@ onMounted(() => { } if (props.initialFolder) { - move(props.initialFolder); + cd(props.initialFolder); } else { initialize(); } From e9ee571ef3595ed5e8c3ba234cbfe763ac55ed55 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 11 May 2025 10:58:48 +0900 Subject: [PATCH 20/62] wip --- .../src/components/MkDrive.folder.vue | 90 ++++++++++-------- .../src/components/MkDrive.navFolder.vue | 58 ++++++++---- packages/frontend/src/components/MkDrive.vue | 92 ++++++++++++------- 3 files changed, 150 insertions(+), 90 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 42746aa0ec..e0d3705663 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -43,6 +43,7 @@ import { claimAchievement } from '@/utility/achievements.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { prefer } from '@/preferences.js'; import { DATA_TRANSFER_DRIVE_FILE, DATA_TRANSFER_DRIVE_FILES, DATA_TRANSFER_DRIVE_FOLDER, DATA_TRANSFER_DRIVE_FOLDERS } from '@/consts.js'; +import { globalEvents } from '@/events.js'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; @@ -143,46 +144,65 @@ function onDrop(ev: DragEvent) { } //#region ドライブのファイル - const driveFile = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILE); - if (driveFile != null && driveFile !== '') { - const file = JSON.parse(driveFile); - misskeyApi('drive/files/update', { - fileId: file.id, - folderId: props.folder.id, - }); + { + const driveFile = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILE); + if (driveFile != null && driveFile !== '') { + const file = JSON.parse(driveFile); + misskeyApi('drive/files/update', { + fileId: file.id, + folderId: props.folder.id, + }); + } + } + //#endregion + + //#region ドライブのファイル(複数) + { + const driveFiles = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILES); + if (driveFiles != null && driveFiles !== '') { + const files = JSON.parse(driveFiles); + misskeyApi('drive/files/move-bulk', { + fileIds: files.map(f => f.id), + folderId: props.folder.id, + }).then(() => { + globalEvents.emit('driveFilesMoved', files, props.folder); + }); + } } //#endregion //#region ドライブのフォルダ - const driveFolder = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FOLDER); - if (driveFolder != null && driveFolder !== '') { - const folder = JSON.parse(driveFolder); + { + const driveFolder = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FOLDER); + if (driveFolder != null && driveFolder !== '') { + const folder = JSON.parse(driveFolder); - // 移動先が自分自身ならreject - if (folder.id === props.folder.id) return; + // 移動先が自分自身ならreject + if (folder.id === props.folder.id) return; - misskeyApi('drive/folders/update', { - folderId: folder.id, - parentId: props.folder.id, - }).then(() => { + misskeyApi('drive/folders/update', { + folderId: folder.id, + parentId: props.folder.id, + }).then(() => { // noop - }).catch(err => { - switch (err.code) { - case 'RECURSIVE_NESTING': - claimAchievement('driveFolderCircularReference'); - os.alert({ - type: 'error', - title: i18n.ts.unableToProcess, - text: i18n.ts.circularReferenceFolder, - }); - break; - default: - os.alert({ - type: 'error', - text: i18n.ts.somethingHappened, - }); - } - }); + }).catch(err => { + switch (err.code) { + case 'RECURSIVE_NESTING': + claimAchievement('driveFolderCircularReference'); + os.alert({ + type: 'error', + title: i18n.ts.unableToProcess, + text: i18n.ts.circularReferenceFolder, + }); + break; + default: + os.alert({ + type: 'error', + text: i18n.ts.somethingHappened, + }); + } + }); + } } //#endregion } @@ -204,10 +224,6 @@ function onDragend() { emit('dragend'); } -function go() { - emit('move', props.folder); -} - function rename() { os.inputText({ title: i18n.ts.renameFolder, diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue index d44cbb6e17..bbd1584057 100644 --- a/packages/frontend/src/components/MkDrive.navFolder.vue +++ b/packages/frontend/src/components/MkDrive.navFolder.vue @@ -21,7 +21,8 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { DATA_TRANSFER_DRIVE_FILE, DATA_TRANSFER_DRIVE_FOLDER } from '@/consts.js'; +import { DATA_TRANSFER_DRIVE_FILE, DATA_TRANSFER_DRIVE_FILES, DATA_TRANSFER_DRIVE_FOLDER, DATA_TRANSFER_DRIVE_FOLDERS } from '@/consts.js'; +import { globalEvents } from '@/events.js'; const props = defineProps<{ folder?: Misskey.entities.DriveFolder; @@ -44,9 +45,11 @@ function onDragover(ev: DragEvent) { const isFile = ev.dataTransfer.items[0].kind === 'file'; const isDriveFile = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FILE; + const isDriveFiles = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FILES; const isDriveFolder = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FOLDER; + const isDriveFolders = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FOLDERS; - if (isFile || isDriveFile || isDriveFolder) { + if (isFile || isDriveFile || isDriveFolder || isDriveFiles || isDriveFolders) { switch (ev.dataTransfer.effectAllowed) { case 'all': case 'uninitialized': @@ -92,26 +95,45 @@ function onDrop(ev: DragEvent) { } //#region ドライブのファイル - const driveFile = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILE); - if (driveFile != null && driveFile !== '') { - const file = JSON.parse(driveFile); - misskeyApi('drive/files/update', { - fileId: file.id, - folderId: props.folder ? props.folder.id : null, - }); + { + const driveFile = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILE); + if (driveFile != null && driveFile !== '') { + const file = JSON.parse(driveFile); + misskeyApi('drive/files/update', { + fileId: file.id, + folderId: props.folder ? props.folder.id : null, + }); + } + } + //#endregion + + //#region ドライブのファイル(複数) + { + const driveFiles = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILES); + if (driveFiles != null && driveFiles !== '') { + const files = JSON.parse(driveFiles); + misskeyApi('drive/files/move-bulk', { + fileIds: files.map(f => f.id), + folderId: props.folder ? props.folder.id : null, + }).then(() => { + globalEvents.emit('driveFilesMoved', files, props.folder ?? null); + }); + } } //#endregion //#region ドライブのフォルダ - const driveFolder = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FOLDER); - if (driveFolder != null && driveFolder !== '') { - const folder = JSON.parse(driveFolder); - // 移動先が自分自身ならreject - if (props.folder && folder.id === props.folder.id) return; - misskeyApi('drive/folders/update', { - folderId: folder.id, - parentId: props.folder ? props.folder.id : null, - }); + { + const driveFolder = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FOLDER); + if (driveFolder != null && driveFolder !== '') { + const folder = JSON.parse(driveFolder); + // 移動先が自分自身ならreject + if (props.folder && folder.id === props.folder.id) return; + misskeyApi('drive/folders/update', { + folderId: folder.id, + parentId: props.folder ? props.folder.id : null, + }); + } } //#endregion } diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index e42165c3bc..d6c645c6ef 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -319,47 +319,66 @@ function onDrop(ev: DragEvent) { } //#region ドライブのファイル - const driveFile = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILE); - if (driveFile != null && driveFile !== '') { - const file = JSON.parse(driveFile); - if (filesPaginator.items.value.some(f => f.id === file.id)) return; - misskeyApi('drive/files/update', { - fileId: file.id, - folderId: folder.value ? folder.value.id : null, - }); + { + const driveFile = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILE); + if (driveFile != null && driveFile !== '') { + const file = JSON.parse(driveFile); + if (filesPaginator.items.value.some(f => f.id === file.id)) return; + misskeyApi('drive/files/update', { + fileId: file.id, + folderId: folder.value ? folder.value.id : null, + }); + } + } + //#endregion + + //#region ドライブのファイル(複数) + { + const driveFiles = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILES); + if (driveFiles != null && driveFiles !== '') { + const files = JSON.parse(driveFiles); + misskeyApi('drive/files/move-bulk', { + fileIds: files.map(f => f.id), + folderId: folder.value ? folder.value.id : null, + }).then(() => { + globalEvents.emit('driveFilesMoved', files, folder.value); + }); + } } //#endregion //#region ドライブのフォルダ - const driveFolder = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FOLDER); - if (driveFolder != null && driveFolder !== '') { - const droppedFolder = JSON.parse(driveFolder); + { + const driveFolder = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FOLDER); + if (driveFolder != null && driveFolder !== '') { + const droppedFolder = JSON.parse(driveFolder); - // 移動先が自分自身ならreject - if (folder.value && droppedFolder.id === folder.value.id) return false; - if (foldersPaginator.items.value.some(f => f.id === droppedFolder.id)) return false; - misskeyApi('drive/folders/update', { - folderId: droppedFolder.id, - parentId: folder.value ? folder.value.id : null, - }).then(() => { + // 移動先が自分自身ならreject + if (folder.value && droppedFolder.id === folder.value.id) return false; + if (foldersPaginator.items.value.some(f => f.id === droppedFolder.id)) return false; + misskeyApi('drive/folders/update', { + folderId: droppedFolder.id, + parentId: folder.value ? folder.value.id : null, + }).then(() => { // noop - }).catch(err => { - switch (err.code) { - case 'RECURSIVE_NESTING': - claimAchievement('driveFolderCircularReference'); - os.alert({ - type: 'error', - title: i18n.ts.unableToProcess, - text: i18n.ts.circularReferenceFolder, - }); - break; - default: - os.alert({ - type: 'error', - text: i18n.ts.somethingHappened, - }); - } - }); + }).catch(err => { + switch (err.code) { + case 'RECURSIVE_NESTING': + claimAchievement('driveFolderCircularReference'); + os.alert({ + type: 'error', + title: i18n.ts.unableToProcess, + text: i18n.ts.circularReferenceFolder, + }); + break; + default: + os.alert({ + type: 'error', + text: i18n.ts.somethingHappened, + }); + } + }); + } } //#endregion } @@ -649,6 +668,9 @@ function closeTip() { } useGlobalEvent('driveFilesMoved', (files, to) => { + for (const f of files) { + filesPaginator.removeItem(f.id); + } if ((to?.id ?? null) === (folder.value?.id ?? null)) { filesPaginator.unshiftItems(files); } From f239e6fc62279d06144496ff82b135e8297dfaa8 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 11 May 2025 11:11:58 +0900 Subject: [PATCH 21/62] wip --- packages/frontend/src/components/MkDrive.folder.vue | 2 ++ packages/frontend/src/components/MkDrive.navFolder.vue | 2 ++ packages/frontend/src/components/MkDrive.vue | 2 ++ packages/frontend/src/components/MkDriveFileSelectDialog.vue | 2 +- packages/frontend/src/components/MkDriveFolderSelectDialog.vue | 2 +- 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index e0d3705663..488844e77a 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -151,6 +151,8 @@ function onDrop(ev: DragEvent) { misskeyApi('drive/files/update', { fileId: file.id, folderId: props.folder.id, + }).then(() => { + globalEvents.emit('driveFilesMoved', [file], props.folder); }); } } diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue index bbd1584057..6b106810cd 100644 --- a/packages/frontend/src/components/MkDrive.navFolder.vue +++ b/packages/frontend/src/components/MkDrive.navFolder.vue @@ -102,6 +102,8 @@ function onDrop(ev: DragEvent) { misskeyApi('drive/files/update', { fileId: file.id, folderId: props.folder ? props.folder.id : null, + }).then(() => { + globalEvents.emit('driveFilesMoved', [file], props.folder ?? null); }); } } diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index d6c645c6ef..6b32820a84 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -327,6 +327,8 @@ function onDrop(ev: DragEvent) { misskeyApi('drive/files/update', { fileId: file.id, folderId: folder.value ? folder.value.id : null, + }).then(() => { + globalEvents.emit('driveFilesMoved', [file], folder.value); }); } } diff --git a/packages/frontend/src/components/MkDriveFileSelectDialog.vue b/packages/frontend/src/components/MkDriveFileSelectDialog.vue index 9cccd4ab5e..50b68b3d0f 100644 --- a/packages/frontend/src/components/MkDriveFileSelectDialog.vue +++ b/packages/frontend/src/components/MkDriveFileSelectDialog.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ multiple ? i18n.ts.selectFiles : i18n.ts.selectFile }} ({{ selected.length }}) - + diff --git a/packages/frontend/src/components/MkDriveFolderSelectDialog.vue b/packages/frontend/src/components/MkDriveFolderSelectDialog.vue index adc85c49e1..2ebab1088f 100644 --- a/packages/frontend/src/components/MkDriveFolderSelectDialog.vue +++ b/packages/frontend/src/components/MkDriveFolderSelectDialog.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ multiple ? i18n.ts.selectFolders : i18n.ts.selectFolder }} ({{ selected.length }}) - + From cf701f389d0c4aa40b992a7ec57a832d70ffe6c6 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 11 May 2025 13:11:05 +0900 Subject: [PATCH 22/62] wip --- .../frontend/src/components/MkDrive.file.vue | 4 +- .../src/components/MkDrive.folder.vue | 24 ++------- .../src/components/MkDrive.navFolder.vue | 22 ++------ packages/frontend/src/components/MkDrive.vue | 54 ++++++++++--------- .../frontend/src/components/MkPostForm.vue | 17 +++--- packages/frontend/src/consts.ts | 2 - .../frontend/src/pages/chat/room.form.vue | 16 +++--- 7 files changed, 57 insertions(+), 82 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index 8cd28375c4..b84badf71f 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -48,7 +48,7 @@ import { $i } from '@/i.js'; import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js'; import { deviceKind } from '@/utility/device-kind.js'; import { useRouter } from '@/router.js'; -import { DATA_TRANSFER_DRIVE_FILE } from '@/consts.js'; +import { DATA_TRANSFER_DRIVE_FILES } from '@/consts.js'; const router = useRouter(); @@ -91,7 +91,7 @@ function onContextmenu(ev: MouseEvent) { function onDragstart(ev: DragEvent) { if (ev.dataTransfer) { ev.dataTransfer.effectAllowed = 'move'; - ev.dataTransfer.setData(DATA_TRANSFER_DRIVE_FILE, JSON.stringify(props.file)); + ev.dataTransfer.setData(DATA_TRANSFER_DRIVE_FILES, JSON.stringify([props.file])); } isDragging.value = true; diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 488844e77a..61de77ade3 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -42,7 +42,7 @@ import { i18n } from '@/i18n.js'; import { claimAchievement } from '@/utility/achievements.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { prefer } from '@/preferences.js'; -import { DATA_TRANSFER_DRIVE_FILE, DATA_TRANSFER_DRIVE_FILES, DATA_TRANSFER_DRIVE_FOLDER, DATA_TRANSFER_DRIVE_FOLDERS } from '@/consts.js'; +import { DATA_TRANSFER_DRIVE_FILES, DATA_TRANSFER_DRIVE_FOLDERS } from '@/consts.js'; import { globalEvents } from '@/events.js'; const props = withDefaults(defineProps<{ @@ -95,12 +95,10 @@ function onDragover(ev: DragEvent) { } const isFile = ev.dataTransfer.items[0].kind === 'file'; - const isDriveFile = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FILE; const isDriveFiles = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FILES; - const isDriveFolder = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FOLDER; const isDriveFolders = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FOLDERS; - if (isFile || isDriveFile || isDriveFolder || isDriveFiles || isDriveFolders) { + if (isFile || isDriveFiles || isDriveFolders) { switch (ev.dataTransfer.effectAllowed) { case 'all': case 'uninitialized': @@ -144,21 +142,6 @@ function onDrop(ev: DragEvent) { } //#region ドライブのファイル - { - const driveFile = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILE); - if (driveFile != null && driveFile !== '') { - const file = JSON.parse(driveFile); - misskeyApi('drive/files/update', { - fileId: file.id, - folderId: props.folder.id, - }).then(() => { - globalEvents.emit('driveFilesMoved', [file], props.folder); - }); - } - } - //#endregion - - //#region ドライブのファイル(複数) { const driveFiles = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILES); if (driveFiles != null && driveFiles !== '') { @@ -174,8 +157,9 @@ function onDrop(ev: DragEvent) { //#endregion //#region ドライブのフォルダ + // TODO { - const driveFolder = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FOLDER); + const driveFolder = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FOLDERS); if (driveFolder != null && driveFolder !== '') { const folder = JSON.parse(driveFolder); diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue index 6b106810cd..dd7031f111 100644 --- a/packages/frontend/src/components/MkDrive.navFolder.vue +++ b/packages/frontend/src/components/MkDrive.navFolder.vue @@ -21,7 +21,7 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { DATA_TRANSFER_DRIVE_FILE, DATA_TRANSFER_DRIVE_FILES, DATA_TRANSFER_DRIVE_FOLDER, DATA_TRANSFER_DRIVE_FOLDERS } from '@/consts.js'; +import { DATA_TRANSFER_DRIVE_FILES, DATA_TRANSFER_DRIVE_FOLDERS } from '@/consts.js'; import { globalEvents } from '@/events.js'; const props = defineProps<{ @@ -44,12 +44,10 @@ function onDragover(ev: DragEvent) { } const isFile = ev.dataTransfer.items[0].kind === 'file'; - const isDriveFile = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FILE; const isDriveFiles = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FILES; - const isDriveFolder = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FOLDER; const isDriveFolders = ev.dataTransfer.types[0] === DATA_TRANSFER_DRIVE_FOLDERS; - if (isFile || isDriveFile || isDriveFolder || isDriveFiles || isDriveFolders) { + if (isFile || isDriveFiles || isDriveFolders) { switch (ev.dataTransfer.effectAllowed) { case 'all': case 'uninitialized': @@ -95,21 +93,6 @@ function onDrop(ev: DragEvent) { } //#region ドライブのファイル - { - const driveFile = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILE); - if (driveFile != null && driveFile !== '') { - const file = JSON.parse(driveFile); - misskeyApi('drive/files/update', { - fileId: file.id, - folderId: props.folder ? props.folder.id : null, - }).then(() => { - globalEvents.emit('driveFilesMoved', [file], props.folder ?? null); - }); - } - } - //#endregion - - //#region ドライブのファイル(複数) { const driveFiles = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FILES); if (driveFiles != null && driveFiles !== '') { @@ -125,6 +108,7 @@ function onDrop(ev: DragEvent) { //#endregion //#region ドライブのフォルダ + // TODO { const driveFolder = ev.dataTransfer.getData(DATA_TRANSFER_DRIVE_FOLDER); if (driveFolder != null && driveFolder !== '') { diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 6b32820a84..a0896d4b4f 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -83,14 +83,22 @@ SPDX-License-Identifier: AGPL-3.0-only
- + -
+ -
+
{{ i18n.ts.loadMore }}
@@ -126,7 +134,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 5d19f32ea5..4fa33e1f63 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -127,7 +127,6 @@ import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import { ensureSignin, notesCount, incNotesCount } from '@/i.js'; import { getAccounts, openAccountMenu as openAccountMenu_ } from '@/accounts.js'; -import { uploadFile } from '@/utility/upload.js'; import { deepClone } from '@/utility/clone.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { miLocalStorage } from '@/local-storage.js'; diff --git a/packages/frontend/src/components/MkUploadDialog.vue b/packages/frontend/src/components/MkUploadDialog.vue new file mode 100644 index 0000000000..e942a8300a --- /dev/null +++ b/packages/frontend/src/components/MkUploadDialog.vue @@ -0,0 +1,375 @@ + + + + + + + diff --git a/packages/frontend/src/components/global/MkSystemIcon.vue b/packages/frontend/src/components/global/MkSystemIcon.vue index 3454cdc9f2..d2ef0fb2d8 100644 --- a/packages/frontend/src/components/global/MkSystemIcon.vue +++ b/packages/frontend/src/components/global/MkSystemIcon.vue @@ -28,13 +28,17 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + @@ -62,6 +66,10 @@ const props = defineProps<{ &.error { color: var(--MI_THEME-error); } + + &.waiting { + color: var(--MI_THEME-accent); + } } .line { @@ -87,6 +95,13 @@ const props = defineProps<{ transform: rotate(-90deg); } +.animCircleWaiting { + stroke-dasharray: var(--l); + stroke-dashoffset: calc(var(--l) / 1.5); + animation: waiting 0.75s linear infinite; + transform-origin: center; +} + .animFade { opacity: 0; animation: fade-in var(--duration, 0.5s) cubic-bezier(0,0,.25,1) 1 forwards; @@ -104,6 +119,15 @@ const props = defineProps<{ } } +@keyframes waiting { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + @keyframes fade-in { 0% { opacity: 0; diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue index e8e944df32..0f4912ece1 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue @@ -95,7 +95,6 @@ import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { validators } from '@/components/grid/cell-validators.js'; import { chooseFileFromDrive, chooseFileFromPc } from '@/utility/select-file.js'; -import { uploadFile } from '@/utility/upload.js'; import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js'; import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue'; import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js'; diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue index 5e84ade08d..cf303301c5 100644 --- a/packages/frontend/src/pages/chat/room.form.vue +++ b/packages/frontend/src/pages/chat/room.form.vue @@ -41,7 +41,6 @@ import { formatTimeString } from '@/utility/format-time-string.js'; import { selectFile } from '@/utility/select-file.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { uploadFile } from '@/utility/upload.js'; import { miLocalStorage } from '@/local-storage.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { prefer } from '@/preferences.js'; diff --git a/packages/frontend/src/pages/debug.vue b/packages/frontend/src/pages/debug.vue index 4a28d513f5..5cd68c2c3a 100644 --- a/packages/frontend/src/pages/debug.vue +++ b/packages/frontend/src/pages/debug.vue @@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only + diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index fcf9fb234d..da20d23cfd 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -65,8 +65,6 @@ SPDX-License-Identifier: AGPL-3.0-only v-on="popup.events" /> - - import('./stream-indicator.vue')); -const XUpload = defineAsyncComponent(() => import('./upload.vue')); const XWidgets = defineAsyncComponent(() => import('./widgets.vue')); const drawerMenuShowing = defineModel('drawerMenuShowing'); diff --git a/packages/frontend/src/ui/_common_/upload.vue b/packages/frontend/src/ui/_common_/upload.vue deleted file mode 100644 index 3e5653e46d..0000000000 --- a/packages/frontend/src/ui/_common_/upload.vue +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/utility/upload/isWebpSupported.ts b/packages/frontend/src/utility/isWebpSupported.ts similarity index 100% rename from packages/frontend/src/utility/upload/isWebpSupported.ts rename to packages/frontend/src/utility/isWebpSupported.ts diff --git a/packages/frontend/src/utility/select-file.ts b/packages/frontend/src/utility/select-file.ts index 731ef58302..fe8a87f111 100644 --- a/packages/frontend/src/utility/select-file.ts +++ b/packages/frontend/src/utility/select-file.ts @@ -3,25 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ref } from 'vue'; +import { defineAsyncComponent, markRaw, ref } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; -import { uploadFile } from '@/utility/upload.js'; import { prefer } from '@/preferences.js'; export function chooseFileFromPc( multiple: boolean, options?: { uploadFolder?: string | null; - keepOriginal?: boolean; nameConverter?: (file: File) => string | undefined; }, ): Promise { const uploadFolder = options?.uploadFolder ?? prefer.s.uploadFolder; - const keepOriginal = options?.keepOriginal ?? false; const nameConverter = options?.nameConverter ?? (() => undefined); return new Promise((res, rej) => { @@ -30,15 +27,15 @@ export function chooseFileFromPc( input.multiple = multiple; input.onchange = () => { if (!input.files) return res([]); - const promises = Array.from( - input.files, - file => uploadFile(file, uploadFolder, nameConverter(file), keepOriginal), - ); - Promise.all(promises).then(driveFiles => { - res(driveFiles); - }).catch(err => { - // アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUploadDialog.vue')), { + files: markRaw(Array.from(input.files)), + folderId: uploadFolder, + }, { + done: driveFiles => { + res(driveFiles); + }, + closed: () => dispose(), }); // 一応廃棄 @@ -100,10 +97,6 @@ function select(src: HTMLElement | EventTarget | null, label: string | null, mul text: label, type: 'label', } : undefined, { - text: i18n.ts.upload + ' (' + i18n.ts.compress + ')', - icon: 'ti ti-upload', - action: () => chooseFileFromPc(multiple, { keepOriginal: false }).then(files => res(files)), - }, { text: i18n.ts.upload, icon: 'ti ti-upload', action: () => chooseFileFromPc(multiple, { keepOriginal: true }).then(files => res(files)), diff --git a/packages/frontend/src/utility/upload.ts b/packages/frontend/src/utility/upload.ts index 03240749e9..22735db844 100644 --- a/packages/frontend/src/utility/upload.ts +++ b/packages/frontend/src/utility/upload.ts @@ -3,160 +3,95 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { reactive, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { v4 as uuid } from 'uuid'; -import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; import { apiUrl } from '@@/js/config.js'; -import { getCompressionConfig } from './upload/compress-config.js'; import { $i } from '@/i.js'; -import { alert } from '@/os.js'; -import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { prefer } from '@/preferences.js'; - -type Uploading = { - id: string; - name: string; - progressMax: number | undefined; - progressValue: number | undefined; - img: string; -}; -export const uploads = ref([]); - -const mimeTypeMap = { - 'image/webp': 'webp', - 'image/jpeg': 'jpg', - 'image/png': 'png', -} as const; - -export function uploadFile( - file: File, - folder?: string | Misskey.entities.DriveFolder | null, - name?: string, - keepOriginal = false, -): Promise { - if ($i == null) throw new Error('Not logged in'); - - const _folder = typeof folder === 'string' ? folder : folder?.id; - - if ((file.size > instance.maxFileSize) || (file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit, - }); - return Promise.reject(); - } +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; +export function uploadFile(file: File | Blob, options: { + name?: string; + folderId?: string | null; + onProgress?: (ctx: { total: number; loaded: number; }) => void; +} = {}): Promise { return new Promise((resolve, reject) => { - const id = uuid(); + if ($i == null) return reject(); - const reader = new FileReader(); - reader.onload = async (): Promise => { - const filename = name ?? file.name ?? 'untitled'; - const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : ''; - - const ctx = reactive({ - id, - name: prefer.s.keepOriginalFilename ? filename : id + extension, - progressMax: undefined, - progressValue: undefined, - img: window.URL.createObjectURL(file), + if ((file.size > instance.maxFileSize) || (file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) { + os.alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit, }); + return reject(); + } - uploads.value.push(ctx); - - const config = !keepOriginal ? await getCompressionConfig(file) : undefined; - let resizedImage: Blob | undefined; - if (config) { - try { - const resized = await readAndCompressImage(file, config); - if (resized.size < file.size || file.type === 'image/webp') { - // The compression may not always reduce the file size - // (and WebP is not browser safe yet) - resizedImage = resized; - } - if (_DEV_) { - const saved = ((1 - resized.size / file.size) * 100).toFixed(2); - console.log(`Image compression: before ${file.size} bytes, after ${resized.size} bytes, saved ${saved}%`); - } - - ctx.name = file.type !== config.mimeType ? `${ctx.name}.${mimeTypeMap[config.mimeType]}` : ctx.name; - } catch (err) { - console.error('Failed to resize image', err); - } - } - - const formData = new FormData(); - formData.append('i', $i!.token); - formData.append('force', 'true'); - formData.append('file', resizedImage ?? file); - formData.append('name', ctx.name); - if (_folder) formData.append('folderId', _folder); - - const xhr = new XMLHttpRequest(); - xhr.open('POST', apiUrl + '/drive/files/create', true); - xhr.onload = ((ev: ProgressEvent) => { - if (xhr.status !== 200 || ev.target == null || ev.target.response == null) { - // TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい - uploads.value = uploads.value.filter(x => x.id !== id); - - if (xhr.status === 413) { - alert({ + const xhr = new XMLHttpRequest(); + xhr.open('POST', apiUrl + '/drive/files/create', true); + xhr.onload = ((ev: ProgressEvent) => { + if (xhr.status !== 200 || ev.target == null || ev.target.response == null) { + if (xhr.status === 413) { + os.alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit, + }); + } else if (ev.target?.response) { + const res = JSON.parse(ev.target.response); + if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') { + os.alert({ type: 'error', title: i18n.ts.failedToUpload, - text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit, + text: i18n.ts.cannotUploadBecauseInappropriate, }); - } else if (ev.target?.response) { - const res = JSON.parse(ev.target.response); - if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: i18n.ts.cannotUploadBecauseInappropriate, - }); - } else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: i18n.ts.cannotUploadBecauseNoFreeSpace, - }); - } else { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`, - }); - } - } else { - alert({ + } else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') { + os.alert({ type: 'error', - title: 'Failed to upload', - text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`, + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseNoFreeSpace, + }); + } else { + os.alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`, }); } - - reject(); - return; + } else { + os.alert({ + type: 'error', + title: 'Failed to upload', + text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`, + }); } - const driveFile = JSON.parse(ev.target.response); + reject(); + return; + } - resolve(driveFile); - - uploads.value = uploads.value.filter(x => x.id !== id); - }) as (ev: ProgressEvent) => any; + const driveFile = JSON.parse(ev.target.response); + resolve(driveFile); + }) as (ev: ProgressEvent) => any; + if (options.onProgress) { xhr.upload.onprogress = ev => { if (ev.lengthComputable) { - ctx.progressMax = ev.total; - ctx.progressValue = ev.loaded; + options.onProgress({ + total: ev.total, + loaded: ev.loaded, + }); } }; + } - xhr.send(formData); - }; - reader.readAsArrayBuffer(file); + const formData = new FormData(); + formData.append('i', $i.token); + formData.append('force', 'true'); + formData.append('file', file); + formData.append('name', options.name ?? file.name ?? 'untitled'); + if (options.folderId) formData.append('folderId', options.folderId); + + xhr.send(formData); }); } diff --git a/packages/frontend/src/utility/upload/compress-config.ts b/packages/frontend/src/utility/upload/compress-config.ts deleted file mode 100644 index 3046b7f518..0000000000 --- a/packages/frontend/src/utility/upload/compress-config.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import isAnimated from 'is-file-animated'; -import { isWebpSupported } from './isWebpSupported.js'; -import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer'; - -const compressTypeMap = { - 'image/jpeg': { quality: 0.90, mimeType: 'image/webp' }, - 'image/png': { quality: 1, mimeType: 'image/webp' }, - 'image/webp': { quality: 0.90, mimeType: 'image/webp' }, - 'image/svg+xml': { quality: 1, mimeType: 'image/webp' }, -} as const; - -const compressTypeMapFallback = { - 'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' }, - 'image/png': { quality: 1, mimeType: 'image/png' }, - 'image/webp': { quality: 0.85, mimeType: 'image/jpeg' }, - 'image/svg+xml': { quality: 1, mimeType: 'image/png' }, -} as const; - -export async function getCompressionConfig(file: File): Promise { - const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type]; - if (!imgConfig || await isAnimated(file)) { - return; - } - - return { - maxWidth: 2048, - maxHeight: 2048, - debug: true, - ...imgConfig, - }; -} From 6c7f24fbb8b3718dffc02fa1c78a52c87ad3eeba Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 13 May 2025 14:03:50 +0900 Subject: [PATCH 25/62] wip --- packages/frontend/src/components/MkDrive.vue | 14 ++++++-------- packages/frontend/src/components/MkPostForm.vue | 8 -------- packages/frontend/src/events.ts | 1 + .../pages/admin/custom-emojis-manager.register.vue | 2 +- packages/frontend/src/pages/chat/room.form.vue | 6 ------ packages/frontend/src/utility/upload.ts | 2 ++ 6 files changed, 10 insertions(+), 23 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index ebea1eb745..b5840dad51 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -446,14 +446,6 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) { }); } -function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null, keepOriginal?: boolean) { - uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal).then(res => { - if (res.folderId === (folder.value?.id ?? null)) { - filesPaginator.prepend(res); - } - }); -} - function onChooseFile(file: Misskey.entities.DriveFile) { const isAlreadySelected = selectedFiles.value.some(f => f.id === file.id); @@ -649,6 +641,12 @@ function closeTip() { store.set('readDriveTip', true); } +useGlobalEvent('driveFileCreated', (file) => { + if (file.folderId === (folder.value?.id ?? null)) { + filesPaginator.prepend(file); + } +}); + useGlobalEvent('driveFilesMoved', (files, to) => { for (const f of files) { filesPaginator.removeItem(f.id); diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 4fa33e1f63..76965f91f5 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -463,14 +463,6 @@ function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities files.value[files.value.findIndex(x => x.id === file.id)] = newFile; } -function upload(file: File, name?: string): void { - if (props.mock) return; - - uploadFile(file, prefer.s.uploadFolder, name).then(res => { - files.value.push(res); - }); -} - function setVisibility() { if (props.channel) { visibility.value = 'public'; diff --git a/packages/frontend/src/events.ts b/packages/frontend/src/events.ts index 83412b365a..c1bbec724e 100644 --- a/packages/frontend/src/events.ts +++ b/packages/frontend/src/events.ts @@ -13,6 +13,7 @@ type Events = { clientNotification: (notification: Misskey.entities.Notification) => void; notePosted: (note: Misskey.entities.Note) => void; noteDeleted: (noteId: Misskey.entities.Note['id']) => void; + driveFileCreated: (file: Misskey.entities.DriveFile) => void; driveFilesMoved: (files: Misskey.entities.DriveFile[], to: Misskey.entities.DriveFolder | null) => void; }; diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue index 0f4912ece1..6b11a181b0 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue @@ -329,7 +329,7 @@ async function onDrop(ev: DragEvent) { Promise.all( droppedFiles.map(async (it) => ({ droppedFile: it, - driveFile: await uploadFile( + driveFile: await uploadFile( // TODO it.file, selectedFolderId.value, it.file.name.replace(/\.[^.]+$/, ''), diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue index cf303301c5..ff3ea6d32b 100644 --- a/packages/frontend/src/pages/chat/room.form.vue +++ b/packages/frontend/src/pages/chat/room.form.vue @@ -176,12 +176,6 @@ function onChangeFile() { if (fileEl.value.files[0]) upload(fileEl.value.files[0]); } -function upload(fileToUpload: File, name?: string) { - uploadFile(fileToUpload, prefer.s.uploadFolder, name).then(res => { - file.value = res; - }); -} - function send() { if (!canSend.value) return; diff --git a/packages/frontend/src/utility/upload.ts b/packages/frontend/src/utility/upload.ts index 22735db844..134d468fff 100644 --- a/packages/frontend/src/utility/upload.ts +++ b/packages/frontend/src/utility/upload.ts @@ -9,6 +9,7 @@ import { $i } from '@/i.js'; import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { globalEvents } from '@/events.js'; export function uploadFile(file: File | Blob, options: { name?: string; @@ -71,6 +72,7 @@ export function uploadFile(file: File | Blob, options: { } const driveFile = JSON.parse(ev.target.response); + globalEvents.emit('driveFileCreated', driveFile); resolve(driveFile); }) as (ev: ProgressEvent) => any; From 90c40cd153c40421a768b83d50f8f63a4c7ada82 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 13 May 2025 14:20:43 +0900 Subject: [PATCH 26/62] wip --- packages/frontend/src/components/MkDrive.vue | 6 ++-- .../frontend/src/components/MkPostForm.vue | 2 +- ...kUploadDialog.vue => MkUploaderDialog.vue} | 0 packages/frontend/src/os.ts | 19 +++++++++++++ .../frontend/src/pages/chat/room.form.vue | 8 ++++-- packages/frontend/src/utility/select-file.ts | 28 +++++++------------ 6 files changed, 39 insertions(+), 24 deletions(-) rename packages/frontend/src/components/{MkUploadDialog.vue => MkUploaderDialog.vue} (100%) diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index b5840dad51..edbde654ec 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -316,9 +316,9 @@ function onDrop(ev: DragEvent) { // ドロップされてきたものがファイルだったら if (ev.dataTransfer.files.length > 0) { - for (const file of Array.from(ev.dataTransfer.files)) { - upload(file, folder.value); - } + os.launchUploader(Array.from(ev.dataTransfer.files), { + folderId: folder.value?.id ?? null, + }); return; } diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 76965f91f5..457a369ddb 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -729,7 +729,7 @@ function onDrop(ev: DragEvent): void { // ファイルだったら if (ev.dataTransfer && ev.dataTransfer.files.length > 0) { ev.preventDefault(); - for (const x of Array.from(ev.dataTransfer.files)) upload(x); + os.launchUploader(Array.from(ev.dataTransfer.files), {}); return; } diff --git a/packages/frontend/src/components/MkUploadDialog.vue b/packages/frontend/src/components/MkUploaderDialog.vue similarity index 100% rename from packages/frontend/src/components/MkUploadDialog.vue rename to packages/frontend/src/components/MkUploaderDialog.vue diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 9d26242c3c..6d58307ea0 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -773,3 +773,22 @@ export function checkExistence(fileData: ArrayBuffer): Promise { }); }); }*/ + +export function launchUploader( + files: File[], + options?: { + folderId?: string | null; + }, +): Promise { + return new Promise((res, rej) => { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUploaderDialog.vue')), { + files: markRaw(files), + folderId: options?.folderId, + }, { + done: driveFiles => { + res(driveFiles); + }, + closed: () => dispose(), + }); + }); +} diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue index ff3ea6d32b..138359e05e 100644 --- a/packages/frontend/src/pages/chat/room.form.vue +++ b/packages/frontend/src/pages/chat/room.form.vue @@ -128,7 +128,7 @@ function onDrop(ev: DragEvent): void { // ファイルだったら if (ev.dataTransfer.files.length === 1) { ev.preventDefault(); - upload(ev.dataTransfer.files[0]); + os.launchUploader([Array.from(ev.dataTransfer.files)[0]], {}); return; } else if (ev.dataTransfer.files.length > 1) { ev.preventDefault(); @@ -173,7 +173,11 @@ function chooseFile(ev: MouseEvent) { function onChangeFile() { if (fileEl.value == null || fileEl.value.files == null) return; - if (fileEl.value.files[0]) upload(fileEl.value.files[0]); + if (fileEl.value.files[0]) { + os.launchUploader(Array.from(fileEl.value.files), {}).then(driveFiles => { + file.value = driveFiles[0]; + }); + } } function send() { diff --git a/packages/frontend/src/utility/select-file.ts b/packages/frontend/src/utility/select-file.ts index fe8a87f111..a5e458b9ff 100644 --- a/packages/frontend/src/utility/select-file.ts +++ b/packages/frontend/src/utility/select-file.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { defineAsyncComponent, markRaw, ref } from 'vue'; +import { } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -12,30 +12,22 @@ import { i18n } from '@/i18n.js'; import { prefer } from '@/preferences.js'; export function chooseFileFromPc( - multiple: boolean, - options?: { - uploadFolder?: string | null; - nameConverter?: (file: File) => string | undefined; - }, + options: { + multiple?: boolean; + folderId?: string | null; + } = {}, ): Promise { - const uploadFolder = options?.uploadFolder ?? prefer.s.uploadFolder; - const nameConverter = options?.nameConverter ?? (() => undefined); - return new Promise((res, rej) => { const input = window.document.createElement('input'); input.type = 'file'; - input.multiple = multiple; + input.multiple = options.multiple ?? false; input.onchange = () => { if (!input.files) return res([]); - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUploadDialog.vue')), { - files: markRaw(Array.from(input.files)), - folderId: uploadFolder, - }, { - done: driveFiles => { - res(driveFiles); - }, - closed: () => dispose(), + os.launchUploader(Array.from(input.files), { + folderId: options.folderId, + }).then(driveFiles => { + res(driveFiles); }); // 一応廃棄 From 566f7293003060221a75f3c18caa0024fbeeb7ff Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 13 May 2025 14:31:05 +0900 Subject: [PATCH 27/62] Update MkPostForm.vue --- .../frontend/src/components/MkPostForm.vue | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 457a369ddb..538dac7e01 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -643,16 +643,25 @@ async function onPaste(ev: ClipboardEvent) { if (props.mock) return; if (!ev.clipboardData) return; + let pastedFiles: File[] = []; for (const { item, i } of Array.from(ev.clipboardData.items, (data, x) => ({ item: data, i: x }))) { if (item.kind === 'file') { const file = item.getAsFile(); if (!file) continue; const lio = file.name.lastIndexOf('.'); const ext = lio >= 0 ? file.name.slice(lio) : ''; - const formatted = `${formatTimeString(new Date(file.lastModified), pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; - upload(file, formatted); + const formattedName = `${formatTimeString(new Date(file.lastModified), pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; + const renamedFile = new File([file], formattedName, { type: file.type }); + pastedFiles.push(renamedFile); } } + if (pastedFiles.length > 0) { + ev.preventDefault(); + os.launchUploader(pastedFiles, {}).then(driveFiles => { + files.value.push(...driveFiles); + }); + return; + } const paste = ev.clipboardData.getData('text'); @@ -685,7 +694,9 @@ async function onPaste(ev: ClipboardEvent) { const fileName = formatTimeString(new Date(), pastedFileName).replace(/{{number}}/g, '0'); const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' }); - upload(file, `${fileName}.txt`); + os.launchUploader([file], {}).then(driveFiles => { + files.value.push(...driveFiles); + }); }); } } @@ -729,7 +740,9 @@ function onDrop(ev: DragEvent): void { // ファイルだったら if (ev.dataTransfer && ev.dataTransfer.files.length > 0) { ev.preventDefault(); - os.launchUploader(Array.from(ev.dataTransfer.files), {}); + os.launchUploader(Array.from(ev.dataTransfer.files), {}).then(driveFiles => { + files.value.push(...driveFiles); + }); return; } From 37f846af8a810700ebd267ab212c9dc9258c5093 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 13 May 2025 15:29:13 +0900 Subject: [PATCH 28/62] wip --- .../src/components/MkCropperDialog.vue | 41 +++---------- packages/frontend/src/components/MkDrive.vue | 4 +- .../src/components/MkPostFormAttaches.vue | 2 +- .../components/MkUserSetupDialog.Profile.vue | 14 +++-- packages/frontend/src/os.ts | 59 +++++++++++++++++-- .../admin/custom-emojis-manager.register.vue | 4 +- .../frontend/src/pages/drive.file.info.vue | 2 +- .../frontend/src/pages/settings/profile.vue | 4 +- .../src/utility/get-drive-file-menu.ts | 2 +- packages/frontend/src/utility/select-file.ts | 25 ++------ 10 files changed, 86 insertions(+), 71 deletions(-) diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 99854bc66e..2174f2ee1f 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -33,27 +33,23 @@ import { onMounted, useTemplateRef, ref } from 'vue'; import * as Misskey from 'misskey-js'; import Cropper from 'cropperjs'; import tinycolor from 'tinycolor2'; -import { apiUrl } from '@@/js/config.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import * as os from '@/os.js'; -import { $i } from '@/i.js'; import { i18n } from '@/i18n.js'; -import { getProxiedImageUrl } from '@/utility/media-proxy.js'; -import { prefer } from '@/preferences.js'; - -const emit = defineEmits<{ - (ev: 'ok', cropped: Misskey.entities.DriveFile): void; - (ev: 'cancel'): void; - (ev: 'closed'): void; -}>(); const props = defineProps<{ - file: Misskey.entities.DriveFile; + imageFile: File; aspectRatio: number; uploadFolder?: string | null; }>(); -const imgUrl = getProxiedImageUrl(props.file.url, undefined, true); +const emit = defineEmits<{ + (ev: 'ok', cropped: File | Blob): void; + (ev: 'cancel'): void; + (ev: 'closed'): void; +}>(); + +const imgUrl = URL.createObjectURL(props.imageFile); const dialogEl = useTemplateRef('dialogEl'); const imgEl = useTemplateRef('imgEl'); let cropper: Cropper | null = null; @@ -71,26 +67,7 @@ const ok = async () => { const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender }); croppedCanvas?.toBlob(blob => { if (!blob) return; - const formData = new FormData(); - formData.append('file', blob); - formData.append('name', `cropped_${props.file.name}`); - formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false'); - if (props.file.comment) { formData.append('comment', props.file.comment);} - formData.append('i', $i!.token); - if (props.uploadFolder) { - formData.append('folderId', props.uploadFolder); - } else if (props.uploadFolder !== null && prefer.s.uploadFolder) { - formData.append('folderId', prefer.s.uploadFolder); - } - - window.fetch(apiUrl + '/drive/files/create', { - method: 'POST', - body: formData, - }) - .then(response => response.json()) - .then(f => { - res(f); - }); + res(blob); }); }); diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index edbde654ec..95a617f491 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -148,7 +148,7 @@ import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { claimAchievement } from '@/utility/achievements.js'; import { prefer } from '@/preferences.js'; -import { chooseFileFromPc } from '@/utility/select-file.js'; +import { chooseFileFromPcAndUpload } from '@/utility/select-file.js'; import { store } from '@/store.js'; import { isSeparatorNeeded, getSeparatorInfo, makeDateGroupedTimelineComputedRef } from '@/utility/timeline-date-separate.js'; import { usePagination } from '@/composables/use-pagination.js'; @@ -555,7 +555,7 @@ function getMenu() { text: i18n.ts.upload, icon: 'ti ti-upload', action: () => { - chooseFileFromPc(true, { uploadFolder: folder.value?.id, keepOriginal: true }); + chooseFileFromPcAndUpload(true, { uploadFolder: folder.value?.id, keepOriginal: true }); }, }, { text: i18n.ts.fromUrl, diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index e8404cbd4f..a35eb1abf8 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -145,7 +145,7 @@ async function describe(file: Misskey.entities.DriveFile) { async function crop(file: Misskey.entities.DriveFile): Promise { if (mock) return; - const newFile = await os.cropImage(file, { aspectRatio: NaN }); + const newFile = await os.createCroppedImageDriveFileFromImageDriveFile(file, { aspectRatio: NaN }); emit('replaceFile', file, newFile); } diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue index 30925b854c..a0d7376c8e 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue @@ -37,7 +37,7 @@ import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import FormSlot from '@/components/form/slot.vue'; import MkInfo from '@/components/MkInfo.vue'; -import { chooseFileFromPc } from '@/utility/select-file.js'; +import { chooseFileFromPcAndUpload } from '@/utility/select-file.js'; import * as os from '@/os.js'; import { ensureSignin } from '@/i.js'; @@ -49,7 +49,7 @@ const description = ref($i.description ?? ''); watch(name, () => { os.apiWithDialog('i/update', { // 空文字列をnullにしたいので??は使うな - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + name: name.value || null, }, undefined, { '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': { @@ -62,13 +62,13 @@ watch(name, () => { watch(description, () => { os.apiWithDialog('i/update', { // 空文字列をnullにしたいので??は使うな - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + description: description.value || null, }); }); function setAvatar(ev) { - chooseFileFromPc(false).then(async (files) => { + os.chooseFileFromPc({ multiple: false }).then(async (files) => { const file = files[0]; let originalOrCropped = file; @@ -81,13 +81,15 @@ function setAvatar(ev) { }); if (!canceled) { - originalOrCropped = await os.cropImage(file, { + originalOrCropped = await os.cropImageFile(file, { aspectRatio: 1, }); } + const driveFile = (await os.launchUploader([originalOrCropped], {}))[0]; + const i = await os.apiWithDialog('i/update', { - avatarId: originalOrCropped.id, + avatarId: driveFile.id, }); $i.avatarId = i.avatarId; $i.avatarUrl = i.avatarUrl; diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 6d58307ea0..e93d7614b4 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -8,6 +8,7 @@ import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue'; import { EventEmitter } from 'eventemitter3'; import * as Misskey from 'misskey-js'; +import { getProxiedImageUrl } from './utility/media-proxy.js'; import type { Component, Ref } from 'vue'; import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { Form, GetFormResultType } from '@/utility/form.js'; @@ -653,15 +654,13 @@ export async function pickEmoji(src: HTMLElement, opts: ComponentProps { +}): Promise { return new Promise(resolve => { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), { - file: image, + imageFile: imageFile, aspectRatio: options.aspectRatio, - uploadFolder: options.uploadFolder, }, { ok: x => { resolve(x); @@ -671,6 +670,29 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: { }); } +export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFile: Misskey.entities.DriveFile, options: { + aspectRatio: number; + uploadFolder?: string | null; +}): Promise { + return new Promise(resolve => { + const imgUrl = getProxiedImageUrl(imageDriveFile.url, undefined, true); + const image = new Image(); + image.src = imgUrl; + image.onload = () => { + const canvas = window.document.createElement('canvas'); + const ctx = canvas.getContext('2d')!; + canvas.width = image.width; + canvas.height = image.height; + ctx.drawImage(image, 0, 0); + const imageFile = new File([canvas.toBlob()], imageDriveFile.name, { type: imageDriveFile.type }); + + cropImageFile(imageFile).then(croppedImageFile => { + + }); + }; + }); +} + export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | null, options?: { align?: string; width?: number; @@ -774,6 +796,32 @@ export function checkExistence(fileData: ArrayBuffer): Promise { }); }*/ +export function chooseFileFromPc( + options: { + multiple?: boolean; + } = {}, +): Promise { + return new Promise((res, rej) => { + const input = window.document.createElement('input'); + input.type = 'file'; + input.multiple = options.multiple ?? false; + input.onchange = () => { + if (!input.files) return res([]); + + res(Array.from(input.files)); + + // 一応廃棄 + (window as any).__misskey_input_ref__ = null; + }; + + // https://qiita.com/fukasawah/items/b9dc732d95d99551013d + // iOS Safari で正常に動かす為のおまじない + (window as any).__misskey_input_ref__ = input; + + input.click(); + }); +} + export function launchUploader( files: File[], options?: { @@ -781,6 +829,7 @@ export function launchUploader( }, ): Promise { return new Promise((res, rej) => { + if (files.length === 0) return; const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUploaderDialog.vue')), { files: markRaw(files), folderId: options?.folderId, diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue index 6b11a181b0..a5345f8fc7 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue @@ -94,7 +94,7 @@ import MkFolder from '@/components/MkFolder.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { validators } from '@/components/grid/cell-validators.js'; -import { chooseFileFromDrive, chooseFileFromPc } from '@/utility/select-file.js'; +import { chooseFileFromDrive, chooseFileFromPcAndUpload } from '@/utility/select-file.js'; import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js'; import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue'; import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js'; @@ -364,7 +364,7 @@ async function onDrop(ev: DragEvent) { } async function onFileSelectClicked() { - const driveFiles = await chooseFileFromPc( + const driveFiles = await chooseFileFromPcAndUpload( true, { uploadFolder: selectedFolderId.value, diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue index fec260be72..a9511a9d17 100644 --- a/packages/frontend/src/pages/drive.file.info.vue +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -130,7 +130,7 @@ function postThis() { function crop() { if (!file.value) return; - os.cropImage(file.value, { + os.createCroppedImageDriveFileFromImageDriveFile(file.value, { aspectRatio: NaN, uploadFolder: file.value.folderId ?? null, }); diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 30b7cf9a86..6ef93d36c0 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -268,7 +268,7 @@ function changeAvatar(ev) { }); if (!canceled) { - originalOrCropped = await os.cropImage(file, { + originalOrCropped = await os.createCroppedImageDriveFileFromImageDriveFile(file, { aspectRatio: 1, }); } @@ -294,7 +294,7 @@ function changeBanner(ev) { }); if (!canceled) { - originalOrCropped = await os.cropImage(file, { + originalOrCropped = await os.createCroppedImageDriveFileFromImageDriveFile(file, { aspectRatio: 2, }); } diff --git a/packages/frontend/src/utility/get-drive-file-menu.ts b/packages/frontend/src/utility/get-drive-file-menu.ts index a5595cb0cb..b87b447e93 100644 --- a/packages/frontend/src/utility/get-drive-file-menu.ts +++ b/packages/frontend/src/utility/get-drive-file-menu.ts @@ -116,7 +116,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss menuItems.push({ text: i18n.ts.cropImage, icon: 'ti ti-crop', - action: () => os.cropImage(file, { + action: () => os.createCroppedImageDriveFileFromImageDriveFile(file, { aspectRatio: NaN, uploadFolder: folder ? folder.id : folder, }), diff --git a/packages/frontend/src/utility/select-file.ts b/packages/frontend/src/utility/select-file.ts index a5e458b9ff..b4d9efa3d5 100644 --- a/packages/frontend/src/utility/select-file.ts +++ b/packages/frontend/src/utility/select-file.ts @@ -11,34 +11,21 @@ import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { prefer } from '@/preferences.js'; -export function chooseFileFromPc( +export function chooseFileFromPcAndUpload( options: { multiple?: boolean; folderId?: string | null; } = {}, ): Promise { return new Promise((res, rej) => { - const input = window.document.createElement('input'); - input.type = 'file'; - input.multiple = options.multiple ?? false; - input.onchange = () => { - if (!input.files) return res([]); - - os.launchUploader(Array.from(input.files), { + os.chooseFileFromPc({ multiple: options.multiple }).then(files => { + if (files.length === 0) return; + os.launchUploader(files, { folderId: options.folderId, }).then(driveFiles => { res(driveFiles); }); - - // 一応廃棄 - (window as any).__misskey_input_ref__ = null; - }; - - // https://qiita.com/fukasawah/items/b9dc732d95d99551013d - // iOS Safari で正常に動かす為のおまじない - (window as any).__misskey_input_ref__ = input; - - input.click(); + }); }); } @@ -91,7 +78,7 @@ function select(src: HTMLElement | EventTarget | null, label: string | null, mul } : undefined, { text: i18n.ts.upload, icon: 'ti ti-upload', - action: () => chooseFileFromPc(multiple, { keepOriginal: true }).then(files => res(files)), + action: () => chooseFileFromPcAndUpload({ multiple }).then(files => res(files)), }, { text: i18n.ts.fromDrive, icon: 'ti ti-cloud', From a96e99815efe0445af010451f727b49b6eabe134 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 13 May 2025 15:44:15 +0900 Subject: [PATCH 29/62] Update room.form.vue --- packages/frontend/src/pages/chat/room.form.vue | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue index 138359e05e..952fafb930 100644 --- a/packages/frontend/src/pages/chat/room.form.vue +++ b/packages/frontend/src/pages/chat/room.form.vue @@ -84,8 +84,11 @@ async function onPaste(ev: ClipboardEvent) { if (!pastedFile) return; const lio = pastedFile.name.lastIndexOf('.'); const ext = lio >= 0 ? pastedFile.name.slice(lio) : ''; - const formatted = formatTimeString(new Date(pastedFile.lastModified), pastedFileName).replace(/{{number}}/g, '1') + ext; - if (formatted) upload(pastedFile, formatted); + const formattedName = formatTimeString(new Date(pastedFile.lastModified), pastedFileName).replace(/{{number}}/g, '1') + ext; + const renamedFile = new File([pastedFile], formattedName, { type: pastedFile.type }); + os.launchUploader([renamedFile], {}).then(driveFiles => { + file.value = driveFiles[0]; + }); } } else { if (items[0].kind === 'file') { From 48f2d2bb1377399b7bb28436499170997dcc3af1 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 13 May 2025 15:49:43 +0900 Subject: [PATCH 30/62] Update os.ts --- packages/frontend/src/os.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index e93d7614b4..dd5720b04b 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -829,12 +829,13 @@ export function launchUploader( }, ): Promise { return new Promise((res, rej) => { - if (files.length === 0) return; + if (files.length === 0) return rej(); const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUploaderDialog.vue')), { files: markRaw(files), folderId: options?.folderId, }, { done: driveFiles => { + if (driveFiles.length === 0) return rej(); res(driveFiles); }, closed: () => dispose(), From 0cb7289eb8339f801fdf044580462570017d812e Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 13 May 2025 16:58:11 +0900 Subject: [PATCH 31/62] wiop --- locales/index.d.ts | 4 +++ locales/ja-JP.yml | 1 + .../src/components/MkUploaderDialog.vue | 35 ++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index 86987da735..7a3bec67dd 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -11915,6 +11915,10 @@ export interface Locale extends ILocale { * アップロードされていないファイルがありますが、中止しますか? */ "abortConfirm": string; + /** + * アップロードされていないファイルがありますが、完了しますか? + */ + "doneConfirm": string; }; } declare const locales: { diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 58d7367856..a9497db4e7 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -3187,3 +3187,4 @@ _uploader: compressedToX: "{x}に圧縮" savedXPercent: "{x}%節約" abortConfirm: "アップロードされていないファイルがありますが、中止しますか?" + doneConfirm: "アップロードされていないファイルがありますが、完了しますか?" diff --git a/packages/frontend/src/components/MkUploaderDialog.vue b/packages/frontend/src/components/MkUploaderDialog.vue index e942a8300a..60f4364592 100644 --- a/packages/frontend/src/components/MkUploaderDialog.vue +++ b/packages/frontend/src/components/MkUploaderDialog.vue @@ -65,7 +65,9 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.cancel }} {{ i18n.ts.upload }} - {{ i18n.ts.retry }} + + {{ i18n.ts.retry }} + {{ i18n.ts.done }}
@@ -79,6 +81,7 @@ import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; import { apiUrl } from '@@/js/config.js'; import isAnimated from 'is-file-animated'; import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer'; +import type { MenuItem } from '@/types/menu.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; import { prefer } from '@/preferences.js'; @@ -116,6 +119,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ (ev: 'done', driveFiles: Misskey.entities.DriveFile[]): void; + (ev: 'canceled'): void; (ev: 'closed'): void; }>(); @@ -139,6 +143,7 @@ const dialog = useTemplateRef('dialog'); const firstUploadAttempted = ref(false); const isUploading = computed(() => items.value.some(item => item.uploading)); const canRetry = computed(() => firstUploadAttempted.value && !items.value.some(item => item.uploading || item.waiting) && items.value.some(item => item.uploaded == null)); +const canDone = computed(() => items.value.some(item => item.uploaded != null)); const compressionLevel = ref<0 | 1 | 2 | 3>(2); const compressionSettings = computed(() => { @@ -164,6 +169,7 @@ const compressionSettings = computed(() => { watch(items, () => { if (items.value.length === 0) { + emit('canceled'); dialog.value?.close(); return; } @@ -183,12 +189,39 @@ async function cancel() { }); if (canceled) return; + emit('canceled'); + dialog.value?.close(); +} + +async function done() { + if (items.value.some(item => item.uploaded == null)) { + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.ts._uploader.doneConfirm, + okText: i18n.ts.yes, + cancelText: i18n.ts.no, + }); + if (canceled) return; + } + emit('done', items.value.filter(item => item.uploaded != null).map(item => item.uploaded!)); dialog.value?.close(); } function showMenu(ev: MouseEvent, item: typeof items.value[0]) { + const menu: MenuItem[] = []; + if (!item.waiting && !item.uploading && !item.uploaded) { + menu.push({ + icon: 'ti ti-x', + text: i18n.ts.remove, + action: () => { + items.value.splice(items.value.indexOf(item), 1); + }, + }); + } + + os.popupMenu(menu, ev.currentTarget ?? ev.target); } async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる From 7c065584a1ead1ae16e1b685bc2f0cf6a7c3fad3 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 13 May 2025 17:12:01 +0900 Subject: [PATCH 32/62] wip --- packages/frontend/src/components/MkDrive.vue | 5 ++++- packages/frontend/src/components/MkUploaderDialog.vue | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 95a617f491..89a2a7b95d 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -555,7 +555,10 @@ function getMenu() { text: i18n.ts.upload, icon: 'ti ti-upload', action: () => { - chooseFileFromPcAndUpload(true, { uploadFolder: folder.value?.id, keepOriginal: true }); + chooseFileFromPcAndUpload({ + multiple: true, + folderId: folder.value?.id, + }); }, }, { text: i18n.ts.fromUrl, diff --git a/packages/frontend/src/components/MkUploaderDialog.vue b/packages/frontend/src/components/MkUploaderDialog.vue index 60f4364592..eae0470f96 100644 --- a/packages/frontend/src/components/MkUploaderDialog.vue +++ b/packages/frontend/src/components/MkUploaderDialog.vue @@ -32,6 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ ctx.name }}
+ {{ ctx.file.type }} {{ bytes(ctx.file.size) }} ({{ i18n.tsx._uploader.compressedToX({ x: bytes(ctx.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - ctx.compressedSize / ctx.file.size) * 100) }) }})
From edeeca8b5f10ebaaa42bea8d3e561ace55ce5506 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 13 May 2025 17:30:56 +0900 Subject: [PATCH 33/62] wip --- .../src/components/MkCropperDialog.vue | 2 +- packages/frontend/src/os.ts | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 2174f2ee1f..06f3886253 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -38,7 +38,7 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ - imageFile: File; + imageFile: File | Blob; aspectRatio: number; uploadFolder?: string | null; }>(); diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index dd5720b04b..3b6887a169 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -9,6 +9,7 @@ import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue'; import { EventEmitter } from 'eventemitter3'; import * as Misskey from 'misskey-js'; import { getProxiedImageUrl } from './utility/media-proxy.js'; +import { uploadFile } from './utility/upload.js'; import type { Component, Ref } from 'vue'; import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { Form, GetFormResultType } from '@/utility/form.js'; @@ -654,7 +655,7 @@ export async function pickEmoji(src: HTMLElement, opts: ComponentProps { return new Promise(resolve => { @@ -672,7 +673,6 @@ export async function cropImageFile(imageFile: File, options: { export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFile: Misskey.entities.DriveFile, options: { aspectRatio: number; - uploadFolder?: string | null; }): Promise { return new Promise(resolve => { const imgUrl = getProxiedImageUrl(imageDriveFile.url, undefined, true); @@ -684,10 +684,17 @@ export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFi canvas.width = image.width; canvas.height = image.height; ctx.drawImage(image, 0, 0); - const imageFile = new File([canvas.toBlob()], imageDriveFile.name, { type: imageDriveFile.type }); - - cropImageFile(imageFile).then(croppedImageFile => { - + canvas.toBlob(blob => { + cropImageFile(blob, { + aspectRatio: options.aspectRatio, + }).then(croppedImageFile => { + uploadFile(croppedImageFile, { + name: imageDriveFile.name, + folderId: imageDriveFile.folderId, + }).then(driveFile => { + resolve(driveFile); + }); + }); }); }; }); From 0397a5046d4aec5d8a377b230ec2de0008c1a845 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 13 May 2025 17:44:59 +0900 Subject: [PATCH 34/62] wip --- .../src/components/MkCropperDialog.vue | 2 - .../components/MkUserSetupDialog.Profile.vue | 47 ++++--- .../frontend/src/pages/settings/profile.vue | 118 ++++++++++++------ 3 files changed, 105 insertions(+), 62 deletions(-) diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 06f3886253..5c3ca4ba51 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -71,8 +71,6 @@ const ok = async () => { }); }); - os.promiseDialog(promise); - const f = await promise; emit('ok', f); diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue index a0d7376c8e..c6e54dc376 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue @@ -67,33 +67,32 @@ watch(description, () => { }); }); -function setAvatar(ev) { - os.chooseFileFromPc({ multiple: false }).then(async (files) => { - const file = files[0]; +async function setAvatar(ev) { + const files = await os.chooseFileFromPc({ multiple: false }); + const file = files[0]; - let originalOrCropped = file; + let originalOrCropped = file; - const { canceled } = await os.confirm({ - type: 'question', - text: i18n.ts.cropImageAsk, - okText: i18n.ts.cropYes, - cancelText: i18n.ts.cropNo, - }); - - if (!canceled) { - originalOrCropped = await os.cropImageFile(file, { - aspectRatio: 1, - }); - } - - const driveFile = (await os.launchUploader([originalOrCropped], {}))[0]; - - const i = await os.apiWithDialog('i/update', { - avatarId: driveFile.id, - }); - $i.avatarId = i.avatarId; - $i.avatarUrl = i.avatarUrl; + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.ts.cropImageAsk, + okText: i18n.ts.cropYes, + cancelText: i18n.ts.cropNo, }); + + if (!canceled) { + originalOrCropped = await os.cropImageFile(file, { + aspectRatio: 1, + }); + } + + const driveFile = (await os.launchUploader([originalOrCropped], {}))[0]; + + const i = await os.apiWithDialog('i/update', { + avatarId: driveFile.id, + }); + $i.avatarId = i.avatarId; + $i.avatarUrl = i.avatarUrl; } diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 6ef93d36c0..3057cde76e 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -257,54 +257,100 @@ function save() { } function changeAvatar(ev) { - selectFile(ev.currentTarget ?? ev.target, i18n.ts.avatar).then(async (file) => { - let originalOrCropped = file; - - const { canceled } = await os.confirm({ - type: 'question', - text: i18n.ts.cropImageAsk, - okText: i18n.ts.cropYes, - cancelText: i18n.ts.cropNo, - }); - - if (!canceled) { - originalOrCropped = await os.createCroppedImageDriveFileFromImageDriveFile(file, { - aspectRatio: 1, - }); - } - + async function done(driveFile) { const i = await os.apiWithDialog('i/update', { - avatarId: originalOrCropped.id, + avatarId: driveFile.id, }); $i.avatarId = i.avatarId; $i.avatarUrl = i.avatarUrl; claimAchievement('profileFilled'); - }); + } + + os.popupMenu([{ + text: i18n.ts.avatar, + type: 'label', + }, { + text: i18n.ts.upload, + icon: 'ti ti-upload', + action: async () => { + const files = await os.chooseFileFromPc({ multiple: false }); + const file = files[0]; + + let originalOrCropped = file; + + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.ts.cropImageAsk, + okText: i18n.ts.cropYes, + cancelText: i18n.ts.cropNo, + }); + + if (!canceled) { + originalOrCropped = await os.cropImageFile(file, { + aspectRatio: 1, + }); + } + + const driveFile = (await os.launchUploader([originalOrCropped], {}))[0]; + done(driveFile); + }, + }, { + text: i18n.ts.fromDrive, + icon: 'ti ti-cloud', + action: () => { + os.selectDriveFile(false).then(files => { + done(files[0]); + }); + }, + }], ev.currentTarget ?? ev.target); } function changeBanner(ev) { - selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner).then(async (file) => { - let originalOrCropped = file; - - const { canceled } = await os.confirm({ - type: 'question', - text: i18n.ts.cropImageAsk, - okText: i18n.ts.cropYes, - cancelText: i18n.ts.cropNo, - }); - - if (!canceled) { - originalOrCropped = await os.createCroppedImageDriveFileFromImageDriveFile(file, { - aspectRatio: 2, - }); - } - + async function done(driveFile) { const i = await os.apiWithDialog('i/update', { - bannerId: originalOrCropped.id, + bannerId: driveFile.id, }); $i.bannerId = i.bannerId; $i.bannerUrl = i.bannerUrl; - }); + } + + os.popupMenu([{ + text: i18n.ts.banner, + type: 'label', + }, { + text: i18n.ts.upload, + icon: 'ti ti-upload', + action: async () => { + const files = await os.chooseFileFromPc({ multiple: false }); + const file = files[0]; + + let originalOrCropped = file; + + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.ts.cropImageAsk, + okText: i18n.ts.cropYes, + cancelText: i18n.ts.cropNo, + }); + + if (!canceled) { + originalOrCropped = await os.cropImageFile(file, { + aspectRatio: 2, + }); + } + + const driveFile = (await os.launchUploader([originalOrCropped], {}))[0]; + done(driveFile); + }, + }, { + text: i18n.ts.fromDrive, + icon: 'ti ti-cloud', + action: () => { + os.selectDriveFile(false).then(files => { + done(files[0]); + }); + }, + }], ev.currentTarget ?? ev.target); } const headerActions = computed(() => []); From df2971e43795b32708773432c1bf0559aef06c3b Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 13 May 2025 18:26:10 +0900 Subject: [PATCH 35/62] wip --- .../src/components/MkUploaderDialog.vue | 19 ++++++++++--------- packages/frontend/src/utility/select-file.ts | 1 + 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/frontend/src/components/MkUploaderDialog.vue b/packages/frontend/src/components/MkUploaderDialog.vue index eae0470f96..a1119b947f 100644 --- a/packages/frontend/src/components/MkUploaderDialog.vue +++ b/packages/frontend/src/components/MkUploaderDialog.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only :key="ctx.id" v-panel :class="[$style.item, ctx.waiting ? $style.itemWaiting : null, ctx.uploaded ? $style.itemCompleted : null, ctx.uploadFailed ? $style.itemFailed : null]" - :style="{ '--p': ctx.progressValue !== null ? `${ctx.progressValue / ctx.progressMax * 100}%` : '0%' }" + :style="{ '--p': ctx.progress != null ? `${ctx.progress.value / ctx.progress.max * 100}%` : '0%' }" >
@@ -127,8 +127,7 @@ const emit = defineEmits<{ const items = ref([] as { id: string; name: string; - progressMax: number | null; - progressValue: number | null; + progress: { max: number; value: number } | null; thumbnail: string; waiting: boolean; uploading: boolean; @@ -263,13 +262,16 @@ async function upload() { // エラーハンドリングなどを考慮してシ folderId: props.folderId, onProgress: (progress) => { item.waiting = false; - item.progressMax = progress.total; - item.progressValue = progress.loaded; + if (item.progress == null) { + item.progress = { max: progress.total, value: progress.loaded }; + } else { + item.progress.value = progress.loaded; + item.progress.max = progress.total; + } }, }).catch(err => { item.uploadFailed = true; - item.progressMax = null; - item.progressValue = null; + item.progress = null; throw err; }).finally(() => { item.uploading = false; @@ -287,8 +289,7 @@ onMounted(() => { items.value.push({ id, name: prefer.s.keepOriginalFilename ? filename : id + extension, - progressMax: null, - progressValue: null, + progress: null, thumbnail: window.URL.createObjectURL(file), waiting: false, uploading: false, diff --git a/packages/frontend/src/utility/select-file.ts b/packages/frontend/src/utility/select-file.ts index b4d9efa3d5..00145169ad 100644 --- a/packages/frontend/src/utility/select-file.ts +++ b/packages/frontend/src/utility/select-file.ts @@ -48,6 +48,7 @@ export function chooseFileFromUrl(): Promise { const marker = Math.random().toString(); // TODO: UUIDとか使う + // TODO: no websocketモード対応 const connection = useStream().useChannel('main'); connection.on('urlUploadFinished', urlResponse => { if (urlResponse.marker === marker) { From 7997ecb8cc41a91f7f5a70f8b7a9684941fc1242 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 13 May 2025 18:45:27 +0900 Subject: [PATCH 36/62] wip --- .../src/components/MkCropperDialog.vue | 6 ++-- .../src/components/MkUploaderDialog.vue | 30 +++++++++++++++---- packages/frontend/src/os.ts | 2 +- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 5c3ca4ba51..7f592fba79 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -39,7 +39,7 @@ import { i18n } from '@/i18n.js'; const props = defineProps<{ imageFile: File | Blob; - aspectRatio: number; + aspectRatio: number | null; uploadFolder?: string | null; }>(); @@ -99,8 +99,8 @@ onMounted(() => { const selection = cropper.getCropperSelection()!; selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(); - selection.aspectRatio = props.aspectRatio; - selection.initialAspectRatio = props.aspectRatio; + if (props.aspectRatio != null) selection.aspectRatio = props.aspectRatio; + selection.initialAspectRatio = props.aspectRatio ?? 1; selection.outlined = true; window.setTimeout(() => { diff --git a/packages/frontend/src/components/MkUploaderDialog.vue b/packages/frontend/src/components/MkUploaderDialog.vue index a1119b947f..aa1ce2e2c2 100644 --- a/packages/frontend/src/components/MkUploaderDialog.vue +++ b/packages/frontend/src/components/MkUploaderDialog.vue @@ -96,14 +96,18 @@ import { isWebpSupported } from '@/utility/isWebpSupported.js'; import { uploadFile } from '@/utility/upload.js'; import * as os from '@/os.js'; -const $i = ensureSignin(); - -const compressionSupportedTypes = [ +const COMPRESSION_SUPPORTED_TYPES = [ 'image/jpeg', 'image/png', 'image/webp', 'image/svg+xml', -] as const; +]; + +const CROPPING_SUPPORTED_TYPES = [ + 'image/jpeg', + 'image/png', + 'image/webp', +]; const mimeTypeMap = { 'image/webp': 'webp', @@ -211,6 +215,21 @@ async function done() { function showMenu(ev: MouseEvent, item: typeof items.value[0]) { const menu: MenuItem[] = []; + if (CROPPING_SUPPORTED_TYPES.includes(item.file.type) && !item.waiting && !item.uploading && !item.uploaded) { + menu.push({ + icon: 'ti ti-crop', + text: i18n.ts.cropImage, + action: async () => { + const cropped = await os.cropImageFile(item.file, { aspectRatio: null }); + items.value.splice(items.value.indexOf(item), 1, { + ...item, + file: markRaw(cropped), + thumbnail: window.URL.createObjectURL(cropped), + }); + }, + }); + } + if (!item.waiting && !item.uploading && !item.uploaded) { menu.push({ icon: 'ti ti-x', @@ -231,7 +250,7 @@ async function upload() { // エラーハンドリングなどを考慮してシ item.waiting = true; item.uploadFailed = false; - const shouldCompress = item.compressedImage == null && compressionLevel.value !== 0 && compressionSettings.value && compressionSupportedTypes.includes(item.file.type) && !(await isAnimated(item.file)); + const shouldCompress = item.compressedImage == null && compressionLevel.value !== 0 && compressionSettings.value && COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) && !(await isAnimated(item.file)); if (shouldCompress) { const config = { @@ -275,6 +294,7 @@ async function upload() { // エラーハンドリングなどを考慮してシ throw err; }).finally(() => { item.uploading = false; + item.waiting = false; }); item.uploaded = driveFile; diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 3b6887a169..652dc52365 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -656,7 +656,7 @@ export async function pickEmoji(src: HTMLElement, opts: ComponentProps { return new Promise(resolve => { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), { From 0e2a3ebc8d534d22bb3fce7049a7c5b3823cade3 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 13 May 2025 19:50:35 +0900 Subject: [PATCH 37/62] wip --- .../admin/custom-emojis-manager.register.vue | 87 +------------------ 1 file changed, 3 insertions(+), 84 deletions(-) diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue index a5345f8fc7..3a7a8df217 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue @@ -35,20 +35,9 @@ SPDX-License-Identifier: AGPL-3.0-only -
-
- {{ i18n.ts._customEmojisManager._local._register.emojiInputAreaCaption }} -
- +
+ {{ i18n.ts.uplaod }} + {{ i18n.ts.fromDrive }}
@@ -310,59 +299,6 @@ async function onClearClicked() { } } -async function onDrop(ev: DragEvent) { - isDragOver.value = false; - - const droppedFiles = await extractDroppedItems(ev).then(it => flattenDroppedFiles(it)); - const confirm = await os.confirm({ - type: 'info', - text: i18n.tsx._customEmojisManager._local._register.confirmUploadEmojisDescription({ count: droppedFiles.length }), - }); - if (confirm.canceled) { - return; - } - - const uploadedItems = Array.of<{ droppedFile: DroppedFile, driveFile: Misskey.entities.DriveFile }>(); - try { - uploadedItems.push( - ...await os.promiseDialog( - Promise.all( - droppedFiles.map(async (it) => ({ - droppedFile: it, - driveFile: await uploadFile( // TODO - it.file, - selectedFolderId.value, - it.file.name.replace(/\.[^.]+$/, ''), - true, - ), - }), - ), - ), - () => { - }, - () => { - }, - ), - ); - } catch (err) { - // ダイアログは共通部品側で出ているはずなので何もしない - return; - } - - const items = uploadedItems.map(({ droppedFile, driveFile }) => { - const item = fromDriveFile(driveFile); - if (directoryToCategory.value) { - item.category = droppedFile.path - .replace(/^\//, '') - .replace(/\/[^/]+$/, '') - .replace(droppedFile.file.name, ''); - } - return item; - }); - - gridItems.value.push(...items); -} - async function onFileSelectClicked() { const driveFiles = await chooseFileFromPcAndUpload( true, @@ -435,23 +371,6 @@ onMounted(async () => { background-color: var(--MI_THEME-infoWarnBg); } -.uploadBox { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 100%; - height: auto; - border: 0.5px dotted var(--MI_THEME-accentedBg); - border-radius: var(--MI-radius); - background-color: var(--MI_THEME-accentedBg); - box-sizing: border-box; - - &.dragOver { - cursor: copy; - } -} - .gridArea { padding-top: 8px; padding-bottom: 8px; From 41032965f1c98c7dfc1354b9c5d6927e7809e5e7 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 13 May 2025 19:55:33 +0900 Subject: [PATCH 38/62] wip --- locales/ja-JP.yml | 4 ---- .../frontend/src/components/MkPreview.vue | 10 ++++++---- .../components/MkUserSetupDialog.Profile.vue | 1 - packages/frontend/src/os.ts | 4 ++-- .../admin/custom-emojis-manager.register.vue | 19 +++++++++---------- .../page-editor/els/page-editor.el.image.vue | 4 ++-- .../frontend/src/pages/settings/profile.vue | 4 ++-- packages/frontend/src/utility/select-file.ts | 8 ++++++-- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a9497db4e7..03321e2cbb 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -3053,10 +3053,6 @@ _customEmojisManager: uploadSettingDescription: "この画面で絵文字アップロードを行う際の動作を設定できます。" directoryToCategoryLabel: "ディレクトリ名を\"category\"に入力する" directoryToCategoryCaption: "ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を\"category\"に入力します。" - emojiInputAreaCaption: "いずれかの方法で登録する絵文字を選択してください。" - emojiInputAreaList1: "この枠に画像ファイルまたはディレクトリをドラッグ&ドロップ" - emojiInputAreaList2: "このリンクをクリックしてPCから選択する" - emojiInputAreaList3: "このリンクをクリックしてドライブから選択する" confirmRegisterEmojisDescription: "リストに表示されている絵文字を新たなカスタム絵文字として登録します。よろしいですか?(負荷を避けるため、一度の操作で登録可能な絵文字は{count}件までです)" confirmClearEmojisDescription: "編集内容を破棄し、リストに表示されている絵文字をクリアします。よろしいですか?" confirmUploadEmojisDescription: "ドラッグ&ドロップされた{count}個のファイルをドライブにアップロードします。実行しますか?" diff --git a/packages/frontend/src/components/MkPreview.vue b/packages/frontend/src/components/MkPreview.vue index d8dfbd1655..a3656ac13c 100644 --- a/packages/frontend/src/components/MkPreview.vue +++ b/packages/frontend/src/components/MkPreview.vue @@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only Pleroma
- This is - the button + This is + the button
@@ -36,13 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue index ce3192e230..4e96eff82e 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue @@ -85,7 +85,7 @@ async function setAvatar(ev) { }); } - const driveFile = (await os.launchUploader([originalOrCropped], {}))[0]; + const driveFile = (await os.launchUploader([originalOrCropped], { multiple: false }))[0]; const i = await os.apiWithDialog('i/update', { avatarId: driveFile.id, diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 54fa5a5af9..6d49408f26 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -772,6 +772,7 @@ export function launchUploader( files: File[], options?: { folderId?: string | null; + multiple?: boolean; }, ): Promise { return new Promise((res, rej) => { @@ -779,6 +780,7 @@ export function launchUploader( const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUploaderDialog.vue')), { files: markRaw(files), folderId: options?.folderId, + multiple: options?.multiple, }, { done: driveFiles => { if (driveFiles.length === 0) return rej(); diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue index 6702cd2ec1..7e3be67230 100644 --- a/packages/frontend/src/pages/chat/room.form.vue +++ b/packages/frontend/src/pages/chat/room.form.vue @@ -86,7 +86,7 @@ async function onPaste(ev: ClipboardEvent) { const ext = lio >= 0 ? pastedFile.name.slice(lio) : ''; const formattedName = formatTimeString(new Date(pastedFile.lastModified), pastedFileName).replace(/{{number}}/g, '1') + ext; const renamedFile = new File([pastedFile], formattedName, { type: pastedFile.type }); - os.launchUploader([renamedFile], {}).then(driveFiles => { + os.launchUploader([renamedFile], { multiple: false }).then(driveFiles => { file.value = driveFiles[0]; }); } @@ -131,7 +131,7 @@ function onDrop(ev: DragEvent): void { // ファイルだったら if (ev.dataTransfer.files.length === 1) { ev.preventDefault(); - os.launchUploader([Array.from(ev.dataTransfer.files)[0]], {}); + os.launchUploader([Array.from(ev.dataTransfer.files)[0]], { multiple: false }); return; } else if (ev.dataTransfer.files.length > 1) { ev.preventDefault(); @@ -177,7 +177,7 @@ function onChangeFile() { if (fileEl.value == null || fileEl.value.files == null) return; if (fileEl.value.files[0]) { - os.launchUploader(Array.from(fileEl.value.files), {}).then(driveFiles => { + os.launchUploader(Array.from(fileEl.value.files), { multiple: false }).then(driveFiles => { file.value = driveFiles[0]; }); } diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index df637d9e9a..cd1565f39e 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -291,7 +291,7 @@ function changeAvatar(ev) { }); } - const driveFile = (await os.launchUploader([originalOrCropped], {}))[0]; + const driveFile = (await os.launchUploader([originalOrCropped], { multiple: false }))[0]; done(driveFile); }, }, { @@ -339,7 +339,7 @@ function changeBanner(ev) { }); } - const driveFile = (await os.launchUploader([originalOrCropped], {}))[0]; + const driveFile = (await os.launchUploader([originalOrCropped], { multiple: false }))[0]; done(driveFile); }, }, {