### Fixed

- No unexpected error (about a replicator) during early stage of initialisation.

### New features

- Now we can configure multiple Remote Databases of the same type, e.g, multiple CouchDBs or S3 remotes.
- We can switch between multiple Remote Databases in the settings dialogue.
This commit is contained in:
vorotamoroz
2026-04-03 13:47:56 +01:00
parent bf556bd9f4
commit f17f1ecd93
6 changed files with 316 additions and 22 deletions

View File

@@ -2,6 +2,7 @@ import {
REMOTE_COUCHDB,
REMOTE_MINIO,
REMOTE_P2P,
DEFAULT_SETTINGS,
type ObsidianLiveSyncSettings,
} from "../../../lib/src/common/types.ts";
import { $msg } from "../../../lib/src/common/i18n.ts";
@@ -21,6 +22,13 @@ import {
import { SETTING_KEY_P2P_DEVICE_NAME } from "../../../lib/src/common/types.ts";
import { SetupManager, UserMode } from "../SetupManager.ts";
import { OnDialogSettingsDefault, type AllSettings } from "./settingConstants.ts";
import { activateRemoteConfiguration } from "../../../lib/src/serviceFeatures/remoteConfig.ts";
import { ConnectionStringParser } from "../../../lib/src/common/ConnectionString.ts";
import type { RemoteConfiguration } from "../../../lib/src/common/models/setting.type.ts";
import SetupRemote from "../SetupWizard/dialogs/SetupRemote.svelte";
import SetupRemoteCouchDB from "../SetupWizard/dialogs/SetupRemoteCouchDB.svelte";
import SetupRemoteBucket from "../SetupWizard/dialogs/SetupRemoteBucket.svelte";
import SetupRemoteP2P from "../SetupWizard/dialogs/SetupRemoteP2P.svelte";
function getSettingsFromEditingSettings(editingSettings: AllSettings): ObsidianLiveSyncSettings {
const workObj = { ...editingSettings } as ObsidianLiveSyncSettings;
@@ -39,17 +47,31 @@ const toggleActiveSyncClass = (el: HTMLElement, isActive: () => boolean) => {
return {};
};
function createRemoteConfigurationId(): string {
return `remote-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
function cloneRemoteConfigurations(
configs: Record<string, RemoteConfiguration> | undefined
): Record<string, RemoteConfiguration> {
return Object.fromEntries(Object.entries(configs || {}).map(([id, config]) => [id, { ...config }]));
}
function serializeRemoteConfiguration(settings: ObsidianLiveSyncSettings): string {
if (settings.remoteType === REMOTE_MINIO) {
return ConnectionStringParser.serialize({ type: "s3", settings });
}
if (settings.remoteType === REMOTE_P2P) {
return ConnectionStringParser.serialize({ type: "p2p", settings });
}
return ConnectionStringParser.serialize({ type: "couchdb", settings });
}
export function paneRemoteConfig(
this: ObsidianLiveSyncSettingTab,
paneEl: HTMLElement,
{ addPanel, addPane }: PageFunctions
): void {
const remoteNameMap = {
[REMOTE_COUCHDB]: $msg("obsidianLiveSyncSettingTab.optionCouchDB"),
[REMOTE_MINIO]: $msg("obsidianLiveSyncSettingTab.optionMinioS3R2"),
[REMOTE_P2P]: "Only Peer-to-Peer",
} as const;
{
/* E2EE */
const E2EEInitialProps = {
@@ -91,24 +113,268 @@ export function paneRemoteConfig(
});
}
{
// TODO: very WIP. need to refactor the UI.
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleRemoteServer"), () => {}).then((paneEl) => {
const setting = new Setting(paneEl).setName($msg("Active Remote Configuration"));
const actions = new Setting(paneEl).setName("Remote Databases");
// actions.addButton((button) =>
// button
// .setButtonText("Change Remote and Setup")
// .setCta()
// .onClick(async () => {
// const setupManager = this.core.getModule(SetupManager);
// const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
// await setupManager.onSelectServer(originalSettings, UserMode.Update);
// })
// );
const el = setting.controlEl.createDiv({});
el.setText(`${remoteNameMap[this.editingSettings.remoteType] || " - "}`);
setting.addButton((button) =>
button
.setButtonText("Change Remote and Setup")
.setCta()
.onClick(async () => {
const setupManager = this.core.getModule(SetupManager);
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onSelectServer(originalSettings, UserMode.Update);
})
// Connection List
const listContainer = paneEl.createDiv({ cls: "sls-remote-list" });
const syncRemoteConfigurationBuffers = () => {
const currentConfigs = cloneRemoteConfigurations(this.core.settings.remoteConfigurations);
this.editingSettings.remoteConfigurations = currentConfigs;
this.editingSettings.activeConfigurationId = this.core.settings.activeConfigurationId;
if (this.initialSettings) {
this.initialSettings.remoteConfigurations = cloneRemoteConfigurations(currentConfigs);
this.initialSettings.activeConfigurationId = this.core.settings.activeConfigurationId;
}
};
const persistRemoteConfigurations = async (synchroniseActiveRemote: boolean = false) => {
await this.services.setting.updateSettings((currentSettings) => {
currentSettings.remoteConfigurations = cloneRemoteConfigurations(
this.editingSettings.remoteConfigurations
);
currentSettings.activeConfigurationId = this.editingSettings.activeConfigurationId;
if (synchroniseActiveRemote && currentSettings.activeConfigurationId) {
const activated = activateRemoteConfiguration(
currentSettings,
currentSettings.activeConfigurationId
);
if (activated) {
return activated;
}
}
return currentSettings;
}, true);
if (synchroniseActiveRemote) {
await this.saveAllDirtySettings();
}
syncRemoteConfigurationBuffers();
this.requestUpdate();
};
const runRemoteSetup = async (
baseSettings: ObsidianLiveSyncSettings,
remoteType?: typeof REMOTE_COUCHDB | typeof REMOTE_MINIO | typeof REMOTE_P2P
): Promise<ObsidianLiveSyncSettings | false> => {
const setupManager = this.core.getModule(SetupManager);
const dialogManager = setupManager.dialogManager;
let targetRemoteType = remoteType;
if (targetRemoteType === undefined) {
const method = await dialogManager.openWithExplicitCancel(SetupRemote);
if (method === "cancelled") {
return false;
}
targetRemoteType =
method === "bucket" ? REMOTE_MINIO : method === "p2p" ? REMOTE_P2P : REMOTE_COUCHDB;
}
if (targetRemoteType === REMOTE_MINIO) {
const bucketConf = await dialogManager.openWithExplicitCancel(SetupRemoteBucket, baseSettings);
if (bucketConf === "cancelled" || typeof bucketConf !== "object") {
return false;
}
return { ...baseSettings, ...bucketConf, remoteType: REMOTE_MINIO };
}
if (targetRemoteType === REMOTE_P2P) {
const p2pConf = await dialogManager.openWithExplicitCancel(SetupRemoteP2P, baseSettings);
if (p2pConf === "cancelled" || typeof p2pConf !== "object") {
return false;
}
return { ...baseSettings, ...p2pConf, remoteType: REMOTE_P2P };
}
const couchConf = await dialogManager.openWithExplicitCancel(SetupRemoteCouchDB, baseSettings);
if (couchConf === "cancelled" || typeof couchConf !== "object") {
return false;
}
return { ...baseSettings, ...couchConf, remoteType: REMOTE_COUCHDB };
};
const createBaseRemoteSettings = (): ObsidianLiveSyncSettings => ({
...DEFAULT_SETTINGS,
...getSettingsFromEditingSettings(this.editingSettings),
});
const createNewRemoteSettings = (): ObsidianLiveSyncSettings => ({
...DEFAULT_SETTINGS,
encrypt: this.editingSettings.encrypt,
usePathObfuscation: this.editingSettings.usePathObfuscation,
passphrase: this.editingSettings.passphrase,
configPassphraseStore: this.editingSettings.configPassphraseStore,
});
const addRemoteConfiguration = async () => {
const name = await this.services.UI.confirm.askString("Remote name", "Display name", "New Remote");
if (name === false) {
return;
}
const nextSettings = await runRemoteSetup(createNewRemoteSettings());
if (!nextSettings) {
return;
}
const id = createRemoteConfigurationId();
const configs = cloneRemoteConfigurations(this.editingSettings.remoteConfigurations);
configs[id] = {
id,
name: name.trim() || "New Remote",
uri: serializeRemoteConfiguration(nextSettings),
isEncrypted: nextSettings.encrypt,
};
this.editingSettings.remoteConfigurations = configs;
if (!this.editingSettings.activeConfigurationId) {
this.editingSettings.activeConfigurationId = id;
}
await persistRemoteConfigurations(this.editingSettings.activeConfigurationId === id);
refreshList();
};
actions.addButton((button) =>
button.setButtonText("Add New Connection").onClick(async () => {
await addRemoteConfiguration();
})
);
const refreshList = () => {
listContainer.empty();
const configs = this.editingSettings.remoteConfigurations || {};
for (const config of Object.values(configs)) {
const row = new Setting(listContainer)
.setName(config.name)
.setDesc(config.uri.split("@").pop() || ""); // Show host part for privacy
if (config.id === this.editingSettings.activeConfigurationId) {
row.nameEl.addClass("sls-active-remote-name");
row.nameEl.appendText(" (Active)");
}
row.addButton((btn) =>
btn.setButtonText("Configure").onClick(async () => {
const parsed = ConnectionStringParser.parse(config.uri);
const workSettings = createBaseRemoteSettings();
if (parsed.type === "couchdb") {
workSettings.remoteType = REMOTE_COUCHDB;
} else if (parsed.type === "s3") {
workSettings.remoteType = REMOTE_MINIO;
} else {
workSettings.remoteType = REMOTE_P2P;
}
Object.assign(workSettings, parsed.settings);
const nextSettings = await runRemoteSetup(workSettings, workSettings.remoteType);
if (!nextSettings) {
return;
}
const nextConfigs = cloneRemoteConfigurations(this.editingSettings.remoteConfigurations);
nextConfigs[config.id] = {
...config,
uri: serializeRemoteConfiguration(nextSettings),
isEncrypted: nextSettings.encrypt,
};
this.editingSettings.remoteConfigurations = nextConfigs;
await persistRemoteConfigurations(config.id === this.editingSettings.activeConfigurationId);
refreshList();
})
);
row.addButton((btn) =>
btn.setButtonText("Rename").onClick(async () => {
const nextName = await this.services.UI.confirm.askString(
"Remote name",
"Display name",
config.name
);
if (nextName === false) {
return;
}
const nextConfigs = cloneRemoteConfigurations(this.editingSettings.remoteConfigurations);
nextConfigs[config.id] = {
...config,
name: nextName.trim() || config.name,
};
this.editingSettings.remoteConfigurations = nextConfigs;
await persistRemoteConfigurations();
refreshList();
})
);
row.addButton((btn) =>
btn.setButtonText("Duplicate").onClick(async () => {
const nextName = await this.services.UI.confirm.askString(
"Duplicate remote",
"Display name",
`${config.name} (Copy)`
);
if (nextName === false) {
return;
}
const nextId = createRemoteConfigurationId();
const nextConfigs = cloneRemoteConfigurations(this.editingSettings.remoteConfigurations);
nextConfigs[nextId] = {
...config,
id: nextId,
name: nextName.trim() || `${config.name} (Copy)`,
};
this.editingSettings.remoteConfigurations = nextConfigs;
await persistRemoteConfigurations();
refreshList();
})
);
row.addButton((btn) =>
btn
.setButtonText("Delete")
.setWarning()
.onClick(async () => {
const confirmed = await this.services.UI.confirm.askYesNoDialog(
`Delete remote configuration '${config.name}'?`,
{ title: "Delete Remote Configuration", defaultOption: "No" }
);
if (confirmed !== "yes") {
return;
}
const nextConfigs = cloneRemoteConfigurations(
this.editingSettings.remoteConfigurations
);
delete nextConfigs[config.id];
this.editingSettings.remoteConfigurations = nextConfigs;
let syncActiveRemote = false;
if (this.editingSettings.activeConfigurationId === config.id) {
const nextActiveId = Object.keys(nextConfigs)[0] || "";
this.editingSettings.activeConfigurationId = nextActiveId;
syncActiveRemote = nextActiveId !== "";
}
await persistRemoteConfigurations(syncActiveRemote);
refreshList();
})
);
row.addButton((btn) =>
btn
.setButtonText("Activate")
.setDisabled(config.id === this.editingSettings.activeConfigurationId)
.onClick(async () => {
this.editingSettings.activeConfigurationId = config.id;
await persistRemoteConfigurations(true);
refreshList();
})
);
}
};
refreshList();
});
}
{
// eslint-disable-next-line no-constant-condition
if (false) {
const initialProps = {
info: getCouchDBConfigSummary(this.editingSettings),
};
@@ -143,7 +409,8 @@ export function paneRemoteConfig(
);
});
}
{
// eslint-disable-next-line no-constant-condition
if (false) {
const initialProps = {
info: getBucketConfigSummary(this.editingSettings),
};
@@ -178,7 +445,8 @@ export function paneRemoteConfig(
);
});
}
{
// eslint-disable-next-line no-constant-condition
if (false) {
const getDevicePeerId = () => this.services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME) || "";
const initialProps = {
info: getP2PConfigSummary(this.editingSettings, {