feat: add WebDAV cloud sync with encrypted secrets support

Add WebDAV-based settings sync for desktop app, including connection
configuration, editor settings, and optional encrypted secrets sync.
Also persist WebDAV remember-password preference across sessions.
This commit is contained in:
t8y2
2026-05-27 01:17:23 +08:00
parent dacb162248
commit 0c9ee34a82
15 changed files with 1328 additions and 15 deletions
@@ -1,8 +1,20 @@
<script setup lang="ts">
import { ref, watch, shallowRef, computed } from "vue";
import { ref, watch, shallowRef, computed, onMounted } from "vue";
import type { EditorView as EditorViewType } from "@codemirror/view";
import { useI18n } from "vue-i18n";
import { CircleHelp, ExternalLink, Loader2, Pencil, RefreshCw, Settings, Trash2 } from "lucide-vue-next";
import {
CircleHelp,
Cloud,
Download,
ExternalLink,
Loader2,
Pencil,
RefreshCw,
Settings,
Trash2,
Upload,
X,
} from "lucide-vue-next";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
@@ -13,6 +25,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import {
useSettingsStore,
AI_PROVIDER_PRESETS,
@@ -27,7 +40,19 @@ import {
import { loadEditorTheme, editorFontTheme } from "@/lib/editorThemes";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { useTheme } from "@/composables/useTheme";
import { aiListModels, aiTestConnection, listSystemFonts, type AiModelInfo } from "@/lib/api";
import {
aiListModels,
aiTestConnection,
forgetWebdavSavedPassword,
listSystemFonts,
saveWebdavSavedPassword,
webdavPasswordStatus,
webdavSyncDownload,
webdavSyncTest,
webdavSyncUpload,
type AiModelInfo,
type WebDavConfig,
} from "@/lib/api";
import { eventToShortcut } from "@/lib/keyboardShortcuts";
import {
SHORTCUT_DEFINITIONS,
@@ -42,9 +67,11 @@ import { uuid } from "@/lib/utils";
import { DEFAULT_SQL_SNIPPETS } from "@/lib/sqlCompletion";
import AiProviderLogo from "@/components/icons/AiProviderLogo.vue";
import type { AppThemeAppearance } from "@/lib/appTheme";
import { useConnectionStore } from "@/stores/connectionStore";
const { t } = useI18n();
const settingsStore = useSettingsStore();
const connectionStore = useConnectionStore();
const { isDark } = useTheme();
const props = defineProps<{
@@ -142,10 +169,6 @@ function confirmDeleteSnippet(snippet: SqlSnippet) {
}
}
function restoreDefaultSnippets() {
editSnippets.value = DEFAULT_SQL_SNIPPETS.map((s) => ({ ...s }));
}
const presetFontLabels = new Map(FONT_FAMILIES.map((font) => [font.value, font.label]));
function cssFontFamilyForName(name: string): string {
@@ -212,6 +235,7 @@ watch(
void loadSystemFontOptions();
}
},
{ immediate: true },
);
watch(
@@ -356,6 +380,7 @@ type SettingsCategory =
| "redis"
| "shortcuts"
| "snippets"
| "sync"
| "ai"
| "security"
| "about";
@@ -366,6 +391,7 @@ const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[
{ value: "redis", label: t("settings.redisTab") },
{ value: "shortcuts", label: t("settings.shortcutsTab") },
{ value: "snippets", label: t("settings.snippetsTab") },
...(isWeb ? [] : [{ value: "sync" as const, label: t("settings.syncTab") }]),
{ value: "ai", label: t("settings.aiTab") },
...(isWeb ? [{ value: "security" as const, label: t("settings.securityTab") }] : []),
{ value: "about", label: t("settings.aboutTab") },
@@ -400,6 +426,138 @@ function openExternalUrl(url: string) {
}
}
// ---------- WebDAV Sync ----------
const webdavEndpoint = ref(localStorage.getItem("dbx-webdav-endpoint") || "");
const webdavUsername = ref(localStorage.getItem("dbx-webdav-username") || "");
const webdavPassword = ref("");
const webdavRememberPassword = ref(localStorage.getItem("dbx-webdav-remember-password") === "true");
const webdavHasSavedPassword = ref(false);
const webdavRemotePath = ref(localStorage.getItem("dbx-webdav-remote-path") || "DBX/sync/snapshot.json");
const webdavSyncSecrets = ref(false);
const webdavSecretsPassphrase = ref("");
const webdavBusy = ref<"" | "test" | "upload" | "download">("");
const webdavMessage = ref("");
const webdavError = ref(false);
const webdavReady = computed(
() =>
!!webdavEndpoint.value.trim() &&
!webdavBusy.value &&
(!webdavSyncSecrets.value || !!webdavSecretsPassphrase.value.trim()),
);
function currentWebDavConfig(): WebDavConfig {
return {
endpoint: webdavEndpoint.value.trim(),
username: webdavUsername.value.trim() || undefined,
password: webdavPassword.value || undefined,
remotePath: webdavRemotePath.value.trim() || "DBX/sync/snapshot.json",
};
}
function currentWebDavAccountConfig(): WebDavConfig {
const config = currentWebDavConfig();
return { ...config, password: undefined };
}
function rememberWebDavFields() {
localStorage.setItem("dbx-webdav-endpoint", webdavEndpoint.value.trim());
localStorage.setItem("dbx-webdav-username", webdavUsername.value.trim());
localStorage.setItem("dbx-webdav-remote-path", webdavRemotePath.value.trim() || "DBX/sync/snapshot.json");
}
function setWebDavResult(message: string, error = false) {
webdavMessage.value = message;
webdavError.value = error;
}
async function runWebDavAction(kind: "test" | "upload" | "download", action: () => Promise<string>) {
webdavBusy.value = kind;
webdavMessage.value = "";
webdavError.value = false;
try {
rememberWebDavFields();
await applyWebDavPasswordPreference();
setWebDavResult(await action());
} catch (e: any) {
setWebDavResult(e?.message || String(e), true);
} finally {
webdavBusy.value = "";
}
}
async function refreshWebDavPasswordStatus() {
if (!webdavEndpoint.value.trim()) {
webdavHasSavedPassword.value = false;
webdavRememberPassword.value = false;
return;
}
try {
const status = await webdavPasswordStatus(currentWebDavAccountConfig());
webdavHasSavedPassword.value = status.hasSavedPassword;
if (status.hasSavedPassword) webdavRememberPassword.value = true;
} catch {
webdavHasSavedPassword.value = false;
}
}
async function applyWebDavPasswordPreference() {
const password = webdavPassword.value;
if (webdavRememberPassword.value && password) {
await saveWebdavSavedPassword(currentWebDavAccountConfig(), password);
webdavHasSavedPassword.value = true;
return;
}
if (!webdavRememberPassword.value && webdavHasSavedPassword.value) {
await forgetWebdavSavedPassword(currentWebDavAccountConfig());
webdavHasSavedPassword.value = false;
}
}
async function testWebDav() {
await runWebDavAction("test", async () => {
await webdavSyncTest(currentWebDavConfig());
return t("settings.syncTestSuccess");
});
}
async function uploadWebDavSnapshot() {
await runWebDavAction("upload", async () => {
const summary = await webdavSyncUpload(
currentWebDavConfig(),
settingsStore.editorSettings,
webdavSyncSecrets.value ? webdavSecretsPassphrase.value : undefined,
);
return t("settings.syncUploadSuccess", { bytes: summary.bytes, path: summary.remotePath });
});
}
async function downloadWebDavSnapshot() {
if (!window.confirm(t("settings.syncDownloadConfirm"))) return;
await runWebDavAction("download", async () => {
const result = await webdavSyncDownload(
currentWebDavConfig(),
webdavSyncSecrets.value ? webdavSecretsPassphrase.value : undefined,
);
if (result.editorSettings && typeof result.editorSettings === "object") {
settingsStore.updateEditorSettings(result.editorSettings as any);
}
await settingsStore.updateDesktopSettings(result.desktopSettings);
await connectionStore.initFromDisk();
const message = t("settings.syncDownloadSuccess", {
bytes: result.summary.bytes,
path: result.summary.remotePath,
});
if (result.applySummary.encryptedSecretsPresent && !result.applySummary.secretsApplied) {
return `${message} ${t("settings.syncSecretsSkipped")}`;
}
if (result.applySummary.secretsApplied) {
return `${message} ${t("settings.syncSecretsApplied")}`;
}
return message;
});
}
watch(
() => props.open,
async (open) => {
@@ -412,10 +570,25 @@ watch(
await settingsStore.initAiConfig();
await settingsStore.initDesktopSettings();
editShowTrayIcon.value = settingsStore.desktopSettings.show_tray_icon;
webdavPassword.value = "";
await refreshWebDavPasswordStatus();
syncAiEditState();
}
},
{ immediate: true },
);
watch([webdavEndpoint, webdavUsername], () => {
void refreshWebDavPasswordStatus();
});
watch(webdavRememberPassword, (val) => {
localStorage.setItem("dbx-webdav-remember-password", String(val));
});
onMounted(() => {
void refreshWebDavPasswordStatus();
});
const oldPassword = ref("");
const newPassword = ref("");
const confirmNewPassword = ref("");
@@ -1040,11 +1213,16 @@ watch(
</div>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<div class="flex items-center gap-2">
<Label for="auto-select-active-sidebar-node">{{ t("settings.autoSelectActiveSidebarNode") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.autoSelectActiveSidebarNodeDescription") }}
</p>
<Tooltip>
<TooltipTrigger as-child>
<CircleHelp class="h-3.5 w-3.5 cursor-help text-muted-foreground hover:text-foreground" />
</TooltipTrigger>
<TooltipContent class="max-w-[320px] text-xs leading-relaxed" side="top" align="start">
{{ t("settings.autoSelectActiveSidebarNodeDescription") }}
</TooltipContent>
</Tooltip>
</div>
<Switch id="auto-select-active-sidebar-node" v-model="editAutoSelectActiveSidebarNode" />
</div>
@@ -1168,11 +1346,103 @@ watch(
</tbody>
</table>
</div>
</section>
<div class="flex justify-end">
<Button variant="outline" size="sm" @click="restoreDefaultSnippets">
{{ t("settings.snippetsRestoreDefaults") }}
</Button>
<section v-else-if="activeSettingsTab === 'sync'" class="flex flex-col gap-5 py-2">
<div class="space-y-1">
<div class="flex items-center gap-2 text-sm font-medium">
<Cloud class="h-4 w-4 text-muted-foreground" />
{{ t("settings.syncWebDavTitle") }}
</div>
<p class="text-xs text-muted-foreground">{{ t("settings.syncWebDavDescription") }}</p>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div class="space-y-2 md:col-span-2">
<Label for="webdav-endpoint">{{ t("settings.syncEndpoint") }}</Label>
<Input
id="webdav-endpoint"
v-model="webdavEndpoint"
autocomplete="off"
placeholder="https://example.com/remote.php/dav/files/user/"
/>
</div>
<div class="space-y-2">
<Label for="webdav-username">{{ t("settings.syncUsername") }}</Label>
<Input id="webdav-username" v-model="webdavUsername" autocomplete="username" />
</div>
<div class="space-y-2">
<Label for="webdav-password">{{ t("settings.syncPassword") }}</Label>
<div class="relative">
<Input
id="webdav-password"
v-model="webdavPassword"
type="password"
:placeholder="webdavHasSavedPassword ? '••••••••' : '输入密码'"
:disabled="webdavHasSavedPassword"
autocomplete="current-password"
/>
<Button
v-if="webdavHasSavedPassword"
variant="ghost"
size="icon-xs"
class="absolute right-1 top-1/2 -translate-y-1/2"
title="清除已保存的密码"
@click="
webdavRememberPassword = false;
forgetWebdavSavedPassword(currentWebDavAccountConfig());
webdavHasSavedPassword = false;
webdavPassword = '';
"
>
<X class="size-3.5" />
</Button>
</div>
<label class="flex items-center gap-2 text-xs text-muted-foreground">
<input v-model="webdavRememberPassword" type="checkbox" class="h-4 w-4 shrink-0 accent-primary" />
<span>
{{ t("settings.syncRememberWebDavPassword") }}
<span v-if="webdavHasSavedPassword">{{ t("settings.syncSavedPassword") }}</span>
</span>
<Tooltip>
<TooltipTrigger as-child>
<CircleHelp class="h-3.5 w-3.5 cursor-help text-muted-foreground hover:text-foreground" />
</TooltipTrigger>
<TooltipContent class="max-w-[320px] text-xs leading-relaxed" side="top" align="start">
{{ t("settings.syncRememberWebDavPasswordDescription") }}
</TooltipContent>
</Tooltip>
</label>
</div>
<div class="space-y-2 md:col-span-2">
<Label for="webdav-remote-path">{{ t("settings.syncRemotePath") }}</Label>
<Input id="webdav-remote-path" v-model="webdavRemotePath" autocomplete="off" />
<p class="text-xs text-muted-foreground">{{ t("settings.syncRemotePathDescription") }}</p>
</div>
</div>
<div class="rounded-md border bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
{{ t("settings.syncSecretNotice") }}
</div>
<div class="space-y-3 rounded-md border bg-muted/20 px-3 py-3">
<div class="flex items-center justify-between gap-4">
<div class="space-y-1">
<Label for="webdav-sync-secrets">{{ t("settings.syncSecrets") }}</Label>
<p class="text-xs text-muted-foreground">{{ t("settings.syncSecretsDescription") }}</p>
</div>
<Switch id="webdav-sync-secrets" v-model="webdavSyncSecrets" />
</div>
<div v-if="webdavSyncSecrets" class="space-y-2">
<Label for="webdav-secrets-passphrase">{{ t("settings.syncSecretsPassphrase") }}</Label>
<Input
id="webdav-secrets-passphrase"
v-model="webdavSecretsPassphrase"
type="password"
autocomplete="new-password"
/>
<p class="text-xs text-muted-foreground">{{ t("settings.syncSecretsPassphraseDescription") }}</p>
</div>
</div>
</section>
@@ -1532,6 +1802,37 @@ watch(
<Button :disabled="!aiHasChanges()" @click="aiApplySettings">{{ t("settings.apply") }}</Button>
</DialogFooter>
<DialogFooter
v-else-if="activeSettingsTab === 'sync'"
class="mx-0 mb-0 shrink-0 rounded-none border-t border-border/60 bg-transparent px-0 pb-0 pt-3 gap-3 sm:gap-3"
>
<Button variant="outline" @click="emit('update:open', false)">
{{ t("common.close") }}
</Button>
<p
v-if="webdavMessage"
class="text-xs self-center truncate max-w-[280px]"
:class="webdavError ? 'text-destructive' : 'text-green-500'"
>
{{ webdavMessage }}
</p>
<div class="flex-1" />
<Button variant="outline" :disabled="!webdavReady" @click="testWebDav">
<Loader2 v-if="webdavBusy === 'test'" class="mr-1 h-3 w-3 animate-spin" />
{{ t("settings.syncTest") }}
</Button>
<Button variant="outline" :disabled="!webdavReady" @click="downloadWebDavSnapshot">
<Loader2 v-if="webdavBusy === 'download'" class="mr-1 h-3 w-3 animate-spin" />
<Download v-else class="mr-1 h-3 w-3" />
{{ t("settings.syncDownload") }}
</Button>
<Button :disabled="!webdavReady" @click="uploadWebDavSnapshot">
<Loader2 v-if="webdavBusy === 'upload'" class="mr-1 h-3 w-3 animate-spin" />
<Upload v-else class="mr-1 h-3 w-3" />
{{ t("settings.syncUpload") }}
</Button>
</DialogFooter>
<DialogFooter
v-else-if="activeSettingsTab === 'security' && isWeb"
class="mx-0 mb-0 shrink-0 rounded-none border-t border-border/60 bg-transparent px-0 pb-0 pt-3"
+29
View File
@@ -1317,6 +1317,7 @@ export default {
redisTab: "Redis",
shortcutsTab: "Shortcuts",
snippetsTab: "Snippets",
syncTab: "Sync",
aiTab: "AI",
jdbcTab: "JDBC Drivers",
securityTab: "Security",
@@ -1368,6 +1369,34 @@ export default {
snippetsRestoreDefaults: "Restore Defaults",
snippetsAddTitle: "Add Snippet",
snippetsEditTitle: "Edit Snippet",
syncWebDavTitle: "WebDAV Sync",
syncWebDavDescription: "Upload or restore a DBX snapshot from a WebDAV-compatible storage service.",
syncEndpoint: "WebDAV URL",
syncUsername: "Username",
syncPassword: "Password",
syncRememberWebDavPassword: "Remember WebDAV app password",
syncSavedPassword: "(saved)",
syncRememberWebDavPasswordDescription:
"The password is encrypted and stored on this device only. It is not synced to WebDAV and does not replace the sync password.",
syncRemotePath: "Remote snapshot path",
syncRemotePathDescription: "DBX will create missing parent folders when uploading.",
syncSecretNotice:
"By default, DBX syncs connection details and settings only. Database passwords, SSH passwords, proxy passwords, connection strings, and AI API keys stay local.",
syncSecrets: "Sync encrypted secrets",
syncSecretsDescription:
"When enabled, DBX encrypts database passwords, SSH passwords, and AI API keys before uploading them to WebDAV.",
syncSecretsPassphrase: "Sync password",
syncSecretsPassphraseDescription:
"This password is only used to encrypt and restore sensitive data. DBX does not save it; enter it again on each device.",
syncTest: "Test",
syncUpload: "Upload",
syncDownload: "Download",
syncTestSuccess: "WebDAV target is reachable.",
syncUploadSuccess: "Uploaded {bytes} bytes to {path}.",
syncDownloadSuccess: "Downloaded and applied {bytes} bytes from {path}.",
syncSecretsApplied: "Encrypted secrets were restored.",
syncSecretsSkipped: "Encrypted secrets were present but were not restored.",
syncDownloadConfirm: "Download and apply the remote DBX snapshot? Local metadata and saved SQL will be replaced.",
apply: "Apply",
reset: "Reset",
resetDefaults: "Reset Defaults",
+42
View File
@@ -1212,6 +1212,8 @@ export default {
navigationTab: "Navegación",
redisTab: "Redis",
shortcutsTab: "Atajos",
snippetsTab: "Fragmentos",
syncTab: "Sync",
aiTab: "IA",
jdbcTab: "Drivers JDBC",
securityTab: "Seguridad",
@@ -1252,6 +1254,46 @@ export default {
sidebarHiddenTablePrefixesDescription:
"Un prefijo por linea. Solo acorta etiquetas de tablas, vistas y colecciones en la barra lateral; las acciones y ayudas usan el nombre completo.",
sidebarHiddenTablePrefixesPlaceholder: "Ejemplo:\nODS_\nT8Y2_LONG_",
snippetsDescription: "Personaliza plantillas SQL activadas en el editor.",
snippetsAdd: "Agregar fragmento",
snippetsLabel: "Etiqueta",
snippetsPrefix: "Prefijo",
snippetsBody: "SQL",
snippetsLabelPlaceholder: "p. ej. select *",
snippetsPrefixPlaceholder: "p. ej. sel",
snippetsBodyPlaceholder: "SELECT *\nFROM table\nLIMIT 100;",
snippetsRestoreDefaults: "Restaurar valores por defecto",
snippetsAddTitle: "Agregar fragmento",
snippetsEditTitle: "Editar fragmento",
syncWebDavTitle: "Sincronización WebDAV",
syncWebDavDescription: "Sube o restaura una instantánea de DBX desde almacenamiento compatible con WebDAV.",
syncEndpoint: "URL WebDAV",
syncUsername: "Usuario",
syncPassword: "Contraseña",
syncRememberWebDavPassword: "Recordar contraseña de app WebDAV",
syncSavedPassword: "(guardada)",
syncRememberWebDavPasswordDescription:
"La contraseña se cifra y se guarda solo en este dispositivo. No se sincroniza a WebDAV ni reemplaza la contraseña de sync.",
syncRemotePath: "Ruta remota",
syncRemotePathDescription: "DBX creará las carpetas padre faltantes al subir.",
syncSecretNotice:
"De forma predeterminada, DBX solo sincroniza detalles de conexión y ajustes. Las contraseñas de base de datos, SSH, proxy, cadenas de conexión y API keys de IA quedan locales.",
syncSecrets: "Sincronizar secretos cifrados",
syncSecretsDescription:
"Al activarlo, DBX cifra contraseñas de base de datos, contraseñas SSH y API keys de IA antes de subirlas a WebDAV.",
syncSecretsPassphrase: "Contraseña de sync",
syncSecretsPassphraseDescription:
"Esta contraseña solo se usa para cifrar y restaurar datos sensibles. DBX no la guarda; tendrás que ingresarla en cada dispositivo.",
syncTest: "Probar",
syncUpload: "Subir",
syncDownload: "Descargar",
syncTestSuccess: "El destino WebDAV está disponible.",
syncUploadSuccess: "Se subieron {bytes} bytes a {path}.",
syncDownloadSuccess: "Se descargaron y aplicaron {bytes} bytes desde {path}.",
syncSecretsApplied: "Los secretos cifrados fueron restaurados.",
syncSecretsSkipped: "Había secretos cifrados, pero no fueron restaurados.",
syncDownloadConfirm:
"¿Descargar y aplicar la instantánea remota de DBX? Se reemplazarán metadatos locales y SQL guardado.",
apply: "Aplicar",
reset: "Restablecer",
resetDefaults: "Restablecer valores por defecto",
+25
View File
@@ -1294,6 +1294,7 @@ export default {
redisTab: "Redis",
shortcutsTab: "快捷键",
snippetsTab: "代码片段",
syncTab: "同步",
aiTab: "AI",
jdbcTab: "JDBC 驱动",
securityTab: "安全",
@@ -1342,6 +1343,30 @@ export default {
snippetsRestoreDefaults: "恢复默认",
snippetsAddTitle: "添加片段",
snippetsEditTitle: "编辑片段",
syncWebDavTitle: "WebDAV 同步",
syncWebDavDescription: "将 DBX 快照上传到兼容 WebDAV 的存储服务,或从远端恢复。",
syncEndpoint: "WebDAV 地址",
syncUsername: "用户名",
syncPassword: "密码",
syncRememberWebDavPassword: "记住 WebDAV 应用密码",
syncSavedPassword: "(已保存)",
syncRememberWebDavPasswordDescription: "密码会加密保存在本机,不会同步到 WebDAV,也不会替代同步密码。",
syncRemotePath: "远端快照路径",
syncRemotePathDescription: "上传时 DBX 会自动创建缺失的父目录。",
syncSecretNotice: "默认只同步连接信息和设置,不会同步数据库密码、SSH 密码、代理密码、连接串或 AI API Key。",
syncSecrets: "同步加密后的敏感信息",
syncSecretsDescription: "开启后,DBX 会先加密数据库密码、SSH 密码和 AI API Key,再上传到 WebDAV。",
syncSecretsPassphrase: "同步密码",
syncSecretsPassphraseDescription: "这个密码只用于加密和恢复敏感信息,DBX 不会保存;换设备恢复时需要再次输入。",
syncTest: "测试",
syncUpload: "上传",
syncDownload: "下载",
syncTestSuccess: "WebDAV 目标可访问。",
syncUploadSuccess: "已上传 {bytes} 字节到 {path}。",
syncDownloadSuccess: "已从 {path} 下载并应用 {bytes} 字节。",
syncSecretsApplied: "已恢复加密敏感信息。",
syncSecretsSkipped: "远端包含加密敏感信息,但本次未恢复。",
syncDownloadConfirm: "要下载并应用远端 DBX 快照吗?本地元信息和保存的 SQL 会被替换。",
apply: "应用",
reset: "重置",
resetDefaults: "恢复默认",
+10
View File
@@ -142,6 +142,12 @@ export const loadDesktopSettings = forward("loadDesktopSettings");
export const saveDesktopSettings = forward("saveDesktopSettings");
export const loadPinnedTreeNodeIds = forward("loadPinnedTreeNodeIds");
export const savePinnedTreeNodeIds = forward("savePinnedTreeNodeIds");
export const webdavSyncTest = forward("webdavSyncTest");
export const webdavPasswordStatus = forward("webdavPasswordStatus");
export const saveWebdavSavedPassword = forward("saveWebdavSavedPassword");
export const forgetWebdavSavedPassword = forward("forgetWebdavSavedPassword");
export const webdavSyncUpload = forward("webdavSyncUpload");
export const webdavSyncDownload = forward("webdavSyncDownload");
export const saveAiConversation = forward("saveAiConversation");
export const loadAiConversations = forward("loadAiConversations");
export const deleteAiConversation = forward("deleteAiConversation");
@@ -237,6 +243,10 @@ export type {
JavaRuntimeMode,
JavaRuntimeConfig,
DriverInstallProgress,
WebDavConfig,
WebDavPasswordStatus,
WebDavSyncSummary,
WebDavDownloadResult,
UpdateInfo,
RedisDatabaseInfo,
RedisKeyInfo,
+59
View File
@@ -776,6 +776,65 @@ export async function saveDesktopSettings(_settings: DesktopSettings): Promise<v
return;
}
export interface WebDavConfig {
endpoint: string;
username?: string;
password?: string;
remotePath?: string;
}
export interface WebDavSyncSummary {
remotePath: string;
bytes: number;
exportedAt?: string;
appVersion?: string;
}
export interface WebDavDownloadResult {
summary: WebDavSyncSummary;
editorSettings?: unknown;
desktopSettings: DesktopSettings;
applySummary: {
encryptedSecretsPresent: boolean;
secretsApplied: boolean;
};
}
export interface WebDavPasswordStatus {
hasSavedPassword: boolean;
}
export async function webdavSyncTest(_config: WebDavConfig): Promise<void> {
throw new Error("WebDAV sync is only available in the desktop app.");
}
export async function webdavPasswordStatus(_config: WebDavConfig): Promise<WebDavPasswordStatus> {
return { hasSavedPassword: false };
}
export async function saveWebdavSavedPassword(_config: WebDavConfig, _password: string): Promise<void> {
throw new Error("WebDAV sync is only available in the desktop app.");
}
export async function forgetWebdavSavedPassword(_config: WebDavConfig): Promise<void> {
throw new Error("WebDAV sync is only available in the desktop app.");
}
export async function webdavSyncUpload(
_config: WebDavConfig,
_editorSettings?: unknown,
_secretsPassphrase?: string,
): Promise<WebDavSyncSummary> {
throw new Error("WebDAV sync is only available in the desktop app.");
}
export async function webdavSyncDownload(
_config: WebDavConfig,
_secretsPassphrase?: string,
): Promise<WebDavDownloadResult> {
throw new Error("WebDAV sync is only available in the desktop app.");
}
export async function loadPinnedTreeNodeIds(): Promise<string[]> {
return get("/api/app-settings/pinned-tree-node-ids");
}
+59
View File
@@ -93,6 +93,34 @@ export interface DesktopSettings {
show_tray_icon: boolean;
}
export interface WebDavConfig {
endpoint: string;
username?: string;
password?: string;
remotePath?: string;
}
export interface WebDavSyncSummary {
remotePath: string;
bytes: number;
exportedAt?: string;
appVersion?: string;
}
export interface WebDavDownloadResult {
summary: WebDavSyncSummary;
editorSettings?: unknown;
desktopSettings: DesktopSettings;
applySummary: {
encryptedSecretsPresent: boolean;
secretsApplied: boolean;
};
}
export interface WebDavPasswordStatus {
hasSavedPassword: boolean;
}
export interface QueryPagination {
limit: number;
offset: number;
@@ -236,6 +264,37 @@ export async function saveDesktopSettings(settings: DesktopSettings): Promise<vo
return invoke("save_desktop_settings", { settings });
}
export async function webdavSyncTest(config: WebDavConfig): Promise<void> {
return invoke("webdav_sync_test", { config });
}
export async function webdavPasswordStatus(config: WebDavConfig): Promise<WebDavPasswordStatus> {
return invoke("webdav_password_status", { config });
}
export async function saveWebdavSavedPassword(config: WebDavConfig, password: string): Promise<void> {
return invoke("save_webdav_saved_password", { config, password });
}
export async function forgetWebdavSavedPassword(config: WebDavConfig): Promise<void> {
return invoke("forget_webdav_saved_password", { config });
}
export async function webdavSyncUpload(
config: WebDavConfig,
editorSettings?: unknown,
secretsPassphrase?: string,
): Promise<WebDavSyncSummary> {
return invoke("webdav_sync_upload", { config, editorSettings, secretsPassphrase });
}
export async function webdavSyncDownload(
config: WebDavConfig,
secretsPassphrase?: string,
): Promise<WebDavDownloadResult> {
return invoke("webdav_sync_download", { config, secretsPassphrase });
}
export async function loadPinnedTreeNodeIds(): Promise<string[]> {
return invoke("load_pinned_tree_node_ids");
}