From 1d6f43aa30a932b1b7539f417f19d0b239cde511 Mon Sep 17 00:00:00 2001 From: CyberRex <hspwinx86@gmail.com> Date: Mon, 20 Mar 2023 12:58:06 +0900 Subject: [PATCH] feat: drive cleaner (#10366) * feat: drive-cleaner * Update CHANGELOG.md --- CHANGELOG.md | 1 + locales/ja-JP.yml | 5 + .../src/server/api/endpoints/drive/files.ts | 10 + .../src/pages/settings/drive-cleaner.vue | 193 ++++++++++++++++++ .../frontend/src/pages/settings/drive.vue | 3 + packages/frontend/src/router.ts | 4 + 6 files changed, 216 insertions(+) create mode 100644 packages/frontend/src/pages/settings/drive-cleaner.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb090f267..30b0ac018c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ - Bookwyrmのユーザーのプロフィールページで「リモートで表示」をタップしても反応がない問題を修正 - 非ログイン時の「Misskeyについて」の表示を修正 - PC版にて「設定」「コントロールパネル」のリンクを2度以上続けてクリックした際に空白のページが表示される問題を修正 +- ドライブクリーナーを追加 ### Server - OpenAPIエンドポイントを復旧 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c4e86fc64a..54742cef96 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -977,6 +977,7 @@ notesSearchNotAvailable: "ノート検索は利用できません。" license: "ライセンス" unfavoriteConfirm: "お気に入り解除しますか?" myClips: "自分のクリップ" +drivecleaner: "ドライブクリーナー" _achievements: earnedAt: "獲得日時" @@ -1922,3 +1923,7 @@ _dialog: _disabledTimeline: title: "無効化されたタイムライン" description: "現在のロールでは、このタイムラインを使用することはできません。" + +_drivecleaner: + orderBySizeDesc: "サイズが大きい順" + orderByCreatedAtAsc: "追加日が古い順" \ No newline at end of file diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts index f6fad50fd9..4609307774 100644 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -31,6 +31,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) }, + sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size'] }, }, required: [], } as const; @@ -63,6 +64,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } } + switch (ps.sort) { + case '+createdAt': query.orderBy('file.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('file.createdAt', 'ASC'); break; + case '+name': query.orderBy('file.name', 'DESC'); break; + case '-name': query.orderBy('file.name', 'ASC'); break; + case '+size': query.orderBy('file.size', 'DESC'); break; + case '-size': query.orderBy('file.size', 'ASC'); break; + } + const files = await query.take(ps.limit).getMany(); return await this.driveFileEntityService.packMany(files, { detail: false, self: true }); diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue new file mode 100644 index 0000000000..ce8ab214e4 --- /dev/null +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -0,0 +1,193 @@ +<template> +<MkSelect v-model="sortModeSelect"> + <template #label>{{ i18n.ts.sort }}</template> + <option v-for="x in sortOptions" :key="x.value" :value="x.value">{{ x.displayName }}</option> +</MkSelect> +<br> +<div v-if="!fetching" class="_gap_m"> + <MkPagination v-slot="{items}" :pagination="pagination" class="driveitem list"> + <div + v-for="file in items" + :key="file.id" + > + <MkA + v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${dateString(file.createdAt)}`" + class="_button" + :to="`${file.url}`" + behavior="browser" + @contextmenu.stop="$event => onContextMenu($event, file.id)" + > + <div class="file"> + <div v-if="file.isSensitive" class="sensitive-label">{{ i18n.ts.sensitive }}</div> + <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> + <div class="body"> + <div style="margin-bottom: 4px;"> + {{ file.name }} + </div> + <div> + <span style="margin-right: 1em;">{{ file.type }}</span> + <span>{{ bytes(file.size) }}</span> + </div> + <div> + <span>{{ i18n.ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span> + </div> + <div v-if="sortModeSelect === 'sizeDesc'"> + <div class="uawsfosz"> + <div class="meter"><div :style="genUsageBar(file.size)"></div></div> + </div> + </div> + </div> + </div> + </MkA> + </div> + </MkPagination> +</div> +<div v-else class="gap_m"> + {{ i18n.ts.checking }} <MkEllipsis/> +</div> +</template> + +<script setup lang="ts"> +import { ref, watch } from 'vue'; +import tinycolor from 'tinycolor2'; +import * as os from '@/os'; +import MkPagination from '@/components/MkPagination.vue'; +import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; +import { i18n } from '@/i18n'; +import bytes from '@/filters/bytes'; +import { dateString } from '@/filters/date'; +import { definePageMetadata } from '@/scripts/page-metadata'; +import MkSelect from '@/components/MkSelect.vue'; + +let sortMode = '+size'; +const pagination = { + endpoint: 'drive/files' as const, + limit: 10, + params: { sort: sortMode }, +}; + +const sortOptions = [ + { value: 'sizeDesc', displayName: i18n.ts._drivecleaner.orderBySizeDesc }, + { value: 'createdAtAsc', displayName: i18n.ts._drivecleaner.orderByCreatedAtAsc }, +]; + +const capacity = ref<number>(0); +const usage = ref<number>(0); +const fetching = ref(true); +const sortModeSelect = ref('sizeDesc'); + +fetchDriveInfo(); + +watch(fetching, () => { + if (fetching.value) { + fetchDriveInfo(); + } +}); + +watch(sortModeSelect, () => { + switch (sortModeSelect.value) { + case 'sizeDesc': + sortMode = '+size'; + fetching.value = true; + break; + + case 'createdAtAsc': + sortMode = '-createdAt'; + fetching.value = true; + break; + } +}); + +function fetchDriveInfo(): void { + os.api('drive').then(info => { + capacity.value = info.capacity; + usage.value = info.usage; + fetching.value = false; + }); +} + +function genUsageBar(fsize: number): object { + return { + width: `${fsize / usage.value * 100}%`, + background: tinycolor({ h: 180 - (fsize / usage.value * 180), s: 0.7, l: 0.5 }), + }; +} + +function onContextMenu(ev: MouseEvent, fileId: string): void { + const target = ev.target as HTMLElement; + const items = [ + { + text: i18n.ts.delete, + icon: 'ti ti-trash-x', + danger: true, + action: async (): Promise<void> => { + const res = await os.confirm({ + type: 'warning', + title: i18n.ts.delete, + text: i18n.ts.deleteConfirm, + }); + if (!res.canceled) { + await os.apiWithDialog('drive/files/delete', { fileId: fileId }); + fetching.value = true; + } + }, + }, + ]; + ev.preventDefault(); + os.popupMenu(items, target, { + viaKeyboard: false, + }); +} + +definePageMetadata({ + title: i18n.ts.drivecleaner, + icon: 'ti ti-trash', +}); +</script> + +<style lang="scss" scoped> + +@use "sass:math"; + +.file { + display: flex; + width: 100%; + box-sizing: border-box; + text-align: left; + align-items: center; + margin-bottom: 16px; + + &: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; + } + } +} + +.uawsfosz { + > .meter { + $size: 12px; + background: rgba(0, 0, 0, 0.1); + border-radius: math.div($size, 2); + overflow: hidden; + + > div { + height: $size; + border-radius: math.div($size, 2); + } + } +} +</style> diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index a23bdfe69e..d3fb422e01 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -32,6 +32,9 @@ <template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template> <template #suffixIcon><i class="ti ti-folder"></i></template> </FormLink> + <FormLink to="/settings/drive/cleaner"> + {{ i18n.ts.drivecleaner }} + </FormLink> <MkSwitch v-model="keepOriginalUploading"> <template #label>{{ i18n.ts.keepOriginalUploading }}</template> <template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template> diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 590c5765fd..c8077edd28 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -65,6 +65,10 @@ export const routes = [{ path: '/drive', name: 'drive', component: page(() => import('./pages/settings/drive.vue')), + }, { + path: '/drive/cleaner', + name: 'drive', + component: page(() => import('./pages/settings/drive-cleaner.vue')), }, { path: '/notifications', name: 'notifications',