🚧 WIP(custom): support sync gallery db file

ISSUES CLOSED: #355,#417
This commit is contained in:
Kuingsmile
2025-12-30 23:03:52 +08:00
parent 95cc7753b4
commit 6a8d3f6bbf
12 changed files with 475 additions and 101 deletions

View File

@@ -59,6 +59,7 @@
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
"fast-xml-parser": "^5.3.3", "fast-xml-parser": "^5.3.3",
"fflate": "^0.8.2",
"form-data": "^4.0.5", "form-data": "^4.0.5",
"fs-extra": "^11.3.3", "fs-extra": "^11.3.3",
"got": "^14.6.5", "got": "^14.6.5",

View File

@@ -5,7 +5,7 @@ import { app } from 'electron'
import fs from 'fs-extra' import fs from 'fs-extra'
import { IRPCActionType, IRPCType } from '~/utils/enum' 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') const STORE_PATH = app.getPath('userData')
@@ -48,6 +48,13 @@ export default [
}, },
type: IRPCType.INVOKE, type: IRPCType.INVOKE,
}, },
{
action: IRPCActionType.CONFIGURE_SYNC_GALLERY_DB,
handler: async () => {
return await syncGallery()
},
type: IRPCType.INVOKE,
},
{ {
action: IRPCActionType.CONFIGURE_UPLOAD_ALL_CONFIG, action: IRPCActionType.CONFIGURE_UPLOAD_ALL_CONFIG,
handler: async () => { handler: async () => {

View File

@@ -4,6 +4,7 @@ import db from '@core/datastore'
import logger from '@core/picgo/logger' import logger from '@core/picgo/logger'
import axios from 'axios' import axios from 'axios'
import { clipboard, Notification, Tray } from 'electron' import { clipboard, Notification, Tray } from 'electron'
import { gunzipSync, gzipSync, strFromU8 } from 'fflate'
import FormData from 'form-data' import FormData from 'form-data'
import fs from 'fs-extra' import fs from 'fs-extra'
import { isReactive, isRef, toRaw, unref } from 'vue' 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 trimPath = (path: string) => path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/')
export const extractData = async (zipPath: string): Promise<Record<string, any>> => {
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<string, any>, zipPath: string): Promise<void> => {
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')
}
}

View File

@@ -122,6 +122,7 @@ export const IRPCActionType = {
CONFIGURE_MIGRATE_FROM_PICGO: 'CONFIGURE_MIGRATE_FROM_PICGO', CONFIGURE_MIGRATE_FROM_PICGO: 'CONFIGURE_MIGRATE_FROM_PICGO',
CONFIGURE_UPLOAD_COMMON_CONFIG: 'CONFIGURE_UPLOAD_COMMON_CONFIG', CONFIGURE_UPLOAD_COMMON_CONFIG: 'CONFIGURE_UPLOAD_COMMON_CONFIG',
CONFIGURE_UPLOAD_MANAGE_CONFIG: 'CONFIGURE_UPLOAD_MANAGE_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_UPLOAD_ALL_CONFIG: 'CONFIGURE_UPLOAD_ALL_CONFIG',
CONFIGURE_DOWNLOAD_COMMON_CONFIG: 'CONFIGURE_DOWNLOAD_COMMON_CONFIG', CONFIGURE_DOWNLOAD_COMMON_CONFIG: 'CONFIGURE_DOWNLOAD_COMMON_CONFIG',
CONFIGURE_DOWNLOAD_MANAGE_CONFIG: 'CONFIGURE_DOWNLOAD_MANAGE_CONFIG', CONFIGURE_DOWNLOAD_MANAGE_CONFIG: 'CONFIGURE_DOWNLOAD_MANAGE_CONFIG',

View File

@@ -1,3 +1,4 @@
import os from 'node:os'
import path from 'node:path' import path from 'node:path'
import db from '@core/datastore' import db from '@core/datastore'
@@ -10,19 +11,60 @@ import { HttpsProxyAgent } from 'hpagent'
import { AuthType, createClient, WebDAVClientOptions } from 'webdav' import { AuthType, createClient, WebDAVClientOptions } from 'webdav'
import type { ISyncConfig } from '#/types/types' import type { ISyncConfig } from '#/types/types'
import { extractData, zipData } from '~/utils/common'
import { formatEndpoint } from '~/utils/common' import { formatEndpoint } from '~/utils/common'
import { configPaths } from '~/utils/configPaths' import { configPaths } from '~/utils/configPaths'
const STORE_PATH = app.getPath('userData') 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 readFileAsBase64 = (filePath: string) => fs.readFileSync(filePath, { encoding: 'base64' })
const isHttpResSuccess = (res: any) => res.status >= 200 && res.status < 300 const isHttpResSuccess = (res: any) => res.status >= 200 && res.status < 300
const uploadOrUpdateMsg = (fileName: string, isUpdate: boolean = true) => const uploadOrUpdateMsg = (fileName: string, isUpdate: boolean = true) =>
isUpdate ? `update ${fileName} from PicList` : `upload ${fileName} from PicList` isUpdate ? `update ${fileName} from PicList` : `upload ${fileName} from PicList`
const getSyncConfig = () => { const emptyDir = async (): Promise<void> => {
return ( await fs.emptyDir(tempDir)
await fs.emptyDir(db1)
await fs.emptyDir(db2)
await fs.emptyDir(dbMerged)
}
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<string>(Object.keys(db1Data.__gallery_KEY__ || {}))
const db2Ids = new Set<string>(Object.keys(db2Data.__gallery_KEY__ || {}))
const idSet = new Set<string>([...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) || { db.get(configPaths.settings.sync) || {
type: 'github', type: 'github',
username: '', username: '',
@@ -31,11 +73,9 @@ const getSyncConfig = () => {
token: '', token: '',
proxy: '', proxy: '',
} }
)
}
const getProxyagent = (proxy: string | undefined) => { const getProxyagent = (proxy: string | undefined) =>
return proxy proxy
? new HttpsProxyAgent({ ? new HttpsProxyAgent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 1000, keepAliveMsecs: 1000,
@@ -44,17 +84,14 @@ const getProxyagent = (proxy: string | undefined) => {
scheduling: 'lifo', scheduling: 'lifo',
}) })
: undefined : undefined
}
function getOctokit(syncConfig: ISyncConfig) { const getOctokit = (syncConfig: ISyncConfig) =>
const { token, proxy } = syncConfig new Octokit({
return new Octokit({ auth: syncConfig.token,
auth: token,
request: { request: {
agent: getProxyagent(proxy), agent: getProxyagent(syncConfig.proxy),
}, },
}) })
}
const isSyncConfigValidate = ({ const isSyncConfigValidate = ({
type, type,
@@ -85,10 +122,10 @@ const isSyncConfigValidate = ({
async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) { async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
const localFilePath = path.join(STORE_PATH, fileName) const localFilePath = path.join(STORE_PATH, fileName)
if (!fs.existsSync(localFilePath)) { if (!fs.existsSync(localFilePath)) return false
return false
}
const { username, repo, branch, token, type } = syncConfig const { username, repo, branch, token, type } = syncConfig
const defaultConfig = { const defaultConfig = {
content: readFileAsBase64(localFilePath), content: readFileAsBase64(localFilePath),
message: uploadOrUpdateMsg(fileName, false), message: uploadOrUpdateMsg(fileName, false),
@@ -97,8 +134,7 @@ async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
try { try {
switch (type) { switch (type) {
case 'gitee': { case 'gitee': {
const url = `https://gitee.com/api/v5/repos/${username}/${repo}/contents/${fileName}` const res = await axios.post(`https://gitee.com/api/v5/repos/${username}/${repo}/contents/${fileName}`, {
const res = await axios.post(url, {
...defaultConfig, ...defaultConfig,
access_token: token, access_token: token,
}) })
@@ -116,11 +152,15 @@ async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
} }
case 'gitea': { case 'gitea': {
const { endpoint = '' } = syncConfig const { endpoint = '' } = syncConfig
const apiUrl = `${endpoint}/api/v1/repos/${username}/${repo}/contents/${fileName}` const res = await axios.post(
const headers = { `${endpoint}/api/v1/repos/${username}/${repo}/contents/${fileName}`,
defaultConfig,
{
headers: {
Authorization: `token ${token}`, Authorization: `token ${token}`,
} },
const res = await axios.post(apiUrl, defaultConfig, { headers }) },
)
return isHttpResSuccess(res) return isHttpResSuccess(res)
} }
case 'webdav': { case 'webdav': {
@@ -136,9 +176,7 @@ async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
const options: WebDAVClientOptions = { const options: WebDAVClientOptions = {
username: webdavUsername, username: webdavUsername,
password: webdavPassword, password: webdavPassword,
} ...(webdavAuthType === 'digest' ? { authType: AuthType.Digest } : {}),
if (webdavAuthType === 'digest') {
options.authType = AuthType.Digest
} }
const client = createClient(webdavEndpointF, options) const client = createClient(webdavEndpointF, options)
const fileContent = fs.readFileSync(localFilePath) const fileContent = fs.readFileSync(localFilePath)
@@ -161,6 +199,31 @@ async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
} }
} }
async function uploadFile(fileName: string[]): Promise<number> {
const syncConfig = getSyncConfig()
if (!isSyncConfigValidate(syncConfig)) {
logger.error('sync config is invalid')
return 0
}
const uploadFunc = async (file: string): Promise<number> => {
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) { async function updateLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
const localFilePath = path.join(STORE_PATH, fileName) const localFilePath = path.join(STORE_PATH, fileName)
if (!fs.existsSync(localFilePath)) { if (!fs.existsSync(localFilePath)) {
@@ -277,31 +340,6 @@ async function updateLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
} }
} }
async function uploadFile(fileName: string[]): Promise<number> {
const syncConfig = getSyncConfig()
if (!isSyncConfigValidate(syncConfig)) {
logger.error('sync config is invalid')
return 0
}
const uploadFunc = async (file: string): Promise<number> => {
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) { async function downloadAndWriteFile(url: string, localFilePath: string, config: any, isWriteJson = false) {
const res = await axios.get(url, config) const res = await axios.get(url, config)
if (isHttpResSuccess(res)) { if (isHttpResSuccess(res)) {
@@ -314,19 +352,31 @@ async function downloadAndWriteFile(url: string, localFilePath: string, config:
return false return false
} }
async function downloadRemoteToLocal(syncConfig: ISyncConfig, fileName: string) { async function downloadRemoteToLocal(syncConfig: ISyncConfig, fileName: string, galleryMode = false) {
const localFilePath = path.join(STORE_PATH, fileName) const storePath = galleryMode ? db2 : STORE_PATH
const localFilePath = path.join(storePath, fileName)
const { username, repo, branch, token, proxy, type } = syncConfig const { username, repo, branch, token, proxy, type } = syncConfig
try { try {
switch (type) { switch (type) {
case 'gitee': { case 'gitee': {
const url = `https://gitee.com/api/v5/repos/${username}/${repo}/contents/${fileName}` const url = `https://gitee.com/api/v5/repos/${username}/${repo}/contents/${fileName}`
return downloadAndWriteFile(url, localFilePath, { const config = {
params: { params: { access_token: token, ref: branch },
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': { case 'github': {
const octokit = getOctokit(syncConfig) const octokit = getOctokit(syncConfig)
@@ -339,6 +389,18 @@ async function downloadRemoteToLocal(syncConfig: ISyncConfig, fileName: string)
if (res.status === 200) { if (res.status === 200) {
const data = res.data as any const data = res.data as any
const downloadUrl = data.download_url const downloadUrl = data.download_url
if (galleryMode) {
const res = await axios.get(downloadUrl, {
httpsAgent: getProxyagent(proxy),
responseType: 'arraybuffer',
})
if (isHttpResSuccess(res)) {
await fs.writeFile(localFilePath, res.data)
return true
} else {
return false
}
} else {
return downloadAndWriteFile( return downloadAndWriteFile(
downloadUrl, downloadUrl,
localFilePath, localFilePath,
@@ -348,10 +410,28 @@ async function downloadRemoteToLocal(syncConfig: ISyncConfig, fileName: string)
true, true,
) )
} }
}
return false return false
} }
case 'gitea': { case 'gitea': {
const { endpoint = '' } = syncConfig 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}` const apiUrl = `${endpoint}/api/v1/repos/${username}/${repo}/contents/${fileName}`
return downloadAndWriteFile(apiUrl, localFilePath, { return downloadAndWriteFile(apiUrl, localFilePath, {
headers: { headers: {
@@ -362,6 +442,7 @@ async function downloadRemoteToLocal(syncConfig: ISyncConfig, fileName: string)
}, },
}) })
} }
}
case 'webdav': { case 'webdav': {
const { const {
webdavEndpoint = '', webdavEndpoint = '',
@@ -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<number> { async function downloadFile(fileName: string[]): Promise<number> {
const syncConfig = getSyncConfig() const syncConfig = getSyncConfig()
if (!isSyncConfigValidate(syncConfig)) { if (!isSyncConfigValidate(syncConfig)) {
@@ -410,4 +567,35 @@ async function downloadFile(fileName: string[]): Promise<number> {
return (await Promise.all(fileName.map(downloadFunc))).reduce((a, b) => a + b, 0) return (await Promise.all(fileName.map(downloadFunc))).reduce((a, b) => a + b, 0)
} }
export { downloadFile, uploadFile } async function syncGallery(): Promise<number> {
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 }

View File

@@ -685,6 +685,7 @@
"commonConfig": "Common Configuration", "commonConfig": "Common Configuration",
"downloadSettings": "Download Settings", "downloadSettings": "Download Settings",
"fileManagement": "File Management", "fileManagement": "File Management",
"galleryDB": "Gallery Database Sync",
"gitea": { "gitea": {
"branch": "Branch Name", "branch": "Branch Name",
"repo": "Repository Name", "repo": "Repository Name",

View File

@@ -685,6 +685,7 @@
"commonConfig": "通用配置", "commonConfig": "通用配置",
"downloadSettings": "下载配置", "downloadSettings": "下载配置",
"fileManagement": "文件管理", "fileManagement": "文件管理",
"galleryDB": "相册数据库同步",
"gitea": { "branch": "分支名", "repo": "仓库名", "token": "访问令牌", "username": "用户名" }, "gitea": { "branch": "分支名", "repo": "仓库名", "token": "访问令牌", "username": "用户名" },
"giteaHost": "Gitea 地址", "giteaHost": "Gitea 地址",
"gitee": { "gitee": {

View File

@@ -685,6 +685,7 @@
"commonConfig": "通用配置", "commonConfig": "通用配置",
"downloadSettings": "下載配置", "downloadSettings": "下載配置",
"fileManagement": "文件管理", "fileManagement": "文件管理",
"galleryDB": "相冊數據庫同步",
"gitea": { "branch": "分支名", "repo": "倉庫名", "token": "訪問令牌", "username": "用戶名" }, "gitea": { "branch": "分支名", "repo": "倉庫名", "token": "訪問令牌", "username": "用戶名" },
"giteaHost": "Gitea 地址", "giteaHost": "Gitea 地址",
"gitee": { "gitee": {

View File

@@ -1224,37 +1224,70 @@
<!-- Upload/Download Config Dialog --> <!-- Upload/Download Config Dialog -->
<div v-if="upDownConfigVisible" class="dialog-overlay" @click="upDownConfigVisible = false"> <div v-if="upDownConfigVisible" class="dialog-overlay" @click="upDownConfigVisible = false">
<div class="dialog" @click.stop> <div class="dialog config-dialog" @click.stop>
<div class="dialog-header"> <div class="dialog-header">
<div class="dialog-header-content">
<RotateCcw :size="20" class="dialog-icon" />
<h3 class="dialog-title"> <h3 class="dialog-title">
{{ t('pages.settings.sync.upDownloadSettings') }} {{ t('pages.settings.sync.upDownloadSettings') }}
</h3> </h3>
</div>
<button class="dialog-close" @click="upDownConfigVisible = false">×</button> <button class="dialog-close" @click="upDownConfigVisible = false">×</button>
</div> </div>
<div class="dialog-content"> <div class="dialog-content">
<div class="form-group"> <!-- Upload Settings Section -->
<label>{{ t('pages.settings.sync.uploadSettings') }}</label> <div class="config-section">
<div class="button-group"> <div class="config-section-header">
<CloudUpload :size="18" />
<h4>{{ t('pages.settings.sync.uploadSettings') }}</h4>
</div>
<div class="config-button-grid">
<button <button
v-for="item in syncTaskList.slice(0, 3)" v-for="item in syncTaskList.slice(0, 3)"
:key="item.task" :key="item.task"
class="btn btn-primary" class="config-button"
@click="syncTaskFn(item.task, item.number)" @click="syncTaskFn(item.task, item.number)"
> >
{{ item.label }} <Import :size="16" class="button-icon" />
<span>{{ item.label }}</span>
</button> </button>
</div> </div>
</div> </div>
<div class="form-group">
<label>{{ t('pages.settings.sync.downloadSettings') }}</label> <!-- Download Settings Section -->
<div class="button-group"> <div class="config-section">
<div class="config-section-header">
<Download :size="18" />
<h4>{{ t('pages.settings.sync.downloadSettings') }}</h4>
</div>
<div class="config-button-grid">
<button <button
v-for="item in syncTaskList.slice(3)" v-for="item in syncTaskList.slice(3, 6)"
:key="item.task" :key="item.task"
class="btn btn-primary" class="config-button"
@click="syncTaskFn(item.task, item.number)" @click="syncTaskFn(item.task, item.number)"
> >
{{ item.label }} <Download :size="16" class="button-icon" />
<span>{{ item.label }}</span>
</button>
</div>
</div>
<!-- Gallery DB Section -->
<div class="config-section">
<div class="config-section-header">
<ImageIcon :size="18" />
<h4>{{ t('pages.settings.sync.galleryDB') }}</h4>
</div>
<div class="config-button-grid full-width">
<button
v-for="item in syncTaskList.slice(6, 7)"
:key="item.task"
class="config-button"
@click="syncTaskFn(item.task, item.number)"
>
<RefreshCw :size="16" class="button-icon" />
<span>{{ item.label }}</span>
</button> </button>
</div> </div>
</div> </div>
@@ -1940,6 +1973,7 @@ const syncTaskList = [
{ task: IRPCActionType.CONFIGURE_DOWNLOAD_COMMON_CONFIG, label: t('pages.settings.sync.commonConfig'), number: 2 }, { 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_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_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) { async function syncTaskFn(task: string, number: number) {

View File

@@ -964,3 +964,121 @@ small {
.rotate { .rotate {
animation: rotate 1s linear infinite; 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%;
}
}

View File

@@ -73,6 +73,7 @@ export const IRPCActionType = {
CONFIGURE_DOWNLOAD_COMMON_CONFIG: 'CONFIGURE_DOWNLOAD_COMMON_CONFIG', CONFIGURE_DOWNLOAD_COMMON_CONFIG: 'CONFIGURE_DOWNLOAD_COMMON_CONFIG',
CONFIGURE_DOWNLOAD_MANAGE_CONFIG: 'CONFIGURE_DOWNLOAD_MANAGE_CONFIG', CONFIGURE_DOWNLOAD_MANAGE_CONFIG: 'CONFIGURE_DOWNLOAD_MANAGE_CONFIG',
CONFIGURE_DOWNLOAD_ALL_CONFIG: 'CONFIGURE_DOWNLOAD_ALL_CONFIG', CONFIGURE_DOWNLOAD_ALL_CONFIG: 'CONFIGURE_DOWNLOAD_ALL_CONFIG',
CONFIGURE_SYNC_GALLERY_DB: 'CONFIGURE_SYNC_GALLERY_DB',
// advanced setting rpc // advanced setting rpc
ADVANCED_UPDATE_SERVER: 'ADVANCED_UPDATE_SERVER', ADVANCED_UPDATE_SERVER: 'ADVANCED_UPDATE_SERVER',

View File

@@ -7249,7 +7249,7 @@ external-editor@^3.0.3, external-editor@^3.1.0:
extract-zip@^2.0.1: extract-zip@^2.0.1:
version "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== integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
dependencies: dependencies:
debug "^4.1.1" debug "^4.1.1"