summaryrefslogtreecommitdiff
path: root/packages/client/src/components/MkDrive.vue
diff options
context:
space:
mode:
Diffstat (limited to 'packages/client/src/components/MkDrive.vue')
-rw-r--r--packages/client/src/components/MkDrive.vue786
1 files changed, 0 insertions, 786 deletions
diff --git a/packages/client/src/components/MkDrive.vue b/packages/client/src/components/MkDrive.vue
deleted file mode 100644
index 002ca58d04..0000000000
--- a/packages/client/src/components/MkDrive.vue
+++ /dev/null
@@ -1,786 +0,0 @@
-<template>
-<div class="yfudmmck">
- <nav>
- <div class="path" @contextmenu.prevent.stop="() => {}">
- <XNavFolder
- :class="{ current: folder == null }"
- :parent-folder="folder"
- @move="move"
- @upload="upload"
- @removeFile="removeFile"
- @removeFolder="removeFolder"
- />
- <template v-for="f in hierarchyFolders">
- <span class="separator"><i class="fas fa-angle-right"></i></span>
- <XNavFolder
- :folder="f"
- :parent-folder="folder"
- @move="move"
- @upload="upload"
- @removeFile="removeFile"
- @removeFolder="removeFolder"
- />
- </template>
- <span v-if="folder != null" class="separator"><i class="fas fa-angle-right"></i></span>
- <span v-if="folder != null" class="folder current">{{ folder.name }}</span>
- </div>
- <button class="menu _button" @click="showMenu"><i class="fas fa-ellipsis-h"></i></button>
- </nav>
- <div
- ref="main" class="main"
- :class="{ uploading: uploadings.length > 0, fetching }"
- @dragover.prevent.stop="onDragover"
- @dragenter="onDragenter"
- @dragleave="onDragleave"
- @drop.prevent.stop="onDrop"
- @contextmenu.stop="onContextmenu"
- >
- <div ref="contents" class="contents">
- <div v-show="folders.length > 0" ref="foldersContainer" class="folders">
- <XFolder
- v-for="(f, i) in folders"
- :key="f.id"
- v-anim="i"
- class="folder"
- :folder="f"
- :select-mode="select === 'folder'"
- :is-selected="selectedFolders.some(x => x.id === f.id)"
- @chosen="chooseFolder"
- @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="padding"></div>
- <MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.ts.loadMore }}</MkButton>
- </div>
- <div v-show="files.length > 0" ref="filesContainer" class="files">
- <XFile
- v-for="(file, i) in files"
- :key="file.id"
- v-anim="i"
- class="file"
- :file="file"
- :select-mode="select === 'file'"
- :is-selected="selectedFiles.some(x => x.id === file.id)"
- @chosen="chooseFile"
- @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="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="empty">
- <p v-if="draghover">{{ i18n.t('empty-draghover') }}</p>
- <p v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p>
- <p v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</p>
- </div>
- </div>
- <MkLoading v-if="fetching"/>
- </div>
- <div v-if="draghover" class="dropzone"></div>
- <input ref="fileInput" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue';
-import * as Misskey from 'misskey-js';
-import MkButton from './MkButton.vue';
-import XNavFolder from '@/components/MkDrive.navFolder.vue';
-import XFolder from '@/components/MkDrive.folder.vue';
-import XFile from '@/components/MkDrive.file.vue';
-import * as os from '@/os';
-import { stream } from '@/stream';
-import { defaultStore } from '@/store';
-import { i18n } from '@/i18n';
-import { uploadFile, uploads } from '@/scripts/upload';
-
-const props = withDefaults(defineProps<{
- initialFolder?: Misskey.entities.DriveFolder;
- type?: string;
- multiple?: boolean;
- select?: 'file' | 'folder' | null;
-}>(), {
- multiple: false,
- select: null,
-});
-
-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: 'cd', v: Misskey.entities.DriveFolder | null): void;
- (ev: 'open-folder', v: Misskey.entities.DriveFolder): void;
-}>();
-
-const loadMoreFiles = ref<InstanceType<typeof MkButton>>();
-const fileInput = ref<HTMLInputElement>();
-
-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 = stream.useChannel('drive');
-const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
-
-// ドロップされようとしているか
-const draghover = ref(false);
-
-// 自身の所有するアイテムがドラッグをスタートさせたか
-// (自分自身の階層にドロップできないようにするためのフラグ)
-const isDragSource = ref(false);
-
-const fetching = ref(true);
-
-const ilFilesObserver = new IntersectionObserver(
- (entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
-);
-
-watch(folder, () => emit('cd', folder.value));
-
-function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
- addFile(file, true);
-}
-
-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 onStreamDriveFileDeleted(fileId: string) {
- removeFile(fileId);
-}
-
-function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder) {
- addFolder(createdFolder, true);
-}
-
-function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) {
- const current = folder.value ? folder.value.id : null;
- if (current !== updatedFolder.parentId) {
- removeFolder(updatedFolder);
- } else {
- addFolder(updatedFolder, true);
- }
-}
-
-function onStreamDriveFolderDeleted(folderId: string) {
- removeFolder(folderId);
-}
-
-function onDragover(ev: DragEvent): any {
- if (!ev.dataTransfer) return;
-
- // ドラッグ元が自分自身の所有するアイテムだったら
- if (isDragSource.value) {
- // 自分自身にはドロップさせない
- ev.dataTransfer.dropEffect = 'none';
- return;
- }
-
- 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) {
- ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
- } else {
- ev.dataTransfer.dropEffect = 'none';
- }
-
- return false;
-}
-
-function onDragenter() {
- if (!isDragSource.value) draghover.value = true;
-}
-
-function onDragleave() {
- draghover.value = false;
-}
-
-function onDrop(ev: DragEvent): any {
- draghover.value = false;
-
- if (!ev.dataTransfer) return;
-
- // ドロップされてきたものがファイルだったら
- if (ev.dataTransfer.files.length > 0) {
- for (const file of Array.from(ev.dataTransfer.files)) {
- upload(file, folder.value);
- }
- 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);
- os.api('drive/files/update', {
- fileId: file.id,
- folderId: folder.value ? folder.value.id : null,
- });
- }
- //#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);
- os.api('drive/folders/update', {
- folderId: droppedFolder.id,
- parentId: folder.value ? folder.value.id : null,
- }).then(() => {
- // noop
- }).catch(err => {
- switch (err) {
- case 'detected-circular-definition':
- os.alert({
- title: i18n.ts.unableToProcess,
- text: i18n.ts.circularReferenceFolder,
- });
- break;
- default:
- os.alert({
- type: 'error',
- text: i18n.ts.somethingHappened,
- });
- }
- });
- }
- //#endregion
-}
-
-function selectLocalFile() {
- fileInput.value?.click();
-}
-
-function urlUpload() {
- os.inputText({
- title: i18n.ts.uploadFromUrl,
- type: 'url',
- placeholder: i18n.ts.uploadFromUrlDescription,
- }).then(({ canceled, result: url }) => {
- if (canceled || !url) return;
- os.api('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({
- title: i18n.ts.createFolder,
- placeholder: i18n.ts.folderName,
- }).then(({ canceled, result: name }) => {
- if (canceled) return;
- os.api('drive/folders/create', {
- name: name,
- parentId: folder.value ? folder.value.id : undefined,
- }).then(createdFolder => {
- addFolder(createdFolder, true);
- });
- });
-}
-
-function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
- os.inputText({
- title: i18n.ts.renameFolder,
- placeholder: i18n.ts.inputNewFolderName,
- default: folderToRename.name,
- }).then(({ canceled, result: name }) => {
- if (canceled) return;
- os.api('drive/folders/update', {
- folderId: folderToRename.id,
- name: name,
- }).then(updatedFolder => {
- // FIXME: 画面を更新するために自分自身に移動
- move(updatedFolder);
- });
- });
-}
-
-function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
- os.api('drive/folders/delete', {
- folderId: folderToDelete.id,
- }).then(() => {
- // 削除時に親フォルダに移動
- move(folderToDelete.parentId);
- }).catch(err => {
- switch (err.id) {
- case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
- os.alert({
- type: 'error',
- title: i18n.ts.unableToDelete,
- text: i18n.ts.hasChildFilesOrFolders,
- });
- break;
- default:
- os.alert({
- type: 'error',
- text: i18n.ts.unableToDelete,
- });
- }
- });
-}
-
-function onChangeFileInput() {
- if (!fileInput.value?.files) return;
- for (const file of Array.from(fileInput.value.files)) {
- upload(file, folder.value);
- }
-}
-
-function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
- uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => {
- addFile(res, 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);
- }
- emit('change-selection', selectedFiles.value);
- } else {
- if (isAlreadySelected) {
- emit('selected', file);
- } else {
- selectedFiles.value = [file];
- emit('change-selection', [file]);
- }
- }
-}
-
-function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
- const isAlreadySelected = selectedFolders.value.some(f => f.id === folderToChoose.id);
- if (props.multiple) {
- if (isAlreadySelected) {
- selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToChoose.id);
- } else {
- selectedFolders.value.push(folderToChoose);
- }
- emit('change-selection', selectedFolders.value);
- } else {
- if (isAlreadySelected) {
- emit('selected', folderToChoose);
- } else {
- selectedFolders.value = [folderToChoose];
- emit('change-selection', [folderToChoose]);
- }
- }
-}
-
-function move(target?: Misskey.entities.DriveFolder) {
- if (!target) {
- goRoot();
- return;
- } else if (typeof target === 'object') {
- target = target.id;
- }
-
- fetching.value = true;
-
- os.api('drive/folders/show', {
- folderId: target,
- }).then(folderToMove => {
- folder.value = folderToMove;
- hierarchyFolders.value = [];
-
- const dive = folderToDive => {
- hierarchyFolders.value.unshift(folderToDive);
- if (folderToDive.parent) dive(folderToDive.parent);
- };
-
- if (folderToMove.parent) dive(folderToMove.parent);
-
- emit('open-folder', folderToMove);
- fetch();
- });
-}
-
-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);
-}
-
-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;
-
- 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 = os.api('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 = os.api('drive/files', {
- folderId: folder.value ? folder.value.id : null,
- type: props.type,
- limit: filesMax + 1,
- }).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 fetchMoreFiles() {
- fetching.value = true;
-
- const max = 30;
-
- // ファイル一覧取得
- os.api('drive/files', {
- folderId: folder.value ? folder.value.id : null,
- type: props.type,
- untilId: files.value[files.value.length - 1].id,
- limit: max + 1,
- }).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;
- });
-}
-
-function getMenu() {
- return [{
- type: 'switch',
- text: i18n.ts.keepOriginalUploading,
- ref: keepOriginal,
- }, null, {
- text: i18n.ts.addFile,
- type: 'label',
- }, {
- text: i18n.ts.upload,
- icon: 'fas fa-upload',
- action: () => { selectLocalFile(); },
- }, {
- text: i18n.ts.fromUrl,
- icon: 'fas fa-link',
- action: () => { urlUpload(); },
- }, null, {
- text: folder.value ? folder.value.name : i18n.ts.drive,
- type: 'label',
- }, folder.value ? {
- text: i18n.ts.renameFolder,
- icon: 'fas fa-i-cursor',
- action: () => { renameFolder(folder.value); },
- } : undefined, folder.value ? {
- text: i18n.ts.deleteFolder,
- icon: 'fas fa-trash-alt',
- action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
- } : undefined, {
- text: i18n.ts.createFolder,
- icon: 'fas fa-folder-plus',
- action: () => { createFolder(); },
- }];
-}
-
-function showMenu(ev: MouseEvent) {
- os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
-}
-
-function onContextmenu(ev: MouseEvent) {
- os.contextMenu(getMenu(), ev);
-}
-
-onMounted(() => {
- if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
- nextTick(() => {
- ilFilesObserver.observe(loadMoreFiles.value?.$el);
- });
- }
-
- connection.on('fileCreated', onStreamDriveFileCreated);
- connection.on('fileUpdated', onStreamDriveFileUpdated);
- connection.on('fileDeleted', onStreamDriveFileDeleted);
- connection.on('folderCreated', onStreamDriveFolderCreated);
- connection.on('folderUpdated', onStreamDriveFolderUpdated);
- connection.on('folderDeleted', onStreamDriveFolderDeleted);
-
- if (props.initialFolder) {
- move(props.initialFolder);
- } else {
- fetch();
- }
-});
-
-onActivated(() => {
- if (defaultStore.state.enableInfiniteScroll) {
- nextTick(() => {
- ilFilesObserver.observe(loadMoreFiles.value?.$el);
- });
- }
-});
-
-onBeforeUnmount(() => {
- connection.dispose();
- ilFilesObserver.disconnect();
-});
-</script>
-
-<style lang="scss" scoped>
-.yfudmmck {
- display: flex;
- flex-direction: column;
- height: 100%;
-
- > nav {
- display: flex;
- z-index: 2;
- width: 100%;
- padding: 0 8px;
- box-sizing: border-box;
- overflow: auto;
- font-size: 0.9em;
- box-shadow: 0 1px 0 var(--divider);
-
- &, * {
- user-select: none;
- }
-
- > .path {
- display: inline-block;
- vertical-align: bottom;
- line-height: 42px;
- white-space: nowrap;
-
- > * {
- display: inline-block;
- margin: 0;
- padding: 0 8px;
- line-height: 42px;
- cursor: pointer;
-
- * {
- pointer-events: none;
- }
-
- &:hover {
- text-decoration: underline;
- }
-
- &.current {
- font-weight: bold;
- cursor: default;
-
- &:hover {
- text-decoration: none;
- }
- }
-
- &.separator {
- margin: 0;
- padding: 0;
- opacity: 0.5;
- cursor: default;
-
- > i {
- margin: 0;
- }
- }
- }
- }
-
- > .menu {
- margin-left: auto;
- padding: 0 12px;
- }
- }
-
- > .main {
- flex: 1;
- overflow: auto;
- padding: var(--margin);
-
- &, * {
- user-select: none;
- }
-
- &.fetching {
- cursor: wait !important;
-
- * {
- pointer-events: none;
- }
-
- > .contents {
- opacity: 0.5;
- }
- }
-
- &.uploading {
- height: calc(100% - 38px - 100px);
- }
-
- > .contents {
-
- > .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;
- }
- }
-
- > .empty {
- padding: 16px;
- text-align: center;
- pointer-events: none;
- opacity: 0.5;
-
- > p {
- margin: 0;
- }
- }
- }
- }
-
- > .dropzone {
- position: absolute;
- left: 0;
- top: 38px;
- width: 100%;
- height: calc(100% - 38px);
- border: dashed 2px var(--focus);
- pointer-events: none;
- }
-
- > input {
- display: none;
- }
-}
-</style>