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