From 2336483927dcdda8501eac93999e05a4b51727da 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: Tue, 15 Aug 2023 00:00:27 -0700
Subject: [PATCH] :sparkles: Feature: optimize filename display

---
 src/main/manage/apis/local.ts            | 40 +++++++------
 src/renderer/components/ImageLocal.vue   | 76 ------------------------
 src/renderer/manage/pages/bucketPage.vue | 33 +++++-----
 src/renderer/manage/pages/logIn.vue      |  7 ++-
 src/renderer/manage/utils/common.ts      |  4 +-
 src/universal/utils/common.ts            | 21 +++++++
 6 files changed, 65 insertions(+), 116 deletions(-)
 delete mode 100644 src/renderer/components/ImageLocal.vue

diff --git a/src/main/manage/apis/local.ts b/src/main/manage/apis/local.ts
index 6fe4de2..80d90c8 100644
--- a/src/main/manage/apis/local.ts
+++ b/src/main/manage/apis/local.ts
@@ -50,12 +50,12 @@ class LocalApi {
   transBack (filePath: string | undefined) {
     if (!filePath) return ''
     return this.isWindows
-      ? filePath.split(path.posix.sep).join(path.sep).replace(/^\\+/, '')
-      : `/${filePath.replace(/^\/+/, '')}`
+      ? filePath.split(path.posix.sep).join(path.sep).replace(/^\\+|\\+$/g, '')
+      : `/${filePath.replace(/^\/+|\/+$/g, '')}`
   }
 
   formatFolder (item: fs.Stats, urlPrefix: string, fileName: string, filePath: string) {
-    const key = this.transPathToUnix(filePath)
+    const key = `${this.transPathToUnix(filePath)}/`.replace(/\/+$/, '/')
     return {
       ...item,
       key,
@@ -71,8 +71,8 @@ class LocalApi {
     }
   }
 
-  formatFile (item: fs.Stats, urlPrefix: string, fileName: string, filePath: string) {
-    const key = this.transPathToUnix(filePath)
+  formatFile (item: fs.Stats, urlPrefix: string, fileName: string, filePath: string, isDownload = false) {
+    const key = isDownload ? filePath : this.transPathToUnix(filePath)
     return {
       ...item,
       key,
@@ -106,7 +106,7 @@ class LocalApi {
       finished: false
     }
     try {
-      res = fsWalk.walkSync(prefix, {
+      res = fsWalk.walkSync(this.transBack(prefix), {
         followSymbolicLinks: true,
         fs,
         stats: true,
@@ -114,9 +114,9 @@ class LocalApi {
       })
       if (res.length) {
         result.fullList.push(
-          ...res.data
+          ...res
             .filter((item: fsWalk.Entry) => item.stats?.isFile())
-            .map((item: any) => this.formatFile(item, urlPrefix, item.name, item.path))
+            .map((item: any) => this.formatFile(item, urlPrefix, item.name, item.path, true))
         )
         result.success = true
       }
@@ -133,7 +133,7 @@ class LocalApi {
     const { customUrl = '', cancelToken, baseDir } = configMap
     let prefix = configMap.prefix
     prefix = this.transBack(prefix)
-    let urlPrefix = customUrl.replace(/\/+$/, '')
+    const urlPrefix = customUrl.replace(/\/+$/, '')
     let webPath = configMap.webPath || ''
     if (webPath && customUrl && webPath !== '/') {
       webPath = webPath.replace(/^\/+|\/+$/, '')
@@ -156,20 +156,22 @@ class LocalApi {
         withFileTypes: true
       })
       if (res.length) {
+        let urlPrefixF
         res.forEach((item: fs.Dirent) => {
           const pathOfFile = path.join(prefix, item.name)
-          const relativePath = path.relative(baseDir, pathOfFile)
-          const relative = webPath && urlPrefix + `/${path.join(webPath, relativePath)}`.replace(/\\/g, '/').replace(/\/+/g, '/')
-          if (webPath && customUrl) {
-            urlPrefix = relative
+          let relative
+          if (customUrl) {
+            const relativePath = path.relative(this.transBack(baseDir), pathOfFile)
+            relative = urlPrefix + `/${path.join(webPath, relativePath)}`.replace(/\\/g, '/').replace(/\/+/g, '/')
+            urlPrefixF = this.isWindows ? relative.replace(/\/[a-zA-Z]:\//, '/') : relative
           } else {
-            urlPrefix = pathOfFile
+            urlPrefixF = pathOfFile
           }
           const stats = fs.statSync(pathOfFile)
           if (item.isDirectory()) {
-            result.fullList.push(this.formatFolder(stats, urlPrefix, item.name, relativePath))
+            result.fullList.push(this.formatFolder(stats, urlPrefixF, item.name, pathOfFile))
           } else {
-            result.fullList.push(this.formatFile(stats, urlPrefix, item.name, relativePath))
+            result.fullList.push(this.formatFile(stats, urlPrefixF, item.name, pathOfFile))
           }
         })
         result.success = true
@@ -210,7 +212,7 @@ class LocalApi {
     const { key } = configMap
     let result = false
     try {
-      await fs.rmdir(this.transBack(key), {
+      await fs.rm(this.transBack(key), {
         recursive: true
       })
       result = true
@@ -241,7 +243,7 @@ class LocalApi {
         noProgress: true
       })
       try {
-        fs.ensureFileSync(filePath)
+        fs.ensureFileSync(this.transBack(key))
         await fs.copyFile(filePath, this.transBack(key))
         instance.updateUploadTask({
           id,
@@ -281,7 +283,7 @@ class LocalApi {
     const instance = UpDownTaskQueue.getInstance()
     for (const item of fileArray) {
       const { alias, bucketName, key, fileName } = item
-      const savedFilePath = path.join(downloadPath, fileName)
+      const savedFilePath = path.join(downloadPath, fileName.replace(/[:*?"<>|]/g, ''))
       const id = `${alias}-${bucketName}-local-${key}`
       if (instance.getDownloadTask(id)) {
         continue
diff --git a/src/renderer/components/ImageLocal.vue b/src/renderer/components/ImageLocal.vue
deleted file mode 100644
index 9e0468f..0000000
--- a/src/renderer/components/ImageLocal.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<template>
-  <el-image
-    :src="isShowThumbnail && item.isImage ?
-      base64Image
-      : require(`../manage/pages/assets/icons/${getFileIconPath(item.fileName ?? '')}`)"
-    fit="contain"
-    style="height: 100px;width: 100%;margin: 0 auto;"
-  >
-    <template #placeholder>
-      <el-icon>
-        <Loading />
-      </el-icon>
-    </template>
-    <template #error>
-      <el-image
-        :src="require(`../manage/pages/assets/icons/${getFileIconPath(item.fileName ?? '')}`)"
-        fit="contain"
-        style="height: 100px;width: 100%;margin: 0 auto;"
-      />
-    </template>
-  </el-image>
-</template>
-
-<script lang="ts" setup>
-import { ref, onBeforeMount, watch } from 'vue'
-import { getFileIconPath } from '@/manage/utils/common'
-import { Loading } from '@element-plus/icons-vue'
-import fs from 'fs-extra'
-import mime from 'mime-types'
-import path from 'path'
-
-const base64Image = ref('')
-const props = defineProps(
-  {
-    isShowThumbnail: {
-      type: Boolean,
-      required: true
-    },
-    item: {
-      type: Object,
-      required: true
-    },
-    localPath: {
-      type: String,
-      required: true
-    }
-  }
-)
-
-watch(
-  () => props.localPath,
-  async (newLocalPath, oldLocalPath) => {
-    if (newLocalPath !== oldLocalPath) {
-      try {
-        await createBase64Image()
-      } catch (e) {
-        console.log(e)
-      }
-    }
-  }
-)
-
-const createBase64Image = async () => {
-  const filePath = path.normalize(props.localPath)
-  const base64 = await fs.readFile(filePath, 'base64')
-  base64Image.value = `data:${mime.lookup(filePath) || 'image/png'};base64,${base64}`
-}
-
-onBeforeMount(async () => {
-  try {
-    await createBase64Image()
-  } catch (e) {
-    console.log(e)
-  }
-})
-</script>
diff --git a/src/renderer/manage/pages/bucketPage.vue b/src/renderer/manage/pages/bucketPage.vue
index 00e75d7..c310052 100644
--- a/src/renderer/manage/pages/bucketPage.vue
+++ b/src/renderer/manage/pages/bucketPage.vue
@@ -59,7 +59,7 @@
               class="icon"
               size="25px"
             >
-              <DocumentAdd />
+              <Upload />
             </el-icon>
           </el-tooltip>
         </el-button>
@@ -80,7 +80,7 @@
               size="25px"
               style="margin-left: 5px;"
             >
-              <Upload />
+              <UploadFilled />
             </el-icon>
           </el-tooltip>
         </el-button>
@@ -497,7 +497,7 @@ https://www.baidu.com/img/bd_logo1.png"
               shadow="hover"
             >
               <el-image
-                v-if="!item.isDir && currentPicBedName !== 'webdavplist' && currentPicBedName !== 'local'"
+                v-if="!item.isDir && currentPicBedName !== 'webdavplist'"
                 :src="isShowThumbnail && item.isImage ?
                   item.url
                   : require(`./assets/icons/${getFileIconPath(item.fileName ?? '')}`)"
@@ -526,13 +526,6 @@ https://www.baidu.com/img/bd_logo1.png"
                 :url="item.url"
                 @click="handleClickFile(item)"
               />
-              <ImageLocal
-                v-else-if="!item.isDir && currentPicBedName === 'local'"
-                :is-show-thumbnail="isShowThumbnail"
-                :item="item"
-                :local-path="item.key"
-                @click="handleClickFile(item)"
-              />
               <el-image
                 v-else
                 :src="require('./assets/icons/folder.webp')"
@@ -554,7 +547,7 @@ https://www.baidu.com/img/bd_logo1.png"
                     :underline="false"
                     :type="item.checked ? 'primary' : 'info'"
                   >
-                    {{ formatFileName(item.fileName ?? '', 8) }}
+                    {{ formatFileName(item.fileName ?? '', 15) }}
                   </el-link>
                 </el-tooltip>
               </div>
@@ -1422,7 +1415,7 @@ import { ref, reactive, watch, onBeforeMount, computed, onBeforeUnmount } from '
 import { useRoute } from 'vue-router'
 
 // Element Plus 图标
-import { InfoFilled, Grid, Fold, Close, Folder, FolderAdd, Upload, CircleClose, Loading, CopyDocument, Edit, DocumentAdd, Link, Refresh, ArrowRight, HomeFilled, Document, Coin, Download, DeleteFilled, Sort, FolderOpened } from '@element-plus/icons-vue'
+import { InfoFilled, Grid, Fold, Close, Folder, FolderAdd, Upload, CircleClose, Loading, CopyDocument, Edit, UploadFilled, Link, Refresh, ArrowRight, HomeFilled, Document, Coin, Download, DeleteFilled, Sort, FolderOpened } from '@element-plus/icons-vue'
 
 // 状态管理相关
 import { useManageStore } from '../store/manageStore'
@@ -1492,7 +1485,6 @@ import { videoExt } from '../utils/videofile'
 
 // 组件
 import ImageWebdav from '@/components/ImageWebdav.vue'
-import ImageLocal from '@/components/ImageLocal.vue'
 
 // 国际化函数
 import { T as $T } from '@/i18n'
@@ -2113,6 +2105,16 @@ async function initCustomDomainList () {
       currentCustomDomain.value = endpoint
     }
     handleChangeCustomUrl()
+  } else if (currentPicBedName.value === 'local') {
+    const currentConfigs = await getConfig<any>('picBed')
+    const currentConfig = currentConfigs[configMap.alias]
+    const currentTransformedConfig = JSON.parse(currentConfig.transformedConfig ?? '{}')
+    if (currentTransformedConfig[configMap.bucketName] && currentTransformedConfig[configMap.bucketName]?.customUrl) {
+      currentCustomDomain.value = currentTransformedConfig[configMap.bucketName].customUrl ?? ''
+    } else {
+      currentCustomDomain.value = ''
+    }
+    handleChangeCustomUrl()
   }
 }
 
@@ -2535,7 +2537,7 @@ function handleCreateFolder () {
   ElMessageBox.prompt($T('MANAGE_BUCKET_CREATE_FOLDER_BOX_TITLE'), $T('MANAGE_BUCKET_CREATE_FOLDER_BOX_TIP'), {
     confirmButtonText: $T('MANAGE_BUCKET_CREATE_FOLDER_BOX_CONFIRM'),
     cancelButtonText: $T('MANAGE_BUCKET_CREATE_FOLDER_BOX_CANCEL'),
-    inputPattern: /^[\u4e00-\u9fff_a-zA-Z0-9/]+$/,
+    inputPattern: /^[\p{Unified_Ideograph}_a-zA-Z0-9-]+$/u,
     inputErrorMessage: $T('MANAGE_BUCKET_CREATE_FOLDER_ERROR_MSG')
   }).then(async ({ value }) => {
     let formatedPath = value
@@ -2801,7 +2803,6 @@ async function getBucketFileListBackStage () {
     param.baseDir = configMap.baseDir
     param.webPath = configMap.webPath
   }
-  console.log(param)
   ipcRenderer.send('getBucketListBackstage', configMap.alias, param)
   ipcRenderer.on('refreshFileTransferList', (evt: IpcRendererEvent, data) => {
     fileTransferStore.refreshFileTransferList(data)
@@ -3445,7 +3446,7 @@ const columns: Column<any>[] = [
             <div
               style="font-size: 14px;color: #303133;font-family: Arial, Helvetica, sans-serif;"
             >
-              {formatFileName(item.fileName ?? '')}
+              {formatFileName(item.fileName ?? '', 40)}
             </div>
           </ElTooltip>
         </div>
diff --git a/src/renderer/manage/pages/logIn.vue b/src/renderer/manage/pages/logIn.vue
index 873eab5..9423f1d 100644
--- a/src/renderer/manage/pages/logIn.vue
+++ b/src/renderer/manage/pages/logIn.vue
@@ -74,9 +74,9 @@
                       effect="light"
                       :content="item.alias"
                       placement="top"
-                      :disabled="item.alias.length <= 15"
+                      :disabled="isNeedToShorten(item.alias)"
                     >
-                      {{ item.alias.length > 15 ? item.alias.slice(0, 8) + '...' + item.alias.slice(-6) : item.alias }}
+                      {{ isNeedToShorten(item.alias) ? safeSliceF(item.alias, 17) + '...' : item.alias }}
                     </el-tooltip>
                   </el-button>
                 </template>
@@ -290,6 +290,7 @@ import { getConfig as getPicBedsConfig } from '@/utils/dataSender'
 
 // 端点地址格式化
 import { formatEndpoint } from '~/main/manage/utils/common'
+import { isNeedToShorten, safeSliceF } from '#/utils/common'
 
 // 国际化函数
 import { T as $T } from '@/i18n'
@@ -373,7 +374,7 @@ async function handleConfigChange (name: string) {
   const aliasList = getAliasList()
   const allKeys = Object.keys(supportedPicBedList[name].configOptions)
   const resultMap:IStringKeyMap = {}
-  const reg = /^[\u4e00-\u9fff_a-zA-Z0-9-]+$/
+  const reg = /^[\p{Unified_Ideograph}_a-zA-Z0-9-]+$/u
   for (const key of allKeys) {
     const resultKey = name + '.' + key
     if (supportedPicBedList[name].configOptions[key].required) {
diff --git a/src/renderer/manage/utils/common.ts b/src/renderer/manage/utils/common.ts
index 6ada585..6e7ae79 100644
--- a/src/renderer/manage/utils/common.ts
+++ b/src/renderer/manage/utils/common.ts
@@ -14,7 +14,7 @@ import { availableIconList } from './icon'
 import { getConfig } from './dataSender'
 
 // 工具函数
-import { handleUrlEncode } from '~/universal/utils/common'
+import { handleUrlEncode, safeSliceF, isNeedToShorten } from '~/universal/utils/common'
 
 export function randomStringGenerator (length: number): string {
   const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
@@ -103,7 +103,7 @@ export function formatFileName (fileName: string, length: number = 20) {
   let ext = path.extname(fileName)
   ext = ext.length > 5 ? ext.slice(ext.length - 5) : ext
   const name = path.basename(fileName, ext)
-  return name.length > length ? `${name.slice(0, length)}...${ext}` : fileName
+  return isNeedToShorten(fileName, length) ? `${safeSliceF(name, length - 3 - ext.length)}...${ext}` : fileName
 }
 
 export const getExtension = (fileName: string) => path.extname(fileName).slice(1)
diff --git a/src/universal/utils/common.ts b/src/universal/utils/common.ts
index 9875843..7fdbd23 100644
--- a/src/universal/utils/common.ts
+++ b/src/universal/utils/common.ts
@@ -44,3 +44,24 @@ export const isDev = process.env.NODE_ENV === 'development'
 export const trimValues = <T extends IStringKeyMap>(obj: T): {[K in keyof T]: T[K] extends string ? string : T[K]} => {
   return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, typeof value === 'string' ? value.trim() : value])) as {[K in keyof T]: T[K] extends string ? string : T[K]}
 }
+
+export function isNeedToShorten (alias: string, cutOff: number = 20) {
+  let totalLen = 0
+  for (const s of alias) {
+    totalLen += s.charCodeAt(0) > 255 ? 2 : 1
+  }
+  return totalLen > cutOff
+}
+
+export function safeSliceF (str:string, total: number) {
+  let result = ''
+  let totalLen = 0
+  for (const s of str) {
+    if (totalLen >= total) {
+      break
+    }
+    result += s
+    totalLen += s.charCodeAt(0) > 255 ? 2 : 1
+  }
+  return result
+}