Rewrote Frontend

This commit is contained in:
2022-09-03 23:32:20 +02:00
parent 0939525cf3
commit 16876e090d
98 changed files with 4995 additions and 1757 deletions

View File

@@ -0,0 +1,31 @@
<script setup async lang="ts">
import type { TokenInjectType } from '@/api';
import { inject, ref } from 'vue';
import { NImage } from 'naive-ui';
import { check_token, FS, isErrorResponse } from '@/api';
const props = defineProps<{
alt: string;
id: number;
}>();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const success = ref(false);
const data = ref('');
const token = await check_token(jwt);
if (token) {
const resp = await FS.download_preview(jwt.jwt.value ?? '', props.id);
if (!isErrorResponse(resp)) {
data.value = resp.data;
success.value = true;
}
}
</script>
<template>
<NImage v-if="success" :alt="alt" :src="data" />
</template>
<style lang="scss"></style>

View File

@@ -0,0 +1,90 @@
import type { TokenInjectType } from '@/api';
import { ref } from 'vue';
import { NProgress, NButton, NIcon } from 'naive-ui';
import filesize from 'filesize';
import { Archive, Download } from '@vicons/carbon';
import { FS, check_token, isErrorResponse } from '@/api';
import type { DialogApiInjection } from 'naive-ui/es/dialog/src/DialogProvider';
export default function createZipDialog(
nodes: number[],
dialog: DialogApiInjection,
jwt: TokenInjectType
) {
const progress = ref(0);
const total = ref(1);
const percentage = ref(0);
const done = ref(false);
const dia = dialog.create({
title: 'Create Archive...',
closable: false,
closeOnEsc: false,
maskClosable: false,
icon: () => <Archive />,
content: () => (
<NProgress
type="line"
percentage={percentage.value}
height={20}
status="info"
showIndicator={false}
/>
),
action: () =>
done.value ? (
<NButton
onClick={async () => {
const token = await check_token(jwt);
if (!token) return;
if (nodes.length == 1)
FS.download_file(token, nodes[0]);
else FS.download_multi_file(token, nodes);
dia.destroy();
}}
>
{{
icon: () => (
<NIcon>
<Download />
</NIcon>
),
default: () => 'Download archive'
}}
</NButton>
) : (
<div>
{filesize(progress.value, {
base: 2,
standard: 'jedec'
})}
/
{filesize(total.value, {
base: 2,
standard: 'jedec'
})}
- {Math.floor(percentage.value * 1000) / 1000}%
</div>
)
});
let updateRunning = false;
const updateInterval = setInterval(async () => {
if (updateRunning) return;
updateRunning = true;
const token = await check_token(jwt);
if (!token) return;
const resp = await FS.create_zip(token, nodes);
if (isErrorResponse(resp)) return;
if (resp.done) {
percentage.value = 100;
clearInterval(updateInterval);
done.value = true;
} else {
progress.value = resp.progress ?? 0;
total.value = resp.total ?? 1;
if (total.value == 0) total.value = 1;
percentage.value = (progress.value / total.value) * 100;
}
updateRunning = false;
}, 500);
return dia;
}

View File

