380 lines
7.9 KiB
Vue
Raw Normal View History

2022-09-03 23:32:20 +02:00
<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>