support directory drag-drop

This commit is contained in:
samunohito 2024-01-30 22:59:32 +09:00
parent d453196c9f
commit dfb57afa11
1 changed files with 124 additions and 20 deletions

View File

@ -17,6 +17,11 @@
<template #label>{{ i18n.ts.keepOriginalUploading }}</template> <template #label>{{ i18n.ts.keepOriginalUploading }}</template>
<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template> <template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template>
</MkSwitch> </MkSwitch>
<MkSwitch v-model="directoryToCategory">
<template #label>ディレクトリ名を"category"に入力する</template>
<template #caption>ディレクトリをドラッグドロップした時にディレクトリ名を"category"に入力します</template>
</MkSwitch>
</div> </div>
</MkFolder> </MkFolder>
@ -50,7 +55,7 @@
いずれかの方法で登録する絵文字を選択してください いずれかの方法で登録する絵文字を選択してください
</div> </div>
<ul> <ul>
<li>この枠にディレクトリまたは画像ファイルをドラッグドロップ</li> <li>この枠に画像ファイルまたはディレクトリ対応ブラウザのみをドラッグドロップ</li>
<li><a @click="onFileSelectClicked">このリンクをクリックしてPCから選択する</a></li> <li><a @click="onFileSelectClicked">このリンクをクリックしてPCから選択する</a></li>
<li><a @click="onDriveSelectClicked">このリンクをクリックしてドライブから選択する</a></li> <li><a @click="onDriveSelectClicked">このリンクをクリックしてドライブから選択する</a></li>
</ul> </ul>
@ -84,14 +89,12 @@
<script setup lang="ts"> <script setup lang="ts">
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { GridItem, IGridItem } from '@/pages/admin/custom-emojis-grid.impl.js'; import { GridItem, IGridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import MkGrid from '@/components/grid/MkGrid.vue'; import MkGrid from '@/components/grid/MkGrid.vue';
import { CellValueChangedEvent, ColumnSetting, GridRow } from '@/components/grid/grid.js'; import { CellValueChangedEvent, ColumnSetting, GridRow } from '@/components/grid/grid.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import { uploadFile } from '@/scripts/upload.js';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
@ -99,6 +102,7 @@ import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { required, ValidateViolation } from '@/components/grid/cell-validators.js'; import { required, ValidateViolation } from '@/components/grid/cell-validators.js';
import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js'; import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js';
import { uploadFile } from '@/scripts/upload.js';
type FolderItem = { type FolderItem = {
id?: string; id?: string;
@ -119,6 +123,20 @@ type RegisterLogItem = {
error?: string; error?: string;
}; };
type DroppedItem = DroppedFile | DroppedDirectory;
type DroppedFile = {
isFile: true;
path: string;
file: File;
};
type DroppedDirectory = {
isFile: false;
path: string;
children: DroppedItem[];
}
const columnSettings: ColumnSetting[] = [ const columnSettings: ColumnSetting[] = [
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: true, width: 'auto', validators: [required] }, { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: true, width: 'auto', validators: [required] },
{ bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140, validators: [required] }, { bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140, validators: [required] },
@ -145,6 +163,8 @@ const uploadFolders = ref<FolderItem[]>([]);
const gridItems = ref<IGridItem[]>([]); const gridItems = ref<IGridItem[]>([]);
const selectedFolderId = ref(defaultStore.state.uploadFolder); const selectedFolderId = ref(defaultStore.state.uploadFolder);
const keepOriginalUploading = ref(defaultStore.state.keepOriginalUploading); const keepOriginalUploading = ref(defaultStore.state.keepOriginalUploading);
const directoryToCategory = ref<boolean>(true);
const registerButtonDisabled = ref<boolean>(false); const registerButtonDisabled = ref<boolean>(false);
const registerLogs = ref<RegisterLogItem[]>([]); const registerLogs = ref<RegisterLogItem[]>([]);
@ -223,30 +243,56 @@ async function onDrop(ev: DragEvent) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
const dropFiles = ev.dataTransfer?.files; const dropItems = ev.dataTransfer?.items;
if (!dropFiles) { if (!dropItems || dropItems.length === 0) {
return;
}
const droppedFiles = Array.of<DroppedFile>();
const apiTestItem = dropItems[0];
if ('webkitGetAsEntry' in apiTestItem) {
const droppedItems = await eachDroppedItems(dropItems);
droppedFiles.push(...flattenDroppedItems(droppedItems).filter(it => it.isFile));
} else {
// webkitGetAsEntryfiles
const dropFiles = ev.dataTransfer.files;
if (!dropFiles || dropFiles.length === 0) {
return; return;
} }
const uploadingPromises = Array.of<Promise<Misskey.entities.DriveFile>>();
for (let i = 0; i < dropFiles.length; i++) { for (let i = 0; i < dropFiles.length; i++) {
const file = dropFiles.item(i); const file = dropFiles.item(i);
if (file) { if (file) {
const name = file.name.replace(/\.[a-zA-Z0-9]+$/, ''); droppedFiles.push({
uploadingPromises.push( isFile: true,
uploadFile( path: file.name,
file, file,
selectedFolderId.value, });
name, }
keepOriginalUploading.value,
),
);
} }
} }
const uploadedFiles = await Promise.all(uploadingPromises); const uploadedItems = await Promise.all(
for (const uploadedFile of uploadedFiles) { droppedFiles.map(async (it) => ({
const item = GridItem.fromDriveFile(uploadedFile); droppedFile: it,
driveFile: await uploadFile(
it.file,
selectedFolderId.value,
it.file.name.replace(/\.[^.]+$/, ''),
keepOriginalUploading.value,
),
}),
),
);
for (const { droppedFile, driveFile } of uploadedItems) {
const item = GridItem.fromDriveFile(driveFile);
if (directoryToCategory.value) {
item.category = droppedFile.path
.replace(/^\//, '')
.replace(/\/[^/]+$/, '')
.replace(droppedFile.file.name, '');
}
gridItems.value.push(item); gridItems.value.push(item);
} }
} }
@ -285,6 +331,64 @@ function onChangeCellValue(event: CellValueChangedEvent) {
item[event.column.setting.bindTo] = event.value; item[event.column.setting.bindTo] = event.value;
} }
async function eachDroppedItems(itemList: DataTransferItemList): Promise<DroppedItem[]> {
async function readEntry(entry: FileSystemEntry): Promise<DroppedItem> {
if (entry.isFile) {
return {
isFile: true,
path: entry.fullPath,
file: await readFile(entry as FileSystemFileEntry),
};
} else {
return {
isFile: false,
path: entry.fullPath,
children: await readDirectory(entry as FileSystemDirectoryEntry),
};
}
}
function readFile(fileSystemFileEntry: FileSystemFileEntry): Promise<File> {
return new Promise((resolve, reject) => {
fileSystemFileEntry.file(resolve, reject);
});
}
function readDirectory(fileSystemDirectoryEntry: FileSystemDirectoryEntry): Promise<DroppedItem[]> {
return new Promise((resolve, reject) => {
fileSystemDirectoryEntry.createReader().readEntries(
async (entries) => resolve(await Promise.all(entries.map(readEntry))),
reject,
);
});
}
//
const items = Array.of<DataTransferItem>();
for (let i = 0; i < itemList.length; i++) {
items.push(itemList[i]);
}
return Promise.all(
items
.map(it => it.webkitGetAsEntry())
.filter(it => it)
.map(it => readEntry(it!)),
);
}
function flattenDroppedItems(items: DroppedItem[]): DroppedFile[] {
const result = Array.of<DroppedFile>();
for (const item of items) {
if (item.isFile) {
result.push(item);
} else {
result.push(...flattenDroppedItems(item.children));
}
}
return result;
}
async function refreshUploadFolders() { async function refreshUploadFolders() {
const result = await misskeyApi('drive/folders', {}); const result = await misskeyApi('drive/folders', {});
uploadFolders.value = Array.of<FolderItem>({ name: '-' }, ...result); uploadFolders.value = Array.of<FolderItem>({ name: '-' }, ...result);