@@ -0,0 +1,74 @@
<script setup lang="ts">
import type { TokenInjectType } from '@/api';
import type { LogInst } from 'naive-ui';
import { ref, inject } from 'vue';
import { check_token } from '@/api';
import { NCard, NLog } from 'naive-ui';
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const log = ref('');
const logInst = ref<LogInst>();
function getLogWriter() {
const decoder = new TextDecoder();
return new WritableStream<Uint8Array>({
write(chunk) {
log.value += decoder.decode(chunk, { stream: true });
logInst.value?.scrollTo({ position: 'top' });
},
close() {
log.value += decoder.decode(new Uint8Array(0), { stream: false });
logInst.value?.scrollTo({ position: 'top' });
},
abort(err) {
log.value += `Error: ${err}\n`;
logInst.value?.scrollTo({ position: 'top' });
}
});
}
const props = defineProps<{
nodes: number[];
}>();
async function startDelete() {
const token = await check_token(jwt);
if (!token) return;
for (const node of props.nodes) {
try {
const logWriter = getLogWriter();
const resp = await fetch(`/api/fs/delete/${node}`, {
method: 'post',
headers: {
Authorization: 'Bearer ' + token
}
});
if (!resp.ok) continue;
if (!resp.body) continue;
await resp.body.pipeTo(logWriter);
} catch (err) {
log.value += `Error: ${err}\n`;
logInst.value?.scrollTo({ position: 'top' });
}
}
}
defineExpose({
startDelete
});
</script>
<template>
<n-card title="Deleting..." style="margin: 20px">
<n-log ref="logInst" class="log-code" :log="log" :rows="50"></n-log>
<!--<n-code class="log-code">
</n-code>-->
</n-card>
</template>
<style scoped lang="scss">
.log-code {
margin: 8px;
background-color: rgb(250, 250, 252);
}
</style>

View File

@@ -0,0 +1,124 @@
<script setup lang="tsx">
import type { TokenInjectType, Responses } from '@/api';
import type { CSSProperties } from 'vue';
import { inject, ref, watch } from 'vue';
import {
useMessage,
useDialog,
NSwitch,
NGrid,
NGi,
NButton,
NIcon,
NInput
} from 'naive-ui';
import { FolderAdd } from '@vicons/carbon';
import { FS, check_token } from '@/api';
import DirViewerTable from '@/components/DirViewer/DirViewerTable.vue';
import { loadingMsgWrapper } from '@/utils';
const message = useMessage();
const dialog = useDialog();
const props = defineProps<{
node: Responses.GetNode;
}>();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const emit = defineEmits<{
(e: 'reloadNode'): void;
}>();
const showPreview = ref(false);
const nodes = ref<Responses.GetNodeEntry[]>([]);
watch(
() => props.node,
async (to) => {
nodes.value = [];
if (to.parent != null)
nodes.value.push({
id: to.parent,
isFile: false,
parent: null,
name: '..',
preview: false
});
if (to.children) nodes.value.push(...to.children);
},
{ immediate: true }
);
const newFolder = loadingMsgWrapper(message, async (name: string) => {
const token = await check_token(jwt);
if (!token) return;
await FS.create_folder(token, props.node.id, name);
emit('reloadNode');
});
function previewSwitchRailStyle(state: { focused: boolean; checked: boolean }) {
const style: CSSProperties = {};
style.background = state.checked ? '#0b0' : '#d00';
if (state.focused)
style.boxShadow = `0 0 0 2px ${
state.checked ? '#00bb0040' : '#dd000040'
}`;
return style;
}
function createNewFolderDialog() {
let newFolderName = '';
const dia = dialog.create({
title: 'New Folder',
icon: () => <FolderAdd />,
content: () => (
<NInput
type="text"
placeholder="Folder name"
onInput={(e) => (newFolderName = e)}
onKeyup={(e) => {
if (e.key === 'Enter')
newFolder(newFolderName).then(() => dia.destroy());
}}
/>
),
negativeText: 'Cancel',
positiveText: 'Create',
positiveButtonProps: { type: 'success' },
onPositiveClick: () => newFolder(newFolderName)
});
return dia;
}
</script>
<template>
<n-grid cols="2" x-gap="16" y-gap="16">
<n-gi>
<n-button @click="createNewFolderDialog">
<template #icon>
<n-icon><FolderAdd /></n-icon>
</template>
Create folder
</n-button>
</n-gi>
<n-gi style="text-align: right">
<n-switch
:rail-style="previewSwitchRailStyle"
v-model:value="showPreview"
>
<template #checked>Show preview</template>
<template #unchecked>Hide preview</template>
</n-switch>
</n-gi>
<n-gi span="2">
<DirViewerTable
:nodes="nodes"
:show-preview="showPreview"
@reloadNode="emit('reloadNode')"
/>
</n-gi>
</n-grid>
</template>
<style scoped></style>

