feat(sync): extend auto-upload interval range and improve hints

This commit is contained in:
_Mr丶s ゛😁®
2026-10-10 15:15:48 +08:00
committed by GitHub
parent 7653e860dd
commit 8d61a7e944
18 changed files with 185 additions and 9 deletions
@@ -339,7 +339,18 @@ import {
} from "@/lib/settings/settingsSearch";
import { findSettingsSearchHighlightTarget } from "@/lib/settings/settingsSearchHighlight";
import { LOCALE_OPTIONS } from "@/lib/app/localeOptions";
import { DEFAULT_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES, DEFAULT_WEB_DAV_REMOTE_PATH, normalizedWebDavAutoUploadInterval, readSyncMethod, writeWebDavAutoUploadFields, writeWebDavBackupSelection, writeSyncMethod, type SyncMethod } from "@/lib/webdav/webdavAutoUploadConfig";
import {
DEFAULT_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES,
DEFAULT_WEB_DAV_REMOTE_PATH,
MAX_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES,
formatWebDavAutoUploadInterval,
normalizedWebDavAutoUploadInterval,
readSyncMethod,
writeWebDavAutoUploadFields,
writeWebDavBackupSelection,
writeSyncMethod,
type SyncMethod,
} from "@/lib/webdav/webdavAutoUploadConfig";
import { apiUrl, webPath } from "@/lib/common/webPath";
import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, normalizeCustomFontFamilyInput, readableFontFamily, SYSTEM_UI_FONT_FAMILY } from "@/lib/app/appFonts";
import { buildFontFamilyOptions, displayFontFamily, isPresetFontFamily, loadSystemFontNames } from "@/lib/app/fontFamilyOptions";
@@ -4460,6 +4471,15 @@ const webdavSecretsPassphrase = ref("");
const webdavHasSavedSecretsPassphrase = ref(false);
const webdavAutoUploadEnabled = ref(localStorage.getItem("dbx-webdav-auto-upload-enabled") === "true");
const webdavAutoUploadIntervalMinutes = ref(Number(localStorage.getItem("dbx-webdav-auto-upload-interval-minutes") || String(DEFAULT_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES)));
const webdavAutoUploadIntervalHint = computed(() => {
const formatted = formatWebDavAutoUploadInterval(webdavAutoUploadIntervalMinutes.value, locale.value, {
minute: t("settings.syncAutoUploadUnitMinute"),
hour: t("settings.syncAutoUploadUnitHour"),
day: t("settings.syncAutoUploadUnitDay"),
});
const key = formatted.approximate ? "settings.syncAutoUploadScheduleHintApproximate" : "settings.syncAutoUploadScheduleHint";
return t(key, { interval: formatted.interval });
});
const webdavBusy = ref<"" | "test" | "upload" | "download">("");
const webdavMessage = ref("");
const webdavError = ref(false);
@@ -9858,9 +9878,12 @@ LIMIT 100;</pre
</label>
<div class="flex items-center gap-2">
<Label for="webdav-auto-upload-interval" class="text-xs text-muted-foreground">{{ t("settings.syncAutoUploadInterval") }}</Label>
<Input id="webdav-auto-upload-interval" v-model.number="webdavAutoUploadIntervalMinutes" type="number" min="1" max="1440" step="1" class="h-7 w-24 text-xs" :disabled="!webdavAutoUploadEnabled" />
<Input id="webdav-auto-upload-interval" v-model.number="webdavAutoUploadIntervalMinutes" type="number" min="1" :max="MAX_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES" step="1" class="h-7 w-24 text-xs" :disabled="!webdavAutoUploadEnabled" />
<span class="text-xs text-muted-foreground">{{ t("settings.syncAutoUploadMinutes") }}</span>
</div>
<p class="text-xs text-muted-foreground" aria-live="polite">
{{ webdavAutoUploadIntervalHint }}
</p>
<p class="text-xs text-muted-foreground">
{{ t("settings.syncAutoUploadDescription") }}
</p>
@@ -6,23 +6,46 @@ import { readWebDavAutoUploadConfig, readWebDavBackupSelection, WEB_DAV_AUTO_UPL
export function useWebDavAutoUpload() {
const settingsStore = useSettingsStore();
let timer: ReturnType<typeof window.setInterval> | undefined;
const maxTimeoutDelayMs = 2_147_000_000;
let timer: ReturnType<typeof window.setTimeout> | undefined;
let nextUploadAt = 0;
let uploading = false;
function clearTimer() {
if (!timer) return;
window.clearInterval(timer);
if (timer === undefined) return;
window.clearTimeout(timer);
timer = undefined;
}
function armTimer() {
const remainingMs = Math.max(0, nextUploadAt - Date.now());
timer = window.setTimeout(
() => {
if (Date.now() < nextUploadAt) {
armTimer();
return;
}
void runAutoUpload();
const config = readWebDavAutoUploadConfig();
if (!config.enabled || !config.webDavConfig) {
clearTimer();
return;
}
nextUploadAt = Date.now() + config.intervalMinutes * 60_000;
armTimer();
},
Math.min(remainingMs, maxTimeoutDelayMs),
);
}
function schedule() {
clearTimer();
const config = readWebDavAutoUploadConfig();
if (!config.enabled || !config.webDavConfig) return;
timer = window.setInterval(() => {
void runAutoUpload();
}, config.intervalMinutes * 60_000);
nextUploadAt = Date.now() + config.intervalMinutes * 60_000;
armTimer();
}
async function runAutoUpload() {
+5
View File
@@ -7690,6 +7690,11 @@ export default withEnglishFallback({
syncAutoUpload: "Anlıq nüsxələri avtomatik yüklə",
syncAutoUploadInterval: "Hər",
syncAutoUploadMinutes: "dəqiqə",
syncAutoUploadScheduleHint: "Snapshot hər {interval} avtomatik yüklənəcək.",
syncAutoUploadScheduleHintApproximate: "Snapshot təxminən hər {interval} avtomatik yüklənəcək.",
syncAutoUploadUnitMinute: "dəq.",
syncAutoUploadUnitHour: "saat",
syncAutoUploadUnitDay: "gün",
syncAutoUploadDescription: "DBX açıq olduğu müddətdə işləyir. Yerli yadda saxlanılmış SQL-in gözlənilmədən əvəz olunmaması üçün endirmələr yenə əl ilə təsdiq tələb edir.",
syncAutoUploadSuccess: "{path} yoluna {bytes} bayt avtomatik yükləndi.",
syncDownloadSuccess: "{path} yolundan {bytes} bayt endirildi və tətbiq edildi.",
+5
View File
@@ -8752,6 +8752,11 @@ export default {
syncAutoUpload: "Automatically upload snapshots",
syncAutoUploadInterval: "Every",
syncAutoUploadMinutes: "minutes",
syncAutoUploadScheduleHint: "A snapshot will be uploaded every {interval}.",
syncAutoUploadScheduleHintApproximate: "A snapshot will be uploaded about every {interval}.",
syncAutoUploadUnitMinute: "min",
syncAutoUploadUnitHour: "h",
syncAutoUploadUnitDay: "d",
syncAutoUploadDescription: "Runs while DBX is open. Downloads still require manual confirmation to avoid replacing local saved SQL unexpectedly.",
syncAutoUploadSuccess: "Auto-uploaded {bytes} bytes to {path}.",
syncDownloadSuccess: "Downloaded and applied {bytes} bytes from {path}.",
+5
View File
@@ -8018,6 +8018,11 @@ export default withEnglishFallback({
syncAutoUpload: "Subir instantáneas automáticamente",
syncAutoUploadInterval: "Cada",
syncAutoUploadMinutes: "minutos",
syncAutoUploadScheduleHint: "Se subirá una instantánea automáticamente cada {interval}.",
syncAutoUploadScheduleHintApproximate: "Se subirá una instantánea automáticamente aproximadamente cada {interval}.",
syncAutoUploadUnitMinute: "min",
syncAutoUploadUnitHour: "h",
syncAutoUploadUnitDay: "d",
syncAutoUploadDescription: "Se ejecuta mientras DBX está abierto. Las descargas aún requieren confirmación manual para evitar reemplazar SQL guardado localmente.",
syncAutoUploadSuccess: "Se subieron automáticamente {bytes} bytes a {path}.",
syncDownloadSuccess: "Se descargaron y aplicaron {bytes} bytes desde {path}.",
+5
View File
@@ -8474,6 +8474,11 @@ export default withEnglishFallback({
syncAutoUpload: "Unggah snapshot secara otomatis",
syncAutoUploadInterval: "Setiap",
syncAutoUploadMinutes: "menit",
syncAutoUploadScheduleHint: "Snapshot akan diunggah otomatis setiap {interval}.",
syncAutoUploadScheduleHintApproximate: "Snapshot akan diunggah otomatis sekitar setiap {interval}.",
syncAutoUploadUnitMinute: "mnt",
syncAutoUploadUnitHour: "jam",
syncAutoUploadUnitDay: "hari",
syncAutoUploadDescription: "Berjalan selama DBX terbuka. Unduhan tetap memerlukan konfirmasi manual untuk menghindari penggantian SQL tersimpan lokal secara tidak terduga.",
syncAutoUploadSuccess: "Auto-unggah {bytes} byte ke {path}.",
syncDownloadSuccess: "Berhasil mengunduh dan menerapkan {bytes} byte dari {path}.",
+5
View File
@@ -7795,6 +7795,11 @@ export default withEnglishFallback({
syncAutoUpload: "Carica automaticamente gli snapshot",
syncAutoUploadInterval: "Ogni",
syncAutoUploadMinutes: "minuti",
syncAutoUploadScheduleHint: "Lo snapshot verrà caricato automaticamente ogni {interval}.",
syncAutoUploadScheduleHintApproximate: "Lo snapshot verrà caricato automaticamente circa ogni {interval}.",
syncAutoUploadUnitMinute: "min",
syncAutoUploadUnitHour: "h",
syncAutoUploadUnitDay: "g",
syncAutoUploadDescription: "Viene eseguito mentre DBX è aperto. I download richiedono ancora conferma manuale per evitare di sostituire SQL salvati localmente.",
syncAutoUploadSuccess: "Caricati automaticamente {bytes} byte su {path}.",
syncDownloadSuccess: "Scaricati e applicati {bytes} byte da {path}.",
+5
View File
@@ -7808,6 +7808,11 @@ export default withEnglishFallback({
syncAutoUpload: "スナップショットを自動アップロード",
syncAutoUploadInterval: "間隔",
syncAutoUploadMinutes: "分",
syncAutoUploadScheduleHint: "スナップショットを {interval} ごとに自動アップロードします。",
syncAutoUploadScheduleHintApproximate: "スナップショットを約 {interval} ごとに自動アップロードします。",
syncAutoUploadUnitMinute: "分",
syncAutoUploadUnitHour: "時間",
syncAutoUploadUnitDay: "日",
syncAutoUploadDescription: "DBXを開いている間だけ実行されます。ローカルの保存済みSQLを誤って置き換えないよう、ダウンロードは手動確認が必要です。",
syncAutoUploadSuccess: "{bytes}バイトを{path}に自動アップロードしました。",
syncDownloadSuccess: "{path}から{bytes}バイトをダウンロードして適用しました。",
+5
View File
@@ -7635,6 +7635,11 @@ export default withEnglishFallback({
syncAutoUpload: "자동으로 스냅샷 업로드",
syncAutoUploadInterval: "매",
syncAutoUploadMinutes: "분",
syncAutoUploadScheduleHint: "스냅샷을 {interval}마다 자동 업로드합니다.",
syncAutoUploadScheduleHintApproximate: "스냅샷을 약 {interval}마다 자동 업로드합니다.",
syncAutoUploadUnitMinute: "분",
syncAutoUploadUnitHour: "시간",
syncAutoUploadUnitDay: "일",
syncAutoUploadDescription: "DBX가 열려 있는 동안 실행됩니다. 로컬에 저장된 SQL이 예기치 않게 교체되는 것을 방지하기 위해 다운로드는 여전히 수동 확인이 필요합니다.",
syncAutoUploadSuccess: "{path}에 {bytes}바이트를 자동 업로드했습니다.",
syncDownloadSuccess: "{path}에서 {bytes}바이트를 다운로드하여 적용했습니다.",
+5
View File
@@ -7795,6 +7795,11 @@ export default withEnglishFallback({
syncAutoUpload: "Enviar snapshots automaticamente",
syncAutoUploadInterval: "A cada",
syncAutoUploadMinutes: "minutos",
syncAutoUploadScheduleHint: "Um snapshot será enviado automaticamente a cada {interval}.",
syncAutoUploadScheduleHintApproximate: "Um snapshot será enviado automaticamente aproximadamente a cada {interval}.",
syncAutoUploadUnitMinute: "min",
syncAutoUploadUnitHour: "h",
syncAutoUploadUnitDay: "d",
syncAutoUploadDescription: "Executa enquanto o DBX está aberto. Downloads ainda exigem confirmação manual para evitar substituir SQL salvos localmente.",
syncAutoUploadSuccess: "Enviados automaticamente {bytes} bytes para {path}.",
syncDownloadSuccess: "Baixados e aplicados {bytes} bytes de {path}.",
+5
View File
@@ -9048,6 +9048,11 @@ export default withEnglishFallback({
syncAutoUpload: "Автоматически загружать снимки",
syncAutoUploadInterval: "Каждые",
syncAutoUploadMinutes: "мин.",
syncAutoUploadScheduleHint: "Снимок будет автоматически загружаться каждые {interval}.",
syncAutoUploadScheduleHintApproximate: "Снимок будет автоматически загружаться примерно каждые {interval}.",
syncAutoUploadUnitMinute: "мин.",
syncAutoUploadUnitHour: "ч",
syncAutoUploadUnitDay: "дн.",
syncAutoUploadDescription: "Выполняется, пока DBX открыт. Скачивание по-прежнему требует ручного подтверждения, чтобы не заменить локальный сохранённый SQL неожиданно.",
syncAutoUploadSuccess: "Автоматически загружено {bytes} байт в {path}.",
syncDownloadSuccess: "Скачано и применено {bytes} байт из {path}.",
+5
View File
@@ -7588,6 +7588,11 @@ export default withEnglishFallback({
syncAutoUpload: "Anlık görüntüleri otomatik yükle",
syncAutoUploadInterval: "Her",
syncAutoUploadMinutes: "dakika",
syncAutoUploadScheduleHint: "Anlık görüntü her {interval} otomatik yüklenecek.",
syncAutoUploadScheduleHintApproximate: "Anlık görüntü yaklaşık her {interval} otomatik yüklenecek.",
syncAutoUploadUnitMinute: "dk",
syncAutoUploadUnitHour: "sa",
syncAutoUploadUnitDay: "gün",
syncAutoUploadDescription: "DBX açıkken çalışır. Yerel kayıtlı SQL'in beklenmedik biçimde değiştirilmemesi için indirmeler yine elle onay gerektirir.",
syncAutoUploadSuccess: "{path} konumuna otomatik olarak {bytes} bayt yüklendi.",
syncDownloadSuccess: "{path} konumundan {bytes} bayt indirildi ve uygulandı.",
+5
View File
@@ -8736,6 +8736,11 @@ export default withEnglishFallback({
syncAutoUpload: "自动上传快照",
syncAutoUploadInterval: "每",
syncAutoUploadMinutes: "分钟",
syncAutoUploadScheduleHint: "每 {interval} 自动上传一次快照。",
syncAutoUploadScheduleHintApproximate: "约每 {interval} 自动上传一次快照。",
syncAutoUploadUnitMinute: "分钟",
syncAutoUploadUnitHour: "小时",
syncAutoUploadUnitDay: "天",
syncAutoUploadDescription: "仅在 DBX 打开时运行。下载仍需手动确认,避免意外覆盖本地保存的 SQL。",
syncAutoUploadSuccess: "已自动上传 {bytes} 字节到 {path}。",
syncDownloadSuccess: "已从 {path} 下载并应用 {bytes} 字节。",
+5
View File
@@ -7142,6 +7142,11 @@ export default withEnglishFallback({
syncAutoUpload: "自動上傳快照",
syncAutoUploadInterval: "每",
syncAutoUploadMinutes: "分鐘",
syncAutoUploadScheduleHint: "每 {interval} 自動上傳一次快照。",
syncAutoUploadScheduleHintApproximate: "約每 {interval} 自動上傳一次快照。",
syncAutoUploadUnitMinute: "分鐘",
syncAutoUploadUnitHour: "小時",
syncAutoUploadUnitDay: "天",
syncAutoUploadDescription: "僅在 DBX 開啟時執行。下載仍需手動確認,避免意外覆蓋本機已儲存的 SQL。",
syncAutoUploadSuccess: "已自動上傳 {bytes} 位元組到 {path}。",
syncDownloadSuccess: "已從 {path} 下載並套用 {bytes} 位元組。",
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES, MAX_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES, formatWebDavAutoUploadInterval, normalizedWebDavAutoUploadInterval } from "./webdavAutoUploadConfig";
const zhCnUnits = { minute: "分钟", hour: "小时", day: "天" };
describe("WebDAV auto-upload interval", () => {
it("keeps configured intervals in minutes and accepts up to one year", () => {
expect(normalizedWebDavAutoUploadInterval(90)).toBe(90);
expect(normalizedWebDavAutoUploadInterval(MAX_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES + 1)).toBe(MAX_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES);
expect(normalizedWebDavAutoUploadInterval("invalid")).toBe(DEFAULT_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES);
});
it.each([
[30, "30 分钟", false],
[60, "1 小时", false],
[90, "1 小时 30 分钟", false],
[95, "1 小时 35 分钟", false],
[1439, "23 小时 59 分钟", false],
[1440, "1 天", false],
[1441, "1 天 1 分钟", false],
[1530, "1 天 1 小时 30 分钟", false],
])("formats %i minutes as a readable interval", (minutes, interval, approximate) => {
expect(formatWebDavAutoUploadInterval(minutes, "zh-CN", zhCnUnits)).toEqual({ interval, approximate });
});
});
@@ -7,6 +7,7 @@ export const WEB_DAV_AUTO_UPLOAD_STORAGE_KEYS = ["dbx-webdav-endpoint", "dbx-web
export const DEFAULT_WEB_DAV_REMOTE_PATH = "DBX/sync/snapshot.json";
export const DEFAULT_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES = 30;
export const MAX_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES = 365 * 24 * 60;
export type SyncMethod = "webdav" | "snippet" | "local";
@@ -28,7 +29,48 @@ export interface WebDavAutoUploadConfig {
export function normalizedWebDavAutoUploadInterval(value: unknown): number {
const numberValue = Number(value);
if (!Number.isFinite(numberValue)) return DEFAULT_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES;
return Math.max(1, Math.min(1440, Math.round(numberValue)));
return Math.max(1, Math.min(MAX_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES, Math.round(numberValue)));
}
export interface WebDavAutoUploadIntervalUnits {
minute: string;
hour: string;
day: string;
}
export interface FormattedWebDavAutoUploadInterval {
interval: string;
approximate: boolean;
}
export function formatWebDavAutoUploadInterval(value: unknown, locale: string, units: WebDavAutoUploadIntervalUnits): FormattedWebDavAutoUploadInterval {
const minutes = normalizedWebDavAutoUploadInterval(value);
const formatNumber = (number: number) => new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(number);
const formatHourMinutes = (hourMinutes: number) => {
const hours = Math.floor(hourMinutes / 60);
const remainderMinutes = hourMinutes % 60;
if (hours === 0) return { interval: `${formatNumber(remainderMinutes)} ${units.minute}`, approximate: false };
if (remainderMinutes === 0) return { interval: `${formatNumber(hours)} ${units.hour}`, approximate: false };
return { interval: `${formatNumber(hours)} ${units.hour} ${formatNumber(remainderMinutes)} ${units.minute}`, approximate: false };
};
if (minutes < 60) {
return { interval: `${formatNumber(minutes)} ${units.minute}`, approximate: false };
}
if (minutes < 24 * 60) {
return formatHourMinutes(minutes);
}
const days = Math.floor(minutes / (24 * 60));
const remainingMinutes = minutes % (24 * 60);
if (remainingMinutes === 0) {
return { interval: `${formatNumber(days)} ${units.day}`, approximate: false };
}
const remaining = formatHourMinutes(remainingMinutes);
return {
interval: `${formatNumber(days)} ${units.day} ${remaining.interval}`,
approximate: false,
};
}
export function readWebDavAutoUploadConfig(): WebDavAutoUploadConfig {
+1
View File
@@ -64,6 +64,7 @@ const backendUrl = process.env.DBX_BACKEND_URL || "http://localhost:4224";
export default defineConfig(async () => ({
root: import.meta.dirname,
cacheDir: process.env.DBX_DEV_CACHE_DIR,
base: viteBase,
plugins: [connectionTypesPlugin(), publicBasePathRedirectPlugin(publicBasePath), vue(), tailwindcss()],
resolve: {
+2
View File
@@ -56,6 +56,8 @@ function installWindow() {
Object.defineProperty(globalThis, "window", {
configurable: true,
value: {
setTimeout: globalThis.setTimeout.bind(globalThis),
clearTimeout: globalThis.clearTimeout.bind(globalThis),
setInterval: globalThis.setInterval.bind(globalThis),
clearInterval: globalThis.clearInterval.bind(globalThis),
addEventListener: (type: string, listener: (event: Event) => void) => {