Complete Code Push

This commit is contained in:
Brandon
2025-05-18 19:45:30 -04:00
parent ff5983217c
commit b594083bb1
19 changed files with 528 additions and 11 deletions
+11
View File
@@ -0,0 +1,11 @@
<template>
<v-app>
<v-main>
<download-list />
</v-main>
</v-app>
</template>
<script setup lang="ts">
import DownloadList from './components/DownloadList.vue'
</script>
+227
View File
@@ -0,0 +1,227 @@
<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>
+20
View File
@@ -0,0 +1,20 @@
/* src/main.css */
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
body {
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
overflow: hidden;
}
#app,
.v-application {
background-image: url('/carbon.jpg');
background-repeat: no-repeat;
background-position: center;
position: relative;
z-index: 0;
height: 100vh;
overflow-y: auto;
}
+9
View File
@@ -0,0 +1,9 @@
import { createApp } from 'vue'
import App from './App.vue'
import { vuetify } from './plugins/vuetify'
import '@mdi/font/css/materialdesignicons.css'
import './main.css';
const app = createApp(App)
app.use(vuetify)
app.mount('#app')
+34
View File
@@ -0,0 +1,34 @@
// src/plugins/vuetify.ts
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import { aliases, mdi } from 'vuetify/iconsets/mdi'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
export const vuetify = createVuetify({
components,
directives,
icons: {
defaultSet: 'mdi',
aliases,
sets: { mdi },
},
theme: {
defaultTheme: 'dark',
themes: {
dark: {
dark: true,
colors: {
background: '#121212',
surface: '#1E1E1E',
primary: '#BB86FC',
secondary: '#03DAC6',
error: '#CF6679',
info: '#2196F3',
success: '#4CAF50',
warning: '#FB8C00',
},
},
},
},
})
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />