From 6a8d3f6bbf52c081f24ce01f335ee21942b79c84 Mon Sep 17 00:00:00 2001 From: Kuingsmile <96409857+Kuingsmile@users.noreply.github.com> Date: Tue, 30 Dec 2025 23:03:52 +0800 Subject: [PATCH] :construction: WIP(custom): support sync gallery db file ISSUES CLOSED: #355,#417 --- package.json | 1 + .../events/rpc/routes/setting/configure.ts | 9 +- src/main/utils/common.ts | 21 ++ src/main/utils/enum.ts | 1 + src/main/utils/syncSettings.ts | 356 +++++++++++++----- src/renderer/i18n/locales/en.json | 1 + src/renderer/i18n/locales/zh-CN.json | 1 + src/renderer/i18n/locales/zh-TW.json | 1 + src/renderer/pages/PicGoSetting.vue | 64 +++- src/renderer/pages/css/PicgoSetting.css | 118 ++++++ src/renderer/utils/enum.ts | 1 + yarn.lock | 2 +- 12 files changed, 475 insertions(+), 101 deletions(-) diff --git a/package.json b/package.json index 2c3b1859..98849551 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "dayjs": "^1.11.19", "electron-updater": "^6.6.2", "fast-xml-parser": "^5.3.3", + "fflate": "^0.8.2", "form-data": "^4.0.5", "fs-extra": "^11.3.3", "got": "^14.6.5", diff --git a/src/main/events/rpc/routes/setting/configure.ts b/src/main/events/rpc/routes/setting/configure.ts index 1e826626..6df9ea67 100644 --- a/src/main/events/rpc/routes/setting/configure.ts +++ b/src/main/events/rpc/routes/setting/configure.ts @@ -5,7 +5,7 @@ import { app } from 'electron' import fs from 'fs-extra' import { IRPCActionType, IRPCType } from '~/utils/enum' -import { downloadFile, uploadFile } from '~/utils/syncSettings' +import { downloadFile, syncGallery, uploadFile } from '~/utils/syncSettings' const STORE_PATH = app.getPath('userData') @@ -48,6 +48,13 @@ export default [ }, type: IRPCType.INVOKE, }, + { + action: IRPCActionType.CONFIGURE_SYNC_GALLERY_DB, + handler: async () => { + return await syncGallery() + }, + type: IRPCType.INVOKE, + }, { action: IRPCActionType.CONFIGURE_UPLOAD_ALL_CONFIG, handler: async () => { diff --git a/src/main/utils/common.ts b/src/main/utils/common.ts index e7c5de7c..b32d0680 100644 --- a/src/main/utils/common.ts +++ b/src/main/utils/common.ts @@ -4,6 +4,7 @@ import db from '@core/datastore' import logger from '@core/picgo/logger' import axios from 'axios' import { clipboard, Notification, Tray } from 'electron' +import { gunzipSync, gzipSync, strFromU8 } from 'fflate' import FormData from 'form-data' import fs from 'fs-extra' import { isReactive, isRef, toRaw, unref } from 'vue' @@ -321,3 +322,23 @@ export function encodeFilePath(filePath: string) { } export const trimPath = (path: string) => path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/') + +export const extractData = async (zipPath: string): Promise> => { + try { + const buffer = await fs.readFile(zipPath) + const str = strFromU8(gunzipSync(buffer)) + return JSON.parse(str) + } catch (_err) { + throw new Error('Extract failed') + } +} + +export const zipData = async (data: Record, zipPath: string): Promise => { + try { + const buffer = Buffer.from(JSON.stringify(data)) + const compressed = gzipSync(buffer) + await fs.writeFile(zipPath, Buffer.from(compressed)) + } catch (_err) { + throw new Error('Zip failed') + } +} diff --git a/src/main/utils/enum.ts b/src/main/utils/enum.ts index 9b104133..92fb681e 100644 --- a/src/main/utils/enum.ts +++ b/src/main/utils/enum.ts @@ -122,6 +122,7 @@ export const IRPCActionType = { CONFIGURE_MIGRATE_FROM_PICGO: 'CONFIGURE_MIGRATE_FROM_PICGO', CONFIGURE_UPLOAD_COMMON_CONFIG: 'CONFIGURE_UPLOAD_COMMON_CONFIG', CONFIGURE_UPLOAD_MANAGE_CONFIG: 'CONFIGURE_UPLOAD_MANAGE_CONFIG', + CONFIGURE_SYNC_GALLERY_DB: 'CONFIGURE_SYNC_GALLERY_DB', CONFIGURE_UPLOAD_ALL_CONFIG: 'CONFIGURE_UPLOAD_ALL_CONFIG', CONFIGURE_DOWNLOAD_COMMON_CONFIG: 'CONFIGURE_DOWNLOAD_COMMON_CONFIG', CONFIGURE_DOWNLOAD_MANAGE_CONFIG: 'CONFIGURE_DOWNLOAD_MANAGE_CONFIG', diff --git a/src/main/utils/syncSettings.ts b/src/main/utils/syncSettings.ts index 18d436ea..aea07dc5 100644 --- a/src/main/utils/syncSettings.ts +++ b/src/main/utils/syncSettings.ts @@ -1,3 +1,4 @@ +import os from 'node:os' import path from 'node:path' import db from '@core/datastore' @@ -10,32 +11,71 @@ import { HttpsProxyAgent } from 'hpagent' import { AuthType, createClient, WebDAVClientOptions } from 'webdav' import type { ISyncConfig } from '#/types/types' +import { extractData, zipData } from '~/utils/common' import { formatEndpoint } from '~/utils/common' import { configPaths } from '~/utils/configPaths' const STORE_PATH = app.getPath('userData') +const tempDir = path.join(os.tmpdir(), `piclist-sync-tmp`) +const db1 = path.join(tempDir, 'db1') +const db2 = path.join(tempDir, 'db2') +const dbMerged = path.join(tempDir, 'db-merged') +const galleryDBList = ['piclist.db', 'piclist.bak.db'] const readFileAsBase64 = (filePath: string) => fs.readFileSync(filePath, { encoding: 'base64' }) const isHttpResSuccess = (res: any) => res.status >= 200 && res.status < 300 + const uploadOrUpdateMsg = (fileName: string, isUpdate: boolean = true) => isUpdate ? `update ${fileName} from PicList` : `upload ${fileName} from PicList` -const getSyncConfig = () => { - return ( - db.get(configPaths.settings.sync) || { - type: 'github', - username: '', - repo: '', - branch: '', - token: '', - proxy: '', - } - ) +const emptyDir = async (): Promise => { + await fs.emptyDir(tempDir) + await fs.emptyDir(db1) + await fs.emptyDir(db2) + await fs.emptyDir(dbMerged) } -const getProxyagent = (proxy: string | undefined) => { - return proxy +const mergeGalleryDB = async (targetFile: string) => { + try { + const db1Data = await extractData(path.join(db1, targetFile)) + const db2Data = await extractData(path.join(db2, targetFile)) + const mergedData: any = { + gallery: [], + __gallery_KEY__: {}, + } + const db1Ids = new Set(Object.keys(db1Data.__gallery_KEY__ || {})) + const db2Ids = new Set(Object.keys(db2Data.__gallery_KEY__ || {})) + const idSet = new Set([...db1Ids, ...db2Ids]) + for (const id of idSet) { + if (db2Ids.has(id)) { + mergedData.gallery.push(db2Data.gallery.find((item: any) => item.id === id)) + } else if (db1Ids.has(id)) { + mergedData.gallery.push(db1Data.gallery.find((item: any) => item.id === id)) + } + } + for (const item of mergedData.gallery) { + mergedData.__gallery_KEY__[item.id] = 1 + } + await zipData(mergedData, path.join(dbMerged, targetFile)) + await fs.copyFile(path.join(dbMerged, targetFile), path.join(STORE_PATH, targetFile)) + } catch (err: any) { + logger.error('merge gallery db failed:', String(err)) + } +} + +const getSyncConfig = () => + db.get(configPaths.settings.sync) || { + type: 'github', + username: '', + repo: '', + branch: '', + token: '', + proxy: '', + } + +const getProxyagent = (proxy: string | undefined) => + proxy ? new HttpsProxyAgent({ keepAlive: true, keepAliveMsecs: 1000, @@ -44,17 +84,14 @@ const getProxyagent = (proxy: string | undefined) => { scheduling: 'lifo', }) : undefined -} -function getOctokit(syncConfig: ISyncConfig) { - const { token, proxy } = syncConfig - return new Octokit({ - auth: token, +const getOctokit = (syncConfig: ISyncConfig) => + new Octokit({ + auth: syncConfig.token, request: { - agent: getProxyagent(proxy), + agent: getProxyagent(syncConfig.proxy), }, }) -} const isSyncConfigValidate = ({ type, @@ -85,10 +122,10 @@ const isSyncConfigValidate = ({ async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) { const localFilePath = path.join(STORE_PATH, fileName) - if (!fs.existsSync(localFilePath)) { - return false - } + if (!fs.existsSync(localFilePath)) return false + const { username, repo, branch, token, type } = syncConfig + const defaultConfig = { content: readFileAsBase64(localFilePath), message: uploadOrUpdateMsg(fileName, false), @@ -97,8 +134,7 @@ async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) { try { switch (type) { case 'gitee': { - const url = `https://gitee.com/api/v5/repos/${username}/${repo}/contents/${fileName}` - const res = await axios.post(url, { + const res = await axios.post(`https://gitee.com/api/v5/repos/${username}/${repo}/contents/${fileName}`, { ...defaultConfig, access_token: token, }) @@ -116,11 +152,15 @@ async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) { } case 'gitea': { const { endpoint = '' } = syncConfig - const apiUrl = `${endpoint}/api/v1/repos/${username}/${repo}/contents/${fileName}` - const headers = { - Authorization: `token ${token}`, - } - const res = await axios.post(apiUrl, defaultConfig, { headers }) + const res = await axios.post( + `${endpoint}/api/v1/repos/${username}/${repo}/contents/${fileName}`, + defaultConfig, + { + headers: { + Authorization: `token ${token}`, + }, + }, + ) return isHttpResSuccess(res) } case 'webdav': { @@ -136,9 +176,7 @@ async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) { const options: WebDAVClientOptions = { username: webdavUsername, password: webdavPassword, - } - if (webdavAuthType === 'digest') { - options.authType = AuthType.Digest + ...(webdavAuthType === 'digest' ? { authType: AuthType.Digest } : {}), } const client = createClient(webdavEndpointF, options) const fileContent = fs.readFileSync(localFilePath) @@ -161,6 +199,31 @@ async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) { } } +async function uploadFile(fileName: string[]): Promise { + const syncConfig = getSyncConfig() + if (!isSyncConfigValidate(syncConfig)) { + logger.error('sync config is invalid') + return 0 + } + const uploadFunc = async (file: string): Promise => { + let result = false + try { + result = await updateLocalToRemote(syncConfig, file) + } catch (_e: any) { + result = await uploadLocalToRemote(syncConfig, file) + } + logger.info(`upload ${file} ${result ? 'success' : 'failed'}`) + return result ? 1 : 0 + } + + let count = 0 + for (const file of fileName) { + count += await uploadFunc(file) + } + + return count +} + async function updateLocalToRemote(syncConfig: ISyncConfig, fileName: string) { const localFilePath = path.join(STORE_PATH, fileName) if (!fs.existsSync(localFilePath)) { @@ -277,31 +340,6 @@ async function updateLocalToRemote(syncConfig: ISyncConfig, fileName: string) { } } -async function uploadFile(fileName: string[]): Promise { - const syncConfig = getSyncConfig() - if (!isSyncConfigValidate(syncConfig)) { - logger.error('sync config is invalid') - return 0 - } - const uploadFunc = async (file: string): Promise => { - let result = false - try { - result = await updateLocalToRemote(syncConfig, file) - } catch (_e: any) { - result = await uploadLocalToRemote(syncConfig, file) - } - logger.info(`upload ${file} ${result ? 'success' : 'failed'}`) - return result ? 1 : 0 - } - - let count = 0 - for (const file of fileName) { - count += await uploadFunc(file) - } - - return count -} - async function downloadAndWriteFile(url: string, localFilePath: string, config: any, isWriteJson = false) { const res = await axios.get(url, config) if (isHttpResSuccess(res)) { @@ -314,19 +352,31 @@ async function downloadAndWriteFile(url: string, localFilePath: string, config: return false } -async function downloadRemoteToLocal(syncConfig: ISyncConfig, fileName: string) { - const localFilePath = path.join(STORE_PATH, fileName) +async function downloadRemoteToLocal(syncConfig: ISyncConfig, fileName: string, galleryMode = false) { + const storePath = galleryMode ? db2 : STORE_PATH + const localFilePath = path.join(storePath, fileName) const { username, repo, branch, token, proxy, type } = syncConfig try { switch (type) { case 'gitee': { const url = `https://gitee.com/api/v5/repos/${username}/${repo}/contents/${fileName}` - return downloadAndWriteFile(url, localFilePath, { - params: { - access_token: token, - ref: branch, - }, - }) + const config = { + params: { access_token: token, ref: branch }, + } + if (galleryMode) { + const res = await axios.get(url, config) + if (isHttpResSuccess(res)) { + const downloadUrl = res.data.download_url + const fileRes = await axios.get(downloadUrl, { responseType: 'arraybuffer' }) + if (isHttpResSuccess(fileRes)) { + await fs.writeFile(localFilePath, fileRes.data) + return true + } + } + return false + } else { + return downloadAndWriteFile(url, localFilePath, config) + } } case 'github': { const octokit = getOctokit(syncConfig) @@ -339,28 +389,59 @@ async function downloadRemoteToLocal(syncConfig: ISyncConfig, fileName: string) if (res.status === 200) { const data = res.data as any const downloadUrl = data.download_url - return downloadAndWriteFile( - downloadUrl, - localFilePath, - { + if (galleryMode) { + const res = await axios.get(downloadUrl, { httpsAgent: getProxyagent(proxy), - }, - true, - ) + responseType: 'arraybuffer', + }) + if (isHttpResSuccess(res)) { + await fs.writeFile(localFilePath, res.data) + return true + } else { + return false + } + } else { + return downloadAndWriteFile( + downloadUrl, + localFilePath, + { + httpsAgent: getProxyagent(proxy), + }, + true, + ) + } } return false } case 'gitea': { - const { endpoint = '' } = syncConfig - const apiUrl = `${endpoint}/api/v1/repos/${username}/${repo}/contents/${fileName}` - return downloadAndWriteFile(apiUrl, localFilePath, { - headers: { - Authorization: `token ${token}`, - }, - params: { - ref: branch, - }, - }) + const { endpoint = '', token, username, repo, branch } = syncConfig + if (galleryMode) { + const rawUrl = `${endpoint}/api/v1/repos/${username}/${repo}/raw/${fileName}` + const res = await axios.get(rawUrl, { + headers: { + Authorization: `token ${token}`, + }, + params: { + ref: branch, + }, + responseType: 'arraybuffer', + }) + if (isHttpResSuccess(res)) { + await fs.writeFile(localFilePath, res.data) + return true + } + return false + } else { + const apiUrl = `${endpoint}/api/v1/repos/${username}/${repo}/contents/${fileName}` + return downloadAndWriteFile(apiUrl, localFilePath, { + headers: { + Authorization: `token ${token}`, + }, + params: { + ref: branch, + }, + }) + } } case 'webdav': { const { @@ -394,6 +475,82 @@ async function downloadRemoteToLocal(syncConfig: ISyncConfig, fileName: string) } } +async function checkCloudFileExist(syncConfig: ISyncConfig, fileName: string) { + const { username, repo, branch, token, type } = syncConfig + try { + switch (type) { + case 'gitee': { + const url = `https://gitee.com/api/v5/repos/${username}/${repo}/contents/${fileName}` + try { + const res = await axios.get(url, { + params: { access_token: token, ref: branch }, + }) + return isHttpResSuccess(res) + } catch (error: any) { + if (error.response?.status === 404) return false + throw error + } + } + case 'github': { + const octokit = getOctokit(syncConfig) + try { + const res = await octokit.rest.repos.getContent({ + owner: username, + repo, + path: fileName, + ref: branch, + }) + return res.status === 200 + } catch (error: any) { + if (Number(error.status) === 404) return false + throw error + } + } + case 'gitea': { + const { endpoint = '' } = syncConfig + const apiUrl = `${endpoint}/api/v1/repos/${username}/${repo}/contents/${fileName}` + try { + const res = await axios.get(apiUrl, { + headers: { Authorization: `token ${token}` }, + params: { ref: branch }, + }) + return isHttpResSuccess(res) + } catch (error: any) { + if (error.response?.status === 404) return false + throw error + } + } + case 'webdav': { + const { + webdavEndpoint = '', + webdavUsername, + webdavPassword, + webdavAuthType = 'basic', + webdavSslEnabled = true, + webdavSavePath = '', + } = syncConfig + const webdavEndpointF = formatEndpoint(webdavEndpoint, webdavSslEnabled) + const options: WebDAVClientOptions = { + username: webdavUsername, + password: webdavPassword, + } + if (webdavAuthType === 'digest') { + options.authType = AuthType.Digest + } + const client = createClient(webdavEndpointF, options) + const remoteFilePath = (webdavSavePath ? path.join(webdavSavePath, fileName) : fileName).replace(/\\/g, '/') + const exists = await client.exists(remoteFilePath) + return exists + } + default: + throw new Error('unsupported sync type') + } + } catch (error: any) { + logger.error(error) + throw new Error('check file exist failed') + } +} + async function downloadFile(fileName: string[]): Promise { const syncConfig = getSyncConfig() if (!isSyncConfigValidate(syncConfig)) { @@ -410,4 +567,35 @@ async function downloadFile(fileName: string[]): Promise { return (await Promise.all(fileName.map(downloadFunc))).reduce((a, b) => a + b, 0) } -export { downloadFile, uploadFile } +async function syncGallery(): Promise { + const syncConfig = getSyncConfig() + if (!isSyncConfigValidate(syncConfig)) { + logger.error('sync config is invalid') + return 0 + } + let successCount = 0 + for (const file of galleryDBList) { + await emptyDir() + try { + const exists = await checkCloudFileExist(syncConfig, file) + if (!exists) { + await uploadLocalToRemote(syncConfig, file) + logger.info(`gallery db ${file} not exist in cloud, upload local file instead`) + successCount++ + continue + } + } catch (err: any) { + logger.error(`check gallery db ${file} exist failed:`, String(err)) + continue + } + await downloadRemoteToLocal(syncConfig, file, true) + await fs.copyFile(path.join(STORE_PATH, file), path.join(db1, file)) + await mergeGalleryDB(file) + await updateLocalToRemote(syncConfig, file) + logger.info(`sync gallery db ${file} success`) + successCount++ + } + return successCount +} + +export { downloadFile, syncGallery, uploadFile } diff --git a/src/renderer/i18n/locales/en.json b/src/renderer/i18n/locales/en.json index 54f1cddf..2f0f0ff4 100644 --- a/src/renderer/i18n/locales/en.json +++ b/src/renderer/i18n/locales/en.json @@ -685,6 +685,7 @@ "commonConfig": "Common Configuration", "downloadSettings": "Download Settings", "fileManagement": "File Management", + "galleryDB": "Gallery Database Sync", "gitea": { "branch": "Branch Name", "repo": "Repository Name", diff --git a/src/renderer/i18n/locales/zh-CN.json b/src/renderer/i18n/locales/zh-CN.json index 6108454b..ec1c7509 100644 --- a/src/renderer/i18n/locales/zh-CN.json +++ b/src/renderer/i18n/locales/zh-CN.json @@ -685,6 +685,7 @@ "commonConfig": "通用配置", "downloadSettings": "下载配置", "fileManagement": "文件管理", + "galleryDB": "相册数据库同步", "gitea": { "branch": "分支名", "repo": "仓库名", "token": "访问令牌", "username": "用户名" }, "giteaHost": "Gitea 地址", "gitee": { diff --git a/src/renderer/i18n/locales/zh-TW.json b/src/renderer/i18n/locales/zh-TW.json index d4719bab..b0ad52bb 100644 --- a/src/renderer/i18n/locales/zh-TW.json +++ b/src/renderer/i18n/locales/zh-TW.json @@ -685,6 +685,7 @@ "commonConfig": "通用配置", "downloadSettings": "下載配置", "fileManagement": "文件管理", + "galleryDB": "相冊數據庫同步", "gitea": { "branch": "分支名", "repo": "倉庫名", "token": "訪問令牌", "username": "用戶名" }, "giteaHost": "Gitea 地址", "gitee": { diff --git a/src/renderer/pages/PicGoSetting.vue b/src/renderer/pages/PicGoSetting.vue index 55732409..05fbad47 100644 --- a/src/renderer/pages/PicGoSetting.vue +++ b/src/renderer/pages/PicGoSetting.vue @@ -1224,37 +1224,70 @@
-
+
-

- {{ t('pages.settings.sync.upDownloadSettings') }} -

+
+ +

+ {{ t('pages.settings.sync.upDownloadSettings') }} +

+
-
- -
+ +
+
+ +

{{ t('pages.settings.sync.uploadSettings') }}

+
+
-
- -
+ + +
+
+ +

{{ t('pages.settings.sync.downloadSettings') }}

+
+
+
+
+ + +
+
+ +

{{ t('pages.settings.sync.galleryDB') }}

+
+
+
@@ -1940,6 +1973,7 @@ const syncTaskList = [ { task: IRPCActionType.CONFIGURE_DOWNLOAD_COMMON_CONFIG, label: t('pages.settings.sync.commonConfig'), number: 2 }, { task: IRPCActionType.CONFIGURE_DOWNLOAD_MANAGE_CONFIG, label: t('pages.settings.sync.manageConfig'), number: 2 }, { task: IRPCActionType.CONFIGURE_DOWNLOAD_ALL_CONFIG, label: t('pages.settings.sync.allConfig'), number: 4 }, + { task: IRPCActionType.CONFIGURE_SYNC_GALLERY_DB, label: t('pages.settings.sync.galleryDB'), number: 2 }, ] async function syncTaskFn(task: string, number: number) { diff --git a/src/renderer/pages/css/PicgoSetting.css b/src/renderer/pages/css/PicgoSetting.css index 40249c71..9298b844 100644 --- a/src/renderer/pages/css/PicgoSetting.css +++ b/src/renderer/pages/css/PicgoSetting.css @@ -964,3 +964,121 @@ small { .rotate { animation: rotate 1s linear infinite; } + +/* Config Dialog Styles */ +.config-dialog { + width: 90%; + max-width: 600px; +} + +.dialog-header-content { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.dialog-icon { + color: var(--color-accent); +} + +.config-section { + border: 1px solid var(--color-border); + border-radius: 12px; + padding: 1.25rem; + background: var(--color-background-secondary); + transition: all 0.2s ease; +} + +.config-section:hover { + border-color: var(--color-accent); + box-shadow: 0 2px 12px rgb(64 158 255 / 15%); +} + +.config-section:not(:last-child) { + margin-bottom: 1rem; +} + +.config-section-header { + display: flex; + align-items: center; + margin-bottom: 1rem; + gap: 0.5rem; +} + +.config-section-header h4 { + margin: 0; + font-size: 0.95rem; + font-weight: 600; + color: var(--color-text-primary); +} + +.config-section-header svg { + color: var(--color-accent); +} + +.config-button-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; +} + +.config-button-grid.full-width { + grid-template-columns: 1fr; +} + +.config-button { + display: flex; + justify-content: center; + align-items: center; + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 0.875rem 1rem; + min-height: 48px; + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text-primary); + background: var(--color-background-primary); + transition: all 0.2s ease; + cursor: pointer; + gap: 0.5rem; + +} + +.config-button:hover { + border-color: var(--color-accent); + color: var(--color-accent); + background: var(--color-background-hover); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgb(64 158 255 / 20%); +} + +.config-button:active { + transform: translateY(0); + box-shadow: 0 2px 6px rgb(64 158 255 / 15%); +} + +.config-button .button-icon { + flex-shrink: 0; + color: var(--color-accent); + transition: transform 0.2s ease; +} + +.config-button:hover .button-icon { + transform: scale(1.1); +} + +.config-button span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (width <= 768px) { + .config-button-grid { + grid-template-columns: 1fr; + } + + .config-dialog { + max-width: 95%; + } +} diff --git a/src/renderer/utils/enum.ts b/src/renderer/utils/enum.ts index 18bd98dd..0f6e29fb 100644 --- a/src/renderer/utils/enum.ts +++ b/src/renderer/utils/enum.ts @@ -73,6 +73,7 @@ export const IRPCActionType = { CONFIGURE_DOWNLOAD_COMMON_CONFIG: 'CONFIGURE_DOWNLOAD_COMMON_CONFIG', CONFIGURE_DOWNLOAD_MANAGE_CONFIG: 'CONFIGURE_DOWNLOAD_MANAGE_CONFIG', CONFIGURE_DOWNLOAD_ALL_CONFIG: 'CONFIGURE_DOWNLOAD_ALL_CONFIG', + CONFIGURE_SYNC_GALLERY_DB: 'CONFIGURE_SYNC_GALLERY_DB', // advanced setting rpc ADVANCED_UPDATE_SERVER: 'ADVANCED_UPDATE_SERVER', diff --git a/yarn.lock b/yarn.lock index de6ee850..94da8da1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7249,7 +7249,7 @@ external-editor@^3.0.3, external-editor@^3.1.0: extract-zip@^2.0.1: version "2.0.1" - resolved "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== dependencies: debug "^4.1.1"