380 lines
7.9 KiB
Vue
380 lines
7.9 KiB
Vue
|
<script setup lang="tsx">
|
||
|
import type { TokenInjectType, Responses } from '@/api';
|
||
|
import type {
|
||
|
DropdownOption,
|
||
|
DropdownGroupOption,
|
||
|
DropdownDividerOption,
|
||
|
DropdownRenderOption,
|
||
|
DataTableColumn
|
||
|
} from 'naive-ui';
|
||
|
import type { SummaryCell } from 'naive-ui/es/data-table/src/interface';
|
||
|
import { inject, ref, nextTick, Suspense } from 'vue';
|
||
|
import filesize from 'filesize';
|
||
|
import { check_token, FS } from '@/api';
|
||
|
import { loadingMsgWrapper } from '@/utils';
|
||
|
import {
|
||
|
useMessage,
|
||
|
useDialog,
|
||
|
NDataTable,
|
||
|
NText,
|
||
|
NIcon,
|
||
|
NDropdown,
|
||
|
NPopover,
|
||
|
NSpin,
|
||
|
NImageGroup,
|
||
|
NButtonGroup,
|
||
|
NButton,
|
||
|
NModal
|
||
|
} from 'naive-ui';
|
||
|
import {
|
||
|
Folder,
|
||
|
FolderParent,
|
||
|
DocumentBlank,
|
||
|
Delete,
|
||
|
Download
|
||
|
} from '@vicons/carbon';
|
||
|
import NLink from '@/components/NLink.vue';
|
||
|
import AsyncImage from '@/components/AsyncImage.vue';
|
||
|
import createZipDialog from '@/components/DirViewer/CreateZipDialog';
|
||
|
import DeleteModal from '@/components/DirViewer/DeleteModal.vue';
|
||
|
|
||
|
const message = useMessage();
|
||
|
const dialog = useDialog();
|
||
|
|
||
|
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
|
||
|
|
||
|
const emit = defineEmits<{
|
||
|
(e: 'reloadNode'): void;
|
||
|
}>();
|
||
|
|
||
|
type DropdownOptionsType = Array<
|
||
|
| DropdownOption
|
||
|
| DropdownGroupOption
|
||
|
| DropdownDividerOption
|
||
|
| DropdownRenderOption
|
||
|
>;
|
||
|
|
||
|
const props = defineProps<{
|
||
|
nodes: Responses.GetNodeEntry[];
|
||
|
showPreview: boolean;
|
||
|
}>();
|
||
|
|
||
|
const checkedRows = ref<number[]>([]);
|
||
|
const deleteNodes = ref<number[]>([]);
|
||
|
const deleteDialog = ref();
|
||
|
const deleteDialogShow = ref(false);
|
||
|
|
||
|
const dropdownX = ref(0);
|
||
|
const dropdownY = ref(0);
|
||
|
const dropdownShow = ref(false);
|
||
|
let dropdownCurrentNode: Responses.GetNodeEntry | null = null;
|
||
|
|
||
|
const dropdownOptions = ref<DropdownOptionsType>();
|
||
|
const dropdownOptionsFolder: DropdownOptionsType = [
|
||
|
{
|
||
|
label: () => <NText>Download</NText>,
|
||
|
key: 'download',
|
||
|
icon: () => (
|
||
|
<NIcon>
|
||
|
<Download />
|
||
|
</NIcon>
|
||
|
)
|
||
|
},
|
||
|
{
|
||
|
label: () => <NText type="error">Delete</NText>,
|
||
|
key: 'delete',
|
||
|
icon: () => (
|
||
|
<NIcon>
|
||
|
<Delete />
|
||
|
</NIcon>
|
||
|
)
|
||
|
}
|
||
|
];
|
||
|
const dropdownOptionsFile: DropdownOptionsType = [
|
||
|
{
|
||
|
label: () => <NText>Download</NText>,
|
||
|
key: 'download',
|
||
|
icon: () => (
|
||
|
<NIcon>
|
||
|
<Download />
|
||
|
</NIcon>
|
||
|
)
|
||
|
},
|
||
|
{
|
||
|
label: () => <NText type="error">Delete</NText>,
|
||
|
key: 'delete',
|
||
|
icon: () => (
|
||
|
<NIcon>
|
||
|
<Delete />
|
||
|
</NIcon>
|
||
|
)
|
||
|
}
|
||
|
];
|
||
|
|
||
|
const dropdownSelect = loadingMsgWrapper(message, async (key: string) => {
|
||
|
dropdownShow.value = false;
|
||
|
const token = await check_token(jwt);
|
||
|
if (!token) return;
|
||
|
if (!dropdownCurrentNode) return;
|
||
|
switch (key) {
|
||
|
case 'download':
|
||
|
if (dropdownCurrentNode.isFile)
|
||
|
await FS.download_file(token, dropdownCurrentNode.id);
|
||
|
else createZipDialog([dropdownCurrentNode.id], dialog, jwt);
|
||
|
break;
|
||
|
case 'delete':
|
||
|
dialog.warning({
|
||
|
title: 'Really delete?',
|
||
|
content: `Are you sure you want to delete "${dropdownCurrentNode.name}"`,
|
||
|
positiveText: 'Yes',
|
||
|
negativeText: 'No',
|
||
|
onPositiveClick: () => {
|
||
|
if (!dropdownCurrentNode) return;
|
||
|
deleteNodes.value = [dropdownCurrentNode.id];
|
||
|
showDeleteDialog();
|
||
|
}
|
||
|
});
|
||
|
break;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const columns: DataTableColumn<Responses.GetNodeEntry>[] = [
|
||
|
{
|
||
|
type: 'selection',
|
||
|
options: [
|
||
|
{
|
||
|
label: 'Select all folders',
|
||
|
key: 'folders',
|
||
|
onSelect(data) {
|
||
|
checkedRows.value = data
|
||
|
.filter((node) => !node.isFile)
|
||
|
.map((node) => node.id);
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
label: 'Select all files',
|
||
|
key: 'files',
|
||
|
onSelect(data) {
|
||
|
checkedRows.value = data
|
||
|
.filter((node) => node.isFile)
|
||
|
.map((node) => node.id);
|
||
|
}
|
||
|
}
|
||
|
],
|
||
|
disabled(node) {
|
||
|
return node.parent == null;
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
title: 'Name',
|
||
|
key: 'name',
|
||
|
minWidth: 720,
|
||
|
render(node) {
|
||
|
return (
|
||
|
<NLink to={`/fs/${node.id}`}>
|
||
|
<div>
|
||
|
<NIcon
|
||
|
size="1.2em"
|
||
|
color="#111"
|
||
|
component={
|
||
|
node.isFile
|
||
|
? DocumentBlank
|
||
|
: node.name == '..'
|
||
|
? FolderParent
|
||
|
: Folder
|
||
|
}
|
||
|
style="top: 0.25em; margin-right: 0.5em"
|
||
|
/>
|
||
|
{node.name}
|
||
|
</div>
|
||
|
</NLink>
|
||
|
);
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
title: 'Size',
|
||
|
key: 'size',
|
||
|
minWidth: 100,
|
||
|
render(node) {
|
||
|
return !node.isFile ? (
|
||
|
''
|
||
|
) : (
|
||
|
<NPopover trigger="hover">
|
||
|
{{
|
||
|
default: () => `${node.size?.toLocaleString()} bytes`,
|
||
|
trigger: () =>
|
||
|
filesize(node.size ?? 0, {
|
||
|
base: 2,
|
||
|
standard: 'jedec'
|
||
|
})
|
||
|
}}
|
||
|
</NPopover>
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
];
|
||
|
const previewColumns: DataTableColumn<Responses.GetNodeEntry>[] = [
|
||
|
columns[0],
|
||
|
{
|
||
|
title: 'Preview',
|
||
|
key: 'preview',
|
||
|
render(node) {
|
||
|
return node.preview ? (
|
||
|
<Suspense>
|
||
|
{{
|
||
|
default: () => (
|
||
|
<AsyncImage alt={node.name} id={node.id} />
|
||
|
),
|
||
|
fallback: () => <NSpin size="small" />
|
||
|
}}
|
||
|
</Suspense>
|
||
|
) : (
|
||
|
''
|
||
|
);
|
||
|
}
|
||
|
},
|
||
|
...columns.slice(1)
|
||
|
];
|
||
|
|
||
|
const massDownload = loadingMsgWrapper(message, async () => {
|
||
|
const token = await check_token(jwt);
|
||
|
if (!token) return;
|
||
|
const nodes = checkedRows.value;
|
||
|
if (nodes.length == 1) {
|
||
|
const node = props.nodes.find((n) => n.id == nodes[0]);
|
||
|
if (!node) return;
|
||
|
if (node.isFile) await FS.download_file(token, nodes[0]);
|
||
|
else createZipDialog(nodes, dialog, jwt);
|
||
|
} else createZipDialog(nodes, dialog, jwt);
|
||
|
checkedRows.value = [];
|
||
|
});
|
||
|
|
||
|
const massDelete = loadingMsgWrapper(message, async () => {
|
||
|
const token = await check_token(jwt);
|
||
|
if (!token) return;
|
||
|
dialog.warning({
|
||
|
title: 'Really delete?',
|
||
|
content: `Are you sure you want to delete "${checkedRows.value.length} folders/files"`,
|
||
|
positiveText: 'Yes',
|
||
|
negativeText: 'No',
|
||
|
onPositiveClick: loadingMsgWrapper(message, async () => {
|
||
|
deleteNodes.value = checkedRows.value;
|
||
|
showDeleteDialog();
|
||
|
})
|
||
|
});
|
||
|
});
|
||
|
|
||
|
const selectionCell = (): SummaryCell => {
|
||
|
return {
|
||
|
value:
|
||
|
checkedRows.value.length != 0 ? (
|
||
|
<NButtonGroup>
|
||
|
<NButton onClick={massDownload}>Download</NButton>
|
||
|
<NButton onClick={massDelete} type="error">
|
||
|
Delete
|
||
|
</NButton>
|
||
|
</NButtonGroup>
|
||
|
) : (
|
||
|
''
|
||
|
),
|
||
|
colSpan: props.showPreview ? 2 : 1
|
||
|
};
|
||
|
};
|
||
|
|
||
|
const sizeCell = (data: Responses.GetNodeEntry[]): SummaryCell => {
|
||
|
return {
|
||
|
value: (
|
||
|
<span>
|
||
|
{filesize(
|
||
|
data.reduce((cur, node) => cur + (node.size ?? 0), 0),
|
||
|
{
|
||
|
base: 2,
|
||
|
standard: 'jedec'
|
||
|
}
|
||
|
)}
|
||
|
</span>
|
||
|
)
|
||
|
};
|
||
|
};
|
||
|
|
||
|
function createPreviewSummary(data: Responses.GetNodeEntry[]) {
|
||
|
return {
|
||
|
preview: selectionCell(),
|
||
|
size: sizeCell(data)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function createSummary(data: Responses.GetNodeEntry[]) {
|
||
|
return {
|
||
|
name: selectionCell(),
|
||
|
size: sizeCell(data)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function rowProps(node: Responses.GetNodeEntry) {
|
||
|
if (!('isFile' in node)) return {};
|
||
|
return {
|
||
|
onContextmenu: (e: MouseEvent) => {
|
||
|
e.preventDefault();
|
||
|
dropdownShow.value = false;
|
||
|
dropdownCurrentNode = node;
|
||
|
dropdownOptions.value = node.isFile
|
||
|
? dropdownOptionsFile
|
||
|
: dropdownOptionsFolder;
|
||
|
nextTick().then(() => {
|
||
|
dropdownShow.value = true;
|
||
|
dropdownX.value = e.clientX;
|
||
|
dropdownY.value = e.clientY;
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
const rowKey = (node: Responses.GetNodeEntry): number => node.id;
|
||
|
|
||
|
function showDeleteDialog() {
|
||
|
if (deleteNodes.value.length == 0) return;
|
||
|
deleteDialogShow.value = true;
|
||
|
}
|
||
|
|
||
|
async function onShowDeleteDialog() {
|
||
|
await deleteDialog.value?.startDelete();
|
||
|
deleteDialogShow.value = false;
|
||
|
emit('reloadNode');
|
||
|
}
|
||
|
</script>
|
||
|
|
||
|
<template>
|
||
|
<n-image-group>
|
||
|
<n-data-table
|
||
|
:columns="showPreview ? previewColumns : columns"
|
||
|
:data="nodes"
|
||
|
:row-key="rowKey"
|
||
|
:row-props="rowProps"
|
||
|
:summary="showPreview ? createPreviewSummary : createSummary"
|
||
|
v-model:checked-row-keys="checkedRows"
|
||
|
/>
|
||
|
</n-image-group>
|
||
|
<n-dropdown
|
||
|
placement="bottom-start"
|
||
|
trigger="manual"
|
||
|
:x="dropdownX"
|
||
|
:y="dropdownY"
|
||
|
:show="dropdownShow"
|
||
|
:show-arrow="true"
|
||
|
:options="dropdownOptions"
|
||
|
:on-clickoutside="() => (dropdownShow = false)"
|
||
|
@select="dropdownSelect"
|
||
|
/>
|
||
|
<n-modal
|
||
|
v-model:show="deleteDialogShow"
|
||
|
:close-on-esc="false"
|
||
|
:mask-closable="false"
|
||
|
:on-after-enter="onShowDeleteDialog"
|
||
|
>
|
||
|
<DeleteModal ref="deleteDialog" :nodes="deleteNodes" />
|
||
|
</n-modal>
|
||
|
</template>
|
||
|
|
||
|
<style scoped></style>
|