mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-06 20:42:57 +08:00
🚧 WIP(custom): support sync gallery db file
ISSUES CLOSED: #355,#417
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<void> => {
|
||||
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<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) || {
|
||||
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<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) {
|
||||
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<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) {
|
||||
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<number> {
|
||||
const syncConfig = getSyncConfig()
|
||||
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)
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
@@ -685,6 +685,7 @@
|
||||
"commonConfig": "Common Configuration",
|
||||
"downloadSettings": "Download Settings",
|
||||
"fileManagement": "File Management",
|
||||
"galleryDB": "Gallery Database Sync",
|
||||
"gitea": {
|
||||
"branch": "Branch Name",
|
||||
"repo": "Repository Name",
|
||||
|
||||
@@ -685,6 +685,7 @@
|
||||
"commonConfig": "通用配置",
|
||||
"downloadSettings": "下载配置",
|
||||
"fileManagement": "文件管理",
|
||||
"galleryDB": "相册数据库同步",
|
||||
"gitea": { "branch": "分支名", "repo": "仓库名", "token": "访问令牌", "username": "用户名" },
|
||||
"giteaHost": "Gitea 地址",
|
||||
"gitee": {
|
||||
|
||||
@@ -685,6 +685,7 @@
|
||||
"commonConfig": "通用配置",
|
||||
"downloadSettings": "下載配置",
|
||||
"fileManagement": "文件管理",
|
||||
"galleryDB": "相冊數據庫同步",
|
||||
"gitea": { "branch": "分支名", "repo": "倉庫名", "token": "訪問令牌", "username": "用戶名" },
|
||||
"giteaHost": "Gitea 地址",
|
||||
"gitee": {
|
||||
|
||||
@@ -1224,37 +1224,70 @@
|
||||
|
||||
<!-- Upload/Download Config Dialog -->
|
||||
<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">
|
||||
<h3 class="dialog-title">
|
||||
{{ t('pages.settings.sync.upDownloadSettings') }}
|
||||
</h3>
|
||||
<div class="dialog-header-content">
|
||||
<RotateCcw :size="20" class="dialog-icon" />
|
||||
<h3 class="dialog-title">
|
||||
{{ t('pages.settings.sync.upDownloadSettings') }}
|
||||
</h3>
|
||||
</div>
|
||||
<button class="dialog-close" @click="upDownConfigVisible = false">×</button>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<div class="form-group">
|
||||
<label>{{ t('pages.settings.sync.uploadSettings') }}</label>
|
||||
<div class="button-group">
|
||||
<!-- Upload Settings Section -->
|
||||
<div class="config-section">
|
||||
<div class="config-section-header">
|
||||
<CloudUpload :size="18" />
|
||||
<h4>{{ t('pages.settings.sync.uploadSettings') }}</h4>
|
||||
</div>
|
||||
<div class="config-button-grid">
|
||||
<button
|
||||
v-for="item in syncTaskList.slice(0, 3)"
|
||||
:key="item.task"
|
||||
class="btn btn-primary"
|
||||
class="config-button"
|
||||
@click="syncTaskFn(item.task, item.number)"
|
||||
>
|
||||
{{ item.label }}
|
||||
<Import :size="16" class="button-icon" />
|
||||
<span>{{ item.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ t('pages.settings.sync.downloadSettings') }}</label>
|
||||
<div class="button-group">
|
||||
|
||||
<!-- Download Settings Section -->
|
||||
<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
|
||||
v-for="item in syncTaskList.slice(3)"
|
||||
v-for="item in syncTaskList.slice(3, 6)"
|
||||
:key="item.task"
|
||||
class="btn btn-primary"
|
||||
class="config-button"
|
||||
@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>
|
||||
</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_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) {
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user