enhance(client): improve files page of control panel
This commit is contained in:
		
							parent
							
								
									fdba255b9a
								
							
						
					
					
						commit
						4a55425fdb
					
				
					 5 changed files with 105 additions and 96 deletions
				
			
		|  | @ -13,6 +13,7 @@ You should also include the user name that made the change. | ||||||
| 
 | 
 | ||||||
| ### Improvements | ### Improvements | ||||||
| - Server: Add rate limit to i/notifications @tamaina | - Server: Add rate limit to i/notifications @tamaina | ||||||
|  | - Client: Improve files page of control panel @syuilo | ||||||
| 
 | 
 | ||||||
| ### Bugfixes | ### Bugfixes | ||||||
| - Server: Fix GenerateVideoThumbnail failed @mei23 | - Server: Fix GenerateVideoThumbnail failed @mei23 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <div ref="thumbnail" class="zdjebgpv"> | <div ref="thumbnail" class="zdjebgpv"> | ||||||
| 	<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/> | 	<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/> | ||||||
| 	<i v-else-if="is === 'image'" class="fas fa-file-image icon"></i> | 	<i v-else-if="is === 'image'" class="fas fa-file-image icon"></i> | ||||||
| 	<i v-else-if="is === 'video'" class="fas fa-file-video icon"></i> | 	<i v-else-if="is === 'video'" class="fas fa-file-video icon"></i> | ||||||
| 	<i v-else-if="is === 'audio' || is === 'midi'" class="fas fa-music icon"></i> | 	<i v-else-if="is === 'audio' || is === 'midi'" class="fas fa-music icon"></i> | ||||||
|  | @ -33,16 +33,16 @@ const is = computed(() => { | ||||||
| 	if (props.file.type.endsWith('/pdf')) return 'pdf'; | 	if (props.file.type.endsWith('/pdf')) return 'pdf'; | ||||||
| 	if (props.file.type.startsWith('text/')) return 'textfile'; | 	if (props.file.type.startsWith('text/')) return 'textfile'; | ||||||
| 	if ([ | 	if ([ | ||||||
| 			"application/zip", | 		'application/zip', | ||||||
| 			"application/x-cpio", | 		'application/x-cpio', | ||||||
| 			"application/x-bzip", | 		'application/x-bzip', | ||||||
| 			"application/x-bzip2", | 		'application/x-bzip2', | ||||||
| 			"application/java-archive", | 		'application/java-archive', | ||||||
| 			"application/x-rar-compressed", | 		'application/x-rar-compressed', | ||||||
| 			"application/x-tar", | 		'application/x-tar', | ||||||
| 			"application/gzip", | 		'application/gzip', | ||||||
| 			"application/x-7z-compressed" | 		'application/x-7z-compressed', | ||||||
| 		].some(archiveType => archiveType === props.file.type)) return 'archive'; | 	].some(archiveType => archiveType === props.file.type)) return 'archive'; | ||||||
| 	return 'unknown'; | 	return 'unknown'; | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,10 @@ | ||||||
| <template> | <template> | ||||||
| <transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="emit('closed')"> | <transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="emit('closed')"> | ||||||
| 	<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }"> | 	<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }"> | ||||||
| 		<slot>{{ text }}</slot> | 		<slot> | ||||||
|  | 			<Mfm v-if="asMfm" :text="text"/> | ||||||
|  | 			<span v-else>{{ text }}</span> | ||||||
|  | 		</slot> | ||||||
| 	</div> | 	</div> | ||||||
| </transition> | </transition> | ||||||
| </template> | </template> | ||||||
|  | @ -16,6 +19,7 @@ const props = withDefaults(defineProps<{ | ||||||
| 	x?: number; | 	x?: number; | ||||||
| 	y?: number; | 	y?: number; | ||||||
| 	text?: string; | 	text?: string; | ||||||
|  | 	asMfm?: boolean; | ||||||
| 	maxWidth?: number; | 	maxWidth?: number; | ||||||
| 	direction?: 'top' | 'bottom' | 'right' | 'left'; | 	direction?: 'top' | 'bottom' | 'right' | 'left'; | ||||||
| 	innerMargin?: number; | 	innerMargin?: number; | ||||||
|  | @ -170,8 +174,6 @@ const setPosition = () => { | ||||||
| 				return { left, top, transformOrigin: 'left center' }; | 				return { left, top, transformOrigin: 'left center' }; | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 |  | ||||||
| 		return null as never; |  | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const { left, top, transformOrigin } = calc(); | 	const { left, top, transformOrigin } = calc(); | ||||||
|  |  | ||||||
|  | @ -48,6 +48,7 @@ export default { | ||||||
| 			popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), { | 			popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), { | ||||||
| 				showing, | 				showing, | ||||||
| 				text: self.text, | 				text: self.text, | ||||||
|  | 				asMfm: binding.modifiers.mfm, | ||||||
| 				targetElement: el, | 				targetElement: el, | ||||||
| 			}, {}, 'closed'); | 			}, {}, 'closed'); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,61 +1,50 @@ | ||||||
| <template> | <template> | ||||||
| <div class="xrmjdkdw"> | <div class="xrmjdkdw"> | ||||||
| 	<MkContainer :foldable="true" class="lookup"> | 	<div> | ||||||
| 		<template #header><i class="fas fa-search"></i> {{ $ts.lookup }}</template> | 		<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> | ||||||
| 		<div class="xrmjdkdw-lookup"> | 			<MkSelect v-model="origin" style="margin: 0; flex: 1;"> | ||||||
| 			<MkInput v-model="q" class="item" type="text" @enter="find()"> | 				<template #label>{{ $ts.instance }}</template> | ||||||
| 				<template #label>{{ $ts.fileIdOrUrl }}</template> | 				<option value="combined">{{ $ts.all }}</option> | ||||||
|  | 				<option value="local">{{ $ts.local }}</option> | ||||||
|  | 				<option value="remote">{{ $ts.remote }}</option> | ||||||
|  | 			</MkSelect> | ||||||
|  | 			<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'"> | ||||||
|  | 				<template #label>{{ $ts.host }}</template> | ||||||
| 			</MkInput> | 			</MkInput> | ||||||
| 			<MkButton primary @click="find()"><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton> |  | ||||||
| 		</div> | 		</div> | ||||||
| 	</MkContainer> | 		<div class="inputs" style="display: flex; padding-top: 1.2em;"> | ||||||
| 
 | 			<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;"> | ||||||
| 	<div class="_section"> | 				<template #label>MIME type</template> | ||||||
| 		<div class="_content"> | 			</MkInput> | ||||||
| 			<div class="inputs" style="display: flex;"> | 		</div> | ||||||
| 				<MkSelect v-model="origin" style="margin: 0; flex: 1;"> | 		<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }"> | ||||||
| 					<template #label>{{ $ts.instance }}</template> | 			<button v-for="file in items" :key="file.id" v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`" class="file _panel _button" @click="show(file, $event)"> | ||||||
| 					<option value="combined">{{ $ts.all }}</option> | 				<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | ||||||
| 					<option value="local">{{ $ts.local }}</option> | 				<div v-if="viewMode === 'list'" class="body"> | ||||||
| 					<option value="remote">{{ $ts.remote }}</option> | 					<div> | ||||||
| 				</MkSelect> | 						<small style="opacity: 0.7;">{{ file.name }}</small> | ||||||
| 				<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'"> |  | ||||||
| 					<template #label>{{ $ts.host }}</template> |  | ||||||
| 				</MkInput> |  | ||||||
| 			</div> |  | ||||||
| 			<div class="inputs" style="display: flex; padding-top: 1.2em;"> |  | ||||||
| 				<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;"> |  | ||||||
| 					<template #label>MIME type</template> |  | ||||||
| 				</MkInput> |  | ||||||
| 			</div> |  | ||||||
| 			<MkPagination v-slot="{items}" :pagination="pagination" class="urempief"> |  | ||||||
| 				<button v-for="file in items" :key="file.id" class="file _panel _button _gap" @click="show(file, $event)"> |  | ||||||
| 					<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> |  | ||||||
| 					<div class="body"> |  | ||||||
| 						<div> |  | ||||||
| 							<small style="opacity: 0.7;">{{ file.name }}</small> |  | ||||||
| 						</div> |  | ||||||
| 						<div> |  | ||||||
| 							<MkAcct v-if="file.user" :user="file.user"/> |  | ||||||
| 							<div v-else>{{ $ts.system }}</div> |  | ||||||
| 						</div> |  | ||||||
| 						<div> |  | ||||||
| 							<span style="margin-right: 1em;">{{ file.type }}</span> |  | ||||||
| 							<span>{{ bytes(file.size) }}</span> |  | ||||||
| 						</div> |  | ||||||
| 						<div> |  | ||||||
| 							<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span> |  | ||||||
| 						</div> |  | ||||||
| 					</div> | 					</div> | ||||||
| 				</button> | 					<div> | ||||||
| 			</MkPagination> | 						<MkAcct v-if="file.user" :user="file.user"/> | ||||||
| 		</div> | 						<div v-else>{{ $ts.system }}</div> | ||||||
|  | 					</div> | ||||||
|  | 					<div> | ||||||
|  | 						<span style="margin-right: 1em;">{{ file.type }}</span> | ||||||
|  | 						<span>{{ bytes(file.size) }}</span> | ||||||
|  | 					</div> | ||||||
|  | 					<div> | ||||||
|  | 						<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			</button> | ||||||
|  | 		</MkPagination> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed, defineAsyncComponent } from 'vue'; | import { computed, defineAsyncComponent } from 'vue'; | ||||||
|  | import * as Acct from 'misskey-js/built/acct'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import MkSelect from '@/components/form/select.vue'; | import MkSelect from '@/components/form/select.vue'; | ||||||
|  | @ -67,10 +56,10 @@ import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; | import * as symbols from '@/symbols'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| 
 | 
 | ||||||
