feat: add webdav settings component and backup user data files #69
This commit is contained in:
parent
2e1b433365
commit
33b83bf242
@ -40,7 +40,7 @@
|
|||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"unzipper": "^0.12.3",
|
"unzipper": "^0.12.3",
|
||||||
"webdav": "^5.7.1"
|
"webdav": "4.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.24.3",
|
"@anthropic-ai/sdk": "^0.24.3",
|
||||||
|
|||||||
@ -29,8 +29,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
|
|
||||||
ipcMain.handle('reload', () => mainWindow.reload())
|
ipcMain.handle('reload', () => mainWindow.reload())
|
||||||
|
|
||||||
ipcMain.handle('backup:save', backupManager.backup)
|
ipcMain.handle('backup:backup', backupManager.backup)
|
||||||
ipcMain.handle('backup:restore', backupManager.restore)
|
ipcMain.handle('backup:restore', backupManager.restore)
|
||||||
|
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
|
||||||
|
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
|
||||||
|
|
||||||
ipcMain.handle('file:open', fileManager.open)
|
ipcMain.handle('file:open', fileManager.open)
|
||||||
ipcMain.handle('file:save', fileManager.save)
|
ipcMain.handle('file:save', fileManager.save)
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { WebDavConfig } from '@types'
|
||||||
import archiver from 'archiver'
|
import archiver from 'archiver'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
@ -5,16 +6,25 @@ import * as fs from 'fs-extra'
|
|||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as unzipper from 'unzipper'
|
import * as unzipper from 'unzipper'
|
||||||
|
|
||||||
|
import WebDav from './WebDav'
|
||||||
|
|
||||||
class BackupManager {
|
class BackupManager {
|
||||||
private tempDir: string
|
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
|
||||||
|
private backupDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup')
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.tempDir = path.join(app.getPath('temp'), 'CherryStudio', 'backup')
|
|
||||||
this.backup = this.backup.bind(this)
|
this.backup = this.backup.bind(this)
|
||||||
this.restore = this.restore.bind(this)
|
this.restore = this.restore.bind(this)
|
||||||
|
this.backupToWebdav = this.backupToWebdav.bind(this)
|
||||||
|
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
async backup(_: Electron.IpcMainInvokeEvent, data: string, fileName: string, destinationPath: string): Promise<void> {
|
async backup(
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
fileName: string,
|
||||||
|
data: string,
|
||||||
|
destinationPath: string = this.backupDir
|
||||||
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
// 创建临时目录
|
// 创建临时目录
|
||||||
await fs.ensureDir(this.tempDir)
|
await fs.ensureDir(this.tempDir)
|
||||||
@ -29,7 +39,7 @@ class BackupManager {
|
|||||||
await fs.copy(sourcePath, tempDataDir)
|
await fs.copy(sourcePath, tempDataDir)
|
||||||
|
|
||||||
// 创建 zip 文件
|
// 创建 zip 文件
|
||||||
const output = fs.createWriteStream(path.join(destinationPath, `${fileName}.zip`))
|
const output = fs.createWriteStream(path.join(destinationPath, fileName))
|
||||||
const archive = archiver('zip', { zlib: { level: 9 } })
|
const archive = archiver('zip', { zlib: { level: 9 } })
|
||||||
|
|
||||||
archive.pipe(output)
|
archive.pipe(output)
|
||||||
@ -40,42 +50,60 @@ class BackupManager {
|
|||||||
await fs.remove(this.tempDir)
|
await fs.remove(this.tempDir)
|
||||||
|
|
||||||
Logger.log('Backup completed successfully')
|
Logger.log('Backup completed successfully')
|
||||||
|
|
||||||
|
const backupedFilePath = path.join(destinationPath, fileName)
|
||||||
|
|
||||||
|
return backupedFilePath
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error('Backup failed:', error)
|
Logger.error('Backup failed:', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<{ data: string; success: boolean }> {
|
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
|
||||||
try {
|
// 创建临时目录
|
||||||
// 创建临时目录
|
await fs.ensureDir(this.tempDir)
|
||||||
await fs.ensureDir(this.tempDir)
|
|
||||||
|
|
||||||
// 解压备份文件到临时目录
|
// 解压备份文件到临时目录
|
||||||
await fs
|
await fs
|
||||||
.createReadStream(backupPath)
|
.createReadStream(backupPath)
|
||||||
.pipe(unzipper.Extract({ path: this.tempDir }))
|
.pipe(unzipper.Extract({ path: this.tempDir }))
|
||||||
.promise()
|
.promise()
|
||||||
|
|
||||||
// 读取 data.json
|
// 读取 data.json
|
||||||
const dataPath = path.join(this.tempDir, 'data.json')
|
const dataPath = path.join(this.tempDir, 'data.json')
|
||||||
const data = await fs.readFile(dataPath, 'utf-8')
|
const data = await fs.readFile(dataPath, 'utf-8')
|
||||||
|
|
||||||
// 恢复 Data 目录
|
// 恢复 Data 目录
|
||||||
const sourcePath = path.join(this.tempDir, 'Data')
|
const sourcePath = path.join(this.tempDir, 'Data')
|
||||||
const destPath = path.join(app.getPath('userData'), 'Data')
|
const destPath = path.join(app.getPath('userData'), 'Data')
|
||||||
await fs.remove(destPath)
|
await fs.remove(destPath)
|
||||||
await fs.copy(sourcePath, destPath)
|
await fs.copy(sourcePath, destPath)
|
||||||
|
|
||||||
// 清理临时目录
|
// 清理临时目录
|
||||||
await fs.remove(this.tempDir)
|
await fs.remove(this.tempDir)
|
||||||
|
|
||||||
Logger.log('Restore completed successfully')
|
Logger.log('Restore completed successfully')
|
||||||
return { data, success: true }
|
|
||||||
} catch (error) {
|
return data
|
||||||
Logger.error('Restore failed:', error)
|
}
|
||||||
return { data: '', success: false }
|
|
||||||
}
|
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
|
||||||
|
const filename = 'cherry-studio.backup.zip'
|
||||||
|
const backupedFilePath = await this.backup(_, filename, data)
|
||||||
|
const webdavClient = new WebDav(webdavConfig)
|
||||||
|
return await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
|
||||||
|
overwrite: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
|
||||||
|
const filename = 'cherry-studio.backup.zip'
|
||||||
|
const webdavClient = new WebDav(webdavConfig)
|
||||||
|
const retrievedFile = await webdavClient.getFileContents(filename)
|
||||||
|
const backupedFilePath = path.join(this.backupDir, filename)
|
||||||
|
await fs.writeFileSync(backupedFilePath, retrievedFile as Buffer)
|
||||||
|
return await this.restore(_, backupedFilePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
66
src/main/services/WebDav.ts
Normal file
66
src/main/services/WebDav.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { WebDavConfig } from '@types'
|
||||||
|
import Logger from 'electron-log'
|
||||||
|
import Stream from 'stream'
|
||||||
|
import { BufferLike, createClient, GetFileContentsOptions, PutFileContentsOptions, WebDAVClient } from 'webdav'
|
||||||
|
|
||||||
|
export default class WebDav {
|
||||||
|
public instance: WebDAVClient | undefined
|
||||||
|
private webdavPath: string
|
||||||
|
|
||||||
|
constructor(params: WebDavConfig) {
|
||||||
|
this.webdavPath = params.webdavPath.replace('/', '')
|
||||||
|
|
||||||
|
this.instance = createClient(params.webdavHost, {
|
||||||
|
username: params.webdavUser,
|
||||||
|
password: params.webdavPass
|
||||||
|
})
|
||||||
|
|
||||||
|
this.putFileContents = this.putFileContents.bind(this)
|
||||||
|
this.getFileContents = this.getFileContents.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
public putFileContents = async (
|
||||||
|
filename: string,
|
||||||
|
data: string | BufferLike | Stream.Readable,
|
||||||
|
options?: PutFileContentsOptions
|
||||||
|
) => {
|
||||||
|
if (!this.instance) {
|
||||||
|
return new Error('WebDAV client not initialized')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!(await this.instance.exists(this.webdavPath))) {
|
||||||
|
await this.instance.createDirectory(this.webdavPath, {
|
||||||
|
recursive: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[WebDAV] Error creating directory on WebDAV:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteFilePath = `${this.webdavPath}/${filename}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.instance.putFileContents(remoteFilePath, data, options)
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[WebDAV] Error putting file contents on WebDAV:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFileContents = async (filename: string, options?: GetFileContentsOptions) => {
|
||||||
|
if (!this.instance) {
|
||||||
|
throw new Error('WebDAV client not initialized')
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteFilePath = `${this.webdavPath}/${filename}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.instance.getFileContents(remoteFilePath, options)
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[WebDAV] Error getting file contents on WebDAV:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/preload/index.d.ts
vendored
8
src/preload/index.d.ts
vendored
@ -1,6 +1,8 @@
|
|||||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||||
import { FileType } from '@renderer/types'
|
import { FileType } from '@renderer/types'
|
||||||
|
import { WebDavConfig } from '@renderer/types'
|
||||||
import type { OpenDialogOptions } from 'electron'
|
import type { OpenDialogOptions } from 'electron'
|
||||||
|
import { Readable } from 'stream'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -20,8 +22,10 @@ declare global {
|
|||||||
compress: (text: string) => Promise<Buffer>
|
compress: (text: string) => Promise<Buffer>
|
||||||
decompress: (text: Buffer) => Promise<string>
|
decompress: (text: Buffer) => Promise<string>
|
||||||
backup: {
|
backup: {
|
||||||
save: (data: string, fileName: string, destinationPath: string) => Promise<void>
|
backup: (fileName: string, data: string, destinationPath?: string) => Promise<Readable>
|
||||||
restore: (backupPath: string) => Promise<{ data: string; success: boolean }>
|
restore: (backupPath: string) => Promise<string>
|
||||||
|
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise<boolean>
|
||||||
|
restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise<string>
|
||||||
}
|
}
|
||||||
file: {
|
file: {
|
||||||
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { electronAPI } from '@electron-toolkit/preload'
|
import { electronAPI } from '@electron-toolkit/preload'
|
||||||
|
import { WebDavConfig } from '@types'
|
||||||
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
@ -11,10 +12,12 @@ const api = {
|
|||||||
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
|
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
|
||||||
reload: () => ipcRenderer.invoke('reload'),
|
reload: () => ipcRenderer.invoke('reload'),
|
||||||
backup: {
|
backup: {
|
||||||
save: (data: string, fileName: string, destinationPath: string) => {
|
backup: (fileName: string, data: string, destinationPath?: string) =>
|
||||||
ipcRenderer.invoke('backup:save', data, fileName, destinationPath)
|
ipcRenderer.invoke('backup:backup', fileName, data, destinationPath),
|
||||||
},
|
restore: (backupPath: string) => ipcRenderer.invoke('backup:restore', backupPath),
|
||||||
restore: (backupPath: string) => ipcRenderer.invoke('backup:restore', backupPath)
|
backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
|
||||||
|
ipcRenderer.invoke('backup:backupToWebdav', data, webdavConfig),
|
||||||
|
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig)
|
||||||
},
|
},
|
||||||
file: {
|
file: {
|
||||||
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
||||||
|
|||||||
@ -46,12 +46,14 @@
|
|||||||
"error.enter.api.host": "Please enter your API host first",
|
"error.enter.api.host": "Please enter your API host first",
|
||||||
"error.enter.model": "Please select a model first",
|
"error.enter.model": "Please select a model first",
|
||||||
"error.invalid.proxy.url": "Invalid proxy URL",
|
"error.invalid.proxy.url": "Invalid proxy URL",
|
||||||
|
"error.invalid.webdav": "Invalid WebDAV settings",
|
||||||
"api.connection.failed": "Connection failed",
|
"api.connection.failed": "Connection failed",
|
||||||
"api.connection.success": "Connection successful",
|
"api.connection.success": "Connection successful",
|
||||||
"chat.completion.paused": "Chat completion paused",
|
"chat.completion.paused": "Chat completion paused",
|
||||||
"switch.disabled": "Switching is disabled while the assistant is generating",
|
"switch.disabled": "Switching is disabled while the assistant is generating",
|
||||||
"restore.success": "Restored successfully",
|
"restore.success": "Restored successfully",
|
||||||
"backup.success": "Backup successful",
|
"backup.success": "Backup successful",
|
||||||
|
"backup.failed": "Backup failed",
|
||||||
"reset.confirm.content": "Are you sure you want to clear all data?",
|
"reset.confirm.content": "Are you sure you want to clear all data?",
|
||||||
"reset.double.confirm.title": "DATA LOST !!!",
|
"reset.double.confirm.title": "DATA LOST !!!",
|
||||||
"reset.double.confirm.content": "All data will be lost, do you want to continue?",
|
"reset.double.confirm.content": "All data will be lost, do you want to continue?",
|
||||||
@ -180,11 +182,14 @@
|
|||||||
"general.backup.title": "Data Backup and Recovery",
|
"general.backup.title": "Data Backup and Recovery",
|
||||||
"general.backup.button": "Backup",
|
"general.backup.button": "Backup",
|
||||||
"general.restore.button": "Restore",
|
"general.restore.button": "Restore",
|
||||||
|
"general.view_webdav_settings": "View WebDAV settings",
|
||||||
"general.webdav.title": "WebDAV",
|
"general.webdav.title": "WebDAV",
|
||||||
"general.webdav.host": "WebDAV Host, e.g. http://localhost:8080",
|
"general.webdav.host": "WebDAV Host",
|
||||||
|
"general.webdav.host.placeholder": "http://localhost:8080",
|
||||||
"general.webdav.user": "WebDAV User",
|
"general.webdav.user": "WebDAV User",
|
||||||
"general.webdav.password": "WebDAV Password",
|
"general.webdav.password": "WebDAV Password",
|
||||||
"general.webdav.path": "WebDAV Path, e.g. /backup",
|
"general.webdav.path": "WebDAV Path",
|
||||||
|
"general.webdav.path.placeholder": "/backup",
|
||||||
"general.webdav.backup.button": "Backup to WebDAV",
|
"general.webdav.backup.button": "Backup to WebDAV",
|
||||||
"general.webdav.restore.button": "Restore from WebDAV",
|
"general.webdav.restore.button": "Restore from WebDAV",
|
||||||
"general.reset.title": "Data Reset",
|
"general.reset.title": "Data Reset",
|
||||||
|
|||||||
@ -46,12 +46,14 @@
|
|||||||
"error.enter.api.host": "请输入您的 API 地址",
|
"error.enter.api.host": "请输入您的 API 地址",
|
||||||
"error.enter.model": "请选择一个模型",
|
"error.enter.model": "请选择一个模型",
|
||||||
"error.invalid.proxy.url": "无效的代理地址",
|
"error.invalid.proxy.url": "无效的代理地址",
|
||||||
|
"error.invalid.webdav": "无效的 WebDAV 设置",
|
||||||
"api.connection.failed": "连接失败",
|
"api.connection.failed": "连接失败",
|
||||||
"api.connection.success": "连接成功",
|
"api.connection.success": "连接成功",
|
||||||
"chat.completion.paused": "会话已停止",
|
"chat.completion.paused": "会话已停止",
|
||||||
"switch.disabled": "模型回复完成后才能切换",
|
"switch.disabled": "模型回复完成后才能切换",
|
||||||
"restore.success": "恢复成功",
|
"restore.success": "恢复成功",
|
||||||
"backup.success": "备份成功",
|
"backup.success": "备份成功",
|
||||||
|
"backup.failed": "备份失败",
|
||||||
"reset.confirm.content": "确定要重置所有数据吗?",
|
"reset.confirm.content": "确定要重置所有数据吗?",
|
||||||
"reset.double.confirm.title": "数据丢失!!!",
|
"reset.double.confirm.title": "数据丢失!!!",
|
||||||
"reset.double.confirm.content": "你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?",
|
"reset.double.confirm.content": "你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?",
|
||||||
@ -182,11 +184,14 @@
|
|||||||
"general.restore.button": "恢复",
|
"general.restore.button": "恢复",
|
||||||
"general.reset.title": "重置数据",
|
"general.reset.title": "重置数据",
|
||||||
"general.reset.button": "重置",
|
"general.reset.button": "重置",
|
||||||
|
"general.view_webdav_settings": "查看 WebDAV 设置",
|
||||||
"general.webdav.title": "WebDAV",
|
"general.webdav.title": "WebDAV",
|
||||||
"general.webdav.host": "WebDAV Host, e.g. http://localhost:8080",
|
"general.webdav.host": "WebDAV 地址",
|
||||||
"general.webdav.user": "WebDAV User",
|
"general.webdav.host.placeholder": "http://localhost:8080",
|
||||||
"general.webdav.password": "WebDAV Password",
|
"general.webdav.user": "WebDAV 用户名",
|
||||||
"general.webdav.path": "WebDAV Path, e.g. /backup",
|
"general.webdav.password": "WebDAV 密码",
|
||||||
|
"general.webdav.path": "WebDAV 路径",
|
||||||
|
"general.webdav.path.placeholder": "/backup",
|
||||||
"general.webdav.backup.button": "备份到 WebDAV",
|
"general.webdav.backup.button": "备份到 WebDAV",
|
||||||
"general.webdav.restore.button": "从 WebDAV 恢复",
|
"general.webdav.restore.button": "从 WebDAV 恢复",
|
||||||
"general.check_update_setting": "更新设置",
|
"general.check_update_setting": "更新设置",
|
||||||
|
|||||||
@ -46,12 +46,14 @@
|
|||||||
"error.enter.api.host": "請先輸入您的 API 主機地址",
|
"error.enter.api.host": "請先輸入您的 API 主機地址",
|
||||||
"error.enter.model": "請先選擇一個模型",
|
"error.enter.model": "請先選擇一個模型",
|
||||||
"error.invalid.proxy.url": "無效的代理 URL",
|
"error.invalid.proxy.url": "無效的代理 URL",
|
||||||
|
"error.invalid.webdav": "無效的 WebDAV 設定",
|
||||||
"api.connection.failed": "連接失敗",
|
"api.connection.failed": "連接失敗",
|
||||||
"api.connection.success": "連接成功",
|
"api.connection.success": "連接成功",
|
||||||
"chat.completion.paused": "聊天完成已暫停",
|
"chat.completion.paused": "聊天完成已暫停",
|
||||||
"switch.disabled": "助手生成回覆時無法切換",
|
"switch.disabled": "助手生成回覆時無法切換",
|
||||||
"restore.success": "恢復成功",
|
"restore.success": "恢復成功",
|
||||||
"backup.success": "備份成功",
|
"backup.success": "備份成功",
|
||||||
|
"backup.failed": "備份失敗",
|
||||||
"reset.confirm.content": "確定要清除所有資料嗎?",
|
"reset.confirm.content": "確定要清除所有資料嗎?",
|
||||||
"reset.double.confirm.title": "資料將會丟失!!!",
|
"reset.double.confirm.title": "資料將會丟失!!!",
|
||||||
"reset.double.confirm.content": "所有資料將會被清除,您確定要繼續嗎?",
|
"reset.double.confirm.content": "所有資料將會被清除,您確定要繼續嗎?",
|
||||||
@ -180,11 +182,14 @@
|
|||||||
"general.backup.title": "資料備份與復原",
|
"general.backup.title": "資料備份與復原",
|
||||||
"general.backup.button": "備份",
|
"general.backup.button": "備份",
|
||||||
"general.restore.button": "復原",
|
"general.restore.button": "復原",
|
||||||
|
"general.view_webdav_settings": "查看 WebDAV 設定",
|
||||||
"general.webdav.title": "WebDAV",
|
"general.webdav.title": "WebDAV",
|
||||||
"general.webdav.host": "WebDAV Host, e.g. http://localhost:8080",
|
"general.webdav.host": "WebDAV 主機位址",
|
||||||
"general.webdav.user": "WebDAV User",
|
"general.webdav.host.placeholder": "http://localhost:8080",
|
||||||
"general.webdav.password": "WebDAV Password",
|
"general.webdav.user": "WebDAV 使用者名稱",
|
||||||
"general.webdav.path": "WebDAV Path, e.g. /backup",
|
"general.webdav.password": "WebDAV 密碼",
|
||||||
|
"general.webdav.path": "WebDAV Path",
|
||||||
|
"general.webdav.path.placeholder": "/backup",
|
||||||
"general.webdav.backup.button": "從 WebDAV 備份",
|
"general.webdav.backup.button": "從 WebDAV 備份",
|
||||||
"general.webdav.restore.button": "從 WebDAV 恢復",
|
"general.webdav.restore.button": "從 WebDAV 恢復",
|
||||||
"general.reset.title": "資料重置",
|
"general.reset.title": "資料重置",
|
||||||
|
|||||||
@ -1,248 +0,0 @@
|
|||||||
import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
|
|
||||||
import { HStack } from '@renderer/components/Layout'
|
|
||||||
import { isMac } from '@renderer/config/constant'
|
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
|
||||||
import i18n from '@renderer/i18n'
|
|
||||||
import { backup, reset, restore, backupToWebdav, restoreFromWebdav } from '@renderer/services/backup'
|
|
||||||
import { useAppDispatch } from '@renderer/store'
|
|
||||||
import { setClickAssistantToShowTopic, setLanguage, setManualUpdateCheck } from '@renderer/store/settings'
|
|
||||||
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
|
|
||||||
import {
|
|
||||||
setWebdavHost as _setWebdavHost,
|
|
||||||
setWebdavPass as _setWebdavPass,
|
|
||||||
setWebdavPath as _setWebdavPath,
|
|
||||||
setWebdavUser as _setWebdavUser
|
|
||||||
} from '@renderer/store/settings'
|
|
||||||
import { ThemeMode } from '@renderer/types'
|
|
||||||
import { isValidProxyUrl } from '@renderer/utils'
|
|
||||||
import { Button, Input, Select, Switch } from 'antd'
|
|
||||||
import { FC, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '.'
|
|
||||||
|
|
||||||
const GeneralSettings: FC = () => {
|
|
||||||
const {
|
|
||||||
language,
|
|
||||||
proxyUrl: storeProxyUrl,
|
|
||||||
theme,
|
|
||||||
windowStyle,
|
|
||||||
topicPosition,
|
|
||||||
clickAssistantToShowTopic,
|
|
||||||
manualUpdateCheck,
|
|
||||||
setTheme,
|
|
||||||
setWindowStyle,
|
|
||||||
setTopicPosition,
|
|
||||||
|
|
||||||
webdavHost: webDAVHost,
|
|
||||||
webdavUser: webDAVUser,
|
|
||||||
webdavPass: webDAVPass,
|
|
||||||
webdavPath: webDAVPath
|
|
||||||
} = useSettings()
|
|
||||||
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
|
|
||||||
const [webdavHost, setWebdavHost] = useState<string | undefined>(webDAVHost)
|
|
||||||
const [webdavUser, setWebdavUser] = useState<string | undefined>(webDAVUser)
|
|
||||||
const [webdavPass, setWebdavPass] = useState<string | undefined>(webDAVPass)
|
|
||||||
const [webdavPath, setWebdavPath] = useState<string | undefined>(webDAVPath)
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const onSelectLanguage = (value: string) => {
|
|
||||||
dispatch(setLanguage(value))
|
|
||||||
localStorage.setItem('language', value)
|
|
||||||
i18n.changeLanguage(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSetProxyUrl = () => {
|
|
||||||
if (proxyUrl && !isValidProxyUrl(proxyUrl)) {
|
|
||||||
window.message.error({ content: t('message.error.invalid.proxy.url'), key: 'proxy-error' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(_setProxyUrl(proxyUrl))
|
|
||||||
window.api.setProxy(proxyUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSetWebdav = () => {
|
|
||||||
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
|
|
||||||
window.message.error({ content: t('message.error.invalid.webdav'), key: 'webdav-error' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('webdav', webdavHost, webdavUser, webdavPass, webdavPath)
|
|
||||||
|
|
||||||
dispatch(_setWebdavHost(webdavHost))
|
|
||||||
dispatch(_setWebdavUser(webdavUser))
|
|
||||||
dispatch(_setWebdavPass(webdavPass))
|
|
||||||
dispatch(_setWebdavPath(webdavPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingContainer>
|
|
||||||
<SettingTitle>{t('settings.general.title')}</SettingTitle>
|
|
||||||
<SettingDivider />
|
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>{t('common.language')}</SettingRowTitle>
|
|
||||||
<Select
|
|
||||||
defaultValue={language || 'en-US'}
|
|
||||||
style={{ width: 180 }}
|
|
||||||
onChange={onSelectLanguage}
|
|
||||||
options={[
|
|
||||||
{ value: 'zh-CN', label: '中文' },
|
|
||||||
{ value: 'zh-TW', label: '中文(繁体)' },
|
|
||||||
{ value: 'en-US', label: 'English' }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
<SettingDivider />
|
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
|
|
||||||
<Select
|
|
||||||
defaultValue={theme}
|
|
||||||
style={{ width: 180 }}
|
|
||||||
onChange={setTheme}
|
|
||||||
options={[
|
|
||||||
{ value: ThemeMode.light, label: t('settings.theme.light') },
|
|
||||||
{ value: ThemeMode.dark, label: t('settings.theme.dark') },
|
|
||||||
{ value: ThemeMode.auto, label: t('settings.theme.auto') }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
{isMac && (
|
|
||||||
<>
|
|
||||||
<SettingDivider />
|
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>{t('settings.theme.window.style.title')}</SettingRowTitle>
|
|
||||||
<Select
|
|
||||||
defaultValue={windowStyle || 'opaque'}
|
|
||||||
style={{ width: 180 }}
|
|
||||||
onChange={setWindowStyle}
|
|
||||||
options={[
|
|
||||||
{ value: 'transparent', label: t('settings.theme.window.style.transparent') },
|
|
||||||
{ value: 'opaque', label: t('settings.theme.window.style.opaque') }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<SettingDivider />
|
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>{t('settings.topic.position')}</SettingRowTitle>
|
|
||||||
<Select
|
|
||||||
defaultValue={topicPosition || 'right'}
|
|
||||||
style={{ width: 180 }}
|
|
||||||
onChange={setTopicPosition}
|
|
||||||
options={[
|
|
||||||
{ value: 'left', label: t('settings.topic.position.left') },
|
|
||||||
{ value: 'right', label: t('settings.topic.position.right') }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
<SettingDivider />
|
|
||||||
{topicPosition === 'left' && (
|
|
||||||
<>
|
|
||||||
<SettingRow style={{ minHeight: 32 }}>
|
|
||||||
<SettingRowTitle>{t('settings.advanced.click_assistant_switch_to_topics')}</SettingRowTitle>
|
|
||||||
<Switch
|
|
||||||
checked={clickAssistantToShowTopic}
|
|
||||||
onChange={(checked) => dispatch(setClickAssistantToShowTopic(checked))}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
<SettingDivider />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>{t('settings.general.check_update_setting')}</SettingRowTitle>
|
|
||||||
<Select
|
|
||||||
defaultValue={manualUpdateCheck ?? false}
|
|
||||||
style={{ width: 180 }}
|
|
||||||
onChange={(v) => dispatch(setManualUpdateCheck(v))}
|
|
||||||
options={[
|
|
||||||
{ value: false, label: t('settings.general.auto_update_check') },
|
|
||||||
{ value: true, label: t('settings.general.manual_update_check') }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
<SettingDivider />
|
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
|
|
||||||
<Input
|
|
||||||
placeholder="socks5://127.0.0.1:6153"
|
|
||||||
value={proxyUrl}
|
|
||||||
onChange={(e) => setProxyUrl(e.target.value)}
|
|
||||||
style={{ width: 180 }}
|
|
||||||
onBlur={() => onSetProxyUrl()}
|
|
||||||
type="url"
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
<SettingDivider />
|
|
||||||
<SettingRow>
|
|
||||||
{/* 把之前备份的文件定时上传到 webdav,首先先配置 webdav 的 host, port, user, pass, path */}
|
|
||||||
<SettingRowTitle>{t('settings.general.webdav.title')}</SettingRowTitle>
|
|
||||||
<HStack gap="5px">
|
|
||||||
<Input
|
|
||||||
placeholder={t('settings.general.webdav.host')}
|
|
||||||
value={webdavHost}
|
|
||||||
onChange={(e) => setWebdavHost(e.target.value)}
|
|
||||||
style={{ width: 280 }}
|
|
||||||
type="url"
|
|
||||||
onBlur={onSetWebdav}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={t('settings.general.webdav.user')}
|
|
||||||
value={webdavUser}
|
|
||||||
onChange={(e) => setWebdavUser(e.target.value)}
|
|
||||||
style={{ width: 120 }}
|
|
||||||
onBlur={onSetWebdav}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={t('settings.general.webdav.password')}
|
|
||||||
value={webdavPass}
|
|
||||||
onChange={(e) => setWebdavPass(e.target.value)}
|
|
||||||
style={{ width: 140 }}
|
|
||||||
onBlur={onSetWebdav}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={t('settings.general.webdav.path')}
|
|
||||||
value={webdavPath}
|
|
||||||
onChange={(e) => setWebdavPath(e.target.value)}
|
|
||||||
style={{ width: 220 }}
|
|
||||||
onBlur={onSetWebdav}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</SettingRow>
|
|
||||||
<SettingDivider />
|
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
|
||||||
<HStack gap="5px" justifyContent="space-between">
|
|
||||||
<Button onClick={backup} icon={<SaveOutlined />}>
|
|
||||||
{t('settings.general.backup.button')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={restore} icon={<FolderOpenOutlined />}>
|
|
||||||
{t('settings.general.restore.button')}
|
|
||||||
</Button>
|
|
||||||
{/* 添加 在线备份 在线还原 按钮 */}
|
|
||||||
<Button onClick={backupToWebdav} icon={<SaveOutlined />}>
|
|
||||||
{t('settings.general.webdav.backup.button')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={restoreFromWebdav} icon={<FolderOpenOutlined />}>
|
|
||||||
{t('settings.general.webdav.restore.button')}
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</SettingRow>
|
|
||||||
<SettingDivider />
|
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>{t('settings.general.reset.title')}</SettingRowTitle>
|
|
||||||
<HStack gap="5px">
|
|
||||||
<Button onClick={reset} danger>
|
|
||||||
{t('settings.general.reset.button')}
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</SettingRow>
|
|
||||||
<SettingDivider />
|
|
||||||
</SettingContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GeneralSettings
|
|
||||||
@ -0,0 +1,195 @@
|
|||||||
|
import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
|
||||||
|
import { HStack, VStack } from '@renderer/components/Layout'
|
||||||
|
import { isMac } from '@renderer/config/constant'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import i18n from '@renderer/i18n'
|
||||||
|
import { backup, reset, restore } from '@renderer/services/backup'
|
||||||
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import { setClickAssistantToShowTopic, setLanguage, setManualUpdateCheck } from '@renderer/store/settings'
|
||||||
|
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
|
||||||
|
import { ThemeMode } from '@renderer/types'
|
||||||
|
import { isValidProxyUrl } from '@renderer/utils'
|
||||||
|
import { Button, Input, Select, Switch } from 'antd'
|
||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link, Route, Routes } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||||
|
import WebDavSettings from './WebDavSettings'
|
||||||
|
|
||||||
|
const GeneralSettings: FC = () => {
|
||||||
|
const {
|
||||||
|
language,
|
||||||
|
proxyUrl: storeProxyUrl,
|
||||||
|
theme,
|
||||||
|
windowStyle,
|
||||||
|
topicPosition,
|
||||||
|
clickAssistantToShowTopic,
|
||||||
|
manualUpdateCheck,
|
||||||
|
setTheme,
|
||||||
|
setWindowStyle,
|
||||||
|
setTopicPosition
|
||||||
|
} = useSettings()
|
||||||
|
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const onSelectLanguage = (value: string) => {
|
||||||
|
dispatch(setLanguage(value))
|
||||||
|
localStorage.setItem('language', value)
|
||||||
|
i18n.changeLanguage(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSetProxyUrl = () => {
|
||||||
|
if (proxyUrl && !isValidProxyUrl(proxyUrl)) {
|
||||||
|
window.message.error({ content: t('message.error.invalid.proxy.url'), key: 'proxy-error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(_setProxyUrl(proxyUrl))
|
||||||
|
window.api.setProxy(proxyUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<SettingContainer>
|
||||||
|
<SettingTitle>{t('settings.general.title')}</SettingTitle>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('common.language')}</SettingRowTitle>
|
||||||
|
<Select
|
||||||
|
defaultValue={language || 'en-US'}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
onChange={onSelectLanguage}
|
||||||
|
options={[
|
||||||
|
{ value: 'zh-CN', label: '中文' },
|
||||||
|
{ value: 'zh-TW', label: '中文(繁体)' },
|
||||||
|
{ value: 'en-US', label: 'English' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
|
||||||
|
<Select
|
||||||
|
defaultValue={theme}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
onChange={setTheme}
|
||||||
|
options={[
|
||||||
|
{ value: ThemeMode.light, label: t('settings.theme.light') },
|
||||||
|
{ value: ThemeMode.dark, label: t('settings.theme.dark') },
|
||||||
|
{ value: ThemeMode.auto, label: t('settings.theme.auto') }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
{isMac && (
|
||||||
|
<>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.theme.window.style.title')}</SettingRowTitle>
|
||||||
|
<Select
|
||||||
|
defaultValue={windowStyle || 'opaque'}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
onChange={setWindowStyle}
|
||||||
|
options={[
|
||||||
|
{ value: 'transparent', label: t('settings.theme.window.style.transparent') },
|
||||||
|
{ value: 'opaque', label: t('settings.theme.window.style.opaque') }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.topic.position')}</SettingRowTitle>
|
||||||
|
<Select
|
||||||
|
defaultValue={topicPosition || 'right'}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
onChange={setTopicPosition}
|
||||||
|
options={[
|
||||||
|
{ value: 'left', label: t('settings.topic.position.left') },
|
||||||
|
{ value: 'right', label: t('settings.topic.position.right') }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
{topicPosition === 'left' && (
|
||||||
|
<>
|
||||||
|
<SettingRow style={{ minHeight: 32 }}>
|
||||||
|
<SettingRowTitle>{t('settings.advanced.click_assistant_switch_to_topics')}</SettingRowTitle>
|
||||||
|
<Switch
|
||||||
|
checked={clickAssistantToShowTopic}
|
||||||
|
onChange={(checked) => dispatch(setClickAssistantToShowTopic(checked))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.general.check_update_setting')}</SettingRowTitle>
|
||||||
|
<Select
|
||||||
|
defaultValue={manualUpdateCheck ?? false}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
onChange={(v) => dispatch(setManualUpdateCheck(v))}
|
||||||
|
options={[
|
||||||
|
{ value: false, label: t('settings.general.auto_update_check') },
|
||||||
|
{ value: true, label: t('settings.general.manual_update_check') }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
|
||||||
|
<Input
|
||||||
|
placeholder="socks5://127.0.0.1:6153"
|
||||||
|
value={proxyUrl}
|
||||||
|
onChange={(e) => setProxyUrl(e.target.value)}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
onBlur={() => onSetProxyUrl()}
|
||||||
|
type="url"
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow style={{ minHeight: 32 }}>
|
||||||
|
<SettingRowTitle>{t('settings.general.webdav.title')}</SettingRowTitle>
|
||||||
|
<VStack gap="5px">
|
||||||
|
<Link to="/settings/general/webdav" style={{ color: 'var(--color-text-2)' }}>
|
||||||
|
{t('settings.general.view_webdav_settings')}
|
||||||
|
</Link>
|
||||||
|
</VStack>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||||
|
<HStack gap="5px" justifyContent="space-between">
|
||||||
|
<Button onClick={backup} icon={<SaveOutlined />}>
|
||||||
|
{t('settings.general.backup.button')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={restore} icon={<FolderOpenOutlined />}>
|
||||||
|
{t('settings.general.restore.button')}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.general.reset.title')}</SettingRowTitle>
|
||||||
|
<HStack gap="5px">
|
||||||
|
<Button onClick={reset} danger>
|
||||||
|
{t('settings.general.reset.button')}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
</SettingContainer>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path="webdav" element={<WebDavSettings />} />
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GeneralSettings
|
||||||
@ -0,0 +1,137 @@
|
|||||||
|
import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
|
||||||
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { backupToWebdav, restoreFromWebdav } from '@renderer/services/backup'
|
||||||
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import {
|
||||||
|
setWebdavHost as _setWebdavHost,
|
||||||
|
setWebdavPass as _setWebdavPass,
|
||||||
|
setWebdavPath as _setWebdavPath,
|
||||||
|
setWebdavUser as _setWebdavUser
|
||||||
|
} from '@renderer/store/settings'
|
||||||
|
import { Breadcrumb, Button, Input } from 'antd'
|
||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||||
|
|
||||||
|
const WebDavSettings: FC = () => {
|
||||||
|
const {
|
||||||
|
webdavHost: webDAVHost,
|
||||||
|
webdavUser: webDAVUser,
|
||||||
|
webdavPass: webDAVPass,
|
||||||
|
webdavPath: webDAVPath
|
||||||
|
} = useSettings()
|
||||||
|
|
||||||
|
const [webdavHost, setWebdavHost] = useState<string | undefined>(webDAVHost)
|
||||||
|
const [webdavUser, setWebdavUser] = useState<string | undefined>(webDAVUser)
|
||||||
|
const [webdavPass, setWebdavPass] = useState<string | undefined>(webDAVPass)
|
||||||
|
const [webdavPath, setWebdavPath] = useState<string | undefined>(webDAVPath)
|
||||||
|
|
||||||
|
const [backuping, setBackuping] = useState(false)
|
||||||
|
const [restoring, setRestoring] = useState(false)
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
// 把之前备份的文件定时上传到 webdav,首先先配置 webdav 的 host, port, user, pass, path
|
||||||
|
|
||||||
|
const onBackup = async () => {
|
||||||
|
if (!webdavHost) {
|
||||||
|
window.message.error({ content: t('message.error.invalid.webdav'), key: 'webdav-error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setBackuping(true)
|
||||||
|
await backupToWebdav()
|
||||||
|
setBackuping(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRestore = async () => {
|
||||||
|
if (!webdavHost) {
|
||||||
|
window.message.error({ content: t('message.error.invalid.webdav'), key: 'webdav-error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setRestoring(true)
|
||||||
|
await restoreFromWebdav()
|
||||||
|
setRestoring(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingContainer>
|
||||||
|
<Breadcrumb
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
title: t('settings.general.title'),
|
||||||
|
href: '#/settings/general'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('settings.general.webdav.title')
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<SettingTitle style={{ marginTop: 20 }}>{t('settings.general.webdav.title')}</SettingTitle>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.general.webdav.host')}</SettingRowTitle>
|
||||||
|
<Input
|
||||||
|
placeholder={t('settings.general.webdav.host.placeholder')}
|
||||||
|
value={webdavHost}
|
||||||
|
onChange={(e) => setWebdavHost(e.target.value)}
|
||||||
|
style={{ width: 250 }}
|
||||||
|
type="url"
|
||||||
|
onBlur={() => dispatch(_setWebdavHost(webdavHost || ''))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.general.webdav.user')}</SettingRowTitle>
|
||||||
|
<Input
|
||||||
|
placeholder={t('settings.general.webdav.user')}
|
||||||
|
value={webdavUser}
|
||||||
|
onChange={(e) => setWebdavUser(e.target.value)}
|
||||||
|
style={{ width: 250 }}
|
||||||
|
onBlur={() => dispatch(_setWebdavUser(webdavUser || ''))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.general.webdav.password')}</SettingRowTitle>
|
||||||
|
<Input.Password
|
||||||
|
placeholder={t('settings.general.webdav.password')}
|
||||||
|
value={webdavPass}
|
||||||
|
onChange={(e) => setWebdavPass(e.target.value)}
|
||||||
|
style={{ width: 250 }}
|
||||||
|
onBlur={() => dispatch(_setWebdavPass(webdavPass || ''))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.general.webdav.path')}</SettingRowTitle>
|
||||||
|
<Input
|
||||||
|
placeholder={t('settings.general.webdav.path.placeholder')}
|
||||||
|
value={webdavPath}
|
||||||
|
onChange={(e) => setWebdavPath(e.target.value)}
|
||||||
|
style={{ width: 250 }}
|
||||||
|
onBlur={() => dispatch(_setWebdavPath(webdavPath || ''))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||||
|
<HStack gap="5px" justifyContent="space-between">
|
||||||
|
{/* 添加 在线备份 在线还原 按钮 */}
|
||||||
|
<Button onClick={onBackup} icon={<SaveOutlined />} loading={backuping}>
|
||||||
|
{t('settings.general.webdav.backup.button')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onRestore} icon={<FolderOpenOutlined />} loading={restoring}>
|
||||||
|
{t('settings.general.webdav.restore.button')}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
</SettingContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WebDavSettings
|
||||||
@ -8,7 +8,7 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
import AboutSettings from './AboutSettings'
|
import AboutSettings from './AboutSettings'
|
||||||
import AssistantSettings from './AssistantSettings'
|
import AssistantSettings from './AssistantSettings'
|
||||||
import GeneralSettings from './GeneralSettings'
|
import GeneralSettings from './GeneralSettings/GeneralSettings'
|
||||||
import ModelSettings from './ModelSettings'
|
import ModelSettings from './ModelSettings'
|
||||||
import ProvidersList from './ProviderSettings'
|
import ProvidersList from './ProviderSettings'
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ const SettingsPage: FC = () => {
|
|||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
const isRoute = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
@ -65,7 +65,7 @@ const SettingsPage: FC = () => {
|
|||||||
<Route path="provider" element={<ProvidersList />} />
|
<Route path="provider" element={<ProvidersList />} />
|
||||||
<Route path="model" element={<ModelSettings />} />
|
<Route path="model" element={<ModelSettings />} />
|
||||||
<Route path="assistant" element={<AssistantSettings />} />
|
<Route path="assistant" element={<AssistantSettings />} />
|
||||||
<Route path="general" element={<GeneralSettings />} />
|
<Route path="general/*" element={<GeneralSettings />} />
|
||||||
<Route path="about" element={<AboutSettings />} />
|
<Route path="about" element={<AboutSettings />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</SettingContent>
|
</SettingContent>
|
||||||
|
|||||||
@ -1,29 +1,15 @@
|
|||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
|
import store from '@renderer/store'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
import store from '@renderer/store'
|
|
||||||
|
|
||||||
import { createClient } from 'webdav'
|
|
||||||
|
|
||||||
export async function backup() {
|
export async function backup() {
|
||||||
const version = 3
|
const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}.zip`
|
||||||
const time = new Date().getTime()
|
const fileContnet = await getBackupData()
|
||||||
|
|
||||||
const data = {
|
|
||||||
time,
|
|
||||||
version,
|
|
||||||
localStorage,
|
|
||||||
indexedDB: await backupDatabase()
|
|
||||||
}
|
|
||||||
|
|
||||||
const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}`
|
|
||||||
const fileContnet = JSON.stringify(data)
|
|
||||||
|
|
||||||
const selectFolder = await window.api.file.selectFolder()
|
const selectFolder = await window.api.file.selectFolder()
|
||||||
|
|
||||||
if (selectFolder) {
|
if (selectFolder) {
|
||||||
await window.api.backup.save(fileContnet, filename, selectFolder)
|
await window.api.backup.backup(filename, fileContnet, selectFolder)
|
||||||
window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,7 +24,7 @@ export async function restore() {
|
|||||||
// zip backup file
|
// zip backup file
|
||||||
if (file?.fileName.endsWith('.zip')) {
|
if (file?.fileName.endsWith('.zip')) {
|
||||||
const restoreData = await window.api.backup.restore(file.filePath)
|
const restoreData = await window.api.backup.restore(file.filePath)
|
||||||
data = JSON.parse(restoreData.data)
|
data = JSON.parse(restoreData)
|
||||||
} else {
|
} else {
|
||||||
data = JSON.parse(await window.api.decompress(file.content))
|
data = JSON.parse(await window.api.decompress(file.content))
|
||||||
}
|
}
|
||||||
@ -78,127 +64,73 @@ export async function reset() {
|
|||||||
|
|
||||||
// 备份到 webdav
|
// 备份到 webdav
|
||||||
export async function backupToWebdav() {
|
export async function backupToWebdav() {
|
||||||
// 先走之前的 backup 流程,存储到临时文件
|
|
||||||
const version = 3
|
|
||||||
const time = new Date().getTime()
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
time,
|
|
||||||
version,
|
|
||||||
localStorage,
|
|
||||||
indexedDB: await backupDatabase()
|
|
||||||
}
|
|
||||||
|
|
||||||
const filename = `cherry-studio.backup.json`
|
|
||||||
const fileContent = JSON.stringify(data)
|
|
||||||
|
|
||||||
// 获取 userSetting 里的 WebDAV 配置
|
|
||||||
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
||||||
// console.log('backup.backupToWebdav', webdavHost, webdavUser, webdavPass, webdavPath)
|
|
||||||
|
|
||||||
let host = webdavHost
|
const backupData = await getBackupData()
|
||||||
if (!host.startsWith('http://') && !host.startsWith('https://')) {
|
|
||||||
host = `http://${host}`
|
|
||||||
}
|
|
||||||
console.log('backup.backupToWebdav', host)
|
|
||||||
|
|
||||||
// 创建 WebDAV 客户端
|
console.debug({
|
||||||
const client = createClient(
|
webdavHost,
|
||||||
host, // WebDAV 服务器地址
|
webdavUser,
|
||||||
{
|
webdavPass,
|
||||||
username: webdavUser, // 用户名
|
webdavPath
|
||||||
password: webdavPass // 密码
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 上传文件到 WebDAV
|
|
||||||
const remoteFilePath = `${webdavPath}/${filename}`
|
|
||||||
|
|
||||||
// 先检查创建目录
|
|
||||||
try {
|
|
||||||
if (!(await client.exists(webdavPath))) {
|
|
||||||
await client.createDirectory(webdavPath)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating directory on WebDAV:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传文件
|
// 上传文件
|
||||||
try {
|
try {
|
||||||
await client.putFileContents(remoteFilePath, fileContent, { overwrite: true })
|
const success = await window.api.backup.backupToWebdav(backupData, {
|
||||||
console.log('File uploaded successfully!')
|
webdavHost,
|
||||||
window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
webdavUser,
|
||||||
} catch (error) {
|
webdavPass,
|
||||||
console.error('Error uploading file to WebDAV:', error)
|
webdavPath
|
||||||
|
})
|
||||||
|
if (success) {
|
||||||
|
window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
||||||
|
} else {
|
||||||
|
window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' })
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[backup] backupToWebdav: Error uploading file to WebDAV:', error)
|
||||||
|
window.modal.error({
|
||||||
|
title: i18n.t('message.backup.failed'),
|
||||||
|
content: error.message
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 webdav 恢复
|
// 从 webdav 恢复
|
||||||
export async function restoreFromWebdav() {
|
export async function restoreFromWebdav() {
|
||||||
const filename = `cherry-studio.backup.json`
|
|
||||||
|
|
||||||
// 获取 userSetting 里的 WebDAV 配置
|
|
||||||
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
||||||
// console.log('backup.restoreFromWebdav', webdavHost, webdavUser, webdavPass, webdavPath)
|
let data = ''
|
||||||
|
|
||||||
let host = webdavHost
|
|
||||||
if (!host.startsWith('http://') && !host.startsWith('https://')) {
|
|
||||||
host = `http://${host}`
|
|
||||||
}
|
|
||||||
console.log('backup.restoreFromWebdav', host)
|
|
||||||
|
|
||||||
// 创建 WebDAV 客户端
|
|
||||||
const client = createClient(
|
|
||||||
host, // WebDAV 服务器地址
|
|
||||||
{
|
|
||||||
username: webdavUser, // 用户名
|
|
||||||
password: webdavPass // 密码
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 上传文件到 WebDAV
|
|
||||||
const remoteFilePath = `${webdavPath}/${filename}`
|
|
||||||
|
|
||||||
// 下载文件
|
|
||||||
try {
|
try {
|
||||||
// 下载文件内容
|
data = await window.api.backup.restoreFromWebdav({ webdavHost, webdavUser, webdavPass, webdavPath })
|
||||||
const fileContent = await client.getFileContents(remoteFilePath, { format: 'text' })
|
} catch (error: any) {
|
||||||
console.log('File downloaded successfully!', fileContent)
|
console.error('[backup] restoreFromWebdav: Error downloading file from WebDAV:', error)
|
||||||
|
window.modal.error({
|
||||||
|
title: i18n.t('message.restore.failed'),
|
||||||
|
content: error.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 处理文件内容
|
try {
|
||||||
const data = parseFileContent(fileContent.toString())
|
await handleData(JSON.parse(data))
|
||||||
console.log('Parsed file content:', data)
|
|
||||||
|
|
||||||
await handleData(data)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error downloading file from WebDAV:', error)
|
console.error('[backup] Error downloading file from WebDAV:', error)
|
||||||
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/************************************* Backup Utils ************************************** */
|
async function getBackupData() {
|
||||||
|
return JSON.stringify({
|
||||||
function parseFileContent(fileContent: string | Buffer | { data: string | Buffer } | ArrayBuffer): any {
|
time: new Date().getTime(),
|
||||||
let fileContentString: string
|
version: 3,
|
||||||
|
localStorage,
|
||||||
if (typeof fileContent === 'string') {
|
indexedDB: await backupDatabase()
|
||||||
fileContentString = fileContent
|
})
|
||||||
} else if (Buffer.isBuffer(fileContent)) {
|
|
||||||
fileContentString = fileContent.toString('utf-8')
|
|
||||||
} else if (fileContent instanceof ArrayBuffer) {
|
|
||||||
fileContentString = Buffer.from(fileContent).toString('utf-8')
|
|
||||||
} else if (fileContent && typeof fileContent.data === 'string') {
|
|
||||||
fileContentString = fileContent.data
|
|
||||||
} else if (fileContent && Buffer.isBuffer(fileContent.data)) {
|
|
||||||
fileContentString = fileContent.data.toString('utf-8')
|
|
||||||
} else {
|
|
||||||
throw new Error('Unsupported file content type')
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.parse(fileContentString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleData(data: any) {
|
/************************************* Backup Utils ************************************** */
|
||||||
|
async function handleData(data: Record<string, any>) {
|
||||||
if (data.version === 1) {
|
if (data.version === 1) {
|
||||||
await clearDatabase()
|
await clearDatabase()
|
||||||
|
|
||||||
|
|||||||
@ -116,3 +116,10 @@ export enum ThemeMode {
|
|||||||
dark = 'dark',
|
dark = 'dark',
|
||||||
auto = 'auto'
|
auto = 'auto'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WebDavConfig = {
|
||||||
|
webdavHost: string
|
||||||
|
webdavUser: string
|
||||||
|
webdavPass: string
|
||||||
|
webdavPath: string
|
||||||
|
}
|
||||||
|
|||||||
166
yarn.lock
166
yarn.lock
@ -2292,6 +2292,7 @@ __metadata:
|
|||||||
unzipper: "npm:^0.12.3"
|
unzipper: "npm:^0.12.3"
|
||||||
uuid: "npm:^10.0.0"
|
uuid: "npm:^10.0.0"
|
||||||
vite: "npm:^5.0.12"
|
vite: "npm:^5.0.12"
|
||||||
|
webdav: "npm:4.11.4"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^17.0.0 || ^18.0.0
|
react: ^17.0.0 || ^18.0.0
|
||||||
react-dom: ^17.0.0 || ^18.0.0
|
react-dom: ^17.0.0 || ^18.0.0
|
||||||
@ -2826,6 +2827,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"axios@npm:^0.27.2":
|
||||||
|
version: 0.27.2
|
||||||
|
resolution: "axios@npm:0.27.2"
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: "npm:^1.14.9"
|
||||||
|
form-data: "npm:^4.0.0"
|
||||||
|
checksum: 10c0/76d673d2a90629944b44d6f345f01e58e9174690f635115d5ffd4aca495d99bcd8f95c590d5ccb473513f5ebc1d1a6e8934580d0c57cdd0498c3a101313ef771
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"axios@npm:^1.7.3":
|
"axios@npm:^1.7.3":
|
||||||
version: 1.7.7
|
version: 1.7.7
|
||||||
resolution: "axios@npm:1.7.7"
|
resolution: "axios@npm:1.7.7"
|
||||||
@ -2865,6 +2876,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"base-64@npm:^1.0.0":
|
||||||
|
version: 1.0.0
|
||||||
|
resolution: "base-64@npm:1.0.0"
|
||||||
|
checksum: 10c0/d886cb3236cee0bed9f7075675748b59b32fad623ddb8ce1793c790306aa0f76a03238cad4b3fb398abda6527ce08a5588388533a4ccade0b97e82b9da660e28
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"base64-arraybuffer@npm:^1.0.2":
|
"base64-arraybuffer@npm:^1.0.2":
|
||||||
version: 1.0.2
|
version: 1.0.2
|
||||||
resolution: "base64-arraybuffer@npm:1.0.2"
|
resolution: "base64-arraybuffer@npm:1.0.2"
|
||||||
@ -3075,6 +3093,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"byte-length@npm:^1.0.2":
|
||||||
|
version: 1.0.2
|
||||||
|
resolution: "byte-length@npm:1.0.2"
|
||||||
|
checksum: 10c0/98778b938318494c2eadedf83b415e63da406d905575101eb102b7eefb5fafbbe21fbe83001914283664bb2fb93bd46f99245af2e8420a928ba90ffbb58041a1
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"cac@npm:^6.7.14":
|
"cac@npm:^6.7.14":
|
||||||
version: 6.7.14
|
version: 6.7.14
|
||||||
resolution: "cac@npm:6.7.14"
|
resolution: "cac@npm:6.7.14"
|
||||||
@ -3265,6 +3290,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"charenc@npm:0.0.2":
|
||||||
|
version: 0.0.2
|
||||||
|
resolution: "charenc@npm:0.0.2"
|
||||||
|
checksum: 10c0/a45ec39363a16799d0f9365c8dd0c78e711415113c6f14787a22462ef451f5013efae8a28f1c058f81fc01f2a6a16955f7a5fd0cd56247ce94a45349c89877d8
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"chokidar@npm:>=3.0.0 <4.0.0":
|
"chokidar@npm:>=3.0.0 <4.0.0":
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
resolution: "chokidar@npm:3.6.0"
|
resolution: "chokidar@npm:3.6.0"
|
||||||
@ -3586,6 +3618,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"crypt@npm:0.0.2":
|
||||||
|
version: 0.0.2
|
||||||
|
resolution: "crypt@npm:0.0.2"
|
||||||
|
checksum: 10c0/adbf263441dd801665d5425f044647533f39f4612544071b1471962209d235042fb703c27eea2795c7c53e1dfc242405173003f83cf4f4761a633d11f9653f18
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"css-box-model@npm:^1.2.1":
|
"css-box-model@npm:^1.2.1":
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
resolution: "css-box-model@npm:1.2.1"
|
resolution: "css-box-model@npm:1.2.1"
|
||||||
@ -4825,6 +4864,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"fast-xml-parser@npm:^4.2.4":
|
||||||
|
version: 4.5.0
|
||||||
|
resolution: "fast-xml-parser@npm:4.5.0"
|
||||||
|
dependencies:
|
||||||
|
strnum: "npm:^1.0.5"
|
||||||
|
bin:
|
||||||
|
fxparser: src/cli/cli.js
|
||||||
|
checksum: 10c0/71d206c9e137f5c26af88d27dde0108068a5d074401901d643c500c36e95dfd828333a98bda020846c41f5b9b364e2b0e9be5b19b0bdcab5cf31559c07b80a95
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"fastq@npm:^1.6.0":
|
"fastq@npm:^1.6.0":
|
||||||
version: 1.17.1
|
version: 1.17.1
|
||||||
resolution: "fastq@npm:1.17.1"
|
resolution: "fastq@npm:1.17.1"
|
||||||
@ -4944,7 +4994,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"follow-redirects@npm:^1.15.6":
|
"follow-redirects@npm:^1.14.9, follow-redirects@npm:^1.15.6":
|
||||||
version: 1.15.9
|
version: 1.15.9
|
||||||
resolution: "follow-redirects@npm:1.15.9"
|
resolution: "follow-redirects@npm:1.15.9"
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
@ -5627,6 +5677,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"he@npm:^1.2.0":
|
||||||
|
version: 1.2.0
|
||||||
|
resolution: "he@npm:1.2.0"
|
||||||
|
bin:
|
||||||
|
he: bin/he
|
||||||
|
checksum: 10c0/a27d478befe3c8192f006cdd0639a66798979dfa6e2125c6ac582a19a5ebfec62ad83e8382e6036170d873f46e4536a7e795bf8b95bf7c247f4cc0825ccc8c17
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"highlight.js@npm:^10.4.1, highlight.js@npm:~10.7.0":
|
"highlight.js@npm:^10.4.1, highlight.js@npm:~10.7.0":
|
||||||
version: 10.7.3
|
version: 10.7.3
|
||||||
resolution: "highlight.js@npm:10.7.3"
|
resolution: "highlight.js@npm:10.7.3"
|
||||||
@ -5659,6 +5718,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"hot-patcher@npm:^1.0.0":
|
||||||
|
version: 1.0.0
|
||||||
|
resolution: "hot-patcher@npm:1.0.0"
|
||||||
|
checksum: 10c0/0c3ee3e1cb45f8b09ecb6d9af11b35b05f94b0767e09a303d89a7b6073b55ee98bd5c9b563ff17bfa1add55bbe3ff7598bbbb8c035578e05dd12631f2351cdb6
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"html-parse-stringify@npm:^3.0.1":
|
"html-parse-stringify@npm:^3.0.1":
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
resolution: "html-parse-stringify@npm:3.0.1"
|
resolution: "html-parse-stringify@npm:3.0.1"
|
||||||
@ -6015,6 +6081,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"is-buffer@npm:~1.1.6":
|
||||||
|
version: 1.1.6
|
||||||
|
resolution: "is-buffer@npm:1.1.6"
|
||||||
|
checksum: 10c0/ae18aa0b6e113d6c490ad1db5e8df9bdb57758382b313f5a22c9c61084875c6396d50bbf49315f5b1926d142d74dfb8d31b40d993a383e0a158b15fea7a82234
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7":
|
"is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7":
|
||||||
version: 1.2.7
|
version: 1.2.7
|
||||||
resolution: "is-callable@npm:1.2.7"
|
resolution: "is-callable@npm:1.2.7"
|
||||||
@ -6655,6 +6728,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"layerr@npm:^0.1.2":
|
||||||
|
version: 0.1.2
|
||||||
|
resolution: "layerr@npm:0.1.2"
|
||||||
|
checksum: 10c0/e329ec13a31cd676c2fdf2127d43b794dab692991d7fa64cfd752d36e0c17799341e208b4727d944373d0a8c91fdd263023d66498e3152f8672238de47f9c602
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lazy-val@npm:^1.0.4, lazy-val@npm:^1.0.5":
|
"lazy-val@npm:^1.0.4, lazy-val@npm:^1.0.5":
|
||||||
version: 1.0.5
|
version: 1.0.5
|
||||||
resolution: "lazy-val@npm:1.0.5"
|
resolution: "lazy-val@npm:1.0.5"
|
||||||
@ -6905,6 +6985,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"md5@npm:^2.3.0":
|
||||||
|
version: 2.3.0
|
||||||
|
resolution: "md5@npm:2.3.0"
|
||||||
|
dependencies:
|
||||||
|
charenc: "npm:0.0.2"
|
||||||
|
crypt: "npm:0.0.2"
|
||||||
|
is-buffer: "npm:~1.1.6"
|
||||||
|
checksum: 10c0/14a21d597d92e5b738255fbe7fe379905b8cb97e0a49d44a20b58526a646ec5518c337b817ce0094ca94d3e81a3313879c4c7b510d250c282d53afbbdede9110
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"mdast-util-find-and-replace@npm:^3.0.0":
|
"mdast-util-find-and-replace@npm:^3.0.0":
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
resolution: "mdast-util-find-and-replace@npm:3.0.1"
|
resolution: "mdast-util-find-and-replace@npm:3.0.1"
|
||||||
@ -7768,6 +7859,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"nested-property@npm:^4.0.0":
|
||||||
|
version: 4.0.0
|
||||||
|
resolution: "nested-property@npm:4.0.0"
|
||||||
|
checksum: 10c0/7bc0514f3d10460cc07ea27a39ce75f81471a28b8b019d4bfd9eda41dcd92c1fcb291598d7e168ae8bf1324109b36325e3a563d6aa2537d13015bea9258b8b72
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"node-addon-api@npm:^1.6.3":
|
"node-addon-api@npm:^1.6.3":
|
||||||
version: 1.7.2
|
version: 1.7.2
|
||||||
resolution: "node-addon-api@npm:1.7.2"
|
resolution: "node-addon-api@npm:1.7.2"
|
||||||
@ -8236,6 +8334,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"path-posix@npm:^1.0.0":
|
||||||
|
version: 1.0.0
|
||||||
|
resolution: "path-posix@npm:1.0.0"
|
||||||
|
checksum: 10c0/00fbadb9b60fb513f316f92e0b5535e55d832f4f20067586d151f6d7bed57178dec31b1a0f514694500a9a1f2b69798c066a3cdcf0b0289cfee63e39845bfd02
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"path-scurry@npm:^1.11.1":
|
"path-scurry@npm:^1.11.1":
|
||||||
version: 1.11.1
|
version: 1.11.1
|
||||||
resolution: "path-scurry@npm:1.11.1"
|
resolution: "path-scurry@npm:1.11.1"
|
||||||
@ -8603,6 +8708,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"querystringify@npm:^2.1.1":
|
||||||
|
version: 2.2.0
|
||||||
|
resolution: "querystringify@npm:2.2.0"
|
||||||
|
checksum: 10c0/3258bc3dbdf322ff2663619afe5947c7926a6ef5fb78ad7d384602974c467fadfc8272af44f5eb8cddd0d011aae8fabf3a929a8eee4b86edcc0a21e6bd10f9aa
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"queue-microtask@npm:^1.2.2":
|
"queue-microtask@npm:^1.2.2":
|
||||||
version: 1.2.3
|
version: 1.2.3
|
||||||
resolution: "queue-microtask@npm:1.2.3"
|
resolution: "queue-microtask@npm:1.2.3"
|
||||||
@ -9668,6 +9780,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"requires-port@npm:^1.0.0":
|
||||||
|
version: 1.0.0
|
||||||
|
resolution: "requires-port@npm:1.0.0"
|
||||||
|
checksum: 10c0/b2bfdd09db16c082c4326e573a82c0771daaf7b53b9ce8ad60ea46aa6e30aaf475fe9b164800b89f93b748d2c234d8abff945d2551ba47bf5698e04cd7713267
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"reselect@npm:^5.1.0":
|
"reselect@npm:^5.1.0":
|
||||||
version: 5.1.1
|
version: 5.1.1
|
||||||
resolution: "reselect@npm:5.1.1"
|
resolution: "reselect@npm:5.1.1"
|
||||||
@ -10445,6 +10564,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"strnum@npm:^1.0.5":
|
||||||
|
version: 1.0.5
|
||||||
|
resolution: "strnum@npm:1.0.5"
|
||||||
|
checksum: 10c0/64fb8cc2effbd585a6821faa73ad97d4b553c8927e49086a162ffd2cc818787643390b89d567460a8e74300148d11ac052e21c921ef2049f2987f4b1b89a7ff1
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"strtok3@npm:^6.2.4":
|
"strtok3@npm:^6.2.4":
|
||||||
version: 6.3.0
|
version: 6.3.0
|
||||||
resolution: "strtok3@npm:6.3.0"
|
resolution: "strtok3@npm:6.3.0"
|
||||||
@ -11083,6 +11209,23 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"url-join@npm:^4.0.1":
|
||||||
|
version: 4.0.1
|
||||||
|
resolution: "url-join@npm:4.0.1"
|
||||||
|
checksum: 10c0/ac65e2c7c562d7b49b68edddcf55385d3e922bc1dd5d90419ea40b53b6de1607d1e45ceb71efb9d60da02c681d13c6cb3a1aa8b13fc0c989dfc219df97ee992d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"url-parse@npm:^1.5.10":
|
||||||
|
version: 1.5.10
|
||||||
|
resolution: "url-parse@npm:1.5.10"
|
||||||
|
dependencies:
|
||||||
|
querystringify: "npm:^2.1.1"
|
||||||
|
requires-port: "npm:^1.0.0"
|
||||||
|
checksum: 10c0/bd5aa9389f896974beb851c112f63b466505a04b4807cea2e5a3b7092f6fbb75316f0491ea84e44f66fed55f1b440df5195d7e3a8203f64fcefa19d182f5be87
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"use-memo-one@npm:^1.1.3":
|
"use-memo-one@npm:^1.1.3":
|
||||||
version: 1.1.3
|
version: 1.1.3
|
||||||
resolution: "use-memo-one@npm:1.1.3"
|
resolution: "use-memo-one@npm:1.1.3"
|
||||||
@ -11300,6 +11443,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"webdav@npm:4.11.4":
|
||||||
|
version: 4.11.4
|
||||||
|
resolution: "webdav@npm:4.11.4"
|
||||||
|
dependencies:
|
||||||
|
axios: "npm:^0.27.2"
|
||||||
|
base-64: "npm:^1.0.0"
|
||||||
|
byte-length: "npm:^1.0.2"
|
||||||
|
fast-xml-parser: "npm:^4.2.4"
|
||||||
|
he: "npm:^1.2.0"
|
||||||
|
hot-patcher: "npm:^1.0.0"
|
||||||
|
layerr: "npm:^0.1.2"
|
||||||
|
md5: "npm:^2.3.0"
|
||||||
|
minimatch: "npm:^5.1.0"
|
||||||
|
nested-property: "npm:^4.0.0"
|
||||||
|
path-posix: "npm:^1.0.0"
|
||||||
|
url-join: "npm:^4.0.1"
|
||||||
|
url-parse: "npm:^1.5.10"
|
||||||
|
checksum: 10c0/8b1ae47c4df6c3ee832ff1abe05ca8c03c5284519a07b41ae816e417688de0aae6f9c5ced04ecedd14daae5ec7367c6af010fda55d3458b424e51a95a817133e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"webidl-conversions@npm:^3.0.0":
|
"webidl-conversions@npm:^3.0.0":
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
resolution: "webidl-conversions@npm:3.0.1"
|
resolution: "webidl-conversions@npm:3.0.1"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user