From 38e48dfd13f5c571953ee827a19d9ab8ee14ab95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=90=8C=E8=90=8C=E5=93=92=E8=B5=AB=E8=90=9D?= <ma_shiqing@163.com> Date: Thu, 6 Apr 2023 20:39:34 +0800 Subject: [PATCH] :sparkles: Feature: add batch rename , sort and more search options for gallery --- public/i18n/en.yml | 5 + public/i18n/zh-CN.yml | 5 + public/i18n/zh-TW.yml | 5 + src/renderer/pages/Gallery.vue | 351 +++++++++++++++++++++++++++++++-- src/universal/types/i18n.d.ts | 5 + 5 files changed, 358 insertions(+), 13 deletions(-) diff --git a/public/i18n/en.yml b/public/i18n/en.yml index b43081b..293f8d4 100644 --- a/public/i18n/en.yml +++ b/public/i18n/en.yml @@ -65,6 +65,11 @@ GALLERY_SYNC_DELETE: Cloud Sync Delete GALLERY_SYNC_DELETE_NOTICE_TITLE: Notice GALLERY_SYNC_DELETE_NOTICE_SUCCEED: Cloud Delete Succeed GALLERY_SYNC_DELETE_NOTICE_FAILED: Cloud Delete Failed +GALLERY_CHANGE_URL: Rename +GALLERY_CHANGE_URL_TITLE: Batch Change Image URL +GALLERY_SEARCH_FILENAME: Search by Filename +GALLERY_SEARCH_URL: Search by URL +GALLERY_MATCHED: ' Matched: ' UPLOAD_PAGE_IMAGE_PROCESS_NAME: Image Processing UPLOAD_PAGE_IMAGE_PROCESS_DIALOG_TITLE: Image Processing Settings diff --git a/public/i18n/zh-CN.yml b/public/i18n/zh-CN.yml index a9c3fc8..6971cb1 100644 --- a/public/i18n/zh-CN.yml +++ b/public/i18n/zh-CN.yml @@ -65,6 +65,11 @@ GALLERY_SYNC_DELETE: 删除云端 GALLERY_SYNC_DELETE_NOTICE_TITLE: 通知 GALLERY_SYNC_DELETE_NOTICE_SUCCEED: 云端删除成功 GALLERY_SYNC_DELETE_NOTICE_FAILED: 云端删除失败 +GALLERY_CHANGE_URL: 修改 +GALLERY_CHANGE_URL_TITLE: 批量修改图片URL +GALLERY_SEARCH_FILENAME: 搜索文件名 +GALLERY_SEARCH_URL: 搜索URL +GALLERY_MATCHED: ' 匹配到: ' UPLOAD_PAGE_IMAGE_PROCESS_NAME: 图片处理 UPLOAD_PAGE_IMAGE_PROCESS_DIALOG_TITLE: 图片处理设置 diff --git a/public/i18n/zh-TW.yml b/public/i18n/zh-TW.yml index 3f187a9..e9b29ec 100644 --- a/public/i18n/zh-TW.yml +++ b/public/i18n/zh-TW.yml @@ -65,6 +65,11 @@ GALLERY_SYNC_DELETE: 刪除雲端 GALLERY_SYNC_DELETE_NOTICE_TITLE: 通知 GALLERY_SYNC_DELETE_NOTICE_SUCCEED: 雲端刪除成功 GALLERY_SYNC_DELETE_NOTICE_FAILED: 雲端刪除失敗 +GALLERY_CHANGE_URL: 修改 +GALLERY_CHANGE_URL_TITLE: 批量修改圖片URL +GALLERY_SEARCH_FILENAME: 搜尋文件名 +GALLERY_SEARCH_URL: 搜尋URL +GALLERY_MATCHED: ' 匹配到: ' UPLOAD_PAGE_IMAGE_PROCESS_NAME: 圖片處理 UPLOAD_PAGE_IMAGE_PROCESS_DIALOG_TITLE: 圖片處理設置 diff --git a/src/renderer/pages/Gallery.vue b/src/renderer/pages/Gallery.vue index 7fb8750..544d52d 100644 --- a/src/renderer/pages/Gallery.vue +++ b/src/renderer/pages/Gallery.vue @@ -31,7 +31,7 @@ class="handle-bar" :gutter="16" > - <el-col :span="12"> + <el-col :span="6"> <el-select v-model="choosedPicBed" multiple @@ -48,7 +48,25 @@ /> </el-select> </el-col> - <el-col :span="12"> + <el-col :span="10"> + <el-date-picker + v-model="dateRange" + type="daterange" + unlink-panels + range-separator="To" + start-placeholder="Start date" + end-placeholder="End date" + size="small" + /> + </el-col> + <el-col :span="1"> + <el-divider + direction="vertical" + style="height: 100%;" + border-style="hidden" + /> + </el-col> + <el-col :span="5"> <el-select v-model="pasteStyle" size="small" @@ -64,15 +82,40 @@ /> </el-select> </el-col> + <el-col :span="2"> + <el-dropdown> + <el-button + size="small" + type="primary" + :icon="Sort" + > + {{ $T('MANAGE_BUCKET_SORT_TITLE') }} + </el-button> + <template #dropdown> + <el-dropdown-item @click="sortFile('name')"> + {{ $T('MANAGE_BUCKET_SORT_NAME') }} + </el-dropdown-item> + <el-dropdown-item @click="sortFile('ext')"> + {{ $T('MANAGE_BUCKET_SORT_TYPE') }} + </el-dropdown-item> + <el-dropdown-item @click="sortFile('time')"> + {{ $T('MANAGE_BUCKET_SORT_TIME') }} + </el-dropdown-item> + <el-dropdown-item @click="sortFile('check')"> + {{ $T('MANAGE_BUCKET_SORT_SELECTED') }} + </el-dropdown-item> + </template> + </el-dropdown> + </el-col> </el-row> <el-row class="handle-bar" :gutter="16" > - <el-col :span="12"> + <el-col :span="6"> <el-input v-model="searchText" - :placeholder="$T('SEARCH')" + :placeholder="$T('GALLERY_SEARCH_FILENAME')" size="small" > <template #suffix> @@ -86,7 +129,24 @@ </template> </el-input> </el-col> - <el-col :span="4"> + <el-col :span="6"> + <el-input + v-model="searchTextURL" + :placeholder="$T('GALLERY_SEARCH_URL')" + size="small" + > + <template #suffix> + <el-icon + class="el-input__icon" + style="cursor: pointer;" + @click="cleanSearchUrl" + > + <close /> + </el-icon> + </template> + </el-input> + </el-col> + <el-col :span="3"> <div class="item-base copy round" :class="{ active: isMultiple(choosedList) }" @@ -95,7 +155,16 @@ {{ $T('COPY') }} </div> </el-col> - <el-col :span="4"> + <el-col :span="3"> + <div + class="item-base all-pick round" + :class="{ active: filterList.length > 0 }" + @click="() => isShowBatchRenameDialog = true" + > + {{ $T('GALLERY_CHANGE_URL') }} + </div> + </el-col> + <el-col :span="3"> <div class="item-base delete round" :class="{ active: isMultiple(choosedList) }" @@ -104,7 +173,7 @@ {{ $T('DELETE') }} </div> </el-col> - <el-col :span="4"> + <el-col :span="3"> <div class="item-base all-pick round" :class="{ active: filterList.length > 0 }" @@ -213,13 +282,124 @@ </el-button> </template> </el-dialog> + <el-dialog + v-model="isShowBatchRenameDialog" + :title="$T('CHANGE_IMAGE_URL')" + center + align-center + draggable + destroy-on-close + > + <el-link + :underline="false" + style="margin-bottom: 10px;" + > + <span> + {{ $T('MANAGE_BUCKET_RENAME_FILE_INPUT_A') + $T('GALLERY_MATCHED') + mathcedCount + ' ' }} + <el-tooltip + effect="dark" + :content="$T('MANAGE_BUCKET_RENAME_FILE_INPUT_A_TIPS')" + placement="right" + > + <el-icon + color="#409EFF" + > + <InfoFilled /> + </el-icon> + </el-tooltip> + </span> + </el-link> + <el-input + v-model="batchRenameMatch" + :placeholder="$T('MANAGE_BUCKET_RENAME_FILE_INPUT_A_PLACEHOLDER')" + clearable + /> + <el-link + :underline="false" + style="margin-bottom: 10px;margin-top: 10px;" + > + <span> + {{ $T('MANAGE_BUCKET_RENAME_FILE_INPUT_B') }} + <el-popover + effect="light" + placement="right" + width="280" + > + <template #reference> + <el-icon + color="#409EFF" + > + <InfoFilled /> + </el-icon> + </template> + <el-descriptions + :column="1" + style="width: 250px;" + border + > + <el-descriptions-item + v-for="(item, index) in customRenameFormatTable" + :key="index" + :label="item.placeholder" + align="center" + label-style="width: 100px;" + > + {{ item.description }} + </el-descriptions-item> + <el-descriptions-item + v-for="(item, index) in customRenameFormatTable.slice(0, customRenameFormatTable.length-1)" + :key="index" + :label="item.placeholderB" + align="center" + label-style="width: 100px;" + > + {{ item.descriptionB }} + </el-descriptions-item> + <el-descriptions-item + label="{auto}" + align="center" + label-style="width: 100px;" + > + {{ $T('MANAGE_BUCKET_RENAME_FILE_TABLE_IID') }} + </el-descriptions-item> + </el-descriptions> + </el-popover> + </span> + </el-link> + <el-input + v-model="batchRenameReplace" + placeholder="Ex. {Y}-{m}-{uuid}" + clearable + /> + <div + style="margin-top: 10px;align-items: center;display: flex;justify-content: flex-end;" + > + <el-button + type="danger" + style="margin-right: 30px;" + plain + :icon="Close" + @click="() => {isShowBatchRenameDialog = false}" + > + {{ $T('MANAGE_BUCKET_RENAME_FILE_CANCEL') }} + </el-button> + <el-button + type="primary" + plain + :icon="Edit" + @click="handelBatchRename()" + > + {{ $T('MANAGE_BUCKET_RENAME_FILE_CONFIRM') }} + </el-button> + </div> + </el-dialog> </div> </template> <script lang="ts" setup> import type { IResult } from '@picgo/store/dist/types' import { PASTE_TEXT, GET_PICBEDS } from '#/events/constants' -import { CheckboxValueType, ElMessageBox, ElNotification } from 'element-plus' -import { Close, CaretBottom, Document, Edit, Delete, CaretTop } from '@element-plus/icons-vue' +import { CheckboxValueType, ElMessageBox, ElNotification, ElMessage } from 'element-plus' +import { InfoFilled, Close, CaretBottom, Document, Edit, Delete, CaretTop, Sort } from '@element-plus/icons-vue' import { ipcRenderer, clipboard, @@ -231,6 +411,7 @@ import { onBeforeRouteUpdate } from 'vue-router' import { T as $T } from '@/i18n/index' import $$db from '@/utils/db' import ALLApi from '@/apis/allApi' +import { customRenameFormatTable, customStrMatch, customStrReplace } from '../manage/utils/common' const images = ref<ImgInfo[]>([]) const dialogVisible = ref(false) const imgInfo = reactive({ @@ -248,7 +429,8 @@ const choosedPicBed = ref<string[]>([]) const lastChoosed = ref<number>(-1) const isShiftKeyPress = ref<boolean>(false) const searchText = ref<string>('') -const handleBarActive = ref<boolean>(false) +const searchTextURL = ref<string>('') +const handleBarActive = ref<boolean>(true) const pasteStyle = ref<string>('') const pasteStyleMap = { Markdown: 'markdown', @@ -257,6 +439,22 @@ const pasteStyleMap = { UBB: 'UBB', Custom: 'Custom' } +const fileSortNameReverse = ref(false) +const fileSortTimeReverse = ref(false) +const fileSortExtReverse = ref(false) +const isShowBatchRenameDialog = ref(false) +const batchRenameMatch = ref('') +const batchRenameReplace = ref('') +const mathcedCount = computed(() => { + const matchedFiles = [] as any[] + images.value.forEach((item: any) => { + if (customStrMatch(item.imgUrl, batchRenameMatch.value)) { + matchedFiles.push(item) + } + }) + return matchedFiles.length +}) +const dateRange = ref('') const picBed = ref<IPicBedType[]>([]) onBeforeRouteUpdate((to, from) => { if (from.name === 'gallery') { @@ -317,18 +515,29 @@ function getPicBeds (event: IpcRendererEvent, picBeds: IPicBedType[]) { } function getGallery (): IGalleryItem[] { - if (searchText.value || choosedPicBed.value.length > 0) { + if (searchText.value || choosedPicBed.value.length > 0 || searchTextURL.value || dateRange.value) { + console.log(dateRange.value) return images.value .filter(item => { let isInChoosedPicBed = true let isIncludesSearchText = true + let isIncludesSearchTextURL = true + let isIncludesDateRange = true if (choosedPicBed.value.length > 0) { isInChoosedPicBed = choosedPicBed.value.some(type => type === item.type) } if (searchText.value) { - isIncludesSearchText = item.fileName?.includes(searchText.value) || false + isIncludesSearchText = customStrMatch(item.fileName || '', searchText.value) } - return isIncludesSearchText && isInChoosedPicBed + if (searchTextURL.value) { + isIncludesSearchTextURL = customStrMatch(item.imgUrl || '', searchTextURL.value) + } + if (dateRange.value) { + const [start, end] = dateRange.value + const date = new Date(item.updatedAt).getTime() + isIncludesDateRange = date >= new Date(start).getTime() && date <= new Date(end).getTime() + 86400000 + } + return isIncludesSearchText && isInChoosedPicBed && isIncludesSearchTextURL && isIncludesDateRange }).map(item => { return { ...item, @@ -497,6 +706,10 @@ function cleanSearch () { searchText.value = '' } +function cleanSearchUrl () { + searchTextURL.value = '' +} + function isMultiple (obj: IObj) { return Object.values(obj).some(item => item) } @@ -623,6 +836,118 @@ async function handlePasteStyleChange (val: string) { pasteStyle.value = val } +function sortFile (type: 'name' | 'time' | 'ext' | 'check') { + switch (type) { + case 'name': + fileSortNameReverse.value = !fileSortNameReverse.value + images.value.sort((a: any, b: any) => { + if (fileSortNameReverse.value) { + return a.fileName.localeCompare(b.fileName) + } else { + return b.fileName.localeCompare(a.fileName) + } + }) + break + case 'time': + fileSortTimeReverse.value = !fileSortTimeReverse.value + images.value.sort((a: any, b: any) => { + if (fileSortTimeReverse.value) { + return a.updatedAt - b.updatedAt + } else { + return b.updatedAt - a.updatedAt + } + }) + break + case 'ext': + fileSortExtReverse.value = !fileSortExtReverse.value + images.value.sort((a: any, b: any) => { + if (fileSortExtReverse.value) { + return a.extname.localeCompare(b.extname) + } else { + return b.extname.localeCompare(a.extname) + } + }) + break + case 'check': + images.value.sort((a: any, b: any) => { + if (choosedList[a.id] && !choosedList[b.id]) { + return -1 + } else if (!choosedList[a.id] && choosedList[b.id]) { + return 1 + } else { + return 0 + } + }) + break + } +} + +function handelBatchRename () { + isShowBatchRenameDialog.value = false + if (batchRenameMatch.value === '') { + ElMessage.warning($T('MANAGE_BUCKET_BATCH_RENAME_ERROR_MSG')) + return + } + let matchedFiles = [] as any[] + images.value.forEach((item: any) => { + if (customStrMatch(item.imgUrl, batchRenameMatch.value)) { + matchedFiles.push(item) + } + }) + if (matchedFiles.length === 0) { + ElMessage.warning($T('MANAGE_BUCKET_BATCH_RENAME_ERROR_MSG2')) + return + } + for (let i = 0; i < matchedFiles.length; i++) { + matchedFiles[i].newUrl = customStrReplace(matchedFiles[i].imgUrl, batchRenameMatch.value, batchRenameReplace.value) + } + matchedFiles = matchedFiles.filter((item: any) => item.imgUrl !== item.newUrl) + if (matchedFiles.length === 0) { + ElMessage.warning($T('MANAGE_BUCKET_BATCH_RENAME_ERROR_MSG3')) + } + for (let i = 0; i < matchedFiles.length; i++) { + matchedFiles[i].newUrl = matchedFiles[i].newUrl.replaceAll('{auto}', (i + 1).toString()) + } + const duplicateFilesNum = matchedFiles.filter((item: any) => matchedFiles.filter((item2: any) => item2.newUrl === item.newUrl).length > 1).length + const renamefunc = async (item: any) => { + await $$db.updateById(item.id, { + imgUrl: item.newUrl + }) + } + const rename = () => { + const promiseList = [] as any[] + for (let i = 0; i < matchedFiles.length; i++) { + promiseList.push(renamefunc(matchedFiles[i])) + } + Promise.all(promiseList).then(() => { + const obj = { + title: $T('OPERATION_SUCCEED'), + body: '' + } + const myNotification = new Notification(obj.title, obj) + myNotification.onclick = () => { + return true + } + updateGallery() + }).catch(() => { + return true + }) + } + if (duplicateFilesNum > 0) { + ElMessageBox.confirm(`${$T('MANAGE_BUCKET_BATCH_RENAME_REPEATED_MSG_A')} ${duplicateFilesNum} ${$T('MANAGE_BUCKET_BATCH_RENAME_REPEATED_MSG_B')}`, $T('MANAGE_BUCKET_BATCH_RENAME_REPEATED_MSG_C'), { + confirmButtonText: $T('MANAGE_BUCKET_BATCH_RENAME_REPEATED_CONFIRM'), + cancelButtonText: $T('MANAGE_BUCKET_BATCH_RENAME_REPEATED_CANCEL'), + type: 'warning' + }).then(() => { + rename() + }).catch(() => { + ElMessage.info($T('MANAGE_BUCKET_BATCH_RENAME_CANCEL')) + }) + } else { + rename() + } +} + onBeforeUnmount(() => { ipcRenderer.removeAllListeners('updateGallery') ipcRenderer.removeListener(GET_PICBEDS, getPicBeds) diff --git a/src/universal/types/i18n.d.ts b/src/universal/types/i18n.d.ts index ef33ba6..8eae98a 100644 --- a/src/universal/types/i18n.d.ts +++ b/src/universal/types/i18n.d.ts @@ -63,6 +63,11 @@ interface ILocales { GALLERY_SYNC_DELETE_NOTICE_TITLE: string GALLERY_SYNC_DELETE_NOTICE_SUCCEED: string GALLERY_SYNC_DELETE_NOTICE_FAILED: string + GALLERY_CHANGE_URL: string + GALLERY_CHANGE_URL_TITLE: string + GALLERY_SEARCH_FILENAME: string + GALLERY_SEARCH_URL: string + GALLERY_MATCHED: string UPLOAD_PAGE_IMAGE_PROCESS_NAME: string UPLOAD_PAGE_IMAGE_PROCESS_DIALOG_TITLE: string UPLOAD_PAGE_IMAGE_PROCESS_ISADDWM: string