View File

@@ -0,0 +1,379 @@
<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>

View File

@@ -1,41 +0,0 @@
<script setup lang="ts">
import type { TokenInjectType } from "@/api";
import { defineEmits, defineProps, inject } from "vue";
import { check_token, FS, Responses } from "@/api";
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const props = defineProps<{
node: Responses.FS.GetNodeResponse;
}>();
const emit = defineEmits<{
(e: "reloadNode"): void;
}>();
async function del() {
const token = await check_token(jwt);
if (!token) return;
await FS.delete_node(token, props.node.id);
emit("reloadNode");
}
async function download() {
const token = await check_token(jwt);
if (!token) return;
FS.download_file(token, props.node.id);
}
</script>
<template>
<td>
<router-link :to="'/fs/' + props.node.id">{{ node.name }}</router-link>
</td>
<td>
<a href="#" @click="download()" v-if="props.node.isFile">Download</a>
</td>
<td>
<a href="#" @click="del()" v-if="props.node.name !== '..'">delete</a>
</td>
</template>
<style scoped></style>

View File

@@ -1,102 +0,0 @@
<script setup lang="ts">
import type { TokenInjectType } from "@/api";
import { defineEmits, defineProps, inject, reactive, ref, watch } from "vue";
import { FS, Responses, check_token } from "@/api";
import DirEntry from "@/components/FSView/DirEntry.vue";
import UploadFileDialog from "@/components/UploadDialog/UploadFileDialog.vue";
import { NModal } from "naive-ui";
const props = defineProps<{
node: Responses.FS.GetNodeResponse;
}>();
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const emit = defineEmits<{
(e: "reloadNode"): void;
(e: "gotoRoot"): void;
}>();
const fileInput = ref<HTMLInputElement>();
const uploadDialog = ref();
const uploadDialogShow = ref(false);
const new_folder_name = ref("");
const files = ref<File[]>([]);
const nodes = ref<Responses.FS.GetNodeResponse[]>([]);
const hasParent = ref(false);
const parentNode = reactive<Responses.FS.GetNodeResponse>({
id: 0,
statusCode: 200,
isFile: false,
parent: null,
name: "..",
});
watch(
() => props.node,
async (to) => {
parentNode.id = to.parent ?? 0;
hasParent.value = to.parent != null;
nodes.value = [];
const token = await check_token(jwt);
if (!token) return;
await Promise.all(
to.children?.map(async (child) => {
nodes.value.push(
(await FS.get_node(token, child)) as Responses.FS.GetNodeResponse
);
}) ?? []
);
},
{ immediate: true }
);
async function newFolder() {
const token = await check_token(jwt);
if (!token) return;
await FS.create_folder(token, props.node.id, new_folder_name.value);
emit("reloadNode");
}
async function uploadFiles() {
files.value = Array.from(fileInput.value?.files ?? []);
if (files.value.length == 0) return;
uploadDialogShow.value = true;
}
async function uploadFilesDialogOpen() {
await uploadDialog.value?.startUpload(props.node.id);
uploadDialogShow.value = false;
if (fileInput.value) fileInput.value.value = "";
emit("reloadNode");
}
</script>
<template>
<div>
<input type="text" placeholder="Folder name" v-model="new_folder_name" />
<a href="#" @click="newFolder()">create folder</a>
</div>
<div>
<input type="file" ref="fileInput" multiple />
<a href="#" @click="uploadFiles()">upload files</a>
</div>
<table>
<tr v-if="hasParent">
<DirEntry :node="parentNode" @reloadNode="emit('reloadNode')" />
</tr>
<tr v-for="n in nodes" :key="n.id">
<DirEntry :node="n" @reloadNode="emit('reloadNode')" />
</tr>
</table>
<n-modal
v-model:show="uploadDialogShow"
:close-on-esc="false"
:mask-closable="false"
:on-after-enter="uploadFilesDialogOpen"
>
<UploadFileDialog ref="uploadDialog" :files="files" />
</n-modal>
</template>
<style scoped></style>

View File

@@ -1,39 +0,0 @@
<script setup lang="ts">
import type { TokenInjectType } from "@/api";
import { defineProps, inject } from "vue";
import { check_token, FS, Responses } from "@/api";
const props = defineProps<{
node: Responses.FS.GetNodeResponse;
}>();
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function del() {
const token = await check_token(jwt);
if (!token) return;
await FS.delete_node(token, props.node.id);
}
async function download() {
const token = await check_token(jwt);
if (!token) return;
FS.download_file(token, props.node.id);
}
</script>
<template>
<div>
<router-link :to="'/fs/' + props.node.parent ?? 0">..</router-link>
</div>
<div>
<a href="#" @click="download()" v-if="props.node.isFile">Download</a>
</div>
<div>
<router-link :to="'/fs/' + props.node.parent ?? 0" @click="del()">
delete
</router-link>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,45 @@
import { ref } from 'vue';
import { NProgress } from 'naive-ui';
import filesize from 'filesize';
import { Music, Video } from '@vicons/carbon';
import type { DialogApiInjection } from 'naive-ui/es/dialog/src/DialogProvider';
export default function createAudioVideoDialog(
dialog: DialogApiInjection,
video: boolean
) {
const progress = ref(0);
const total = ref(1);
const percentage = ref(0);
const dia = dialog.create({
title: video ? 'Loading video...' : 'Loading audio...',
closable: false,
closeOnEsc: false,
maskClosable: false,
icon: () => (video ? <Video /> : <Music />),
content: () => (
<NProgress
type="line"
percentage={percentage.value}
height={20}
status="info"
showIndicator={false}
/>
),
action: () => (
<div>
{filesize(progress.value, {
base: 2,
standard: 'jedec'
})}
/
{filesize(total.value, {
base: 2,
standard: 'jedec'
})}
- {Math.floor(percentage.value * 1000) / 1000}%
</div>
)
});
return { progress, total, percentage, dia };
}

View File

@@ -0,0 +1,129 @@
<script setup lang="ts">
import type { TokenInjectType, Responses } from '@/api';
import { inject, ref, watch } from 'vue';
import { Download, Play } from '@vicons/carbon';
import { useDialog, NGrid, NGi, NButton, NImage, NSpin, NIcon } from 'naive-ui';
import { check_token, FS, isErrorResponse } from '@/api';
import createAudioVideoDialog from '@/components/FileViewer/AudioVideoDownload';
import axios from 'axios';
const props = defineProps<{
node: Responses.GetNode;
}>();
const dialog = useDialog();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
enum fileTypes {
UNKNOWN,
LOADING,
IMAGE,
AUDIO,
VIDEO
}
const fileType = ref<fileTypes>(fileTypes.UNKNOWN);
const src = ref('');
async function download() {
const token = await check_token(jwt);
if (!token) return;
FS.download_file(token, props.node.id);
}
async function loadAudioOrVideo() {
const token = await check_token(jwt);
if (!token) return;
const { progress, total, percentage, dia } = createAudioVideoDialog(
dialog,
fileType.value === fileTypes.VIDEO
);
total.value = props.node.size ?? 1;
const params = new URLSearchParams();
params.append('jwtToken', token);
params.append('id', props.node.id.toString());
const resp = await axios.post('/api/fs/download', params, {
responseType: 'blob',
onDownloadProgress: (e: ProgressEvent) => {
progress.value = e.loaded;
percentage.value = (e.loaded / e.total) * 100;
}
});
dia.destroy();
if (resp.status != 200) return;
src.value = URL.createObjectURL(resp.data as Blob);
}
async function getType(node: Responses.GetNode) {
fileType.value = fileTypes.LOADING;
if (src.value.startsWith('blob')) URL.revokeObjectURL(src.value);
src.value = '';
const token = await check_token(jwt);
if (!token) return;
const resp = await FS.get_type(token, node.id);
if (isErrorResponse(resp)) return;
if (resp.type.startsWith('image')) {
const dataResp = await FS.download_base64(token, node.id);
if (isErrorResponse(dataResp)) return;
src.value = dataResp.data;
fileType.value = fileTypes.IMAGE;
}
if (resp.type.startsWith('audio')) fileType.value = fileTypes.AUDIO;
if (resp.type.startsWith('video')) fileType.value = fileTypes.VIDEO;
}
watch(
() => props.node,
async (to) => {
await getType(to);
if (fileType.value === fileTypes.LOADING)
fileType.value = fileTypes.UNKNOWN;
},
{ immediate: true }
);
</script>
<template>
<n-grid cols="1" x-gap="16" y-gap="16">
<n-gi style="text-align: right">
<n-button @click="download()">
<template #icon>
<n-icon><Download /></n-icon>
</template>
Download
</n-button>
</n-gi>
<n-gi style="text-align: center">
<n-spin v-if="fileType === fileTypes.LOADING" size="large" />
<n-image
v-else-if="fileType === fileTypes.IMAGE"
:src="src"
:alt="node.name"
/>
<template
v-else-if="
fileType === fileTypes.VIDEO || fileType === fileTypes.AUDIO
"
>
<video
v-if="fileType === fileTypes.VIDEO && src !== ''"
:src="src"
controls
/>
<audio
v-else-if="fileType === fileTypes.AUDIO && src !== ''"
:src="src"
controls
/>
<n-button v-else @click="loadAudioOrVideo">
<template #icon>
<n-icon><Play /></n-icon>
</template>
Load and play
</n-button>
</template>
</n-gi>
</n-grid>
</template>
<style scoped></style>

View File

@@ -1,140 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br />
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener"
>vue-cli documentation</a
>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
target="_blank"
rel="noopener"
>babel</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router"
target="_blank"
rel="noopener"
>router</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex"
target="_blank"
rel="noopener"
>vuex</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
target="_blank"
rel="noopener"
>eslint</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
target="_blank"
rel="noopener"
>typescript</a
>
</li>
</ul>
<h3>Essential Links</h3>
<ul>
<li>
<a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
</li>
<li>
<a href="https://forum.vuejs.org" target="_blank" rel="noopener"
>Forum</a
>
</li>
<li>
<a href="https://chat.vuejs.org" target="_blank" rel="noopener"
>Community Chat</a
>
</li>
<li>
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
>Twitter</a
>
</li>
<li>
<a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
</li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li>
<a href="https://router.vuejs.org" target="_blank" rel="noopener"
>vue-router</a
>
</li>
<li>
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
</li>
<li>
<a
href="https://github.com/vuejs/vue-devtools#vue-devtools"
target="_blank"
rel="noopener"
>vue-devtools</a
>
</li>
<li>
<a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
>vue-loader</a
>
</li>
<li>
<a
href="https://github.com/vuejs/awesome-vue"
target="_blank"
rel="noopener"
>awesome-vue</a
>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "HelloWorld",
props: {
msg: String,
},
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import { NA } from 'naive-ui';
defineProps<{
to: string;
}>();
</script>
<template>
<router-link :to="to" #="{ navigate, href }" custom>
<n-a :href="href" @click="navigate"><slot /></n-a>
</router-link>
</template>

View File