| let q = $ref(null); |  | ||||||
| let origin = $ref('local'); | let origin = $ref('local'); | ||||||
| let type = $ref(null); | let type = $ref(null); | ||||||
| let searchHost = $ref(''); | let searchHost = $ref(''); | ||||||
|  | let viewMode = $ref('grid'); | ||||||
| const pagination = { | const pagination = { | ||||||
| 	endpoint: 'admin/drive/files' as const, | 	endpoint: 'admin/drive/files' as const, | ||||||
| 	limit: 10, | 	limit: 10, | ||||||
|  | @ -94,18 +83,24 @@ function clear() { | ||||||
| 
 | 
 | ||||||
| function show(file) { | function show(file) { | ||||||
| 	os.popup(defineAsyncComponent(() => import('./file-dialog.vue')), { | 	os.popup(defineAsyncComponent(() => import('./file-dialog.vue')), { | ||||||
| 		fileId: file.id | 		fileId: file.id, | ||||||
| 	}, {}, 'closed'); | 	}, {}, 'closed'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function find() { | async function find() { | ||||||
|  | 	const { canceled, result: q } = await os.inputText({ | ||||||
|  | 		title: i18n.ts.fileIdOrUrl, | ||||||
|  | 		allowEmpty: false, | ||||||
|  | 	}); | ||||||
|  | 	if (canceled) return; | ||||||
|  | 
 | ||||||
| 	os.api('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => { | 	os.api('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => { | ||||||
| 		show(file); | 		show(file); | ||||||
| 	}).catch(err => { | 	}).catch(err => { | ||||||
| 		if (err.code === 'NO_SUCH_FILE') { | 		if (err.code === 'NO_SUCH_FILE') { | ||||||
| 			os.alert({ | 			os.alert({ | ||||||
| 				type: 'error', | 				type: 'error', | ||||||
| 				text: i18n.ts.notFound | 				text: i18n.ts.notFound, | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 	}); | 	}); | ||||||
|  | @ -117,6 +112,10 @@ defineExpose({ | ||||||
| 		icon: 'fas fa-cloud', | 		icon: 'fas fa-cloud', | ||||||
| 		bg: 'var(--bg)', | 		bg: 'var(--bg)', | ||||||
| 		actions: [{ | 		actions: [{ | ||||||
|  | 			text: i18n.ts.lookup, | ||||||
|  | 			icon: 'fas fa-search', | ||||||
|  | 			handler: find, | ||||||
|  | 		}, { | ||||||
| 			text: i18n.ts.clearCachedFiles, | 			text: i18n.ts.clearCachedFiles, | ||||||
| 			icon: 'fas fa-trash-alt', | 			icon: 'fas fa-trash-alt', | ||||||
| 			handler: clear, | 			handler: clear, | ||||||
|  | @ -129,47 +128,53 @@ defineExpose({ | ||||||
| .xrmjdkdw { | .xrmjdkdw { | ||||||
| 	margin: var(--margin); | 	margin: var(--margin); | ||||||
| 
 | 
 | ||||||
| 	> .lookup { |  | ||||||
| 		margin-bottom: 16px; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	.urempief { | 	.urempief { | ||||||
| 		margin-top: var(--margin); | 		margin-top: var(--margin); | ||||||
| 
 | 
 | ||||||
| 		> .file { | 		&.list { | ||||||
| 			display: flex; | 			> .file { | ||||||
| 			width: 100%; | 				display: flex; | ||||||
| 			box-sizing: border-box; | 				width: 100%; | ||||||
| 			text-align: left; | 				box-sizing: border-box; | ||||||
| 			align-items: center; | 				text-align: left; | ||||||
|  | 				align-items: center; | ||||||
| 
 | 
 | ||||||
| 			&:hover { | 				&:hover { | ||||||
| 				color: var(--accent); | 					color: var(--accent); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .thumbnail { | ||||||
|  | 					width: 128px; | ||||||
|  | 					height: 128px; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .body { | ||||||
|  | 					margin-left: 0.3em; | ||||||
|  | 					padding: 8px; | ||||||
|  | 					flex: 1; | ||||||
|  | 
 | ||||||
|  | 					@media (max-width: 500px) { | ||||||
|  | 						font-size: 14px; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 			> .thumbnail { | 		&.grid { | ||||||
| 				width: 128px; | 			display: grid; | ||||||
| 				height: 128px; | 			grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); | ||||||
| 			} | 			grid-gap: 12px; | ||||||
|  | 			margin: var(--margin) 0; | ||||||
| 
 | 
 | ||||||
| 			> .body { | 			> .file { | ||||||
| 				margin-left: 0.3em; | 				aspect-ratio: 1; | ||||||
| 				padding: 8px; | 			 | ||||||
| 				flex: 1; | 				> .thumbnail { | ||||||
| 
 | 					width: 100%; | ||||||
| 				@media (max-width: 500px) { | 					height: 100%; | ||||||
| 					font-size: 14px; |  | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 |  | ||||||
| .xrmjdkdw-lookup { |  | ||||||
| 	padding: 16px; |  | ||||||
| 
 |  | ||||||
| 	> .item { |  | ||||||
| 		margin-bottom: 16px; |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue