From 0f48980b7f47892ca8f7fd243aea1dced5632441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?He=CC=84sperus?= Date: Sun, 9 Aug 2026 00:59:34 +0800 Subject: [PATCH 1/6] fix(storage): fix backend unset bug during encryption toggling --- modules.json | 20 ++--- packages/encryption/src/i18n.ts | 90 +++++++++++++++++++ packages/encryption/src/index.ts | 4 +- packages/encryption/src/setting.ts | 6 +- packages/i18n/src/ru/translations.ts | 6 +- packages/i18n/src/zh-TW/translations.ts | 6 +- packages/i18n/src/zh/translations.ts | 6 +- packages/plugin/CHANGELOG.md | 5 ++ packages/plugin/dist/dev.spec.d.ts | 2 +- ...vHT.spec.d.ts => index-B5tgeZSc.spec.d.ts} | 10 +-- packages/plugin/dist/index.spec.d.ts | 2 +- .../plugin/src/components/MigrationModal.ts | 9 +- packages/plugin/src/en.ts | 8 +- packages/plugin/src/modules/Registrar.ts | 2 +- packages/plugin/src/modules/Storage.ts | 24 +++-- packages/plugin/src/settings/development.ts | 50 +++-------- packages/plugin/src/settings/features.ts | 5 +- packages/s3/src/i18n.ts | 71 +++++++++++++++ packages/s3/src/index.ts | 4 +- packages/smart-merge/src/i18n.ts | 27 ++++++ packages/smart-merge/src/index.ts | 4 +- packages/webdav/src/i18n.ts | 44 +++++++++ packages/webdav/src/index.ts | 4 +- 23 files changed, 315 insertions(+), 94 deletions(-) rename packages/plugin/dist/{index-3gsR9vHT.spec.d.ts => index-B5tgeZSc.spec.d.ts} (99%) diff --git a/modules.json b/modules.json index 29c96ca..833e822 100644 --- a/modules.json +++ b/modules.json @@ -2,7 +2,7 @@ { "id": "webdav", "name": "WebDAV", - "version": "0.1.7", + "version": "0.1.8", "description": "WebDAV backend support.", "icon": "server", "main": "https://sync.consensia.cc/modules/webdav.js", @@ -11,7 +11,7 @@ { "id": "s3", "name": "S3", - "version": "0.0.1", + "version": "0.0.2", "description": "S3 and S3-compatible backend support.", "icon": "server", "main": "https://sync.consensia.cc/modules/s3.js", @@ -20,7 +20,7 @@ { "id": "encryption", "name": "Encryption", - "version": "0.1.0", + "version": "0.1.1", "description": "Client-side encrypt vault files before uploading to backend.", "icon": "key-round", "main": "https://sync.consensia.cc/modules/encryption.js", @@ -29,34 +29,34 @@ { "id": "i18n-zh", "name": "I18n 简体中文", - "version": "0.0.9", + "version": "0.0.10", "description": "Simplified Chinese UI language pack. / 简体中文介面语言包。", "icon": "languages", "main": "https://sync.consensia.cc/modules/i18n-zh.js", - "minPluginVersion": "3.0.0" + "minPluginVersion": "3.0.3" }, { "id": "i18n-zh-TW", "name": "I18n 繁體中文", - "version": "0.0.1", + "version": "0.0.2", "description": "Traditional Chinese UI language pack. / 繁體中文介面語言包。", "icon": "languages", "main": "https://sync.consensia.cc/modules/i18n-zh-TW.js", - "minPluginVersion": "3.0.0" + "minPluginVersion": "3.0.3" }, { "id": "i18n-ru", "name": "I18n Русский", - "version": "0.0.1", + "version": "0.0.2", "description": "Russian UI language pack. / Пакет русского интерфейса.", "icon": "languages", "main": "https://sync.consensia.cc/modules/i18n-ru.js", - "minPluginVersion": "3.0.0" + "minPluginVersion": "3.0.3" }, { "id": "smart-merge", "name": "Smart Merge", - "version": "0.0.5", + "version": "0.0.6", "description": "Smart merge conflict resolution strategy that applies recursive three-way merge.", "icon": "combine", "main": "https://sync.consensia.cc/modules/smart-merge.js", diff --git a/packages/encryption/src/i18n.ts b/packages/encryption/src/i18n.ts index 7bf5a86..84fa436 100644 --- a/packages/encryption/src/i18n.ts +++ b/packages/encryption/src/i18n.ts @@ -75,3 +75,93 @@ export const zh: EncryptionTranslations = { } }, }; + +export const zhTW: EncryptionTranslations = { + encryption: '加密', + encryptionDescription: + '在上傳前加密檔案,並在下載時解密檔案。加密密碼儲存於 Obsidian 金鑰圈中。', + encryptionMigration: (frag, mode) => { + if (mode === 'enable') { + frag.createEl('p', { + text: '⚠️ 在啟用加密前,請務必留意以下幾點:', + }); + const ol = frag.createEl('ol'); + ol.createEl('li', { text: '後續的所有上傳都將進行加密。' }); + ol.createEl('li', { text: '請確保所有裝置皆已啟用加密。' }); + ol.createEl('li', { + text: '若您先前曾於未加密的狀態下進行同步,則必須執行遷移。', + }); + const li = ol.createEl('li', { + text: '您應確保所有裝置上的以下項目完全一致:', + }); + const ul = li.createEl('ul'); + const subItems = ['加密密碼', '伺服器 URL', '帳號名稱']; + subItems.forEach((item) => ul.createEl('li', { text: item })); + ol.createEl('li', { + text: '加密演算法會將解密金鑰與檔案位置及伺服器識別資訊綁定,這能提供極佳的安全性與資料完整性。但也意味著若您使用不同的伺服器,或在未透過相同演算法的情況下將檔案移動至不同位置,您將無法解密該檔案。', + }); + ol.createEl('li', { + text: '請避免在伺服器端手動管理加密檔案。', + }); + } else { + frag.createEl('p', { + text: '⚠️ 在停用加密前,請務必留意以下幾點:', + }); + const ol = frag.createEl('ol'); + ol.createEl('li', { + text: '後續的所有上傳都將以未加密的明文形式進行。', + }); + ol.createEl('li', { text: '請確保所有裝置皆已停用加密。' }); + ol.createEl('li', { + text: '若此儲存庫先前是在啟用加密的狀態下上傳,則必須執行遷移。', + }); + } + }, +}; + +export const ru: EncryptionTranslations = { + encryption: 'Шифрование', + encryptionDescription: + 'Шифровать файлы перед загрузкой на сервер и расшифровывать их при скачивании. Пароль шифрования хранится в связке ключей Obsidian keychain.', + encryptionMigration: (frag, mode) => { + if (mode === 'enable') { + frag.createEl('p', { + text: '⚠️ Пожалуйста, будьте внимательны к следующим моментам перед включением шифрования:', + }); + const ol = frag.createEl('ol'); + ol.createEl('li', { text: 'Все последующие загрузки будут зашифрованы.' }); + ol.createEl('li', { + text: 'Пожалуйста, убедитесь, что шифрование включено на всех устройствах.', + }); + ol.createEl('li', { + text: 'Миграция необходима, если ранее вы синхронизировали данные без шифрования.', + }); + const li = ol.createEl('li', { + text: 'Убедитесь, что следующие параметры совпадают на всех ваших устройствах:', + }); + const ul = li.createEl('ul'); + const subItems = ['пароль шифрования', 'URL-адрес сервера', 'имя аккаунта']; + subItems.forEach((item) => ul.createEl('li', { text: item })); + ol.createEl('li', { + text: 'Алгоритм шифрования привязывает ключ расшифровки к расположению файла и идентификатору сервера, что обеспечивает гораздо более высокую безопасность и целостность данных. Но это также означает, что при использовании другого сервера или перемещении файла в другое место без использования того же алгоритма вы не сможете его расшифровать.', + }); + ol.createEl('li', { + text: 'Пожалуйста, избегайте ручного управления зашифрованными файлами на сервере.', + }); + } else { + frag.createEl('p', { + text: '⚠️ Пожалуйста, будьте внимательны к следующим моментам перед отключением шифрования:', + }); + const ol = frag.createEl('ol'); + ol.createEl('li', { + text: 'Все последующие загрузки будут выполняться в открытом виде без шифрования.', + }); + ol.createEl('li', { + text: 'Пожалуйста, убедитесь, что шифрование отключено на всех устройствах.', + }); + ol.createEl('li', { + text: 'Миграция необходима, если это хранилище (vault) ранее загружалось с включённым шифрованием.', + }); + } + }, +}; diff --git a/packages/encryption/src/index.ts b/packages/encryption/src/index.ts index 58f0f5c..29c2d1b 100644 --- a/packages/encryption/src/index.ts +++ b/packages/encryption/src/index.ts @@ -9,7 +9,7 @@ import type { import type { App } from 'obsidian'; import type { EncryptionDB } from '@/wrapper'; import encryptionWrapper from '@/wrapper'; -import { en, zh } from './i18n'; +import { en, zh, ru, zhTW } from './i18n'; import encryptionSetting from './setting'; export type EncryptionSettings = { @@ -31,6 +31,8 @@ export default class Encryption { ) { ctx.registerI18n('en', en); ctx.registerI18n('zh', zh); + ctx.registerI18n('ru', ru); + ctx.registerI18n('zh-TW', zhTW); } moduleSettings: EncryptionSettings = { diff --git a/packages/encryption/src/setting.ts b/packages/encryption/src/setting.ts index bd5a82b..1926fde 100644 --- a/packages/encryption/src/setting.ts +++ b/packages/encryption/src/setting.ts @@ -1,5 +1,5 @@ import type { EncryptionSettings } from '@'; -import type { Context, Fragment, Translate } from '@hesprs/sync-engine-sdk'; +import type { Context, Fragment, MaybePromise, Translate } from '@hesprs/sync-engine-sdk'; import type { App } from 'obsidian'; import { setNeedMigration } from '@hesprs/sync-engine-sdk'; import { SecretComponent, Setting } from 'obsidian'; @@ -16,7 +16,7 @@ export default function encryptionSetting( translate: Translate; app: App; saveSettings: () => Promise; - recordStoreExists: () => Promise; + recordStoreExists: () => MaybePromise; }, settings: EncryptionSettings, ) { @@ -38,7 +38,7 @@ export default function encryptionSetting( void saveSettings(); }, content: (value) => translate('encryptionMigration', value ? 'enable' : 'disable'), - needMigration: () => recordStoreExists(), + needMigration: recordStoreExists, toggle: toggle.setValue(settings.enabled), }), ); diff --git a/packages/i18n/src/ru/translations.ts b/packages/i18n/src/ru/translations.ts index 7f73fec..075b0b5 100644 --- a/packages/i18n/src/ru/translations.ts +++ b/packages/i18n/src/ru/translations.ts @@ -4,7 +4,6 @@ const ru: Translations = { add: 'Добавить', addRecord: 'Добавить запись', addSecretHeader: 'Добавить секретный заголовок', - allRecordsCleared: 'Все записи очищены', asymmetricStorage: 'Асимметричное хранилище', asymmetricStorageDescription: (frag) => { frag.appendText('Используйте '); @@ -59,11 +58,10 @@ const ru: Translations = { checkConnection: 'Проверить соединение', checkConnectionFailed: 'Ошибка проверки соединения', checkConnectionSuccess: 'Соединение успешно проверено', - clearAllRecords: 'Очистить все записи', + clear: 'Очистить', clearRecords: 'Очистить записи', clearRecordsDescription: 'Sync Engine записывает состояния синхронизации для разрешения операций между локальными и удалёнными файлами. Эта опция позволяет выборочно очищать записи. Внимание: это действие может привести к потере данных.', - clearVaultRecords: 'Очистить записи хранилища', completed: 'Завершено', completedNoop: 'Уже синхронизировано', configurations: 'Конфигурации', @@ -207,6 +205,7 @@ const ru: Translations = { realtimeSyncFastModeDescription: 'Использовать кэшированные данные и избегать лишней проверки удалённых файлов во время синхронизации в реальном времени для ускорения процесса.', realtimeSyncPlaceholder: 'Введите задержку (например, 500ms, 5s)', + recordsCleared: 'Записи очищены', remoteMigration: 'Миграция удалённого хранилища', remove: 'Удалить', removeLocal: 'Удалить локальный файл', @@ -285,7 +284,6 @@ const ru: Translations = { 'Укажите источник, из которого этот модуль будет получать обновления. Оставьте поле пустым, чтобы отключить обновления.', updateSourcePlaceholder: 'https://example.com/modules.json', upload: 'Загрузить', - vaultRecordsCleared: 'Записи хранилища очищены', walkingRemote: 'Сканирование удалённых файлов', }; diff --git a/packages/i18n/src/zh-TW/translations.ts b/packages/i18n/src/zh-TW/translations.ts index 328884a..dc91040 100644 --- a/packages/i18n/src/zh-TW/translations.ts +++ b/packages/i18n/src/zh-TW/translations.ts @@ -4,7 +4,6 @@ const zhTW: Translations = { add: '新增', addRecord: '新增紀錄', addSecretHeader: '新增加密標頭', - allRecordsCleared: '已清除所有紀錄', asymmetricStorage: '非對稱儲存', asymmetricStorageDescription: (frag) => { frag.appendText('使用'); @@ -59,11 +58,10 @@ const zhTW: Translations = { checkConnection: '測試連線', checkConnectionFailed: '連線測試失敗', checkConnectionSuccess: '連線測試成功', - clearAllRecords: '清除所有紀錄', + clear: '清除', clearRecords: '清除紀錄', clearRecordsDescription: 'Sync Engine 會記錄同步狀態以處理本地與遠端檔案之間的變更。此選項允許您選擇性地清除紀錄。警告:此操作可能會導致資料遺失。', - clearVaultRecords: '清除儲存庫紀錄', completed: '已完成', completedNoop: '已是最新狀態', configurations: '設定項目', @@ -200,6 +198,7 @@ const zhTW: Translations = { realtimeSyncFastModeDescription: '在即時同步過程中重複使用快取資料並跳過不必要的遠端掃描,以加快同步速度。', realtimeSyncPlaceholder: '輸入同步延遲(例如 500ms, 5s)', + recordsCleared: '紀錄已清除', remoteMigration: '遠端遷移', remove: '移除', removeLocal: '移除本地', @@ -272,7 +271,6 @@ const zhTW: Translations = { updateSourceDescription: '設定此模組接收更新的模組來源。留空則停用更新。', updateSourcePlaceholder: 'https://example.com/modules.json', upload: '上傳', - vaultRecordsCleared: '已清除儲存庫紀錄', walkingRemote: '正在掃描遠端檔案', }; diff --git a/packages/i18n/src/zh/translations.ts b/packages/i18n/src/zh/translations.ts index c1df986..9b91045 100644 --- a/packages/i18n/src/zh/translations.ts +++ b/packages/i18n/src/zh/translations.ts @@ -4,7 +4,6 @@ const zh: Translations = { add: '添加', addRecord: '添加记录', addSecretHeader: '添加机密请求头', - allRecordsCleared: '所有记录已清除', asymmetricStorage: '非对称存储', asymmetricStorageDescription: (frag) => { frag.appendText('使用 '); @@ -47,11 +46,10 @@ const zh: Translations = { checkConnection: '测试连接', checkConnectionFailed: '测试连接失败', checkConnectionSuccess: '测试连接成功', - clearAllRecords: '清除所有记录', + clear: '清除', clearRecords: '清除记录', clearRecordsDescription: 'Sync Engine 会记录同步状态,以便在本地和远程文件之间解析同步操作。此选项允许您选择性地清除记录。警告:此操作很可能会导致数据丢失。', - clearVaultRecords: '清除库记录', completed: '已完成', completedNoop: '已是最新状态', configurations: '配置', @@ -188,6 +186,7 @@ const zh: Translations = { realtimeSyncFastModeDescription: '在实时同步过程中复用缓存数据并避免不必要的远程探测,以加速同步。', realtimeSyncPlaceholder: '输入同步延迟(例如 500ms, 5s)', + recordsCleared: '记录已清除', remoteMigration: '远程迁移', remove: '移除', removeLocal: '移除本地', @@ -259,7 +258,6 @@ const zh: Translations = { updateSourceDescription: '设置此模块接收更新的模块源。留空以禁用更新。', updateSourcePlaceholder: 'https://example.com/modules.json', upload: '上传', - vaultRecordsCleared: '库记录已清除', walkingRemote: '正在探测远程文件', }; diff --git a/packages/plugin/CHANGELOG.md b/packages/plugin/CHANGELOG.md index 364989b..e8beb10 100644 --- a/packages/plugin/CHANGELOG.md +++ b/packages/plugin/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. +## Sync Engine v3.0.3 - 2026-08-08 + +- Fixed the bug that when a backend is unselected, setting encryption or asymmetric storage does nothing. +- Removed `Clear all records` button as it is confusing and dangerous. + ## Sync Engine v3.0.2 - 2026-08-07 - Animate status bar icon when a sync is running. diff --git a/packages/plugin/dist/dev.spec.d.ts b/packages/plugin/dist/dev.spec.d.ts index 756f24a..c30372f 100644 --- a/packages/plugin/dist/dev.spec.d.ts +++ b/packages/plugin/dist/dev.spec.d.ts @@ -1,4 +1,4 @@ -import { Ct as FolderStat, St as FileStat, f as Request, p as RequestParam, ut as Fs, vt as RootFs, xt as Binary, yt as WrappedFs } from "./index-3gsR9vHT.spec.js"; +import { Ct as FolderStat, St as FileStat, f as Request, p as RequestParam, ut as Fs, vt as RootFs, xt as Binary, yt as WrappedFs } from "./index-B5tgeZSc.spec.js"; //#region src/sdk/debug-wrapper.d.ts declare function debugWrapper(original: Fs, log: (content: string) => void): WrappedFs; //#endregion diff --git a/packages/plugin/dist/index-3gsR9vHT.spec.d.ts b/packages/plugin/dist/index-B5tgeZSc.spec.d.ts similarity index 99% rename from packages/plugin/dist/index-3gsR9vHT.spec.d.ts rename to packages/plugin/dist/index-B5tgeZSc.spec.d.ts index 6beda1d..e0e70e4 100644 --- a/packages/plugin/dist/index-3gsR9vHT.spec.d.ts +++ b/packages/plugin/dist/index-B5tgeZSc.spec.d.ts @@ -262,7 +262,7 @@ declare class Storage { private readonly recordStoreExists; readonly root: { clearRecordStores: () => Promise; - deleteRecordStore: (namespace?: string) => Promise; + deleteRecordStore: (namespace?: string) => MaybePromise; getRecordStore: (namespace?: string) => { get(key: string): Promise; set(key: string, value: RecordStat): Promise; @@ -292,7 +292,7 @@ declare class Storage { setMeta(key: T, value: any): void; dispose(): void; }; - recordStoreExists: (namespace?: string) => Promise; + recordStoreExists: (namespace?: string) => MaybePromise; }; readonly dispose: () => void; } @@ -799,11 +799,9 @@ type ControlsSettingTranslations = { //#region src/settings/development.d.ts type DevelopmentSettingTranslations = { development: string; - vaultRecordsCleared: string; - clearVaultRecords: string; - clearAllRecords: string; - allRecordsCleared: string; clearRecords: string; + recordsCleared: string; + clear: string; clearRecordsDescription: string; export: string; exportLogsDescription: string; diff --git a/packages/plugin/dist/index.spec.d.ts b/packages/plugin/dist/index.spec.d.ts index 879b3dd..86950e5 100644 --- a/packages/plugin/dist/index.spec.d.ts +++ b/packages/plugin/dist/index.spec.d.ts @@ -1,2 +1,2 @@ -import { $ as TranslationResource, A as SyncTerminateReason, B as MoveRemote, C as SelectFromContext, Ct as FolderStat, D as writeWithValue, Dt as RecordStatsMap, E as readWithSize, Et as RecordStat, F as Upload, G as BaseTask, H as Download, I as ResolveConflict, J as TaskNames, K as ConflictResolver, L as RemoveRemote, M as Decider, N as DeciderInput, O as prefixWrapper, Ot as Stat, P as TaskFactory, Q as Translate, R as RemoveRecord, S as ModuleMeta, St as FileStat, T as pipe, Tt as Progress, U as CreateRemoteDir, V as MoveLocal, W as AddRecord, X as Fragment, Y as RecordStore, Z as ObsidianLanguageCode, _ as Events, _t as OutputAtom, a as FsWrapperEntry, at as StoreOperations, b as ExistingMemoryDB, bt as WriteAtom, c as RemoteFsEntry, ct as CustomAtom, d as RemoteRequestMiddlewareEntry, dt as InputAtom, et as Dispatch, f as Request, ft as ListReporter, g as Context, gt as OptimizerOutput, h as SettingEntry, ht as OptimizerInput, i as DeciderEntry, it as StoreAsync, j as CreateLocalDir, k as setNeedMigration, kt as StatsMap, l as RemoteLister, lt as DeleteAtom, m as RequestResponse, mt as MoveAtom, n as CheckConnectionResult, nt as DatabaseAsync, o as LocalRequestMiddlewareEntry, ot as StoreSync, p as RequestParam, pt as MkdirAtom, q as ConflictResolverPayload, r as ConflictResolverEntry, rt as DatabaseSync, s as OptimizerEntry, st as BatchOptimizer, t as VaultRequest, tt as On, u as RemoteListerEntry, ut as Fs, v as Settings, vt as RootFs, w as digOriginal, wt as MaybePromise, x as AugmentedModuleMeta, xt as Binary, y as Translations, yt as WrappedFs, z as RemoveLocal } from "./index-3gsR9vHT.spec.js"; +import { $ as TranslationResource, A as SyncTerminateReason, B as MoveRemote, C as SelectFromContext, Ct as FolderStat, D as writeWithValue, Dt as RecordStatsMap, E as readWithSize, Et as RecordStat, F as Upload, G as BaseTask, H as Download, I as ResolveConflict, J as TaskNames, K as ConflictResolver, L as RemoveRemote, M as Decider, N as DeciderInput, O as prefixWrapper, Ot as Stat, P as TaskFactory, Q as Translate, R as RemoveRecord, S as ModuleMeta, St as FileStat, T as pipe, Tt as Progress, U as CreateRemoteDir, V as MoveLocal, W as AddRecord, X as Fragment, Y as RecordStore, Z as ObsidianLanguageCode, _ as Events, _t as OutputAtom, a as FsWrapperEntry, at as StoreOperations, b as ExistingMemoryDB, bt as WriteAtom, c as RemoteFsEntry, ct as CustomAtom, d as RemoteRequestMiddlewareEntry, dt as InputAtom, et as Dispatch, f as Request, ft as ListReporter, g as Context, gt as OptimizerOutput, h as SettingEntry, ht as OptimizerInput, i as DeciderEntry, it as StoreAsync, j as CreateLocalDir, k as setNeedMigration, kt as StatsMap, l as RemoteLister, lt as DeleteAtom, m as RequestResponse, mt as MoveAtom, n as CheckConnectionResult, nt as DatabaseAsync, o as LocalRequestMiddlewareEntry, ot as StoreSync, p as RequestParam, pt as MkdirAtom, q as ConflictResolverPayload, r as ConflictResolverEntry, rt as DatabaseSync, s as OptimizerEntry, st as BatchOptimizer, t as VaultRequest, tt as On, u as RemoteListerEntry, ut as Fs, v as Settings, vt as RootFs, w as digOriginal, wt as MaybePromise, x as AugmentedModuleMeta, xt as Binary, y as Translations, yt as WrappedFs, z as RemoveLocal } from "./index-B5tgeZSc.spec.js"; export { type AddRecord, type AugmentedModuleMeta, type BaseTask, type BatchOptimizer, type Binary, type CheckConnectionResult, type ConflictResolver, type ConflictResolverEntry, type ConflictResolverPayload, type Context, type CreateLocalDir, type CreateRemoteDir, type CustomAtom, type DatabaseAsync, type DatabaseSync, type Decider, type DeciderEntry, type DeciderInput, type DeleteAtom, type Dispatch, type Download, type Events, type ExistingMemoryDB, type FileStat, type FolderStat, type Fragment, type Fs, type FsWrapperEntry, type InputAtom, type ListReporter, type LocalRequestMiddlewareEntry, type MaybePromise, type MkdirAtom, type ModuleMeta, type MoveAtom, type MoveLocal, type MoveRemote, type ObsidianLanguageCode, type On, type OptimizerEntry, type OptimizerInput, type OptimizerOutput, type OutputAtom, type Progress, type RecordStat, type RecordStatsMap, type RecordStore, type RemoteFsEntry, type RemoteLister, type RemoteListerEntry, type RemoteRequestMiddlewareEntry, type RemoveLocal, type RemoveRecord, type RemoveRemote, type Request, type RequestParam, type RequestResponse, type ResolveConflict, type RootFs, SelectFromContext, type SettingEntry, type Settings, type Stat, type StatsMap, type StoreAsync, type StoreOperations, type StoreSync, type SyncTerminateReason, type TaskFactory, type TaskNames, type Translate, type TranslationResource, type Translations, type Upload, type VaultRequest, type WrappedFs, type WriteAtom, digOriginal, pipe, prefixWrapper, readWithSize, setNeedMigration, writeWithValue }; \ No newline at end of file diff --git a/packages/plugin/src/components/MigrationModal.ts b/packages/plugin/src/components/MigrationModal.ts index 17151d1..e6609f9 100644 --- a/packages/plugin/src/components/MigrationModal.ts +++ b/packages/plugin/src/components/MigrationModal.ts @@ -203,7 +203,7 @@ export default function setNeedMigration( selfTrigger = false; return; } - void Promise.resolve(needMigration?.(value) ?? true).then((need) => { + const showMigration = async (need: boolean) => { if (need) { selfTrigger = true; toggle.setValue(!value); // Revert UI back, not migrated yet @@ -215,7 +215,10 @@ export default function setNeedMigration( }, content: content(value), }).open(); - } else void apply(value); - }); + } else await apply(value); + }; + const need = needMigration?.(value) ?? true; + if (need instanceof Promise) void need.then(showMigration); + else void showMigration(need); }); } diff --git a/packages/plugin/src/en.ts b/packages/plugin/src/en.ts index 428769c..48b7f95 100644 --- a/packages/plugin/src/en.ts +++ b/packages/plugin/src/en.ts @@ -4,7 +4,6 @@ const en: Translations = { add: 'Add', addRecord: 'Add record', addSecretHeader: 'Add secret header', - allRecordsCleared: 'All records cleared', asymmetricStorage: 'Asymmetric storage', asymmetricStorageDescription: (frag) => { frag.appendText('Use '); @@ -59,11 +58,10 @@ const en: Translations = { checkConnection: 'Check connection', checkConnectionFailed: 'Check connection failed', checkConnectionSuccess: 'Check connection succeeded', - clearAllRecords: 'Clear all records', + clear: 'Clear', clearRecords: 'Clear records', clearRecordsDescription: - 'Sync Engine records sync states to resolve sync operations between local and remote files. This option allows you to selectively clear records. Warning: this action is likely to cause data loss.', - clearVaultRecords: 'Clear vault records', + 'Sync Engine records sync states to resolve sync operations between local and remote files. This option allows you to clear records. Warning: this action is likely to cause changes in sync decisions.', completed: 'Completed', completedNoop: 'Already synced', configurations: 'Configurations', @@ -207,6 +205,7 @@ const en: Translations = { realtimeSyncFastModeDescription: 'Reuse cached data and avoid unnecessary remote discovery during real-time sync to accelerate sync.', realtimeSyncPlaceholder: 'Enter sync delay (e.g. 500ms, 5s)', + recordsCleared: 'Records cleared', remoteMigration: 'Remote migration', remove: 'Remove', removeLocal: 'Remove local', @@ -282,7 +281,6 @@ const en: Translations = { 'Set the module source from which this module receives updates. Leave empty to disable update.', updateSourcePlaceholder: 'https://example.com/modules.json', upload: 'Upload', - vaultRecordsCleared: 'Vault records cleared', walkingRemote: 'Discovering remote files', }; diff --git a/packages/plugin/src/modules/Registrar.ts b/packages/plugin/src/modules/Registrar.ts index 353b0ca..6041b7e 100644 --- a/packages/plugin/src/modules/Registrar.ts +++ b/packages/plugin/src/modules/Registrar.ts @@ -114,7 +114,7 @@ export default class Registrar { private readonly createRemoteFs = (remoteFs = this.settings.remoteFs) => { const entry = this.remoteFsRegistry.get(remoteFs); if (!entry) { - if (!remoteFs) throw new Error('Please install a backend!'); + if (!remoteFs) throw new Error('Please set a backend!'); throw new Error(`Backend "${remoteFs}" is not installed!`); } return wrapInOrder(entry.instantiate(this.getRequest()), this.remoteFsWrapperRegistry); diff --git a/packages/plugin/src/modules/Storage.ts b/packages/plugin/src/modules/Storage.ts index a9f3cf1..d6d4b9d 100644 --- a/packages/plugin/src/modules/Storage.ts +++ b/packages/plugin/src/modules/Storage.ts @@ -1,6 +1,6 @@ import type { DatabaseAsync, StoreAsync } from 'uni-kv'; import { deleteMemoryDB, openIndexedDB, openMemoryDB } from 'uni-kv'; -import type { General, RecordStat } from '@/types'; +import type { General, MaybePromise, RecordStat } from '@/types'; export type IndexedDBSchema = Record; export type RecordStore = StoreAsync; @@ -17,15 +17,25 @@ export default class Storage { private readonly getRecordStore = (namespace?: string) => this.indexedDB.getStore(namespace || this.ctx.getNamespace()); - private readonly deleteRecordStore = (namespace?: string) => - this.indexedDB.deleteStore(namespace || this.ctx.getNamespace()); + private readonly deleteRecordStore = (namespace?: string): MaybePromise => { + try { + namespace ??= this.ctx.getNamespace(); + } catch { + return; // When the backend is not set, no need to delete + } + return this.indexedDB.deleteStore(namespace); + }; private readonly clearRecordStores = () => this.indexedDB.clearStores(); - private readonly recordStoreExists = (namespace?: string) => - this.indexedDB - .getStoreNames() - .then((names) => names.includes(namespace || this.ctx.getNamespace())); + private readonly recordStoreExists = (namespace?: string): MaybePromise => { + try { + namespace ??= this.ctx.getNamespace(); + } catch { + return false; // When the backend is not set, assume no store + } + return this.indexedDB.getStoreNames().then((names) => names.includes(namespace)); + }; readonly root = { clearRecordStores: this.clearRecordStores, diff --git a/packages/plugin/src/settings/development.ts b/packages/plugin/src/settings/development.ts index 446e2ae..b66f362 100644 --- a/packages/plugin/src/settings/development.ts +++ b/packages/plugin/src/settings/development.ts @@ -1,14 +1,12 @@ import { Notice, Setting } from 'obsidian'; import type { Translate } from '@/modules/I18n'; -import type { RecordStore } from '@/modules/Storage'; +import type { MaybePromise } from '@/sdk'; export type DevelopmentSettingTranslations = { development: string; - vaultRecordsCleared: string; - clearVaultRecords: string; - clearAllRecords: string; - allRecordsCleared: string; clearRecords: string; + recordsCleared: string; + clear: string; clearRecordsDescription: string; export: string; exportLogsDescription: string; @@ -19,12 +17,11 @@ export default function developmentSettings( el: HTMLElement, ctx: { translate: Translate; - clearRecordStores: () => Promise; - getRecordStore: (namespace?: string) => RecordStore; + deleteRecordStore: (namespace?: string) => MaybePromise; exportLogs: () => Promise; }, ) { - const { translate, exportLogs } = ctx; + const { translate, exportLogs, deleteRecordStore } = ctx; new Setting(el).setName(translate('development')).setHeading(); new Setting(el) @@ -32,43 +29,18 @@ export default function developmentSettings( .setDesc(translate('clearRecordsDescription')) .addButton((button) => button - .setButtonText(translate('clearVaultRecords')) + .setButtonText(translate('clearRecords')) .setWarning() - .onClick(() => void clearVaultRecords(ctx)), - ) - .addButton((button) => - button - .setButtonText(translate('clearAllRecords')) - .setWarning() - .onClick(() => void clearAllRecords(ctx)), + .onClick(async () => { + await deleteRecordStore(); + new Notice(translate('recordsCleared')); + }), ); new Setting(el) .setName(translate('exportLogsToFile')) .setDesc(translate('exportLogsDescription')) .addButton((button) => { - button.setButtonText(translate('export')).onClick(() => void exportLogs()); + button.setButtonText(translate('export')).onClick(exportLogs); }); } - -async function clearVaultRecords({ - translate, - getRecordStore, -}: { - getRecordStore: (namespace?: string) => RecordStore; - translate: Translate; -}) { - await getRecordStore().clear(); - new Notice(translate('vaultRecordsCleared')); -} - -async function clearAllRecords({ - translate, - clearRecordStores, -}: { - translate: Translate; - clearRecordStores: () => Promise; -}) { - await clearRecordStores(); - new Notice(translate('allRecordsCleared')); -} diff --git a/packages/plugin/src/settings/features.ts b/packages/plugin/src/settings/features.ts index d40f690..0408e06 100644 --- a/packages/plugin/src/settings/features.ts +++ b/packages/plugin/src/settings/features.ts @@ -2,6 +2,7 @@ import type { Settings, Context } from '@'; import { Setting } from 'obsidian'; import type { MigrationModalTranslations } from '@/components/MigrationModal'; import type { Fragment, Translate } from '@/modules/I18n'; +import type { MaybePromise } from '@/sdk'; import setNeedMigration from '@/components/MigrationModal'; import { generateSettingEntry } from './generate-entry'; @@ -32,7 +33,7 @@ export default function featuresSettings( startScheduledSync: () => void; stopScheduledSync: () => void; settings: Settings; - recordStoreExists: () => Promise; + recordStoreExists: () => MaybePromise; }, ) { const { @@ -109,7 +110,7 @@ export default function featuresSettings( }, content: (value) => translate('asymmetricStorageMigration', value ? 'enable' : 'disable'), - needMigration: () => recordStoreExists(), + needMigration: recordStoreExists, toggle: toggle.setValue(settings.asymmetricStorage), }), ); diff --git a/packages/s3/src/i18n.ts b/packages/s3/src/i18n.ts index 912e700..6380f52 100644 --- a/packages/s3/src/i18n.ts +++ b/packages/s3/src/i18n.ts @@ -71,3 +71,74 @@ export const zh: S3Translations = { urlStylePath: '路径样式(Path)', urlStyleVirtualHosted: '虚拟主机样式(Virtual-hosted)', }; + +export const ru: S3Translations = { + accessKeyId: 'Идентификатор ключа доступа', + accessKeyIdDescription: 'Введите ваш идентификатор ключа доступа S3.', + accessKeyIdPlaceholder: 'Например, AKIAI...', + bucket: 'Имя бакета', + bucketDescription: 'Введите имя вашего бакета S3.', + bucketPlaceholder: 'my-bucket', + endpoint: 'URL-адрес конечной точки', + endpointDescription: 'Введите URL-адрес конечной точки S3.', + endpointPlaceholder: 'Например, https://s3.us-east-1.amazonaws.com', + prefix: 'Префикс', + prefixDescription: + 'Настройте префикс ключа, с которым будет синхронизироваться ваше хранилище. «/» обозначает корень бакета.', + prefixPlaceholder: 'Например, my-vault/', + proxyUrl: 'URL-адрес прокси', + proxyUrlDescription: + 'Необязательный URL-адрес прокси для маршрутизации запросов S3. Оставьте пустым для прямого подключения.', + proxyUrlPlaceholder: 'Например, https://proxy.example.com', + region: 'Регион', + regionDescription: 'Введите регион вашего бакета S3.', + regionPlaceholder: 'Например, us-east-1', + s3: 'S3', + secretAccessKey: 'Секретный ключ доступа', + secretAccessKeyDescription: + 'Введите ваш секретный ключ доступа S3. Он хранится в связке ключей Obsidian keychain.', + urlStyle: 'Стиль URL', + urlStyleDescription: (frag) => { + frag.appendText('Выберите стиль URL для вашего сервиса S3. Стиль виртуального хостинга: '); + frag.createEl('code', { text: 'https://bucket.s3.amazonaws.com' }); + frag.appendText('. Путевой стиль: '); + frag.createEl('code', { text: 'https://s3.amazonaws.com/bucket' }); + frag.appendText('. Некоторые S3-совместимые сервисы требуют использования путевого стиля.'); + }, + urlStylePath: 'Путевой стиль', + urlStyleVirtualHosted: 'Стиль виртуального хостинга', +}; + +export const zhTW: S3Translations = { + accessKeyId: '存取金鑰 ID', + accessKeyIdDescription: '輸入您的 S3 存取金鑰 ID。', + accessKeyIdPlaceholder: '例如 AKIAI...', + bucket: '儲存桶名稱', + bucketDescription: '輸入您的 S3 儲存桶名稱。', + bucketPlaceholder: 'my-bucket', + endpoint: '端點 URL', + endpointDescription: '輸入 S3 端點 URL', + endpointPlaceholder: '例如 https://s3.us-east-1.amazonaws.com', + prefix: '前綴路徑', + prefixDescription: '設定儲存庫同步目標的 Key 前綴路徑。「/」代表儲存桶的根目錄。', + prefixPlaceholder: '例如 my-vault/', + proxyUrl: '代理伺服器 URL', + proxyUrlDescription: '可選的代理伺服器 URL,用來轉發 S3 請求。若留空則為直連。', + proxyUrlPlaceholder: '例如 https://proxy.example.com', + region: '區域', + regionDescription: '輸入您 S3 儲存桶所在的區域。', + regionPlaceholder: '例如 us-east-1', + s3: 'S3', + secretAccessKey: '私密存取金鑰', + secretAccessKeyDescription: '輸入您的 S3 私密存取金鑰。其將儲存於 Obsidian 金鑰圈中。', + urlStyle: 'URL 樣式', + urlStyleDescription: (frag) => { + frag.appendText('選擇您 S3 服務的 URL 樣式。虛擬主機樣式(Virtual-hosted style):'); + frag.createEl('code', { text: 'https://bucket.s3.amazonaws.com' }); + frag.appendText('。路徑樣式(Path style):'); + frag.createEl('code', { text: 'https://s3.amazonaws.com/bucket' }); + frag.appendText('。某些相容 S3 的服務需要使用路徑樣式。'); + }, + urlStylePath: '路徑樣式', + urlStyleVirtualHosted: '虛擬主機樣式', +}; diff --git a/packages/s3/src/index.ts b/packages/s3/src/index.ts index b2fad66..9d3189f 100644 --- a/packages/s3/src/index.ts +++ b/packages/s3/src/index.ts @@ -16,7 +16,7 @@ import { digOriginal, prefixWrapper } from '@hesprs/sync-engine-sdk'; import type { UrlStyle } from '@/s3/sigv4'; import type { S3Translations } from '@/setting'; import { sigv4Middleware } from '@/s3/sigv4'; -import { en, zh } from './i18n'; +import { en, zh, zhTW, ru } from './i18n'; import s3BatchDeleteOptimizer from './optimizer'; import { checkConnection } from './s3/check-connection'; import S3Fs from './s3/fs'; @@ -55,6 +55,8 @@ export default class S3 { ) { ctx.registerI18n('en', en); ctx.registerI18n('zh', zh); + ctx.registerI18n('zh-TW', zhTW); + ctx.registerI18n('ru', ru); } readonly moduleSettings: S3Settings = { diff --git a/packages/smart-merge/src/i18n.ts b/packages/smart-merge/src/i18n.ts index 07b56b2..c6d02fc 100644 --- a/packages/smart-merge/src/i18n.ts +++ b/packages/smart-merge/src/i18n.ts @@ -37,3 +37,30 @@ export const zh: SmartMergeTranslations = { smartMerge: '智能合并', start: '开始', }; + +export const ru: SmartMergeTranslations = { + conflictOursMarkers: 'Маркеры конфликта «Наши»', + conflictOursMarkersDescription: + 'Укажите маркеры до и после области в конфликте слияния, которая содержит локальные изменения.', + conflictTheirsMarkers: 'Маркеры конфликта «Чужие»', + conflictTheirsMarkersDescription: + 'Укажите маркеры до и после области в конфликте слияния, которая содержит удалённые изменения.', + deletionMarkers: 'Маркеры конфликта удаления', + deletionMarkersDescription: + 'Укажите маркеры до и после области, которая была удалена с одной стороны, но изменена с другой.', + end: 'Конец', + smartMerge: 'Умное слияние', + start: 'Начало', +}; + +export const zhTW: SmartMergeTranslations = { + conflictOursMarkers: '「我方」衝突標記', + conflictOursMarkersDescription: '設定合併衝突中代表本地變更區域的前後標記。', + conflictTheirsMarkers: '「對方」衝突標記', + conflictTheirsMarkersDescription: '設定合併衝突中代表遠端變更區域的前後標記。', + deletionMarkers: '刪除衝突標記', + deletionMarkersDescription: '設定被一方刪除但被另一方修改的區域前後標記。', + end: '結束', + smartMerge: '智慧合併', + start: '開始', +}; diff --git a/packages/smart-merge/src/index.ts b/packages/smart-merge/src/index.ts index e02746b..3b7580c 100644 --- a/packages/smart-merge/src/index.ts +++ b/packages/smart-merge/src/index.ts @@ -12,7 +12,7 @@ import type { } from '@hesprs/sync-engine-sdk'; import type { SmartMergeTranslations } from './i18n'; import type { SmartMergeSettings } from './setting'; -import { en, zh } from './i18n'; +import { en, zh, zhTW, ru } from './i18n'; import smartMergeResolver from './resolver'; import smartMergeSetting from './setting'; import smartMergeBaseTextWrapper from './wrapper'; @@ -40,6 +40,8 @@ export default class SmartMerge { ) { ctx.registerI18n('en', en); ctx.registerI18n('zh', zh); + ctx.registerI18n('zh-TW', zhTW); + ctx.registerI18n('ru', ru); } readonly moduleSettings: SmartMergeSettings = { diff --git a/packages/webdav/src/i18n.ts b/packages/webdav/src/i18n.ts index 657afe7..3a2fe37 100644 --- a/packages/webdav/src/i18n.ts +++ b/packages/webdav/src/i18n.ts @@ -43,3 +43,47 @@ export const zh: WebdavTranslations = { usernamePlaceholder: '请输入您的用户名', webdav: 'WebDAV', }; + +export const ru: WebdavTranslations = { + baseDirectory: 'Базовый каталог', + baseDirectoryDescription: + 'Настройте корневую папку на сервере WebDAV, с которой будет синхронизироваться ваше хранилище. «/» обозначает корневой каталог.', + baseDirectoryPlaceholder: 'Введите путь к каталогу', + chunkedUpload: 'Загрузка частями в стиле Nextcloud', + chunkedUploadDescription: + 'Включите загрузку файлов частями (по сегментам) в стиле Nextcloud вместо прямой отправки файла целиком для снижения нагрузки на память. Большинство серверов WebDAV не поддерживают такую загрузку — это специфическая функция Nextcloud.', + depthInfinity: 'Использовать «Depth: infinity»', + depthInfinityDescription: + '«Depth: infinity» — это специальный заголовок, отправляемый на сервер WebDAV, требующий от него вернуть список всех файлов в одном ответе. Это может ускорить сканирование удалённых файлов, но некоторые серверы могут его не поддерживать. Кроме того, это не даёт прироста производительности, если включено «Асимметричное хранилище».', + endpoint: 'URL-адрес сервера', + endpointDescription: 'Введите URL-адрес вашего сервера WebDAV.', + endpointPlaceholder: 'https://example.com/webdav', + password: 'Пароль', + passwordDescription: + 'Введите пароль от вашего аккаунта. Пароль хранится в связке ключей Obsidian keychain.', + username: 'Имя пользователя', + usernameDescription: 'Введите имя пользователя вашей учётной записи WebDAV.', + usernamePlaceholder: 'Введите имя пользователя', + webdav: 'WebDAV', +}; + +export const zhTW: WebdavTranslations = { + baseDirectory: '基礎目錄', + baseDirectoryDescription: '設定 WebDAV 上儲存庫要同步到的根資料夾。「/」代表根目錄。', + baseDirectoryPlaceholder: '輸入目錄', + chunkedUpload: 'Nextcloud 風格分塊上傳', + chunkedUploadDescription: + '啟用 Nextcloud 風格的分塊上傳以替代直接上傳完整檔案,進而降低記憶體負擔。大多數 WebDAV 伺服器不支援分塊上傳,此為 Nextcloud 特有的功能。', + depthInfinity: '使用「Depth: infinity」', + depthInfinityDescription: + '「Depth: infinity」是發送給 WebDAV 伺服器的特殊標頭,要求伺服器在單一回應中列出所有檔案。這能大幅加速遠端檔案掃描,但部分伺服器可能不支援。此外,若您啟用了「非對稱儲存」,此選項將不會帶來任何效能提升。', + endpoint: '伺服器 URL', + endpointDescription: '輸入您 WebDAV 伺服器的 URL。', + endpointPlaceholder: 'https://example.com/webdav', + password: '密碼', + passwordDescription: '輸入您的帳號密碼。密碼將儲存於 Obsidian 金鑰圈中。', + username: '使用者名稱', + usernameDescription: '輸入您的 WebDAV 帳號使用者名稱。', + usernamePlaceholder: '輸入您的使用者名稱', + webdav: 'WebDAV', +}; diff --git a/packages/webdav/src/index.ts b/packages/webdav/src/index.ts index 72c934f..2af8212 100644 --- a/packages/webdav/src/index.ts +++ b/packages/webdav/src/index.ts @@ -13,7 +13,7 @@ import type { import type { App } from 'obsidian'; import { digOriginal, prefixWrapper } from '@hesprs/sync-engine-sdk'; import type { WebdavTranslations } from './setting'; -import { en, zh } from './i18n'; +import { en, zh, zhTW, ru } from './i18n'; import webdavSetting from './setting'; import { checkConnection } from './webdav/check-connection'; import WebdavFs from './webdav/fs'; @@ -44,6 +44,8 @@ export default class Webdav { this.moduleSettings.baseDirectory = `${ctx.app.vault.getName()}/`; ctx.registerI18n('en', en); ctx.registerI18n('zh', zh); + ctx.registerI18n('zh-TW', zhTW); + ctx.registerI18n('ru', ru); } readonly moduleSettings: WebdavSettings = { From c5356ae7cc7896cd497c8d3ffb2bffa06e90b467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?He=CC=84sperus?= Date: Sun, 9 Aug 2026 09:56:27 +0800 Subject: [PATCH 2/6] fix(observability): reuse mobile toast message - fixes #209 --- packages/plugin/src/modules/Observability.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/plugin/src/modules/Observability.ts b/packages/plugin/src/modules/Observability.ts index 4a2a2ff..7c1aff3 100644 --- a/packages/plugin/src/modules/Observability.ts +++ b/packages/plugin/src/modules/Observability.ts @@ -123,8 +123,10 @@ export default class Observability { syncStage('walkingRemote'); window.clearInterval(updateInterval); sinceLastSyncText(''); - if (settings.noticeStatusOnMobile && Platform.isMobile) - mobileSyncNotice = new Notice(progressText(), 0); + if (settings.noticeStatusOnMobile && Platform.isMobile) { + window.clearTimeout(noticeTimeout); + mobileSyncNotice ??= new Notice(progressText(), 0); + } }), on('requestConfirmDelete', () => syncStage('awaitingConfirmation')), on('requestConfirmTasks', () => syncStage('awaitingConfirmation')), From b0226c0278f10697ae75d9fe0ecc399222963740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?He=CC=84sperus?= Date: Sun, 9 Aug 2026 10:14:18 +0800 Subject: [PATCH 3/6] fix(webdav): add content-type in webdav check connection fixes #211 --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 +- packages/webdav/src/webdav/check-connection.ts | 1 + packages/webdav/src/webdav/fs.ts | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index b5da1f7..62d2897 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -53,7 +53,7 @@ body: - type: textarea id: logs attributes: - label: Support Log + label: Logs description: Please go to plugin settings - `Development` - `Export logs`, click `Export to note`, copy and paste the content of the full log. This will be automatically formatted into code, so no need for backticks. render: shell placeholder: If the support log contains information that you think is improper to disclose, only pasting lines with `ERROR` labels is OK. diff --git a/packages/webdav/src/webdav/check-connection.ts b/packages/webdav/src/webdav/check-connection.ts index 2752d81..d5e1503 100644 --- a/packages/webdav/src/webdav/check-connection.ts +++ b/packages/webdav/src/webdav/check-connection.ts @@ -21,6 +21,7 @@ export async function checkConnection( try { const response = await request({ body: CHECK_CONNECTION_BODY, + contentType: 'application/xml', headers: { Authorization, Depth: '0' }, method: 'PROPFIND', url: buildUrl(normalizeUrl(options.endpoint), '/'), diff --git a/packages/webdav/src/webdav/fs.ts b/packages/webdav/src/webdav/fs.ts index 28abe24..74dacc8 100644 --- a/packages/webdav/src/webdav/fs.ts +++ b/packages/webdav/src/webdav/fs.ts @@ -151,7 +151,8 @@ async function propfind(args: PropfindPayload) { const url = 'url' in args ? args.url : buildUrl(args.endpoint, args.key); const response = await request({ body: PROPFIND_BODY, - headers: { Authorization: auth, 'Content-Type': 'application/xml', Depth: depth }, + contentType: 'application/xml', + headers: { Authorization: auth, Depth: depth }, method: 'PROPFIND', url, }); From 11aff218d7251da720096d19bc0af7ba1744b0af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?He=CC=84sperus?= Date: Sun, 9 Aug 2026 10:26:15 +0800 Subject: [PATCH 4/6] fix(url): use RFC3986 URI encoding uniformly - fixes #207 --- packages/plugin/src/modules/Extensibility.ts | 3 ++- packages/s3/src/s3/sigv4.ts | 3 ++- packages/shared/src/path.ts | 20 ++++++++++---------- packages/webdav/src/webdav/chunked-upload.ts | 3 ++- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/plugin/src/modules/Extensibility.ts b/packages/plugin/src/modules/Extensibility.ts index 50f5a61..e35476a 100644 --- a/packages/plugin/src/modules/Extensibility.ts +++ b/packages/plugin/src/modules/Extensibility.ts @@ -15,6 +15,7 @@ import untilTrue from '@/utils/until-true'; import type { Dispatch } from './EventBus'; import type { Translate } from './I18n'; import { VERSION } from './EventBus'; +import { encodeURIComponent3986 } from '@repo/shared/path'; type WindowAugmentation = { syncEngineApiBridge?: typeof obsidian }; @@ -451,7 +452,7 @@ async function migrateModules({ icon: 'puzzle', id, integrity: await sha256(file), - main: `https://sync.consensia.cc/modules/${encodeURIComponent(name)}.js`, + main: `https://sync.consensia.cc/modules/${encodeURIComponent3986(name)}.js`, name, source: 'https://sync.consensia.cc/modules.json', version, diff --git a/packages/s3/src/s3/sigv4.ts b/packages/s3/src/s3/sigv4.ts index 1df7646..b158851 100644 --- a/packages/s3/src/s3/sigv4.ts +++ b/packages/s3/src/s3/sigv4.ts @@ -1,5 +1,6 @@ import type { Binary, Request, RequestParam } from '@hesprs/sync-engine-sdk'; import { textToUint8Array } from '@repo/shared/binary'; +import { encodeURIComponent3986 } from '@repo/shared/path'; import { md5 } from 'hash-wasm'; export type UrlStyle = 'virtualHosted' | 'path'; @@ -72,7 +73,7 @@ function canonicalizeUrl(url: string): { canonicalUri: string; canonicalQuery: s const values = params.getAll(key); values.sort(); return values - .map((value) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .map((value) => `${encodeURIComponent3986(key)}=${encodeURIComponent3986(value)}`) .join('&'); }) .filter(Boolean) diff --git a/packages/shared/src/path.ts b/packages/shared/src/path.ts index b58ec04..68b278d 100644 --- a/packages/shared/src/path.ts +++ b/packages/shared/src/path.ts @@ -41,19 +41,19 @@ export function normalizeUrl(value: string) { return parsedUrl.toString().replace(/\/+$/v, ''); } +export function encodeURIComponent3986(url: string) { + return encodeURIComponent(url) + .replaceAll('!', '%21') + .replaceAll("'", '%27') + .replaceAll('(', '%28') + .replaceAll(')', '%29') + .replaceAll('*', '%2A'); +} + export function encodeUrl(url: string) { return url .split('/') - .map((segment) => - segment === '' - ? '' - : encodeURIComponent(segment) - .replaceAll('!', '%21') - .replaceAll("'", '%27') - .replaceAll('(', '%28') - .replaceAll(')', '%29') - .replaceAll('*', '%2A'), - ) + .map((segment) => (segment === '' ? '' : encodeURIComponent3986(segment))) .join('/'); } diff --git a/packages/webdav/src/webdav/chunked-upload.ts b/packages/webdav/src/webdav/chunked-upload.ts index 7286b2b..a8dcc73 100644 --- a/packages/webdav/src/webdav/chunked-upload.ts +++ b/packages/webdav/src/webdav/chunked-upload.ts @@ -1,6 +1,7 @@ import type { Binary, Request, Stat } from '@hesprs/sync-engine-sdk'; import { concatBinary } from '@repo/shared/binary'; import { buildUrl, getFileUid, getHeader } from './utils'; +import { encodeURIComponent3986 } from '@repo/shared/path'; const NEXTCLOUD_CHUNK_SIZE = 5 * 1024 * 1024; const NEXTCLOUD_MAX_CONCURRENT = 3; @@ -14,7 +15,7 @@ type NextcloudChunkedUploadOptions = { }; function getUploadEndpoint(endpoint: string, username: string) { - const encodedUsername = encodeURIComponent(username); + const encodedUsername = encodeURIComponent3986(username); const filesMarker = '/files/'; const filesMarkerIndex = endpoint.lastIndexOf(filesMarker); return filesMarkerIndex === -1 From 41a80f0454d7811510724ce224bf4b99d14107fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?He=CC=84sperus?= Date: Sun, 9 Aug 2026 11:46:12 +0800 Subject: [PATCH 5/6] feat(UI): use different colors in file tree display resolves #210 --- packages/i18n/src/ru/translations.ts | 14 ++++++++++- packages/i18n/src/zh-TW/translations.ts | 14 ++++++++++- packages/i18n/src/zh/translations.ts | 14 ++++++++++- packages/plugin/dist/dev.spec.d.ts | 2 +- ...ZSc.spec.d.ts => index-M1OI9qC7.spec.d.ts} | 2 +- packages/plugin/dist/index.spec.d.ts | 2 +- .../src/components/FilterEditorModal.ts | 2 +- .../src/components/HeadersEditorModal.ts | 2 +- .../src/components/SourceEditorModal.ts | 2 +- .../plugin/src/components/file-tree/App.tsx | 2 +- packages/plugin/src/en.ts | 14 ++++++++++- packages/plugin/src/global.css | 6 ----- packages/plugin/src/modules/Extensibility.ts | 2 +- packages/plugin/src/modules/ProgressModal.ts | 4 ++-- packages/plugin/src/settings/head.ts | 8 +++---- packages/plugin/src/sync/tasks/interface.ts | 23 +++++++++++++------ packages/webdav/src/webdav/chunked-upload.ts | 2 +- 17 files changed, 83 insertions(+), 32 deletions(-) rename packages/plugin/dist/{index-B5tgeZSc.spec.d.ts => index-M1OI9qC7.spec.d.ts} (99%) diff --git a/packages/i18n/src/ru/translations.ts b/packages/i18n/src/ru/translations.ts index 075b0b5..4eeedfe 100644 --- a/packages/i18n/src/ru/translations.ts +++ b/packages/i18n/src/ru/translations.ts @@ -72,7 +72,19 @@ const ru: Translations = { confirmDeleteInAutoSync: 'Подтверждать удаления при автосинхронизации', confirmDeleteInAutoSyncDescription: 'Показывать подтверждение для локальных файлов, которые будут удалены во время автоматической синхронизации. Вы сможете выбрать: удалить их или загрузить повторно.', - confirmTasksDescription: 'Пожалуйста, подтвердите операции ниже.', + confirmTasksDescription: (frag) => { + frag.appendText('Пожалуйста, подтвердите операции ниже: '); + frag.createSpan({ cls: 'color-[--color-green] font-bold', text: 'зелёный' }); + frag.appendText(' значок означает локальную операцию; '); + frag.createSpan({ cls: 'color-[--color-blue] font-bold', text: 'синий' }); + frag.appendText(' — удалённую операцию; '); + frag.createSpan({ cls: 'color-[--color-red] font-bold', text: 'красный' }); + frag.appendText(' — локальное удаление; '); + frag.createSpan({ cls: 'color-[--color-pink] font-bold', text: 'розовый' }); + frag.appendText(' — удалённое удаление; а '); + frag.createSpan({ cls: 'color-[--color-yellow] font-bold', text: 'жёлтый' }); + frag.appendText(' — разрешение конфликта.'); + }, confirmTasksInSync: 'Подтверждать операции при ручной синхронизации', confirmTasksInSyncDescription: 'Показывать ожидающие операции и выполнять их только после подтверждения (не влияет на автосинхронизацию).', diff --git a/packages/i18n/src/zh-TW/translations.ts b/packages/i18n/src/zh-TW/translations.ts index dc91040..1d1f48e 100644 --- a/packages/i18n/src/zh-TW/translations.ts +++ b/packages/i18n/src/zh-TW/translations.ts @@ -71,7 +71,19 @@ const zhTW: Translations = { confirmDeleteInAutoSync: '自動同步時確認刪除', confirmDeleteInAutoSyncDescription: '在自動同步過程中刪除本地檔案前顯示確認視窗。您可以選擇刪除或重新上傳。', - confirmTasksDescription: '請確認以下操作。', + confirmTasksDescription: (frag) => { + frag.appendText('請確認以下操作:'); + frag.createSpan({ cls: 'color-[--color-green] font-bold', text: '綠色' }); + frag.appendText('圖示代表本地操作;'); + frag.createSpan({ cls: 'color-[--color-blue] font-bold', text: '藍色' }); + frag.appendText('代表遠端操作;'); + frag.createSpan({ cls: 'color-[--color-red] font-bold', text: '紅色' }); + frag.appendText('代表本地刪除;'); + frag.createSpan({ cls: 'color-[--color-pink] font-bold', text: '粉紅色' }); + frag.appendText('代表遠端刪除;而'); + frag.createSpan({ cls: 'color-[--color-yellow] font-bold', text: '黃色' }); + frag.appendText('則代表衝突解決。'); + }, confirmTasksInSync: '手動同步時確認操作', confirmTasksInSyncDescription: '顯示待處理的操作,並在您確認後執行(不影響自動同步)。', conflictResolveStrategy: '衝突解決策略', diff --git a/packages/i18n/src/zh/translations.ts b/packages/i18n/src/zh/translations.ts index 9b91045..332e332 100644 --- a/packages/i18n/src/zh/translations.ts +++ b/packages/i18n/src/zh/translations.ts @@ -59,7 +59,19 @@ const zh: Translations = { confirmDeleteInAutoSync: '自动同步时确认删除', confirmDeleteInAutoSyncDescription: '在自动触发的同步过程中,显示将被删除的本地文件的确认提示。您可以选择删除或重新上传它们。', - confirmTasksDescription: '请确认以下操作。', + confirmTasksDescription: (frag) => { + frag.appendText('请确认以下操作:'); + frag.createSpan({ cls: 'color-[--color-green] font-bold', text: '绿色' }); + frag.appendText(' 图标表示本地操作;'); + frag.createSpan({ cls: 'color-[--color-blue] font-bold', text: '蓝色' }); + frag.appendText(' 表示远程操作;'); + frag.createSpan({ cls: 'color-[--color-red] font-bold', text: '红色' }); + frag.appendText(' 表示本地删除;'); + frag.createSpan({ cls: 'color-[--color-pink] font-bold', text: '粉色' }); + frag.appendText(' 表示远程删除;'); + frag.createSpan({ cls: 'color-[--color-yellow] font-bold', text: '黄色' }); + frag.appendText(' 则表示冲突解决。'); + }, confirmTasksInSync: '手动同步时确认操作', confirmTasksInSyncDescription: '显示待处理的操作并在确认后执行(不影响自动同步)。', conflictResolveStrategy: '冲突解决策略', diff --git a/packages/plugin/dist/dev.spec.d.ts b/packages/plugin/dist/dev.spec.d.ts index c30372f..529d47c 100644 --- a/packages/plugin/dist/dev.spec.d.ts +++ b/packages/plugin/dist/dev.spec.d.ts @@ -1,4 +1,4 @@ -import { Ct as FolderStat, St as FileStat, f as Request, p as RequestParam, ut as Fs, vt as RootFs, xt as Binary, yt as WrappedFs } from "./index-B5tgeZSc.spec.js"; +import { Ct as FolderStat, St as FileStat, f as Request, p as RequestParam, ut as Fs, vt as RootFs, xt as Binary, yt as WrappedFs } from "./index-M1OI9qC7.spec.js"; //#region src/sdk/debug-wrapper.d.ts declare function debugWrapper(original: Fs, log: (content: string) => void): WrappedFs; //#endregion diff --git a/packages/plugin/dist/index-B5tgeZSc.spec.d.ts b/packages/plugin/dist/index-M1OI9qC7.spec.d.ts similarity index 99% rename from packages/plugin/dist/index-B5tgeZSc.spec.d.ts rename to packages/plugin/dist/index-M1OI9qC7.spec.d.ts index e0e70e4..48866eb 100644 --- a/packages/plugin/dist/index-B5tgeZSc.spec.d.ts +++ b/packages/plugin/dist/index-M1OI9qC7.spec.d.ts @@ -1056,7 +1056,7 @@ declare class ProgressModal extends Modal { completed: string; failedTasksDescription: string; confirmDeleteDescription: string; - confirmTasksDescription: string; + confirmTasksDescription: Fragment; hide: string; confirm: string; cancel: string; diff --git a/packages/plugin/dist/index.spec.d.ts b/packages/plugin/dist/index.spec.d.ts index 86950e5..49647e3 100644 --- a/packages/plugin/dist/index.spec.d.ts +++ b/packages/plugin/dist/index.spec.d.ts @@ -1,2 +1,2 @@ -import { $ as TranslationResource, A as SyncTerminateReason, B as MoveRemote, C as SelectFromContext, Ct as FolderStat, D as writeWithValue, Dt as RecordStatsMap, E as readWithSize, Et as RecordStat, F as Upload, G as BaseTask, H as Download, I as ResolveConflict, J as TaskNames, K as ConflictResolver, L as RemoveRemote, M as Decider, N as DeciderInput, O as prefixWrapper, Ot as Stat, P as TaskFactory, Q as Translate, R as RemoveRecord, S as ModuleMeta, St as FileStat, T as pipe, Tt as Progress, U as CreateRemoteDir, V as MoveLocal, W as AddRecord, X as Fragment, Y as RecordStore, Z as ObsidianLanguageCode, _ as Events, _t as OutputAtom, a as FsWrapperEntry, at as StoreOperations, b as ExistingMemoryDB, bt as WriteAtom, c as RemoteFsEntry, ct as CustomAtom, d as RemoteRequestMiddlewareEntry, dt as InputAtom, et as Dispatch, f as Request, ft as ListReporter, g as Context, gt as OptimizerOutput, h as SettingEntry, ht as OptimizerInput, i as DeciderEntry, it as StoreAsync, j as CreateLocalDir, k as setNeedMigration, kt as StatsMap, l as RemoteLister, lt as DeleteAtom, m as RequestResponse, mt as MoveAtom, n as CheckConnectionResult, nt as DatabaseAsync, o as LocalRequestMiddlewareEntry, ot as StoreSync, p as RequestParam, pt as MkdirAtom, q as ConflictResolverPayload, r as ConflictResolverEntry, rt as DatabaseSync, s as OptimizerEntry, st as BatchOptimizer, t as VaultRequest, tt as On, u as RemoteListerEntry, ut as Fs, v as Settings, vt as RootFs, w as digOriginal, wt as MaybePromise, x as AugmentedModuleMeta, xt as Binary, y as Translations, yt as WrappedFs, z as RemoveLocal } from "./index-B5tgeZSc.spec.js"; +import { $ as TranslationResource, A as SyncTerminateReason, B as MoveRemote, C as SelectFromContext, Ct as FolderStat, D as writeWithValue, Dt as RecordStatsMap, E as readWithSize, Et as RecordStat, F as Upload, G as BaseTask, H as Download, I as ResolveConflict, J as TaskNames, K as ConflictResolver, L as RemoveRemote, M as Decider, N as DeciderInput, O as prefixWrapper, Ot as Stat, P as TaskFactory, Q as Translate, R as RemoveRecord, S as ModuleMeta, St as FileStat, T as pipe, Tt as Progress, U as CreateRemoteDir, V as MoveLocal, W as AddRecord, X as Fragment, Y as RecordStore, Z as ObsidianLanguageCode, _ as Events, _t as OutputAtom, a as FsWrapperEntry, at as StoreOperations, b as ExistingMemoryDB, bt as WriteAtom, c as RemoteFsEntry, ct as CustomAtom, d as RemoteRequestMiddlewareEntry, dt as InputAtom, et as Dispatch, f as Request, ft as ListReporter, g as Context, gt as OptimizerOutput, h as SettingEntry, ht as OptimizerInput, i as DeciderEntry, it as StoreAsync, j as CreateLocalDir, k as setNeedMigration, kt as StatsMap, l as RemoteLister, lt as DeleteAtom, m as RequestResponse, mt as MoveAtom, n as CheckConnectionResult, nt as DatabaseAsync, o as LocalRequestMiddlewareEntry, ot as StoreSync, p as RequestParam, pt as MkdirAtom, q as ConflictResolverPayload, r as ConflictResolverEntry, rt as DatabaseSync, s as OptimizerEntry, st as BatchOptimizer, t as VaultRequest, tt as On, u as RemoteListerEntry, ut as Fs, v as Settings, vt as RootFs, w as digOriginal, wt as MaybePromise, x as AugmentedModuleMeta, xt as Binary, y as Translations, yt as WrappedFs, z as RemoveLocal } from "./index-M1OI9qC7.spec.js"; export { type AddRecord, type AugmentedModuleMeta, type BaseTask, type BatchOptimizer, type Binary, type CheckConnectionResult, type ConflictResolver, type ConflictResolverEntry, type ConflictResolverPayload, type Context, type CreateLocalDir, type CreateRemoteDir, type CustomAtom, type DatabaseAsync, type DatabaseSync, type Decider, type DeciderEntry, type DeciderInput, type DeleteAtom, type Dispatch, type Download, type Events, type ExistingMemoryDB, type FileStat, type FolderStat, type Fragment, type Fs, type FsWrapperEntry, type InputAtom, type ListReporter, type LocalRequestMiddlewareEntry, type MaybePromise, type MkdirAtom, type ModuleMeta, type MoveAtom, type MoveLocal, type MoveRemote, type ObsidianLanguageCode, type On, type OptimizerEntry, type OptimizerInput, type OptimizerOutput, type OutputAtom, type Progress, type RecordStat, type RecordStatsMap, type RecordStore, type RemoteFsEntry, type RemoteLister, type RemoteListerEntry, type RemoteRequestMiddlewareEntry, type RemoveLocal, type RemoveRecord, type RemoveRemote, type Request, type RequestParam, type RequestResponse, type ResolveConflict, type RootFs, SelectFromContext, type SettingEntry, type Settings, type Stat, type StatsMap, type StoreAsync, type StoreOperations, type StoreSync, type SyncTerminateReason, type TaskFactory, type TaskNames, type Translate, type TranslationResource, type Translations, type Upload, type VaultRequest, type WrappedFs, type WriteAtom, digOriginal, pipe, prefixWrapper, readWithSize, setNeedMigration, writeWithValue }; \ No newline at end of file diff --git a/packages/plugin/src/components/FilterEditorModal.ts b/packages/plugin/src/components/FilterEditorModal.ts index fcb5438..362d2b8 100644 --- a/packages/plugin/src/components/FilterEditorModal.ts +++ b/packages/plugin/src/components/FilterEditorModal.ts @@ -81,7 +81,7 @@ export default class FilterEditorModal extends Modal { }); const trash = itemContainer.createEl( 'button', - 'clickable-icon aspect-square color-rose-500', + 'clickable-icon aspect-square color-[--color-red]', ); setIcon(trash, 'trash-2'); trash.onClickEvent(() => { diff --git a/packages/plugin/src/components/HeadersEditorModal.ts b/packages/plugin/src/components/HeadersEditorModal.ts index 493f783..366a3be 100644 --- a/packages/plugin/src/components/HeadersEditorModal.ts +++ b/packages/plugin/src/components/HeadersEditorModal.ts @@ -75,7 +75,7 @@ export default class HeadersEditorModal extends Modal { const trash = itemContainer.createEl( 'button', - 'clickable-icon aspect-square color-rose-500', + 'clickable-icon aspect-square color-[--color-red]', ); setIcon(trash, 'trash-2'); trash.onClickEvent(() => { diff --git a/packages/plugin/src/components/SourceEditorModal.ts b/packages/plugin/src/components/SourceEditorModal.ts index 48710c6..05d7faa 100644 --- a/packages/plugin/src/components/SourceEditorModal.ts +++ b/packages/plugin/src/components/SourceEditorModal.ts @@ -57,7 +57,7 @@ export default class SourceEditorModal extends Modal { input.addEventListener('input', () => (sources[index] = input.value)); const trash = itemContainer.createEl( 'button', - 'clickable-icon aspect-square color-rose-500', + 'clickable-icon aspect-square color-[--color-red]', ); setIcon(trash, 'trash-2'); trash.onClickEvent(() => { diff --git a/packages/plugin/src/components/file-tree/App.tsx b/packages/plugin/src/components/file-tree/App.tsx index 302b045..4e56d79 100644 --- a/packages/plugin/src/components/file-tree/App.tsx +++ b/packages/plugin/src/components/file-tree/App.tsx @@ -33,7 +33,7 @@ export default function App(props: { }} type="checkbox" /> -
setIcon(element, 'files')} /> +
setIcon(element, 'folders')} />
{props.selectAll}
diff --git a/packages/plugin/src/en.ts b/packages/plugin/src/en.ts index 48b7f95..86d3f45 100644 --- a/packages/plugin/src/en.ts +++ b/packages/plugin/src/en.ts @@ -72,7 +72,19 @@ const en: Translations = { confirmDeleteInAutoSync: 'Confirm deletions during auto-sync', confirmDeleteInAutoSyncDescription: 'Show a confirmation of local files that will be deleted during auto-triggered syncs. You can choose to delete or re-upload them.', - confirmTasksDescription: 'Please confirm the operations below.', + confirmTasksDescription: (frag) => { + frag.appendText('Please confirm the operations below: a '); + frag.createSpan({ cls: 'color-[--color-green] font-bold', text: 'green' }); + frag.appendText(' icon means local operation; '); + frag.createSpan({ cls: 'color-[--color-blue] font-bold', text: 'blue' }); + frag.appendText(' means remote operation; '); + frag.createSpan({ cls: 'color-[--color-red] font-bold', text: 'red' }); + frag.appendText(' means local deletion; '); + frag.createSpan({ cls: 'color-[--color-pink] font-bold', text: 'pink' }); + frag.appendText(' means remote deletion; and '); + frag.createSpan({ cls: 'color-[--color-yellow] font-bold', text: 'yellow' }); + frag.appendText(' means conflict resolution.'); + }, confirmTasksInSync: 'Confirm operations in manual sync', confirmTasksInSyncDescription: 'Show pending operations and execute after confirmation (does not affect auto-sync).', diff --git a/packages/plugin/src/global.css b/packages/plugin/src/global.css index 674ca1f..47820cb 100644 --- a/packages/plugin/src/global.css +++ b/packages/plugin/src/global.css @@ -51,12 +51,6 @@ input[type='checkbox']:indeterminate { background-color: rgb(246, 5, 34); } -@container (max-width: 340px) { - .sync-engine-togglable-value { - flex-direction: column; - } -} - @container (max-width: 500px) { .sync-engine-togglable-value { flex-direction: column; diff --git a/packages/plugin/src/modules/Extensibility.ts b/packages/plugin/src/modules/Extensibility.ts index e35476a..31c0632 100644 --- a/packages/plugin/src/modules/Extensibility.ts +++ b/packages/plugin/src/modules/Extensibility.ts @@ -4,6 +4,7 @@ import type { Ref } from 'synthkernel'; import type { StoreOperations } from 'uni-kv'; import loadModule from '$/e2e-utils'; import hash from '@repo/shared/crypto'; +import { encodeURIComponent3986 } from '@repo/shared/path'; import obsidian, { Notice, requestUrl } from 'obsidian'; import { compare } from 'verkit'; import type { DatabaseAsync, StoreAsync } from '@/sdk'; @@ -15,7 +16,6 @@ import untilTrue from '@/utils/until-true'; import type { Dispatch } from './EventBus'; import type { Translate } from './I18n'; import { VERSION } from './EventBus'; -import { encodeURIComponent3986 } from '@repo/shared/path'; type WindowAugmentation = { syncEngineApiBridge?: typeof obsidian }; diff --git a/packages/plugin/src/modules/ProgressModal.ts b/packages/plugin/src/modules/ProgressModal.ts index 7ff82d2..7a72a92 100644 --- a/packages/plugin/src/modules/ProgressModal.ts +++ b/packages/plugin/src/modules/ProgressModal.ts @@ -10,7 +10,7 @@ import renderFailedTasks from '@/components/render-failed-tasks'; import renderProgress from '@/components/render-progress'; import roundPercent from '@/utils/round-percent'; import type { Dispatch, On } from './EventBus'; -import type { Translate } from './I18n'; +import type { Fragment, Translate } from './I18n'; import type { SyncStage } from './Observability'; import type { FailedTaskInfo, TaskInfo } from './Sync'; @@ -120,7 +120,7 @@ export default class ProgressModal extends Modal { completed: string; failedTasksDescription: string; confirmDeleteDescription: string; - confirmTasksDescription: string; + confirmTasksDescription: Fragment; hide: string; confirm: string; cancel: string; diff --git a/packages/plugin/src/settings/head.ts b/packages/plugin/src/settings/head.ts index 796184f..4e68b5d 100644 --- a/packages/plugin/src/settings/head.ts +++ b/packages/plugin/src/settings/head.ts @@ -59,8 +59,8 @@ export default function headSettings( let statusButton: ExtraButtonComponent | undefined; const possibleClasses = [ - 'color-green-400', - 'color-rose-500', + 'color-[--color-green]', + 'color-[--color-red]', 'color-neutral-600', 'animate-spin', ]; @@ -76,14 +76,14 @@ export default function headSettings( const ele = button.extraSettingsEl.firstElementChild; if (!ele) return; ele.removeClasses(possibleClasses); - ele.addClasses(['color-green-400']); + ele.addClasses(['color-[--color-green]']); }; const setError = (button: ExtraButtonComponent) => { button.setIcon('cloud-off'); const ele = button.extraSettingsEl.firstElementChild; if (!ele) return; ele.removeClasses(possibleClasses); - ele.addClasses(['color-rose-500']); + ele.addClasses(['color-[--color-red]']); }; const scheduleCheckConnection = () => window.setTimeout(() => void checkConnection(), CHECK_CONNECTION_INTERVAL); diff --git a/packages/plugin/src/sync/tasks/interface.ts b/packages/plugin/src/sync/tasks/interface.ts index 445c55c..bbd46c9 100644 --- a/packages/plugin/src/sync/tasks/interface.ts +++ b/packages/plugin/src/sync/tasks/interface.ts @@ -54,9 +54,11 @@ export abstract class BaseTask { abstract exec(): MaybePromise; } -const RED_COLOR = 'var(--color-red)'; -const BLUE_COLOR = 'var(--color-blue)'; -const YELLOW_COLOR = 'var(--color-yellow)'; +const RED = 'var(--color-red)'; +const PINK = 'var(--color-pink)'; +const BLUE = 'var(--color-blue)'; +const GREEN = 'var(--color-green)'; +const YELLOW = 'var(--color-yellow)'; export function getTaskIcon(name: TaskNames, isDir: boolean): string { if (name === 'createRemoteDir') return 'folder-up'; @@ -73,14 +75,21 @@ export function getTaskIcon(name: TaskNames, isDir: boolean): string { export function getTaskColor(name: TaskNames): string { switch (name) { case 'resolveConflict': { - return YELLOW_COLOR; + return YELLOW; + } + case 'removeLocal': { + return RED; } - case 'removeLocal': case 'removeRemote': { - return RED_COLOR; + return PINK; + } + case 'createLocalDir': + case 'download': + case 'moveLocal': { + return GREEN; } default: { - return BLUE_COLOR; + return BLUE; } } } diff --git a/packages/webdav/src/webdav/chunked-upload.ts b/packages/webdav/src/webdav/chunked-upload.ts index a8dcc73..3fa47fe 100644 --- a/packages/webdav/src/webdav/chunked-upload.ts +++ b/packages/webdav/src/webdav/chunked-upload.ts @@ -1,7 +1,7 @@ import type { Binary, Request, Stat } from '@hesprs/sync-engine-sdk'; import { concatBinary } from '@repo/shared/binary'; -import { buildUrl, getFileUid, getHeader } from './utils'; import { encodeURIComponent3986 } from '@repo/shared/path'; +import { buildUrl, getFileUid, getHeader } from './utils'; const NEXTCLOUD_CHUNK_SIZE = 5 * 1024 * 1024; const NEXTCLOUD_MAX_CONCURRENT = 3; From 40b16a339572a4a364baa0604f9dd43d9c36a247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?He=CC=84sperus?= Date: Sun, 9 Aug 2026 11:56:18 +0800 Subject: [PATCH 6/6] chore(release): prepare release --- manifest.json | 2 +- packages/plugin/CHANGELOG.md | 8 +++++++- packages/plugin/package.json | 2 +- versions.json | 3 ++- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/manifest.json b/manifest.json index cf166d6..1334694 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "sync-engine", "name": "Sync Engine", - "version": "3.0.2", + "version": "3.0.3", "minAppVersion": "1.12.3", "authorUrl": "https://hesprs.github.io", "description": "The next-generation syncing plugin: Fast · Free · Extend with Modules. Supports WebDAV and S3.", diff --git a/packages/plugin/CHANGELOG.md b/packages/plugin/CHANGELOG.md index e8beb10..b2008e4 100644 --- a/packages/plugin/CHANGELOG.md +++ b/packages/plugin/CHANGELOG.md @@ -2,10 +2,16 @@ All notable changes to this project will be documented in this file. -## Sync Engine v3.0.3 - 2026-08-08 +## Sync Engine v3.0.3 - 2026-08-09 +- Fixed the bug that selecting "Show installed only" in module management modal doesn't show installed but disabled modules. - Fixed the bug that when a backend is unselected, setting encryption or asymmetric storage does nothing. - Removed `Clear all records` button as it is confusing and dangerous. +- Fixed WebDAV check connection doesn't specify `Content-Type`. +- Added more colors to the confirmation file tree to distinguish local and remote operations more distinctively. +- Fixed the bug that causes mobile sync toast notice to stack and never clear. +- Fixed the S3 SigV4 mismatch when synced file names contain RFC 3986 added unreserved characters. +- Added Russian and Traditional Chinese translations for all modules. ## Sync Engine v3.0.2 - 2026-08-07 diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 5bff452..b32310c 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@hesprs/sync-engine-sdk", - "version": "3.0.2", + "version": "3.0.3", "description": "Official SDK for developing modules targeting Sync Engine, the extensible Obsidian syncing plugin.", "keywords": [ "obsidian-plugin", diff --git a/versions.json b/versions.json index 56f3324..15a36c9 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,6 @@ { "3.0.0": "1.12.3", "3.0.1": "1.12.3", - "3.0.2": "1.12.3" + "3.0.2": "1.12.3", + "3.0.3": "1.12.3" }