228 lines
5.9 KiB
Vue
228 lines
5.9 KiB
Vue
<template>
|
|
<v-container class="text-center pt-6">
|
|
<v-img
|
|
src="/UNLOZE_logo_blue.png"
|
|
max-width="200"
|
|
alt="UNLOZE Logo"
|
|
class="mx-auto"
|
|
contain
|
|
/>
|
|
</v-container>
|
|
<v-container>
|
|
<v-card>
|
|
<v-breadcrumbs :items="breadcrumbItems" class="my-4">
|
|
<template v-slot:item="{ item }">
|
|
<span v-if="item.disabled" class="grey--text text--darken-2">
|
|
{{ item.title }}
|
|
</span>
|
|
<span
|
|
v-else
|
|
class="blue--text text--darken-2"
|
|
style="cursor: pointer"
|
|
@click="goToBreadcrumb((item as any).value)"
|
|
>
|
|
{{ item.title }}
|
|
</span>
|
|
</template>
|
|
</v-breadcrumbs>
|
|
<v-text-field
|
|
v-model="search"
|
|
label="Search files..."
|
|
prepend-inner-icon="mdi-magnify"
|
|
hide-details
|
|
density="compact"
|
|
class="ma-4"
|
|
@keyup.enter="onSearch"
|
|
/>
|
|
<v-progress-linear v-if="loading" indeterminate color="blue" />
|
|
<v-data-table
|
|
:headers="headers"
|
|
:items="combinedItems"
|
|
:items-per-page="-1"
|
|
class="elevation-1"
|
|
disable-pagination
|
|
hide-default-footer
|
|
>
|
|
<template #item.name="{ item }">
|
|
<span
|
|
v-if="item.isDir"
|
|
class="blue--text text--darken-2"
|
|
@click="enterFolder(item.path)"
|
|
style="cursor: pointer"
|
|
>
|
|
<v-icon small class="me-2">mdi-folder-outline</v-icon>
|
|
|
|
{{ item.name }}
|
|
</span>
|
|
<span v-else>
|
|
<v-icon small class="me-2">mdi-file-outline</v-icon>
|
|
<a
|
|
:href="`${DOWNLOAD_BASE_URL}${currentPath}${item.name}`"
|
|
target="_blank"
|
|
class="blue--text text--darken-2"
|
|
style="text-decoration: none"
|
|
>
|
|
{{ item.name }}
|
|
</a>
|
|
</span>
|
|
</template>
|
|
|
|
<template #item.size="{ item }">
|
|
{{ item.isDir ? "-" : formatSize(item.size) }}
|
|
</template>
|
|
|
|
<template #item.lastModified="{ item }">
|
|
{{
|
|
item.lastModified
|
|
? new Date(item.lastModified).toLocaleString()
|
|
: "-"
|
|
}}
|
|
</template>
|
|
</v-data-table>
|
|
</v-card>
|
|
</v-container>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { defineComponent, ref, nextTick } from "vue";
|
|
|
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
|
|
|
interface FolderItem {
|
|
name: string;
|
|
path: string;
|
|
isDir: true;
|
|
size: null;
|
|
lastModified: null;
|
|
}
|
|
|
|
interface FileItem {
|
|
name: string;
|
|
path: string;
|
|
isDir: false;
|
|
size: number;
|
|
lastModified: string;
|
|
}
|
|
|
|
interface BreadcrumbItem {
|
|
title: string;
|
|
value: string;
|
|
disabled: boolean;
|
|
}
|
|
|
|
export default defineComponent({
|
|
data() {
|
|
return {
|
|
currentPath: "",
|
|
folders: [] as string[],
|
|
files: [] as {
|
|
key: string;
|
|
size: number;
|
|
lastModified: string;
|
|
}[],
|
|
nextToken: null as string | null,
|
|
loading: false,
|
|
search: "",
|
|
headers: [
|
|
{ title: "Name", key: "name" },
|
|
{ title: "Size", key: "size", width: "120px" },
|
|
{ title: "Last Modified", key: "lastModified", width: "200px" },
|
|
],
|
|
DOWNLOAD_BASE_URL: import.meta.env.VITE_DOWNLOAD_BASE_URL
|
|
};
|
|
},
|
|
computed: {
|
|
combinedItems(): (FolderItem | FileItem)[] {
|
|
const folderItems: FolderItem[] = this.folders.map((p) => ({
|
|
name: this.extractFolderName(p),
|
|
path: p,
|
|
isDir: true,
|
|
size: null,
|
|
lastModified: null,
|
|
}));
|
|
const fileItems: FileItem[] = this.files.map((f) => ({
|
|
name: f.key.split("/").pop() || f.key,
|
|
path: f.key,
|
|
isDir: false,
|
|
size: f.size,
|
|
lastModified: f.lastModified,
|
|
}));
|
|
return [...folderItems, ...fileItems];
|
|
},
|
|
breadcrumbItems(): BreadcrumbItem[] {
|
|
const parts = this.currentPath.split("/").filter((p) => p !== "");
|
|
const items: BreadcrumbItem[] = [
|
|
{ title: "Home", value: "", disabled: parts.length === 0 },
|
|
];
|
|
let cumulativePath = "";
|
|
parts.forEach((part, index) => {
|
|
cumulativePath += part + "/";
|
|
items.push({
|
|
title: part,
|
|
value: cumulativePath,
|
|
disabled: index === parts.length - 1,
|
|
});
|
|
});
|
|
return items;
|
|
},
|
|
},
|
|
methods: {
|
|
async fetchList(reset = false) {
|
|
if (this.loading) return;
|
|
this.loading = true;
|
|
|
|
const url = new URL(`${API_BASE_URL}/api/list`);
|
|
url.searchParams.set("prefix", this.currentPath);
|
|
url.searchParams.set("limit", "50");
|
|
if (this.search) url.searchParams.set("search", this.search);
|
|
if (this.nextToken && !reset) {
|
|
url.searchParams.set("token", this.nextToken);
|
|
}
|
|
|
|
const res = await fetch(url.toString());
|
|
const data = await res.json();
|
|
|
|
if (reset) {
|
|
this.folders = [];
|
|
this.files = [];
|
|
nextTick(() => {
|
|
const container = this.$refs.scrollContainer as HTMLElement;
|
|
if (container) container.scrollTop = 0;
|
|
});
|
|
}
|
|
|
|
this.folders.push(...(data.folders || []));
|
|
this.files.push(...(data.files || []));
|
|
this.nextToken = data.nextToken;
|
|
this.loading = false;
|
|
},
|
|
onSearch() {
|
|
this.nextToken = null;
|
|
this.fetchList(true);
|
|
},
|
|
enterFolder(path: string) {
|
|
this.currentPath = path;
|
|
this.nextToken = null;
|
|
this.fetchList(true);
|
|
},
|
|
goToBreadcrumb(path: string) {
|
|
this.currentPath = path;
|
|
this.nextToken = null;
|
|
this.fetchList(true);
|
|
},
|
|
extractFolderName(path: string): string {
|
|
const parts = path.split("/");
|
|
return parts[parts.length - 2] || path;
|
|
},
|
|
formatSize(size: number): string {
|
|
if (size > 1e6) return (size / 1e6).toFixed(1) + " MB";
|
|
if (size > 1e3) return (size / 1e3).toFixed(1) + " KB";
|
|
return size + " B";
|
|
},
|
|
},
|
|
mounted() {
|
|
this.fetchList(true);
|
|
},
|
|
});
|
|
</script>
|