summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/MkDrive.vue
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-05-21 07:31:24 +0900
committerGitHub <noreply@github.com>2025-05-21 07:31:24 +0900
commit9480120eba1db238072b0bdfc9e6d01b2494cb3b (patch)
tree3a4d7963e7dd9e540713d6f2b26fc98e2c586223 /packages/frontend/src/components/MkDrive.vue
parentenhance(frontend): URLプレビューをユーザーサイドで無効化で... (diff)
downloadmisskey-9480120eba1db238072b0bdfc9e6d01b2494cb3b.tar.gz
misskey-9480120eba1db238072b0bdfc9e6d01b2494cb3b.tar.bz2
misskey-9480120eba1db238072b0bdfc9e6d01b2494cb3b.zip
Feat: ドライブ周りのUIの強化 (#16011)
* wip * wip * Update MkDrive.vue * wip * Update MkDrive.vue * Update MkDrive.vue * wip * Update MkDrive.vue * Update MkDrive.vue * wip * Update MkDrive.vue * wip * wip * wip * wip * Update MkDrive.vue * wip * wip * wip * wip * wip * wip * wip * feat(frontend): upload dialog (#16032) * wip * wip * Update MkUploadDialog.vue * wip * wip * wip * wip * wip * Update MkUploadDialog.vue * wip * wip * Update MkDrive.vue * wip * wip * Update MkPostForm.vue * wip * Update room.form.vue * Update os.ts * wiop * wip * wip * wip * wip * wip * wip * wip * Update select-file.ts * wip * wip * Update MkDrive.vue * Update drag-and-drop.ts * wip * wip * wop * wip * wip * Update MkDrive.vue * Update CHANGELOG.md * wipo * Update MkDrive.folder.vue * wip * Update MkUploaderDialog.vue * wip * wip * Update MkUploaderDialog.vue * wip * Update MkDrive.vue * Update MkDrive.vue * wip * wip
Diffstat (limited to 'packages/frontend/src/components/MkDrive.vue')
-rw-r--r--packages/frontend/src/components/MkDrive.vue795
1 files changed, 403 insertions, 392 deletions
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index a1f76ac563..5604f0226f 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -4,17 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkStickyContainer>
+<MkStickyContainer style="background: var(--MI_THEME-bg);">
<template #header>
<nav :class="$style.nav">
<div :class="$style.navPath" @contextmenu.prevent.stop="() => {}">
<XNavFolder
:class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]"
:parentFolder="folder"
- @move="move"
- @upload="upload"
- @removeFile="removeFile"
- @removeFolder="removeFolder"
+ @click="cd(null)"
+ @upload="onUploadRequested"
/>
<template v-for="f in hierarchyFolders">
<span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
@@ -22,10 +20,8 @@ SPDX-License-Identifier: AGPL-3.0-only
:folder="f"
:parentFolder="folder"
:class="[$style.navPathItem]"
- @move="move"
- @upload="upload"
- @removeFile="removeFile"
- @removeFolder="removeFolder"
+ @click="cd(f)"
+ @upload="onUploadRequested"
/>
</template>
<span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
@@ -35,20 +31,42 @@ SPDX-License-Identifier: AGPL-3.0-only
</nav>
</template>
- <div
- ref="main"
- :class="[$style.main, { [$style.uploading]: uploadings.length > 0, [$style.fetching]: fetching }]"
- @dragover.prevent.stop="onDragover"
- @dragenter="onDragenter"
- @dragleave="onDragleave"
- @drop.prevent.stop="onDrop"
- @contextmenu.stop="onContextmenu"
- >
- <div ref="contents">
- <MkInfo v-if="!store.r.readDriveTip.value" closable @close="closeTip()"><div v-html="i18n.ts.driveAboutTip"></div></MkInfo>
- <div v-show="folders.length > 0" ref="foldersContainer" :class="$style.folders">
+ <div>
+ <div v-if="select === 'folder'">
+ <template v-if="folder == null">
+ <MkButton v-if="!isRootSelected" @click="isRootSelected = true">
+ <i class="ti ti-square"></i> {{ i18n.ts.selectThisFolder }}
+ </MkButton>
+ <MkButton v-else @click="isRootSelected = false">
+ <i class="ti ti-checkbox"></i> {{ i18n.ts.unselectThisFolder }}
+ </MkButton>
+ </template>
+ <template v-else>
+ <MkButton v-if="!selectedFolders.some(f => f.id === folder!.id)" @click="selectedFolders.push(folder)">
+ <i class="ti ti-square"></i> {{ i18n.ts.selectThisFolder }}
+ </MkButton>
+ <MkButton v-else @click="selectedFolders = selectedFolders.filter(f => f.id !== folder!.id)">
+ <i class="ti ti-checkbox"></i> {{ i18n.ts.unselectThisFolder }}
+ </MkButton>
+ </template>
+ </div>
+
+ <div
+ ref="main"
+ :class="[$style.main, { [$style.fetching]: fetching }]"
+ @dragover.prevent.stop="onDragover"
+ @dragenter="onDragenter"
+ @dragleave="onDragleave"
+ @drop.prevent.stop="onDrop"
+ @contextmenu.stop="onContextmenu"
+ >
+ <div v-if="!store.r.readDriveTip.value" style="padding: 8px;">
+ <MkInfo closable @close="closeTip()"><div v-html="i18n.ts.driveAboutTip"></div></MkInfo>
+ </div>
+
+ <div :class="$style.folders">
<XFolder
- v-for="(f, i) in folders"
+ v-for="(f, i) in foldersPaginator.items.value"
:key="f.id"
v-anim="i"
:class="$style.folder"
@@ -57,49 +75,64 @@ SPDX-License-Identifier: AGPL-3.0-only
:isSelected="selectedFolders.some(x => x.id === f.id)"
@chosen="chooseFolder"
@unchose="unchoseFolder"
- @move="move"
- @upload="upload"
- @removeFile="removeFile"
- @removeFolder="removeFolder"
- @dragstart="isDragSource = true"
- @dragend="isDragSource = false"
- />
- <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
- <div v-for="(n, i) in 16" :key="i" :class="$style.padding"></div>
- <MkButton v-if="moreFolders" ref="moreFolders" @click="fetchMoreFolders">{{ i18n.ts.loadMore }}</MkButton>
- </div>
- <div v-show="files.length > 0" ref="filesContainer" :class="$style.files">
- <XFile
- v-for="(file, i) in files"
- :key="file.id"
- v-anim="i"
- :class="$style.file"
- :file="file"
- :folder="folder"
- :selectMode="select === 'file'"
- :isSelected="selectedFiles.some(x => x.id === file.id)"
- @chosen="chooseFile"
+ @click="cd(f)"
+ @upload="onUploadRequested"
@dragstart="isDragSource = true"
@dragend="isDragSource = false"
/>
- <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
- <div v-for="(n, i) in 16" :key="i" :class="$style.padding"></div>
- <MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton>
</div>
- <div v-if="files.length == 0 && folders.length == 0 && !fetching" :class="$style.empty">
+ <MkButton v-if="foldersPaginator.canFetchOlder.value" primary rounded @click="foldersPaginator.fetchOlder()">{{ i18n.ts.loadMore }}</MkButton>
+
+ <MkStickyContainer v-for="(item, i) in filesTimeline" :key="`${item.date.getFullYear()}/${item.date.getMonth() + 1}`">
+ <template #header>
+ <div :class="$style.date">
+ <span><i class="ti ti-chevron-down"></i> {{ item.date.getFullYear() }}/{{ item.date.getMonth() + 1 }}</span>
+ </div>
+ </template>
+
+ <TransitionGroup
+ tag="div"
+ :enterActiveClass="prefer.s.animation ? $style.transition_files_enterActive : ''"
+ :leaveActiveClass="prefer.s.animation ? $style.transition_files_leaveActive : ''"
+ :enterFromClass="prefer.s.animation ? $style.transition_files_enterFrom : ''"
+ :leaveToClass="prefer.s.animation ? $style.transition_files_leaveTo : ''"
+ :moveClass="prefer.s.animation ? $style.transition_files_move : ''"
+ :class="$style.files"
+ >
+ <XFile
+ v-for="file in item.items" :key="file.id"
+ :class="$style.file"
+ :file="file"
+ :folder="folder"
+ :isSelected="selectedFiles.some(x => x.id === file.id)"
+ @click="onFileClick($event, file)"
+ @dragstart="onFileDragstart(file, $event)"
+ @dragend="isDragSource = false"
+ />
+ </TransitionGroup>
+ </MkStickyContainer>
+ <MkButton v-show="filesPaginator.canFetchOlder.value" :class="$style.loadMore" primary rounded @click="filesPaginator.fetchOlder()">{{ i18n.ts.loadMore }}</MkButton>
+
+ <div v-if="filesPaginator.items.value.length == 0 && foldersPaginator.items.value.length == 0 && !fetching" :class="$style.empty">
<div v-if="draghover">{{ i18n.ts['empty-draghover'] }}</div>
<div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.ts['empty-drive-description'] }}</div>
<div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div>
</div>
</div>
<MkLoading v-if="fetching"/>
+ <div v-if="draghover" :class="$style.dropzone"></div>
</div>
- <div v-if="draghover" :class="$style.dropzone"></div>
+
+ <template #footer>
+ <div v-if="isEditMode" :class="$style.footer">
+ <MkButton primary rounded @click="moveFilesBulk()"><i class="ti ti-folder-symlink"></i> {{ i18n.ts.move }}...</MkButton>
+ </div>
+ </template>
</MkStickyContainer>
</template>
<script lang="ts" setup>
-import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
+import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, useTemplateRef, watch, computed, TransitionGroup } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from './MkButton.vue';
import MkInfo from './MkInfo.vue';
@@ -111,14 +144,18 @@ 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, uploads } from '@/utility/upload.js';
import { claimAchievement } from '@/utility/achievements.js';
import { prefer } from '@/preferences.js';
-import { chooseFileFromPc } from '@/utility/select-file.js';
+import { chooseFileFromPcAndUpload, selectDriveFolder } from '@/utility/drive.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';
+import { checkDragDataType, getDragData, setDragData } from '@/drag-and-drop.js';
+import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
const props = withDefaults(defineProps<{
- initialFolder?: Misskey.entities.DriveFolder;
+ initialFolder?: Misskey.entities.DriveFolder['id'] | null;
type?: string;
multiple?: boolean;
select?: 'file' | 'folder' | null;
@@ -128,25 +165,13 @@ 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: 'move-root'): void;
+ (ev: 'changeSelectedFiles', v: Misskey.entities.DriveFile[]): void;
+ (ev: 'changeSelectedFolders', v: (Misskey.entities.DriveFolder | null)[]): void;
(ev: 'cd', v: Misskey.entities.DriveFolder | null): void;
- (ev: 'open-folder', v: Misskey.entities.DriveFolder): void;
}>();
-const loadMoreFiles = useTemplateRef('loadMoreFiles');
-
const folder = ref<Misskey.entities.DriveFolder | null>(null);
-const files = ref<Misskey.entities.DriveFile[]>([]);
-const folders = ref<Misskey.entities.DriveFolder[]>([]);
-const moreFiles = ref(false);
-const moreFolders = ref(false);
const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
-const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
-const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
-const uploadings = uploads;
-const connection = useStream().useChannel('drive');
// ドロップされようとしているか
const draghover = ref(false);
@@ -155,51 +180,87 @@ const draghover = ref(false);
// (自分自身の階層にドロップできないようにするためのフラグ)
const isDragSource = ref(false);
-const fetching = ref(true);
+const isEditMode = ref(false);
+
+const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
+const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
+const isRootSelected = ref(false);
-const ilFilesObserver = new IntersectionObserver(
- (entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
-);
+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<NonNullable<Misskey.entities.DriveFilesRequest['sort']>>('+createdAt');
+const filesPaginator = usePagination({
+ ctx: {
+ endpoint: 'drive/files',
+ limit: 30,
+ canFetchDetection: 'limit',
+ params: computed(() => ({
+ folderId: folder.value ? folder.value.id : null,
+ type: props.type,
+ sort: sortModeSelect.value,
+ })),
+ },
+ autoInit: false,
+ autoReInit: false,
+});
+
+const foldersPaginator = usePagination({
+ ctx: {
+ endpoint: 'drive/folders',
+ limit: 30,
+ canFetchDetection: 'limit',
+ params: computed(() => ({
+ folderId: folder.value ? folder.value.id : null,
+ })),
+ },
+ autoInit: false,
+ autoReInit: false,
+});
+
+const filesTimeline = makeDateGroupedTimelineComputedRef(filesPaginator.items, 'month');
+
watch(folder, () => emit('cd', folder.value));
watch(sortModeSelect, () => {
- fetch();
+ initialize();
});
-function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
- addFile(file, true);
+async function initialize() {
+ fetching.value = true;
+ await Promise.all([
+ foldersPaginator.init(),
+ filesPaginator.init(),
+ ]);
+ fetching.value = false;
}
-function onStreamDriveFileUpdated(file: Misskey.entities.DriveFile) {
- const current = folder.value ? folder.value.id : null;
- if (current !== file.folderId) {
- removeFile(file);
- } else {
- addFile(file, true);
+function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
+ if (file.folderId === (folder.value?.id ?? null)) {
+ filesPaginator.prepend(file);
}
}
-function onStreamDriveFileDeleted(fileId: string) {
- removeFile(fileId);
-}
-
-function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder) {
- addFolder(createdFolder, true);
-}
+function onFileDragstart(file: Misskey.entities.DriveFile, ev: DragEvent) {
+ if (isEditMode.value) {
+ if (!selectedFiles.value.some(f => f.id === file.id)) {
+ selectedFiles.value.push(file);
+ }
-function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) {
- const current = folder.value ? folder.value.id : null;
- if (current !== updatedFolder.parentId) {
- removeFolder(updatedFolder);
- } else {
- addFolder(updatedFolder, true);
+ if (ev.dataTransfer) {
+ ev.dataTransfer.effectAllowed = 'move';
+ setDragData(ev, 'driveFiles', selectedFiles.value);
+ }
}
-}
-function onStreamDriveFolderDeleted(folderId: string) {
- removeFolder(folderId);
+ isDragSource.value = true;
}
function onDragover(ev: DragEvent) {
@@ -213,9 +274,7 @@ 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_;
- if (isFile || isDriveFile || isDriveFolder) {
+ if (isFile || checkDragDataType(ev, ['driveFiles', 'driveFolders'])) {
switch (ev.dataTransfer.effectAllowed) {
case 'all':
case 'uninitialized':
@@ -254,109 +313,123 @@ 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;
}
//#region ドライブのファイル
- 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;
- removeFile(file.id);
- misskeyApi('drive/files/update', {
- fileId: file.id,
- folderId: folder.value ? folder.value.id : null,
- });
+ {
+ const droppedData = getDragData(ev, 'driveFiles');
+ if (droppedData != null) {
+ misskeyApi('drive/files/move-bulk', {
+ fileIds: droppedData.map(f => f.id),
+ folderId: folder.value ? folder.value.id : null,
+ }).then(() => {
+ globalEvents.emit('driveFilesUpdated', droppedData.map(x => ({
+ ...x,
+ folderId: folder.value ? folder.value.id : null,
+ folder: folder.value,
+ })));
+ });
+ }
}
//#endregion
//#region ドライブのフォルダ
- 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 (folders.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,
- }).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,
- });
- }
- });
+ {
+ const droppedData = getDragData(ev, 'driveFolders');
+ if (droppedData != null) {
+ const droppedFolder = droppedData[0];
+ // 移動先が自分自身なら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(() => {
+ globalEvents.emit('driveFoldersUpdated', [droppedFolder].map(x => ({
+ ...x,
+ parentId: folder.value ? folder.value.id : null,
+ parent: folder.value,
+ })));
+ }).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
}
-function urlUpload() {
- os.inputText({
+function onUploadRequested(files: File[], folder: Misskey.entities.DriveFolder | null) {
+ os.launchUploader(files, {
+ folderId: folder?.id ?? null,
+ });
+}
+
+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,
+ });
+
+ globalEvents.emit('driveFoldersUpdated', [updatedFolder]);
}
function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
@@ -364,7 +437,8 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
folderId: folderToDelete.id,
}).then(() => {
// 削除時に親フォルダに移動
- move(folderToDelete.parentId);
+ cd(folderToDelete.parentId);
+ globalEvents.emit('driveFoldersDeleted', [folderToDelete]);
}).catch(err => {
switch (err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
@@ -383,28 +457,38 @@ 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);
- });
-}
+function onFileClick(ev: MouseEvent, file: Misskey.entities.DriveFile) {
+ if (ev.shiftKey) {
+ isEditMode.value = true;
+ }
-function chooseFile(file: Misskey.entities.DriveFile) {
- const isAlreadySelected = selectedFiles.value.some(f => f.id === file.id);
- if (props.multiple) {
- if (isAlreadySelected) {
- selectedFiles.value = selectedFiles.value.filter(f => f.id !== file.id);
- } else {
- selectedFiles.value.push(file);
+ if (props.select === 'file' || isEditMode.value) {
+ 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;
}
- emit('change-selection', selectedFiles.value);
- } else {
- if (isAlreadySelected) {
- emit('selected', file);
+
+ if (props.multiple) {
+ if (isAlreadySelected) {
+ selectedFiles.value = selectedFiles.value.filter(f => f.id !== file.id);
+ } else {
+ selectedFiles.value.push(file);
+ }
} else {
- selectedFiles.value = [file];
- emit('change-selection', [file]);
+ if (isAlreadySelected) {
+ //emit('selected', file);
+ } else {
+ selectedFiles.value = [file];
+ }
}
+ } else {
+ os.popupMenu(getDriveFileMenu(file, folder.value), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}
}
@@ -416,23 +500,20 @@ 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']) {
+function cd(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id' | 'parentId']) {
if (!target) {
goRoot();
return;
@@ -455,168 +536,34 @@ function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFold
if (folderToMove.parent) dive(folderToMove.parent);
- emit('open-folder', folderToMove);
- fetch();
+ initialize();
});
}
-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;
- return;
- }
-
- if (unshift) {
- folders.value.unshift(folderToAdd);
- } else {
- folders.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 (files.value.some(f => f.id === fileToAdd.id)) {
- const exist = files.value.map(f => f.id).indexOf(fileToAdd.id);
- files.value[exist] = fileToAdd;
- return;
- }
-
- if (unshift) {
- files.value.unshift(fileToAdd);
- } else {
- files.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);
-}
-
-function removeFile(file: Misskey.entities.DriveFile | string) {
- const fileId = typeof file === 'object' ? file.id : file;
- files.value = files.value.filter(f => f.id !== fileId);
-}
+async function moveFilesBulk() {
+ if (selectedFiles.value.length === 0) return;
-function appendFile(file: Misskey.entities.DriveFile) {
- addFile(file);
-}
+ const toFolder = await selectDriveFolder(folder.value ? folder.value.id : null);
-function appendFolder(folderToAppend: Misskey.entities.DriveFolder) {
- addFolder(folderToAppend);
-}
+ await os.apiWithDialog('drive/files/move-bulk', {
+ fileIds: selectedFiles.value.map(f => f.id),
+ folderId: toFolder[0] ? toFolder[0].id : null,
+ });
-/*
-function prependFile(file: Misskey.entities.DriveFile) {
- addFile(file, true);
+ globalEvents.emit('driveFilesUpdated', selectedFiles.value.map(x => ({
+ ...x,
+ folderId: toFolder[0] ? toFolder[0].id : null,
+ folder: toFolder[0] ?? null,
+ })));
}
-function prependFolder(folderToPrepend: Misskey.entities.DriveFolder) {
- addFolder(folderToPrepend, true);
-}
-*/
function goRoot() {
// 既にrootにいるなら何もしない
if (folder.value == null) return;
folder.value = null;
hierarchyFolders.value = [];
- emit('move-root');
- fetch();
-}
-
-async function fetch() {
- folders.value = [];
- files.value = [];
- moreFolders.value = false;
- moreFiles.value = false;
- fetching.value = true;
-
- const foldersMax = 30;
- const filesMax = 30;
-
- const foldersPromise = misskeyApi('drive/folders', {
- folderId: folder.value ? folder.value.id : null,
- limit: foldersMax + 1,
- }).then(fetchedFolders => {
- if (fetchedFolders.length === foldersMax + 1) {
- moreFolders.value = true;
- fetchedFolders.pop();
- }
- return fetchedFolders;
- });
-
- const filesPromise = misskeyApi('drive/files', {
- folderId: folder.value ? folder.value.id : null,
- type: props.type,
- limit: filesMax + 1,
- sort: sortModeSelect.value,
- }).then(fetchedFiles => {
- if (fetchedFiles.length === filesMax + 1) {
- moreFiles.value = true;
- fetchedFiles.pop();
- }
- return fetchedFiles;
- });
-
- const [fetchedFolders, fetchedFiles] = await Promise.all([foldersPromise, filesPromise]);
-
- for (const x of fetchedFolders) appendFolder(x);
- for (const x of fetchedFiles) appendFile(x);
-
- fetching.value = false;
-}
-
-function fetchMoreFolders() {
- fetching.value = true;
-
- const max = 30;
-
- misskeyApi('drive/folders', {
- folderId: folder.value ? folder.value.id : null,
- type: props.type,
- untilId: folders.value.at(-1)?.id,
- limit: max + 1,
- }).then(folders => {
- if (folders.length === max + 1) {
- moreFolders.value = true;
- folders.pop();
- } else {
- moreFolders.value = false;
- }
- for (const x of folders) appendFolder(x);
- fetching.value = false;
- });
-}
-
-function fetchMoreFiles() {
- fetching.value = true;
-
- const max = 30;
-
- // ファイル一覧取得
- misskeyApi('drive/files', {
- folderId: folder.value ? folder.value.id : null,
- type: props.type,
- untilId: files.value.at(-1)?.id,
- limit: max + 1,
- sort: sortModeSelect.value,
- }).then(files => {
- if (files.length === max + 1) {
- moreFiles.value = true;
- files.pop();
- } else {
- moreFiles.value = false;
- }
- for (const x of files) appendFile(x);
- fetching.value = false;
- });
+ initialize();
}
function getMenu() {
@@ -626,16 +573,13 @@ function getMenu() {
text: i18n.ts.addFile,
type: 'label',
}, {
- text: i18n.ts.upload + ' (' + i18n.ts.compress + ')',
- icon: 'ti ti-upload',
- action: () => {
- chooseFileFromPc(true, { uploadFolder: folder.value?.id, keepOriginal: false });
- },
- }, {
text: i18n.ts.upload,
icon: 'ti ti-upload',
action: () => {
- chooseFileFromPc(true, { uploadFolder: folder.value?.id, keepOriginal: true });
+ chooseFileFromPcAndUpload({
+ multiple: true,
+ folderId: folder.value?.id,
+ });
},
}, {
text: i18n.ts.fromUrl,
@@ -699,6 +643,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;
@@ -716,42 +665,95 @@ function closeTip() {
store.set('readDriveTip', true);
}
-onMounted(() => {
- if (prefer.s.enableInfiniteScroll && loadMoreFiles.value) {
- nextTick(() => {
- ilFilesObserver.observe(loadMoreFiles.value?.$el);
- });
+useGlobalEvent('driveFileCreated', (file) => {
+ if (file.folderId === (folder.value?.id ?? null)) {
+ filesPaginator.prepend(file);
+ }
+});
+
+useGlobalEvent('driveFilesUpdated', (files) => {
+ for (const f of files) {
+ if (filesPaginator.items.value.some(x => x.id === f.id)) {
+ if (f.folderId === (folder.value?.id ?? null)) {
+ filesPaginator.updateItem(f.id, () => f);
+ } else {
+ filesPaginator.removeItem(f.id);
+ }
+ } else {
+ if (f.folderId === (folder.value?.id ?? null)) {
+ filesPaginator.prepend(f);
+ }
+ }
+ }
+});
+
+useGlobalEvent('driveFilesDeleted', (files) => {
+ for (const f of files) {
+ filesPaginator.removeItem(f.id);
+ }
+});
+
+useGlobalEvent('driveFoldersUpdated', (folders) => {
+ for (const f of folders) {
+ if (foldersPaginator.items.value.some(x => x.id === f.id)) {
+ if (f.parentId === (folder.value?.id ?? null)) {
+ foldersPaginator.updateItem(f.id, () => f);
+ } else {
+ foldersPaginator.removeItem(f.id);
+ }
+ } else {
+ if (f.parentId === (folder.value?.id ?? null)) {
+ foldersPaginator.prepend(f);
+ }
+ }
+ }
+});
+
+useGlobalEvent('driveFoldersDeleted', (folders) => {
+ for (const f of folders) {
+ foldersPaginator.removeItem(f.id);
}
+});
+
+let connection: Misskey.ChannelConnection<Misskey.Channels['drive']> | null = null;
- connection.on('fileCreated', onStreamDriveFileCreated);
- connection.on('fileUpdated', onStreamDriveFileUpdated);
- connection.on('fileDeleted', onStreamDriveFileDeleted);
- connection.on('folderCreated', onStreamDriveFolderCreated);
- connection.on('folderUpdated', onStreamDriveFolderUpdated);
- connection.on('folderDeleted', onStreamDriveFolderDeleted);
+onMounted(() => {
+ if (store.s.realtimeMode) {
+ connection = useStream().useChannel('drive');
+ connection.on('fileCreated', onStreamDriveFileCreated);
+ }
if (props.initialFolder) {
- move(props.initialFolder);
+ cd(props.initialFolder);
} else {
- fetch();
+ initialize();
}
});
onActivated(() => {
- if (prefer.s.enableInfiniteScroll) {
- nextTick(() => {
- ilFilesObserver.observe(loadMoreFiles.value?.$el);
- });
- }
});
onBeforeUnmount(() => {
- connection.dispose();
- ilFilesObserver.disconnect();
+ if (connection != null) {
+ connection.dispose();
+ }
});
</script>
<style lang="scss" module>
+.transition_files_move,
+.transition_files_enterActive,
+.transition_files_leaveActive {
+ transition: all 0.2s ease;
+}
+.transition_files_enterFrom,
+.transition_files_leaveTo {
+ opacity: 0;
+}
+.transition_files_leaveActive {
+ position: absolute;
+}
+
.nav {
display: flex;
width: 100%;
@@ -806,9 +808,7 @@ onBeforeUnmount(() => {
}
.main {
- flex: 1;
- overflow: auto;
- padding: var(--MI-margin);
+ min-height: 100cqh;
user-select: none;
&.fetching {
@@ -816,30 +816,41 @@ onBeforeUnmount(() => {
opacity: 0.5;
pointer-events: none;
}
-
- &.uploading {
- height: calc(100% - 38px - 100px);
- }
}
.folders,
.files {
- display: flex;
- flex-wrap: wrap;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ grid-gap: 12px;
+ padding: 16px 32px;
}
-.folder,
-.file {
- flex-grow: 1;
- width: 128px;
- margin: 4px;
- box-sizing: border-box;
+@container (max-width: 600px) {
+ .folders,
+ .files {
+ padding: 16px;
+ }
}
-.padding {
- flex-grow: 1;
- pointer-events: none;
- width: 128px + 8px;
+.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);
+}
+
+.loadMore {
+ margin: 16px auto;
+}
+
+.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 {