mirror of
https://github.com/MisskeyIO/misskey
synced 2024-12-15 23:28:16 +09:00
780 lines
19 KiB
Vue
780 lines
19 KiB
Vue
<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 XNavFolder from './drive.nav-folder.vue';
|
|
import XFolder from './drive.folder.vue';
|
|
import XFile from './drive.file.vue';
|
|
import MkButton from './ui/button.vue';
|
|
import * as os from '@/os';
|
|
import { stream } from '@/stream';
|
|
import { defaultStore } from '@/store';
|
|
import { i18n } from '@/i18n';
|
|
|
|
const props = withDefaults(defineProps<{
|
|
initialFolder?: Misskey.entities.DriveFolder;
|
|
type?: string;
|
|
multiple?: boolean;
|
|
select?: 'file' | 'folder' | null;
|
|
}>(), {
|
|
multiple: false,
|
|
select: null,
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
|
|
(e: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
|
|
(e: 'move-root'): void;
|
|
(e: 'cd', v: Misskey.entities.DriveFolder | null): void;
|
|
(e: '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 = os.uploads;
|
|
const connection = stream.useChannel('drive');
|
|
|
|
// ドロップされようとしているか
|
|
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(e: DragEvent): any {
|
|
if (!e.dataTransfer) return;
|
|
|
|
// ドラッグ元が自分自身の所有するアイテムだったら
|
|
if (isDragSource.value) {
|
|
// 自分自身にはドロップさせない
|
|
e.dataTransfer.dropEffect = 'none';
|
|
return;
|
|
}
|
|
|
|
const isFile = e.dataTransfer.items[0].kind == 'file';
|
|
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
|
|
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
|
|
if (isFile || isDriveFile || isDriveFolder) {
|
|
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
|
|
} else {
|
|
e.dataTransfer.dropEffect = 'none';
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function onDragenter() {
|
|
if (!isDragSource.value) draghover.value = true;
|
|
}
|
|
|
|
function onDragleave() {
|
|
draghover.value = false;
|
|
}
|
|
|
|
function onDrop(e: DragEvent): any {
|
|
draghover.value = false;
|
|
|
|
if (!e.dataTransfer) return;
|
|
|
|
// ドロップされてきたものがファイルだったら
|
|
if (e.dataTransfer.files.length > 0) {
|
|
for (const file of Array.from(e.dataTransfer.files)) {
|
|
upload(file, folder.value);
|
|
}
|
|
return;
|
|
}
|
|
|
|
//#region ドライブのファイル
|
|
const driveFile = e.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 = e.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) {
|
|
os.upload(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null).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 [{
|
|
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: 50px;
|
|
white-space: nowrap;
|
|
|
|
> * {
|
|
display: inline-block;
|
|
margin: 0;
|
|
padding: 0 8px;
|
|
line-height: 50px;
|
|
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>
|