@@ -1,52 +1,93 @@
<script setup lang="ts">
import type { Status } from "naive-ui/es/progress/src/interface";
import { defineProps, defineExpose, ref } from "vue";
import { isErrorResponse, FS } from "@/api";
import { NProgress } from "naive-ui";
import filesize from "filesize";
import type { Status } from 'naive-ui/es/progress/src/interface';
import type { UploadFile } from '@/api';
import { ref } from 'vue';
import { isErrorResponse, FS } from '@/api';
import { NProgress } from 'naive-ui';
import filesize from 'filesize';
const props = defineProps<{
file: File;
file: UploadFile;
}>();
const progress = ref(0);
const percentage = ref(0);
const err = ref("");
const status = ref<Status>("info");
const err = ref('');
const status = ref<Status>('info');
const shown = ref(true);
async function startUpload(parent: number, token: string) {
const resp = await FS.upload_file(token, parent, props.file, (e) => {
progress.value = e.loaded;
percentage.value = (e.loaded / e.total) * 100;
});
percentage.value = 100;
if (isErrorResponse(resp)) {
err.value = resp.message ?? "Error";
status.value = "error";
} else status.value = "success";
async function startUpload(token: string, done: () => void) {
const resp = await FS.upload_file(token, props.file, (e) => {
progress.value = e.loaded;
percentage.value = (e.loaded / e.total) * 100;
if (e.loaded == e.total) done();
});
percentage.value = 100;
if (isErrorResponse(resp)) {
err.value = resp.message ?? 'Error';
status.value = 'error';
} else {
status.value = 'success';
shown.value = false;
}
}
defineExpose({
startUpload,
startUpload
});
</script>
<template>
<div v-if="percentage < 100">
{{ file.name }} - {{ filesize(progress) }} / {{ filesize(file.size) }} -
{{ Math.floor(percentage * 1000) / 1000 }}%
</div>
<div v-else-if="err !== ''">{{ file.name }} - Error: {{ err }}</div>
<div v-else>{{ file.name }} - Completed</div>
<n-progress
type="line"
:percentage="percentage"
:height="20"
:status="status"
border-radius="10px 0"
fill-border-radius="10px 0"
:show-indicator="false"
/>
<Transition name="slide-up">
<div class="container" v-show="shown">
<div v-if="percentage < 100">
{{ file.fullName }} -
{{
filesize(progress, {
base: 2,
standard: 'jedec'
})
}}
/
{{
filesize(file.file.size, {
base: 2,
standard: 'jedec'
})
}}
- {{ Math.floor(percentage * 1000) / 1000 }}%
</div>
<div v-else-if="err !== ''">
{{ file.fullName }} - Error: {{ err }}
</div>
<div v-else>{{ file.fullName }} - Completed</div>
<n-progress
type="line"
:percentage="percentage"
:height="20"
:status="status"
border-radius="10px 0"
fill-border-radius="10px 0"
:show-indicator="false"
/>
</div>
</Transition>
</template>
<style scoped></style>
<style scoped lang="scss">
.container {
height: 60px;
padding: 8px;
}
.slide-up-leave-active {
transition: all 2s ease-out;
}
.slide-up-leave-to {
height: 0;
padding: 0 8px;
opacity: 0;
transform: translateY(-60px);
}
</style>

View File

@@ -0,0 +1,203 @@
<script setup lang="ts">
import type { TokenInjectType, Responses, UploadFile } from '@/api';
import { inject, ref } from 'vue';
import { useMessage, NModal, NText, NIcon } from 'naive-ui';
import { CloudUpload } from '@vicons/carbon';
import { FS, check_token, isErrorResponse } from '@/api';
import UploadFileDialog from '@/components/UploadDialog/UploadFileDialog.vue';
import { loadingMsgWrapper } from '@/utils';
const message = useMessage();
const props = defineProps<{
node: Responses.GetNode;
}>();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const emit = defineEmits<{
(e: 'reloadNode'): void;
}>();
const uploadArea = ref<HTMLDivElement>();
const fileInput = ref<HTMLInputElement>();
const uploadDialog = ref();
const uploadDialogShow = ref(false);
const files = ref<UploadFile[]>([]);
function startDrag() {
uploadArea.value?.classList.add('uploadActive');
}
function stopDrag() {
uploadArea.value?.classList.remove('uploadActive');
}
function openBrowser() {
fileInput.value?.click();
}
function browserChanged(event: InputEvent) {
files.value = Array.from(
(event.target as HTMLInputElement).files ?? []
).map((file) => {
return {
parent: props.node.id,
fullName: file.name,
file
};
});
uploadFiles();
}
interface FileSystemDirectoryReader {
readEntries(
successCallback: (entries: FileSystemEntry[]) => void,
errorCallback?: (err: DOMException) => void
): void;
}
interface FileSystemEntry {
readonly fullPath: string;
readonly isDirectory: boolean;
readonly isFile: boolean;
readonly name: string;
file(
successCallback: (file: File) => void,
errorCallback?: (err: DOMException) => void
): void;
createReader(): FileSystemDirectoryReader;
}
const asyncReadEntries = async (
reader: FileSystemDirectoryReader
): Promise<FileSystemEntry[]> =>
new Promise((resolve, reject) => reader.readEntries(resolve, reject));
const getFile = async (entry: FileSystemEntry): Promise<File> =>
new Promise((resolve, reject) => entry.file(resolve, reject));
async function processDirOrFile(
entry: FileSystemEntry,
parent: number,
token: string
) {
if (entry.isDirectory) {
const resp = await FS.create_folder(token, parent, entry.name);
if (isErrorResponse(resp)) return;
if ('exists' in resp && resp.isFile) return;
const reader = entry.createReader();
let entries = [];
do {
try {
entries = await asyncReadEntries(reader);
entries.forEach((e) => processDirOrFile(e, resp.id, token));
} catch {
break;
}
} while (entries.length != 0);
} else
files.value.push({
parent: parent,
fullName: entry.fullPath.slice(1),
file: await getFile(entry)
});
}
const filesDropped = loadingMsgWrapper(message, async (event: DragEvent) => {
stopDrag();
if (!event.dataTransfer) return;
const token = await check_token(jwt);
if (!token) return;
files.value = [];
for (const file of event.dataTransfer.items) {
const entry = file.webkitGetAsEntry();
if (entry)
await processDirOrFile(
entry as unknown as FileSystemEntry,
props.node.id,
token
);
}
uploadFiles();
});
function uploadFiles() {
if (files.value.length == 0) return;
uploadDialogShow.value = true;
}
async function uploadFilesDialogOpen() {
await uploadDialog.value?.startUpload();
uploadDialogShow.value = false;
if (fileInput.value) fileInput.value.value = '';
emit('reloadNode');
}
</script>
<template>
<div
class="uploadArea"
ref="uploadArea"
@drop.prevent
@dragenter.prevent
@dragover.prevent
@dragleave.prevent
@dragend.prevent
@click="openBrowser"
@drop="filesDropped"
@dragenter="startDrag"
@dragover="startDrag"
@dragleave="stopDrag"
@dragend="stopDrag"
>
<input type="file" ref="fileInput" multiple @input="browserChanged" />
<div>
<n-icon size="2em">
<CloudUpload />
</n-icon>
</div>
<n-text>
Click&nbsp;or&nbsp;drag&nbsp;here&nbsp;to&nbsp;upload&nbsp;files
</n-text>
</div>
<n-modal
v-model:show="uploadDialogShow"
:close-on-esc="false"
:mask-closable="false"
:on-after-enter="uploadFilesDialogOpen"
>
<UploadFileDialog ref="uploadDialog" :files="files" />
</n-modal>
</template>
<style scoped lang="scss">
.uploadArea {
border: 1px dashed #ddd;
border-radius: 3px;
cursor: pointer;
background-color: rgb(250, 250, 252);
text-align: center;
transition: border-color 250ms ease-out, background-color 250ms ease-out;
padding: 20px;
input {
display: block;
width: 0;
height: 0;
opacity: 0;
}
}
.uploadArea:hover {
border-color: #888;
}
.uploadActive {
background-color: rgb(240, 252, 240);
}
</style>

View File

@@ -1,44 +1,44 @@
<script setup lang="ts">
import type { TokenInjectType } from "@/api";
import { defineProps, defineExpose, ref, inject } from "vue";
import { check_token } from "@/api";
import UploadEntry from "@/components/UploadDialog/UploadEntry.vue";
import { NCard } from "naive-ui";
import type { TokenInjectType, UploadFile } from '@/api';
import { ref, inject } from 'vue';
import { check_token } from '@/api';
import UploadEntry from '@/components/UploadDialog/UploadEntry.vue';
import { NCard } from 'naive-ui';
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const entries = ref<typeof UploadEntry[]>([]);
const done = ref(false);
let canCloseResolve: (value: unknown) => void = () => null;
const canClose = new Promise((r) => (canCloseResolve = r));
async function startUpload(parent: number) {
const token = await check_token(jwt);
if (!token) return;
await Promise.all(
entries.value.map((entry) => entry.startUpload(parent, token))
);
done.value = true;
await canClose;
async function startUpload() {
const token = await check_token(jwt);
if (!token) return;
const ents: typeof UploadEntry[] = entries.value;
const allProms: Promise<void>[] = [];
for (const entry of ents) {
await new Promise<void>((resolve) =>
allProms.push(entry.startUpload(token, resolve))
);
}
await Promise.all(allProms);
}
defineExpose({
startUpload,
startUpload
});
defineProps<{
files: File[];
files: UploadFile[];
}>();
</script>
<template>
<n-card title="Upload Files">
<div>
<UploadEntry v-for="f in files" :key="f.name" ref="entries" :file="f" />
</div>
<div>
<button v-if="done" @click="canCloseResolve(null)">Close</button>
</div>
</n-card>
<n-card title="Uploading files" style="margin: 20px">
<UploadEntry
v-for="f in files"
:key="f.file.name"
ref="entries"
:file="f"
/>
</n-card>
</template>
<style scoped></style>

View File

@@ -0,0 +1,73 @@
<script setup lang="ts">
import type { TokenInjectType } from '@/api';
import { inject, ref } from 'vue';
import { Auth, check_token, isErrorResponse } from '@/api';
import { useMessage, NInput, NGrid, NGi, NButton, NCard } from 'naive-ui';
import { loadingMsgWrapper } from '@/utils';
const message = useMessage();
const oldPw = ref('');
const newPw = ref('');
const newPw2 = ref('');
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const changePw = loadingMsgWrapper(message, async () => {
if (oldPw.value === '' || newPw.value === '' || newPw2.value === '') {
message.error('Password missing');
return;
}
if (newPw.value !== newPw2.value) {
message.error("Passwords don't match");
return;
}
const token = await check_token(jwt);
if (!token) return;
const res = await Auth.change_password(oldPw.value, newPw.value, token);
if (isErrorResponse(res))
message.error(`Password change failed: ${res.message}`);
else jwt.logout();
});
function onKey(event: KeyboardEvent) {
if (event.key == 'Enter') changePw();
}
</script>
<template>
<n-card title="Change password" embedded>
<n-grid cols="1" x-gap="16" y-gap="16">
<n-gi>
<n-input
type="password"
placeholder="Old password"
v-model:value="oldPw"
@keyup="onKey"
/>
</n-gi>
<n-gi>
<n-input
type="password"
placeholder="New password"
v-model:value="newPw"
@keyup="onKey"
/>
</n-gi>
<n-gi>
<n-input
type="password"
placeholder="Repeat new password"
v-model:value="newPw2"
@keyup="onKey"
/>
</n-gi>
<n-gi>
<n-button type="info" @click="changePw">
Change password
</n-button>
</n-gi>
</n-grid>
</n-card>
</template>
<style scoped></style>