bug fixes file deletions and more
This commit is contained in:
7
main.go
7
main.go
@@ -74,10 +74,11 @@ func main() {
|
||||
// everything past this needs auth
|
||||
api.Use(middleware.SessionMiddleware(db))
|
||||
api.GET("/user", routes.GetUser)
|
||||
api.GET("/user/usage", routes.GetUsage)
|
||||
|
||||
api.POST("/upload*", routes.UploadFile)
|
||||
api.GET("/files*", routes.GetFiles)
|
||||
api.POST("/files/upload*", routes.UploadFile)
|
||||
api.GET("/files/get/*", routes.GetFiles)
|
||||
api.GET("/files/download/*", routes.GetFile)
|
||||
api.POST("/files/delete*", routes.DeleteFiles)
|
||||
}
|
||||
|
||||
// redirects to the proper pages if you are trying to access one that expects you have/dont have an api key
|
||||
|
||||
@@ -24,6 +24,7 @@ type User struct {
|
||||
PasswordHash string `bun:"passwordHash,notnull" json:"-"`
|
||||
PlanID int64 `bun:"plan_id,notnull" json:"-"`
|
||||
Plan Plan `bun:"rel:belongs-to,join:plan_id=id" json:"plan"`
|
||||
Usage int64 `bun:"-" json:"usage"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
|
||||
@@ -136,5 +136,13 @@ func GetUser(c echo.Context) error {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"message": "User not found"})
|
||||
}
|
||||
|
||||
basePath := fmt.Sprintf("%s/%s/", os.Getenv("STORAGE_PATH"), user.(*models.User).ID)
|
||||
storageUsage, err := calculateStorageUsage(basePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.(*models.User).Usage = storageUsage
|
||||
|
||||
return c.JSON(http.StatusOK, user.(*models.User))
|
||||
}
|
||||
|
||||
@@ -23,14 +23,51 @@ func UploadFile(c echo.Context) error {
|
||||
fullPath := strings.Trim(c.Param("*"), "/")
|
||||
basePath := fmt.Sprintf("%s/%s/%s/", os.Getenv("STORAGE_PATH"), user.ID, fullPath)
|
||||
|
||||
currentUsage, err := calculateStorageUsage(basePath)
|
||||
currentUsage, err := calculateStorageUsage(fmt.Sprintf("%s/%s", os.Getenv("STORAGE_PATH"), user.ID))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = os.Stat(basePath)
|
||||
directoryExists := err == nil
|
||||
|
||||
// Create the directories if they don't exist
|
||||
if !directoryExists {
|
||||
err = os.MkdirAll(basePath, os.ModePerm)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
reader, err := c.Request().MultipartReader()
|
||||
if err != nil {
|
||||
if err == http.ErrNotMultipart {
|
||||
if directoryExists {
|
||||
// Directories exist, but no file was uploaded
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"message": "A folder with that name already exists"})
|
||||
}
|
||||
// Directories were just created, and no file was provided
|
||||
entry, err := os.Stat(basePath)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
uploadFile := &UploadResponse{
|
||||
Usage: currentUsage + entry.Size(),
|
||||
File: File{
|
||||
Name: entry.Name(),
|
||||
IsDir: entry.IsDir(),
|
||||
Size: entry.Size(),
|
||||
LastModified: entry.ModTime().Format("1/2/2006"),
|
||||
},
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, uploadFile)
|
||||
}
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
@@ -41,6 +78,11 @@ func UploadFile(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
filepath := filepath.Join(basePath, part.FileName())
|
||||
|
||||
if _, err = os.Stat(filepath); err == nil {
|
||||
@@ -173,22 +215,58 @@ func GetFiles(c echo.Context) error {
|
||||
Name: f.Name(),
|
||||
IsDir: f.IsDir(),
|
||||
Size: f.Size(),
|
||||
LastModified: f.ModTime().Format("1/2/2006"),
|
||||
LastModified: f.ModTime().Format("2 Jan 06"),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, jsonFiles)
|
||||
}
|
||||
|
||||
func GetUsage(c echo.Context) error {
|
||||
func GetFile(c echo.Context) error {
|
||||
user := c.Get("user").(*models.User)
|
||||
|
||||
fullPath := strings.Trim(c.Param("*"), "/")
|
||||
basePath := fmt.Sprintf("%s/%s/%s/", os.Getenv("STORAGE_PATH"), user.ID, fullPath)
|
||||
storageUsage, err := calculateStorageUsage(basePath)
|
||||
if err != nil {
|
||||
return err
|
||||
basePath := fmt.Sprintf("%s/%s/%s", os.Getenv("STORAGE_PATH"), user.ID, fullPath)
|
||||
|
||||
return c.File(basePath)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]int64{"usage": storageUsage})
|
||||
type DeleteRequest struct {
|
||||
Files []File `json:"files"`
|
||||
}
|
||||
|
||||
func DeleteFiles(c echo.Context) error {
|
||||
var deleteData DeleteRequest
|
||||
|
||||
if err := c.Bind(&deleteData); err != nil {
|
||||
fmt.Println(err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"message": "An unknown error occoured!"})
|
||||
}
|
||||
|
||||
if len(deleteData.Files) == 0 {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"message": "Files are required!"})
|
||||
}
|
||||
|
||||
user := c.Get("user").(*models.User)
|
||||
|
||||
fullPath := strings.Trim(c.Param("*"), "/")
|
||||
basePath := fmt.Sprintf("%s/%s/%s", os.Getenv("STORAGE_PATH"), user.ID, fullPath)
|
||||
|
||||
for _, file := range deleteData.Files {
|
||||
path := filepath.Join(basePath, file.Name)
|
||||
err := os.RemoveAll(path)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"message": "An unknown error occoured!"})
|
||||
}
|
||||
}
|
||||
|
||||
word := "file"
|
||||
fileLen := len(deleteData.Files)
|
||||
|
||||
if fileLen != 1 {
|
||||
word = word + "s"
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]string{"message": fmt.Sprintf("Successfully deleted %d %s", fileLen, word)})
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ const crumbs = computed(() => {
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="m9 6l6 6l-6 6" />
|
||||
</svg>
|
||||
<a class="hover:text-text" :class="index === crumbs.length - 1 ? 'text-foam' : 'text-subtle'"
|
||||
:href="crumb.link">{{
|
||||
crumb.name }}</a>
|
||||
<NuxtLink class="hover:text-text" :class="index === crumbs.length - 1 ? 'text-foam' : 'text-subtle'"
|
||||
:to="crumb.link">{{
|
||||
crumb.name }}</NuxtLink>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
23
ui/components/Checkbox.vue
Normal file
23
ui/components/Checkbox.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div v-on:click="toggle()" class="w-5 h-5 border rounded cursor-pointer flex items-center justify-center"
|
||||
:class="state === 'unchecked' ? 'hover:bg-muted/5 active:bg-muted/15' : 'bg-foam/10 hover:bg-foam/15 active:bg-foam/25 text-foam'">
|
||||
<div v-if="state === 'some'" class="w-8/12 h-0.5 bg-current rounded-full"></div>
|
||||
<span v-else-if="state === 'checked'">
|
||||
<svg class="w-full h-full" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="m5 12l5 5L20 7" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps(['modelValue'])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
let state = computed(() => props.modelValue)
|
||||
|
||||
const toggle = () => {
|
||||
const newState = state.value === 'unchecked' ? 'checked' : 'unchecked'
|
||||
emit('update:modelValue', newState);
|
||||
}
|
||||
</script>
|
||||
@@ -19,7 +19,6 @@ const percentage = computed(() => {
|
||||
return (props.usageBytes / capacityBytes.value);
|
||||
});
|
||||
|
||||
console.log(percentage.value, props.usageBytes, capacityBytes.value)
|
||||
const offset = computed(() => {
|
||||
return circumference - percentage.value * circumference;
|
||||
});
|
||||
@@ -30,10 +29,6 @@ const capacity = computed(() => {
|
||||
return formatBytes(capacityBytes.value)
|
||||
});
|
||||
|
||||
if (props.usageBytes > capacityBytes.value) {
|
||||
console.log("SCAN SCAN SCAM SCAM")
|
||||
}
|
||||
|
||||
const isAllFilesActive = computed(() => route.path === '/home');
|
||||
|
||||
const isInFolder = computed(() => route.path.startsWith('/home/') && route.path !== '/home');
|
||||
|
||||
@@ -18,7 +18,7 @@ const changeTheme = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="flex h-[var(--nav-height)] px-4 justify-center sticky top-0 z-50 border-b bg-base">
|
||||
<header class="flex h-[var(--nav-height)] px-4 justify-center sticky top-0 z-20 border-b bg-base">
|
||||
<div class="flex w-full items-center justify-between space-x-2.5">
|
||||
<p
|
||||
class="-ml-2.5 flex shrink-0 items-center px-2.5 py-1.5 focus:outline-none focus:ring rounded-m font-semiboldd">
|
||||
|
||||
43
ui/components/Popup.vue
Normal file
43
ui/components/Popup.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps(['modelValue', 'header'])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid place-content-center absolute top-0 left-0 bottom-0 right-0 z-40"
|
||||
:class="{ 'hidden': !modelValue }">
|
||||
<div v-on:click=" $emit('update:modelValue', !modelValue)"
|
||||
class="absolute top-0 left-0 bottom-0 right-0 bg-base/40">
|
||||
</div>
|
||||
|
||||
<transition name="scale-fade">
|
||||
<div v-if="modelValue"
|
||||
class="bg-surface rounded-xl border shadow-md p-6 transition-[transform,opacity] duration-[250ms] origin-center z-50 w-screen h-screen sm:w-[600px] sm:h-auto">
|
||||
<div class="flex justify-between mb-2 items-center">
|
||||
<h3 class="text-xl font-semibold">{{ header }}</h3>
|
||||
<button v-on:click=" $emit('update:modelValue', !modelValue)"
|
||||
class="p-1 border h-fit rounded-md hover:bg-muted/10 active:bg-muted/20 transition-bg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scale-fade-enter-from,
|
||||
.scale-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.90);
|
||||
}
|
||||
|
||||
.scale-fade-enter-to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
</style>
|
||||
@@ -9,12 +9,53 @@ definePageMeta({
|
||||
});
|
||||
|
||||
const user = await getUser()
|
||||
|
||||
let { data: usageBytes } = await useFetch<{ usage: number }>('/api/user/usage')
|
||||
let { data: files } = await useFetch<[File]>('/api/files')
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
let { data: files } = await useFetch<[File]>('/api/files/get/' + route.path.replace(/^\/home/, ''))
|
||||
|
||||
const sortedFiles = computed(() => {
|
||||
files.value?.forEach(file => file.toggled === undefined ? file.toggled = 'unchecked' : {})
|
||||
|
||||
let folders = files.value?.filter(file => file.is_dir).sort((a, b) => {
|
||||
return ('' + a.name).localeCompare(b.name);
|
||||
});
|
||||
let archives = files.value?.filter(file => !file.is_dir).sort((a, b) => {
|
||||
return ('' + a.name).localeCompare(b.name);
|
||||
});
|
||||
|
||||
return folders?.concat(archives)
|
||||
})
|
||||
|
||||
let selectAll = ref('unchecked');
|
||||
let selectedFiles = computed(() => sortedFiles.value?.filter(file => file.toggled === 'checked'))
|
||||
|
||||
watch(sortedFiles, (newVal, oldVal) => {
|
||||
let checkedFilesLength = newVal?.filter(file => file.toggled === 'checked').length;
|
||||
if (checkedFilesLength > 0) {
|
||||
if (checkedFilesLength < newVal?.length) {
|
||||
selectAll.value = 'some';
|
||||
} else {
|
||||
selectAll.value = 'checked';
|
||||
}
|
||||
} else {
|
||||
selectAll.value = 'unchecked';
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectAll, (newVal, oldVal) => {
|
||||
if (newVal === 'some') {
|
||||
return
|
||||
}
|
||||
|
||||
sortedFiles.value?.forEach(file => {
|
||||
file.toggled = newVal
|
||||
})
|
||||
});
|
||||
|
||||
let folderName = ref('');
|
||||
let folder = ref("");
|
||||
let folderError = ref('');
|
||||
let popupVisable = ref(false);
|
||||
let uploadPaneClosed = ref(true);
|
||||
|
||||
if (typeof route.params.name == "object") {
|
||||
@@ -67,7 +108,7 @@ const uploadFile = (file: File) => {
|
||||
uploadPaneClosed.value = false;
|
||||
}
|
||||
|
||||
xhr.open('POST', '/api/upload', true);
|
||||
xhr.open('POST', '/api/files/upload/' + route.path.replace(/^\/home/, ''), true);
|
||||
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (event.lengthComputable) {
|
||||
@@ -94,16 +135,16 @@ const uploadFile = (file: File) => {
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
let data = JSON.parse(xhr.response)
|
||||
usageBytes.value.usage = data.usage
|
||||
files.value?.push(data.file)
|
||||
|
||||
let file = uploadingFiles.value.find(upload => upload.id === id);
|
||||
if (!file) {
|
||||
throw new Error("Upload has finished but file is missing!")
|
||||
}
|
||||
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
let data = JSON.parse(xhr.response)
|
||||
user.usage = data.usage
|
||||
files.value?.push(data.file)
|
||||
|
||||
file.uploading = false;
|
||||
|
||||
file.status = {
|
||||
@@ -165,15 +206,59 @@ const uploadFile = (file: File) => {
|
||||
const openFilePicker = () => {
|
||||
fileInput.value?.click();
|
||||
}
|
||||
|
||||
const createFolder = async () => {
|
||||
let { data, error } = await useFetch('/api/files/upload/' + route.path.replace(/^\/home/, '') + '/' + folderName.value, {
|
||||
method: "POST"
|
||||
})
|
||||
|
||||
if (error.value != null) {
|
||||
folderError.value = error.value.data.message;
|
||||
} else {
|
||||
user.usage = data.value.usage
|
||||
files.value?.push(data.value.file)
|
||||
|
||||
popupVisable.value = false;
|
||||
navigateTo(route.path + '/' + folderName.value);
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFiles = async () => {
|
||||
await useFetch('/api/files/delete' + route.path.replace(/^\/home/, ''), {
|
||||
method: "POST",
|
||||
body: {
|
||||
files: selectedFiles.value?.map(file => ({ name: file.name }))
|
||||
}
|
||||
})
|
||||
|
||||
files.value = files.value?.filter(file => !selectedFiles.value?.includes(file))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex relative min-h-[100dvh]">
|
||||
<div class="fixed md:relative -translate-x-full md:translate-x-0">
|
||||
<FileNav :usageBytes="usageBytes?.usage" />
|
||||
<FileNav :usageBytes="user.usage" />
|
||||
</div>
|
||||
<UploadPane :closed="uploadPaneClosed" v-on:update:closed="(newValue) => uploadPaneClosed = newValue"
|
||||
:uploadingFiles="uploadingFiles" />
|
||||
<Popup v-model="popupVisable" header="New Folder">
|
||||
<div class="flex flex-col p-2">
|
||||
<div class="mb-3 flex flex-col">
|
||||
<label for="folderNameInput" class="text-sm">name</label>
|
||||
<Input id="folderNameInput" v-model="folderName" placeholder="Folder name" />
|
||||
<p class="text-love">{{ folderError }}</p>
|
||||
</div>
|
||||
<div class="ml-auto flex gap-x-1.5">
|
||||
<button v-on:click="popupVisable = !popupVisable"
|
||||
class=" px-2 py-1 rounded-md text-sm border bg-muted/10 hover:bg-muted/15 active:bg-muted/25">Close</button>
|
||||
<button v-on:click="createFolder" :disabled="folderName === ''"
|
||||
class=" px-2 py-1 rounded-md text-sm
|
||||
disabled:bg-highlight-med/50 bg-highlight-med hover:brightness-105 active:brightness-110 transition-[background-color,filter] text-surface disabled:cursor-not-allowed">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
<div class="w-full">
|
||||
<Nav />
|
||||
<div class="pt-6 pl-12 overflow-auto max-h-[calc(100vh-var(--nav-height))]">
|
||||
@@ -191,7 +276,7 @@ const openFilePicker = () => {
|
||||
</svg>
|
||||
Upload
|
||||
</button>
|
||||
<button
|
||||
<button v-on:click="popupVisable = !popupVisable"
|
||||
class="rounded-xl border-2 border-surface flex flex-col gap-y-2 px-2 py-3 w-40 justify-center items-center hover:bg-muted/10 active:bg-muted/20 transition-bg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
@@ -208,36 +293,56 @@ const openFilePicker = () => {
|
||||
<h3 class="font-semibold text-xl">
|
||||
<Breadcrumbs :path="route.path" />
|
||||
</h3>
|
||||
<table class="w-full text-sm mt-2">
|
||||
<div class="mt-2">
|
||||
<div v-if="selectedFiles?.length > 0">
|
||||
<button v-on:click="deleteFiles"
|
||||
class="flex flex-row px-2 py-1 rounded-md transition-bg text-xs border hover:bg-love/10 active:bg-love/20 hover:text-love active:text-love items-center">
|
||||
<svg class="mr-1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 7h16M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3m-5 5l4 4m0-4l-4 4" />
|
||||
</svg>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="w-full text-sm mt-2 table-fixed">
|
||||
<thead class="border-b">
|
||||
<tr class="flex flex-row h-10 group pl-[30px] -ml-7 relative items-center">
|
||||
<th class="left-0 absolute">
|
||||
<div>
|
||||
<input class="w-4 h-4 hidden group-hover:block" type="checkbox" />
|
||||
<Checkbox :class="{ 'hidden': selectAll === 'unchecked' }"
|
||||
v-model="selectAll" class="group-hover:flex" type="checkbox" />
|
||||
</div>
|
||||
</th>
|
||||
<th class="flex-grow text-start">
|
||||
<th v-on:click="selectAll === 'unchecked' ? selectAll = 'checked' : selectAll = 'unchecked'"
|
||||
class="flex-grow min-w-40 text-start flex items-center h-full">
|
||||
Name
|
||||
</th>
|
||||
<th class="min-w-40 text-start">
|
||||
<th class="min-w-32 text-start">
|
||||
Size
|
||||
</th>
|
||||
<th class="min-w-40 text-start sm:block hidden">
|
||||
Last modified
|
||||
<th class="min-w-28 text-start sm:block hidden">
|
||||
Modified
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="block">
|
||||
<tr class="flex flex-row h-10 group items-center border-b hover:bg-muted/10 transition-bg"
|
||||
v-for="file in files">
|
||||
<td class="-ml-7 pr-3.5">
|
||||
<div class="w-4 h-4">
|
||||
<input class="w-4 h-4 hidden group-hover:block" type="checkbox" />
|
||||
v-for="file in sortedFiles">
|
||||
<td class="-ml-7 pr-4 flex-shrink-0">
|
||||
<div class="w-5 h-5">
|
||||
<Checkbox class="group-hover:flex"
|
||||
:class="{ 'hidden': file.toggled === 'unchecked' }"
|
||||
v-model="file.toggled" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="flex-grow text-start">
|
||||
<div class="flex items-center">
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
<td v-on:click="file.toggled === 'unchecked' ? file.toggled = 'checked' : file.toggled = 'unchecked'"
|
||||
class="flex-grow text-start flex items-center h-full min-w-40">
|
||||
<div class="flex items-center min-w-40">
|
||||
<svg v-if="!file.is_dir" class="mr-2 flex-shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
@@ -246,13 +351,24 @@ const openFilePicker = () => {
|
||||
d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2M9 9h1m-1 4h6m-6 4h6" />
|
||||
</g>
|
||||
</svg>
|
||||
<svg v-else class="mr-2 flex-shrink-0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="16" height="16" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M5 4h4l3 3h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2" />
|
||||
</svg>
|
||||
<span class="overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
<NuxtLink v-if="file.is_dir" :to="`${route.path}/${file.name}`">
|
||||
{{ file.name }}
|
||||
</NuxtLink>
|
||||
<span v-else>{{ file.name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="min-w-40 text-start">
|
||||
<td class="min-w-32 text-start">
|
||||
{{ formatBytes(file.size) }}
|
||||
</td>
|
||||
<td class="min-w-40 text-start sm:block hidden">
|
||||
<td class="min-w-28 text-start sm:block hidden">
|
||||
{{ file.last_modified }}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -264,3 +380,12 @@ const openFilePicker = () => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
td,
|
||||
th {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -21,7 +21,6 @@ const submitForm = async () => {
|
||||
})
|
||||
|
||||
if (response.error.value != null) {
|
||||
console.log(response)
|
||||
error.value = response.error.value.data.message
|
||||
setTimeout(() => error.value = "", 15000)
|
||||
} else {
|
||||
|
||||
@@ -23,7 +23,6 @@ const submitForm = async () => {
|
||||
})
|
||||
|
||||
if (response.error.value != null) {
|
||||
console.log(response)
|
||||
error.value = response.error.value.data.message
|
||||
setTimeout(() => error.value = "", 15000)
|
||||
} else {
|
||||
|
||||
@@ -2,5 +2,6 @@ export interface File {
|
||||
name: string,
|
||||
is_dir: boolean,
|
||||
size: number,
|
||||
last_modified: string
|
||||
last_modified: string,
|
||||
toggled: string,
|
||||
}
|
||||
@@ -5,7 +5,8 @@ export interface User {
|
||||
plan: {
|
||||
id: number,
|
||||
max_storage: number
|
||||
}
|
||||
},
|
||||
usage: number,
|
||||
}
|
||||
|
||||
export interface FileUpload {
|
||||
|
||||
Reference in New Issue
Block a user