Compare commits
	
		
			3 Commits
		
	
	
		
			4e68ab4ef0
			...
			2d728e4b07
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2d728e4b07 | |||
| 7ff9605460 | |||
| d3bf9739b5 | 
							
								
								
									
										198
									
								
								DysonNetwork.Drive/Client/src/components/FilePoolSelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								DysonNetwork.Drive/Client/src/components/FilePoolSelect.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,198 @@ | |||||||
|  | <template> | ||||||
|  |   <n-select | ||||||
|  |     :value="modelValue" | ||||||
|  |     @update:value="onUpdate" | ||||||
|  |     :options="pools ?? []" | ||||||
|  |     :render-label="renderPoolSelectLabel" | ||||||
|  |     :render-tag="renderSingleSelectTag" | ||||||
|  |     value-field="id" | ||||||
|  |     label-field="name" | ||||||
|  |     :placeholder="props.placeholder || 'Select a file pool to upload'" | ||||||
|  |     clearable | ||||||
|  |     size="large" | ||||||
|  |   /> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { | ||||||
|  |   NSelect, | ||||||
|  |   NTag, | ||||||
|  |   NDivider, | ||||||
|  |   NTooltip, | ||||||
|  |   type SelectOption, | ||||||
|  |   type SelectRenderTag, | ||||||
|  | } from 'naive-ui' | ||||||
|  | import { h, onMounted, ref, watch } from 'vue' | ||||||
|  | import type { SnFilePool } from '@/types/pool' | ||||||
|  | import { formatBytes } from '@/views/format' | ||||||
|  |  | ||||||
|  | const props = defineProps<{ | ||||||
|  |   modelValue: string | null | ||||||
|  |   placeholder: string | undefined | ||||||
|  | }>() | ||||||
|  |  | ||||||
|  | const emit = defineEmits(['update:modelValue', 'update:pool']) | ||||||
|  |  | ||||||
|  | type SnFilePoolOption = SnFilePool & any | ||||||
|  |  | ||||||
|  | const pools = ref<SnFilePoolOption[] | undefined>() | ||||||
|  | async function fetchPools() { | ||||||
|  |   const resp = await fetch('/api/pools') | ||||||
|  |   pools.value = await resp.json() | ||||||
|  | } | ||||||
|  | onMounted(() => fetchPools()) | ||||||
|  |  | ||||||
|  | function onUpdate(value: string | null) { | ||||||
|  |   emit('update:modelValue', value) | ||||||
|  |   if (value === null) { | ||||||
|  |     emit('update:pool', null) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |   if (pools.value) { | ||||||
|  |     const pool = pools.value.find((p) => p.id === value) ?? null | ||||||
|  |     emit('update:pool', pool) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | watch(pools, (newPools) => { | ||||||
|  |   if (props.modelValue && newPools) { | ||||||
|  |     const pool = newPools.find((p) => p.id === props.modelValue) ?? null | ||||||
|  |     emit('update:pool', pool) | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const renderSingleSelectTag: SelectRenderTag = ({ option }) => { | ||||||
|  |   return h( | ||||||
|  |     'div', | ||||||
|  |     { | ||||||
|  |       style: { | ||||||
|  |         display: 'flex', | ||||||
|  |         alignItems: 'center', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     [option.name as string], | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const perkPrivilegeList = ['Stellar', 'Nova', 'Supernova'] | ||||||
|  |  | ||||||
|  | function renderPoolSelectLabel(option: SelectOption & SnFilePool) { | ||||||
|  |   const policy: any = option.policy_config | ||||||
|  |   return h( | ||||||
|  |     'div', | ||||||
|  |     { | ||||||
|  |       style: { | ||||||
|  |         padding: '8px 2px', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     [ | ||||||
|  |       h('div', null, [option.name as string]), | ||||||
|  |       option.description && | ||||||
|  |         h( | ||||||
|  |           'div', | ||||||
|  |           { | ||||||
|  |             style: { | ||||||
|  |               fontSize: '0.875rem', | ||||||
|  |               opacity: '0.75', | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |           option.description, | ||||||
|  |         ), | ||||||
|  |       h( | ||||||
|  |         'div', | ||||||
|  |         { | ||||||
|  |           style: { | ||||||
|  |             display: 'flex', | ||||||
|  |             marginBottom: '4px', | ||||||
|  |             fontSize: '0.75rem', | ||||||
|  |             opacity: '0.75', | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |         [ | ||||||
|  |           policy.max_file_size && h('span', `Max ${formatBytes(policy.max_file_size)}`), | ||||||
|  |           policy.accept_types && | ||||||
|  |             h( | ||||||
|  |               NTooltip, | ||||||
|  |               {}, | ||||||
|  |               { | ||||||
|  |                 trigger: () => h('span', `Accept limited types`), | ||||||
|  |                 default: () => h('span', policy.accept_types.join(', ')), | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |           policy.require_privilege && | ||||||
|  |             h('span', `Require ${perkPrivilegeList[policy.require_privilege - 1]} Program`), | ||||||
|  |           h('span', `Cost x${option.billing_config.cost_multiplier.toFixed(1)} NSD`), | ||||||
|  |         ] | ||||||
|  |           .filter((el) => el) | ||||||
|  |           .flatMap((el, idx, arr) => | ||||||
|  |             idx < arr.length - 1 ? [el, h(NDivider, { vertical: true })] : [el], | ||||||
|  |           ), | ||||||
|  |       ), | ||||||
|  |       h( | ||||||
|  |         'div', | ||||||
|  |         { | ||||||
|  |           style: { | ||||||
|  |             display: 'flex', | ||||||
|  |             gap: '0.25rem', | ||||||
|  |             marginTop: '2px', | ||||||
|  |             marginLeft: '-2px', | ||||||
|  |             marginRight: '-2px', | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |         [ | ||||||
|  |           policy.public_usable && | ||||||
|  |             h( | ||||||
|  |               NTag, | ||||||
|  |               { | ||||||
|  |                 type: 'info', | ||||||
|  |                 size: 'small', | ||||||
|  |                 round: true, | ||||||
|  |               }, | ||||||
|  |               { default: () => 'Public Shared' }, | ||||||
|  |             ), | ||||||
|  |           policy.public_indexable && | ||||||
|  |             h( | ||||||
|  |               NTag, | ||||||
|  |               { | ||||||
|  |                 type: 'success', | ||||||
|  |                 size: 'small', | ||||||
|  |                 round: true, | ||||||
|  |               }, | ||||||
|  |               { default: () => 'Public Indexable' }, | ||||||
|  |             ), | ||||||
|  |           policy.allow_encryption && | ||||||
|  |             h( | ||||||
|  |               NTag, | ||||||
|  |               { | ||||||
|  |                 type: 'warning', | ||||||
|  |                 size: 'small', | ||||||
|  |                 round: true, | ||||||
|  |               }, | ||||||
|  |               { default: () => 'Allow Encryption' }, | ||||||
|  |             ), | ||||||
|  |           policy.allow_anonymous && | ||||||
|  |             h( | ||||||
|  |               NTag, | ||||||
|  |               { | ||||||
|  |                 type: 'info', | ||||||
|  |                 size: 'small', | ||||||
|  |                 round: true, | ||||||
|  |               }, | ||||||
|  |               { default: () => 'Allow Anonymous' }, | ||||||
|  |             ), | ||||||
|  |           policy.enable_recycle && | ||||||
|  |             h( | ||||||
|  |               NTag, | ||||||
|  |               { | ||||||
|  |                 type: 'info', | ||||||
|  |                 size: 'small', | ||||||
|  |                 round: true, | ||||||
|  |               }, | ||||||
|  |               { default: () => 'Recycle Enabled' }, | ||||||
|  |             ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ], | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  | </script> | ||||||
| @@ -1,6 +1,22 @@ | |||||||
| <template> | <template> | ||||||
|   <section class="h-full px-5 py-4"> |   <section class="h-full px-5 py-4"> | ||||||
|     <n-data-table :columns="tableColumns" :data="files" :pagination="tablePagination" /> |     <div class="flex flex-col gap-3 mb-3"> | ||||||
|  |       <file-pool-select | ||||||
|  |         v-model="filePool" | ||||||
|  |         placeholder="Filter by file pool" | ||||||
|  |         class="max-w-[480px]" | ||||||
|  |         @update:pool="fetchFiles" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |     <n-data-table | ||||||
|  |       remote | ||||||
|  |       :row-key="(row) => row.id" | ||||||
|  |       :columns="tableColumns" | ||||||
|  |       :data="files" | ||||||
|  |       :loading="loading" | ||||||
|  |       :pagination="tablePagination" | ||||||
|  |       @page-change="handlePageChange" | ||||||
|  |     /> | ||||||
|   </section> |   </section> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -27,10 +43,12 @@ import { | |||||||
| import { h, onMounted, ref } from 'vue' | import { h, onMounted, ref } from 'vue' | ||||||
| import { useRouter } from 'vue-router' | import { useRouter } from 'vue-router' | ||||||
| import { formatBytes } from '../format' | import { formatBytes } from '../format' | ||||||
|  | import FilePoolSelect from '@/components/FilePoolSelect.vue' | ||||||
|  |  | ||||||
| const router = useRouter() | const router = useRouter() | ||||||
|  |  | ||||||
| const files = ref<any[]>([]) | const files = ref<any[]>([]) | ||||||
|  | const filePool = ref<string | null>(null) | ||||||
|  |  | ||||||
| const tableColumns: DataTableColumns<any> = [ | const tableColumns: DataTableColumns<any> = [ | ||||||
|   { |   { | ||||||
| @@ -95,7 +113,6 @@ const tableColumns: DataTableColumns<any> = [ | |||||||
|         h( |         h( | ||||||
|           NButton, |           NButton, | ||||||
|           { |           { | ||||||
|             quaternary: true, |  | ||||||
|             circle: true, |             circle: true, | ||||||
|             text: true, |             text: true, | ||||||
|             onClick: () => { |             onClick: () => { | ||||||
| @@ -109,7 +126,6 @@ const tableColumns: DataTableColumns<any> = [ | |||||||
|         h( |         h( | ||||||
|           NButton, |           NButton, | ||||||
|           { |           { | ||||||
|             quaternary: true, |  | ||||||
|             circle: true, |             circle: true, | ||||||
|             text: true, |             text: true, | ||||||
|             type: 'error', |             type: 'error', | ||||||
| @@ -129,11 +145,19 @@ const tableColumns: DataTableColumns<any> = [ | |||||||
| const tablePagination = ref<PaginationProps>({ | const tablePagination = ref<PaginationProps>({ | ||||||
|   page: 1, |   page: 1, | ||||||
|   itemCount: 0, |   itemCount: 0, | ||||||
|  |   pageSize: 10, | ||||||
|  |   showSizePicker: true, | ||||||
|  |   pageSizes: [10, 20, 30, 40, 50], | ||||||
| }) | }) | ||||||
|  |  | ||||||
| async function fetchFiles() { | async function fetchFiles() { | ||||||
|  |   if (loading.value) return | ||||||
|   try { |   try { | ||||||
|     const response = await fetch('/api/files/me') |     loading.value = true | ||||||
|  |     const pag = tablePagination.value | ||||||
|  |     const response = await fetch( | ||||||
|  |       `/api/files/me?take=${pag.pageSize}&offset=${(pag.page! - 1) * pag.pageSize!}${filePool.value ? '&pool=' + filePool.value : ''}`, | ||||||
|  |     ) | ||||||
|     if (!response.ok) { |     if (!response.ok) { | ||||||
|       throw new Error('Network response was not ok') |       throw new Error('Network response was not ok') | ||||||
|     } |     } | ||||||
| @@ -142,10 +166,19 @@ async function fetchFiles() { | |||||||
|     tablePagination.value.itemCount = parseInt(response.headers.get('x-total') ?? '0') |     tablePagination.value.itemCount = parseInt(response.headers.get('x-total') ?? '0') | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     console.error('Failed to fetch files:', error) |     console.error('Failed to fetch files:', error) | ||||||
|  |   } finally { | ||||||
|  |     loading.value = false | ||||||
|   } |   } | ||||||
| } | } | ||||||
| onMounted(() => fetchFiles()) | onMounted(() => fetchFiles()) | ||||||
|  |  | ||||||
|  | function handlePageChange(page: number) { | ||||||
|  |   tablePagination.value.page = page | ||||||
|  |   fetchFiles() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const loading = ref(false) | ||||||
|  |  | ||||||
| const dialog = useDialog() | const dialog = useDialog() | ||||||
| const messageDialog = useMessage() | const messageDialog = useMessage() | ||||||
| const loadingBar = useLoadingBar() | const loadingBar = useLoadingBar() | ||||||
|   | |||||||
| @@ -2,8 +2,8 @@ | |||||||
|   <section class="min-h-full relative flex items-center justify-center"> |   <section class="min-h-full relative flex items-center justify-center"> | ||||||
|     <n-spin v-if="!fileInfo && !error" /> |     <n-spin v-if="!fileInfo && !error" /> | ||||||
|     <n-result status="404" title="No file was found" :description="error" v-else-if="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-card class="max-w-4xl my-4 mx-8" v-else> | ||||||
|       <n-grid :cols="2" x-gap="16"> |       <n-grid cols="1 m:2" x-gap="16" y-gap="16" responsive="screen"> | ||||||
|         <n-gi> |         <n-gi> | ||||||
|           <div v-if="fileInfo.is_encrypted"> |           <div v-if="fileInfo.is_encrypted"> | ||||||
|             <n-alert type="info" size="small" title="Encrypted file"> |             <n-alert type="info" size="small" title="Encrypted file"> | ||||||
| @@ -14,6 +14,15 @@ | |||||||
|           <div v-else> |           <div v-else> | ||||||
|             <n-image v-if="fileType === 'image'" :src="fileSource" class="w-full" /> |             <n-image v-if="fileType === 'image'" :src="fileSource" class="w-full" /> | ||||||
|             <video v-else-if="fileType === 'video'" :src="fileSource" controls class="w-full" /> |             <video v-else-if="fileType === 'video'" :src="fileSource" controls class="w-full" /> | ||||||
|  |             <audio v-else-if="fileType === 'audio'" :src="fileSource" controls class="w-full" /> | ||||||
|  |             <n-result | ||||||
|  |               status="418" | ||||||
|  |               title="Preview Unavailable" | ||||||
|  |               description="How can you preview this file?" | ||||||
|  |               size="small" | ||||||
|  |               class="py-6" | ||||||
|  |               v-else | ||||||
|  |             /> | ||||||
|           </div> |           </div> | ||||||
|         </n-gi> |         </n-gi> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -23,17 +23,7 @@ | |||||||
|       </template> |       </template> | ||||||
|  |  | ||||||
|       <div class="mb-3"> |       <div class="mb-3"> | ||||||
|         <n-select |         <file-pool-select v-model="filePool" @update:pool="currentFilePool = $event" /> | ||||||
|           v-model:value="filePool" |  | ||||||
|           :options="pools ?? []" |  | ||||||
|           :render-label="renderPoolSelectLabel" |  | ||||||
|           :render-tag="renderSingleSelectTag" |  | ||||||
|           value-field="id" |  | ||||||
|           label-field="name" |  | ||||||
|           placeholder="Select a file pool to upload" |  | ||||||
|           clearable |  | ||||||
|           size="large" |  | ||||||
|         /> |  | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <n-collapse-transition :show="modeAdvanced"> |       <n-collapse-transition :show="modeAdvanced"> | ||||||
| @@ -134,6 +124,8 @@ import { useUserStore } from '@/stores/user' | |||||||
| import type { SnFilePool } from '@/types/pool' | import type { SnFilePool } from '@/types/pool' | ||||||
| import { formatBytes } from './format' | import { formatBytes } from './format' | ||||||
|  |  | ||||||
|  | import FilePoolSelect from '@/components/FilePoolSelect.vue' | ||||||
|  |  | ||||||
| import * as tus from 'tus-js-client' | import * as tus from 'tus-js-client' | ||||||
|  |  | ||||||
| const userStore = useUserStore() | const userStore = useUserStore() | ||||||
|   | |||||||
| @@ -146,16 +146,17 @@ public class FileController( | |||||||
|             .Where(e => e.AccountId == accountId) |             .Where(e => e.AccountId == accountId) | ||||||
|             .Include(e => e.Pool) |             .Include(e => e.Pool) | ||||||
|             .OrderByDescending(e => e.CreatedAt) |             .OrderByDescending(e => e.CreatedAt) | ||||||
|             .Skip(offset) |             .AsQueryable(); | ||||||
|             .Take(take); |  | ||||||
|  |  | ||||||
|         if (pool.HasValue) query = query.Where(e => e.PoolId == pool); |         if (pool.HasValue) query = query.Where(e => e.PoolId == pool); | ||||||
|          |          | ||||||
|         var files = await query.ToListAsync(); |  | ||||||
|  |  | ||||||
|         var total = await query.CountAsync(); |         var total = await query.CountAsync(); | ||||||
|         Response.Headers.Append("X-Total", total.ToString()); |         Response.Headers.Append("X-Total", total.ToString()); | ||||||
|  |  | ||||||
|  |         var files = await query | ||||||
|  |             .Skip(offset) | ||||||
|  |             .Take(take) | ||||||
|  |             .ToListAsync(); | ||||||
|  |  | ||||||
|         return Ok(files); |         return Ok(files); | ||||||
|     } |     } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user