unloze-wrapper/src/components/DownloadList.vue
2025-05-18 19:45:30 -04:00

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>