From 4a55425fdb6e94ede0b5872719d0612606d01b45 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Thu, 16 Jun 2022 16:05:43 +0900 Subject: [PATCH] enhance(client): improve files page of control panel --- CHANGELOG.md | 1 + .../src/components/drive-file-thumbnail.vue | 22 +-- packages/client/src/components/ui/tooltip.vue | 8 +- packages/client/src/directives/tooltip.ts | 1 + packages/client/src/pages/admin/files.vue | 169 +++++++++--------- 5 files changed, 105 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffe545c3d5..63d6966658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ You should also include the user name that made the change. ### Improvements - Server: Add rate limit to i/notifications @tamaina +- Client: Improve files page of control panel @syuilo ### Bugfixes - Server: Fix GenerateVideoThumbnail failed @mei23 diff --git a/packages/client/src/components/drive-file-thumbnail.vue b/packages/client/src/components/drive-file-thumbnail.vue index dd24440e82..07cd565c58 100644 --- a/packages/client/src/components/drive-file-thumbnail.vue +++ b/packages/client/src/components/drive-file-thumbnail.vue @@ -1,6 +1,6 @@ <template> <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 === 'video'" class="fas fa-file-video 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.startsWith('text/')) return 'textfile'; if ([ - "application/zip", - "application/x-cpio", - "application/x-bzip", - "application/x-bzip2", - "application/java-archive", - "application/x-rar-compressed", - "application/x-tar", - "application/gzip", - "application/x-7z-compressed" - ].some(archiveType => archiveType === props.file.type)) return 'archive'; + 'application/zip', + 'application/x-cpio', + 'application/x-bzip', + 'application/x-bzip2', + 'application/java-archive', + 'application/x-rar-compressed', + 'application/x-tar', + 'application/gzip', + 'application/x-7z-compressed', + ].some(archiveType => archiveType === props.file.type)) return 'archive'; return 'unknown'; }); diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue index 571d11ba3b..152c939a1a 100644 --- a/packages/client/src/components/ui/tooltip.vue +++ b/packages/client/src/components/ui/tooltip.vue @@ -1,7 +1,10 @@ <template> <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' }"> - <slot>{{ text }}</slot> + <slot> + <Mfm v-if="asMfm" :text="text"/> + <span v-else>{{ text }}</span> + </slot> </div> </transition> </template> @@ -16,6 +19,7 @@ const props = withDefaults(defineProps<{ x?: number; y?: number; text?: string; + asMfm?: boolean; maxWidth?: number; direction?: 'top' | 'bottom' | 'right' | 'left'; innerMargin?: number; @@ -170,8 +174,6 @@ const setPosition = () => { return { left, top, transformOrigin: 'left center' }; } } - - return null as never; }; const { left, top, transformOrigin } = calc(); diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts index 0e69da954e..588bbca3a9 100644 --- a/packages/client/src/directives/tooltip.ts +++ b/packages/client/src/directives/tooltip.ts @@ -48,6 +48,7 @@ export default { popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), { showing, text: self.text, + asMfm: binding.modifiers.mfm, targetElement: el, }, {}, 'closed'); diff --git a/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue index 3cda688698..6f43c2b7bd 100644 --- a/packages/client/src/pages/admin/files.vue +++ b/packages/client/src/pages/admin/files.vue @@ -1,61 +1,50 @@ <template> <div class="xrmjdkdw"> - <MkContainer :foldable="true" class="lookup"> - <template #header><i class="fas fa-search"></i> {{ $ts.lookup }}</template> - <div class="xrmjdkdw-lookup"> - <MkInput v-model="q" class="item" type="text" @enter="find()"> - <template #label>{{ $ts.fileIdOrUrl }}</template> + <div> + <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> + <MkSelect v-model="origin" style="margin: 0; flex: 1;"> + <template #label>{{ $ts.instance }}</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> - <MkButton primary @click="find()"><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton> </div> - </MkContainer> - - <div class="_section"> - <div class="_content"> - <div class="inputs" style="display: flex;"> - <MkSelect v-model="origin" style="margin: 0; flex: 1;"> - <template #label>{{ $ts.instance }}</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> - </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 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" :class="{ grid: viewMode === 'grid' }"> + <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)"> + <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> + <div v-if="viewMode === 'list'" class="body"> + <div> + <small style="opacity: 0.7;">{{ file.name }}</small> </div> - </button> - </MkPagination> - </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> + </button> + </MkPagination> </div> </div> </template> <script lang="ts" setup> import { computed, defineAsyncComponent } from 'vue'; +import * as Acct from 'misskey-js/built/acct'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkSelect from '@/components/form/select.vue'; @@ -67,10 +56,10 @@ import * as os from '@/os'; import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; -let q = $ref(null); let origin = $ref('local'); let type = $ref(null); let searchHost = $ref(''); +let viewMode = $ref('grid'); const pagination = { endpoint: 'admin/drive/files' as const, limit: 10, @@ -94,18 +83,24 @@ function clear() { function show(file) { os.popup(defineAsyncComponent(() => import('./file-dialog.vue')), { - fileId: file.id + fileId: file.id, }, {}, '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 => { show(file); }).catch(err => { if (err.code === 'NO_SUCH_FILE') { os.alert({ type: 'error', - text: i18n.ts.notFound + text: i18n.ts.notFound, }); } }); @@ -117,6 +112,10 @@ defineExpose({ icon: 'fas fa-cloud', bg: 'var(--bg)', actions: [{ + text: i18n.ts.lookup, + icon: 'fas fa-search', + handler: find, + }, { text: i18n.ts.clearCachedFiles, icon: 'fas fa-trash-alt', handler: clear, @@ -129,47 +128,53 @@ defineExpose({ .xrmjdkdw { margin: var(--margin); - > .lookup { - margin-bottom: 16px; - } - .urempief { margin-top: var(--margin); - > .file { - display: flex; - width: 100%; - box-sizing: border-box; - text-align: left; - align-items: center; + &.list { + > .file { + display: flex; + width: 100%; + box-sizing: border-box; + text-align: left; + align-items: center; - &:hover { - color: var(--accent); + &:hover { + 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 { - width: 128px; - height: 128px; - } + &.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + grid-gap: 12px; + margin: var(--margin) 0; - > .body { - margin-left: 0.3em; - padding: 8px; - flex: 1; - - @media (max-width: 500px) { - font-size: 14px; + > .file { + aspect-ratio: 1; + + > .thumbnail { + width: 100%; + height: 100%; } } } } } - -.xrmjdkdw-lookup { - padding: 16px; - - > .item { - margin-bottom: 16px; - } -} </style>