✨ FIle detail page
This commit is contained in:
		| @@ -1,36 +1,182 @@ | ||||
| <template> | ||||
|   <section class="h-full relative flex items-center justify-center"> | ||||
|     <n-card class="max-w-lg" title="Download file"> | ||||
|       <div class="flex flex-col gap-3" v-if="!progress"> | ||||
|         <n-input placeholder="File ID" v-model:value="fileId" /> | ||||
|         <n-input placeholder="Password" v-model:value="filePass" type="password" /> | ||||
|         <n-button @click="downloadFile">Download</n-button> | ||||
|       </div> | ||||
|       <div v-else> | ||||
|         <n-progress :percentage="progress" /> | ||||
|       </div> | ||||
|   <section class="min-h-full relative flex items-center justify-center"> | ||||
|     <n-spin v-if="!fileInfo && !error" /> | ||||
|     <n-result status="404" title="No file was found" :description="error" v-else-if="error" /> | ||||
|     <n-card class="max-w-4xl my-4" v-else> | ||||
|       <n-grid :cols="2" x-gap="16"> | ||||
|         <n-gi> | ||||
|           <div v-if="fileInfo.is_encrypted"> | ||||
|             <n-alert type="info" size="small" title="Encrypted file"> | ||||
|               The file has been encrypted. Preview not available. Please enter the password to | ||||
|               download it. | ||||
|             </n-alert> | ||||
|           </div> | ||||
|           <div v-else> | ||||
|             <n-image v-if="fileType === 'image'" :src="fileSource" /> | ||||
|           </div> | ||||
|         </n-gi> | ||||
|  | ||||
|         <n-gi> | ||||
|           <div class="mb-3"> | ||||
|             <n-card title="File Infomation" size="small"> | ||||
|               <div class="flex gap-2"> | ||||
|                 <span class="flex-grow-1 flex items-center gap-2"> | ||||
|                   <n-icon> | ||||
|                     <info-round /> | ||||
|                   </n-icon> | ||||
|                   File Type | ||||
|                 </span> | ||||
|                 <span>{{ fileInfo.mime_type }} ({{ fileType }})</span> | ||||
|               </div> | ||||
|               <div class="flex gap-2"> | ||||
|                 <span class="flex-grow-1 flex items-center gap-2"> | ||||
|                   <n-icon> | ||||
|                     <data-usage-round /> | ||||
|                   </n-icon> | ||||
|                   File Size | ||||
|                 </span> | ||||
|                 <span>{{ formatBytes(fileInfo.size) }}</span> | ||||
|               </div> | ||||
|               <div class="flex gap-2"> | ||||
|                 <span class="flex-grow-1 flex items-center gap-2"> | ||||
|                   <n-icon> | ||||
|                     <file-upload-outlined /> | ||||
|                   </n-icon> | ||||
|                   Uploaded At | ||||
|                 </span> | ||||
|                 <span>{{ new Date(fileInfo.created_at).toLocaleString() }}</span> | ||||
|               </div> | ||||
|               <div class="flex gap-2"> | ||||
|                 <span class="flex-grow-1 flex items-center gap-2"> | ||||
|                   <n-icon> | ||||
|                     <details-round /> | ||||
|                   </n-icon> | ||||
|                   Techical Info | ||||
|                 </span> | ||||
|                 <n-button text size="small" @click="showTechDetails = !showTechDetails"> | ||||
|                   {{ showTechDetails ? 'Hide' : 'Show' }} | ||||
|                 </n-button> | ||||
|               </div> | ||||
|  | ||||
|               <n-collapse-transition :show="showTechDetails"> | ||||
|                 <div v-if="showTechDetails" class="mt-2 flex flex-col gap-1"> | ||||
|                   <p class="text-xs opacity-75">#{{ fileInfo.id }}</p> | ||||
|  | ||||
|                   <n-card size="small" content-style="padding: 0" embedded> | ||||
|                     <div class="overflow-x-auto px-4 py-2"> | ||||
|                       <n-code | ||||
|                         :code="JSON.stringify(fileInfo.file_meta, null, 4)" | ||||
|                         language="json" | ||||
|                         :hljs="hljs" | ||||
|                       /> | ||||
|                     </div> | ||||
|                   </n-card> | ||||
|                 </div> | ||||
|               </n-collapse-transition> | ||||
|             </n-card> | ||||
|           </div> | ||||
|  | ||||
|           <div class="flex flex-col gap-3" v-if="!progress"> | ||||
|             <n-input | ||||
|               v-if="fileInfo.is_encrypted" | ||||
|               placeholder="Password" | ||||
|               v-model:value="filePass" | ||||
|               type="password" | ||||
|             /> | ||||
|             <n-button @click="downloadFile">Download</n-button> | ||||
|           </div> | ||||
|           <div v-else> | ||||
|             <n-progress processing :percentage="progress" /> | ||||
|           </div> | ||||
|         </n-gi> | ||||
|       </n-grid> | ||||
|     </n-card> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { NCard, NInput, NButton, NProgress, useMessage } from 'naive-ui' | ||||
| import { ref } from 'vue' | ||||
| import { | ||||
|   NCard, | ||||
|   NInput, | ||||
|   NButton, | ||||
|   NProgress, | ||||
|   NResult, | ||||
|   NSpin, | ||||
|   NImage, | ||||
|   NAlert, | ||||
|   NIcon, | ||||
|   NCollapseTransition, | ||||
|   NCode, | ||||
|   NGrid, | ||||
|   NGi, | ||||
|   useMessage, | ||||
| } from 'naive-ui' | ||||
| import { DataUsageRound, InfoRound, DetailsRound, FileUploadOutlined } from '@vicons/material' | ||||
| import { useRoute } from 'vue-router' | ||||
| import { computed, onMounted, ref } from 'vue' | ||||
|  | ||||
| import { downloadAndDecryptFile } from './secure' | ||||
|  | ||||
| import hljs from 'highlight.js/lib/core' | ||||
| import json from 'highlight.js/lib/languages/json' | ||||
|  | ||||
| hljs.registerLanguage('json', json) | ||||
|  | ||||
| const route = useRoute() | ||||
|  | ||||
| const error = ref<string | null>(null) | ||||
|  | ||||
| const filePass = ref<string>('') | ||||
| const fileId = ref<string>('') | ||||
| const fileId = route.params.fileId | ||||
|  | ||||
| const progress = ref<number | undefined>(0) | ||||
|  | ||||
| const showTechDetails = ref<boolean>(false) | ||||
|  | ||||
| const messageDisplay = useMessage() | ||||
|  | ||||
| const fileInfo = ref<any>(null) | ||||
| async function fetchFileInfo() { | ||||
|   try { | ||||
|     const resp = await fetch('/api/files/' + fileId + '/info') | ||||
|     if (!resp.ok) { | ||||
|       throw new Error('Failed to fetch file info: ' + resp.statusText) | ||||
|     } | ||||
|     fileInfo.value = await resp.json() | ||||
|   } catch (err) { | ||||
|     error.value = (err as Error).message | ||||
|   } | ||||
| } | ||||
| onMounted(() => fetchFileInfo()) | ||||
|  | ||||
| const fileType = computed(() => { | ||||
|   if (!fileInfo.value) return 'unknown' | ||||
|   return fileInfo.value.mime_type?.split('/')[0] || 'unknown' | ||||
| }) | ||||
| const fileSource = computed(() => `/api/files/${fileId}`) | ||||
|  | ||||
| function downloadFile() { | ||||
|   downloadAndDecryptFile('/api/files/' + fileId.value, filePass.value, (p: number) => { | ||||
|     progress.value = p * 100 | ||||
|   }).catch((err) => { | ||||
|     messageDisplay.error('Download failed: ' + err.message, { closable: true, duration: 10000 }) | ||||
|   }) | ||||
|   if (fileInfo.value.is_encrypted && !filePass.value) { | ||||
|     messageDisplay.error('Please enter the password to download the file.') | ||||
|     return | ||||
|   } | ||||
|   if (fileInfo.value.is_encrypted) { | ||||
|     downloadAndDecryptFile(fileSource.value, filePass.value, (p: number) => { | ||||
|       progress.value = p * 100 | ||||
|     }).catch((err) => { | ||||
|       messageDisplay.error('Download failed: ' + err.message, { closable: true, duration: 10000 }) | ||||
|     }) | ||||
|   } else { | ||||
|     window.open(fileSource.value, '_blank') | ||||
|   } | ||||
| } | ||||
|  | ||||
| function formatBytes(bytes: number, decimals = 2): string { | ||||
|   if (bytes === 0) return '0 Bytes' | ||||
|   const k = 1024 | ||||
|   const dm = decimals < 0 ? 0 : decimals | ||||
|   const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] | ||||
|   const i = Math.floor(Math.log(bytes) / Math.log(k)) | ||||
|   return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] | ||||
| } | ||||
| </script> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user