mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-10 01:31:54 +00:00
### 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:
@@ -13,6 +13,7 @@ import type { CheckPointInfo } from "./lib/src/replication/journal/JournalSyncTy
|
||||
import type { LiveSyncJournalReplicatorEnv } from "./lib/src/replication/journal/LiveSyncJournalReplicatorEnv";
|
||||
import type { LiveSyncReplicatorEnv } from "./lib/src/replication/LiveSyncAbstractReplicator";
|
||||
import { useTargetFilters } from "./lib/src/serviceFeatures/targetFilter";
|
||||
import { useRemoteConfigurationMigration } from "./lib/src/serviceFeatures/remoteConfig";
|
||||
import type { ServiceContext } from "./lib/src/services/base/ServiceBase";
|
||||
import type { InjectableServiceHub } from "./lib/src/services/InjectableServices";
|
||||
import { AbstractModule } from "./modules/AbstractModule";
|
||||
@@ -272,6 +273,8 @@ export class LiveSyncBaseCore<
|
||||
useTargetFilters(this);
|
||||
// enable target filter feature.
|
||||
usePrepareDatabaseForUse(this);
|
||||
// Migration to multiple remote configurations
|
||||
useRemoteConfigurationMigration(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useOfflineScanner } from "@lib/serviceFeatures/offlineScanner";
|
||||
import { useRedFlagFeatures } from "@/serviceFeatures/redFlag";
|
||||
import { useCheckRemoteSize } from "@lib/serviceFeatures/checkRemoteSize";
|
||||
import { useSetupURIFeature } from "@lib/serviceFeatures/setupObsidian/setupUri";
|
||||
import { useRemoteConfiguration } from "@lib/serviceFeatures/remoteConfig";
|
||||
import { SetupManager } from "@/modules/features/SetupManager";
|
||||
import { useSetupManagerHandlersFeature } from "@/serviceFeatures/setupObsidian/setupManagerHandlers";
|
||||
import { useP2PReplicatorCommands } from "@/lib/src/replication/trystero/useP2PReplicatorCommands";
|
||||
@@ -132,6 +133,7 @@ class LiveSyncWebApp {
|
||||
useOfflineScanner(core);
|
||||
useRedFlagFeatures(core);
|
||||
useCheckRemoteSize(core);
|
||||
useRemoteConfiguration(core);
|
||||
const replicator = useP2PReplicatorFeature(core);
|
||||
useP2PReplicatorCommands(core, replicator);
|
||||
const setupManager = core.getModule(SetupManager);
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: c6229cd14a...d14de2d8fc
@@ -33,6 +33,7 @@ import { SetupManager } from "./modules/features/SetupManager.ts";
|
||||
import { ModuleMigration } from "./modules/essential/ModuleMigration.ts";
|
||||
import { enableI18nFeature } from "./serviceFeatures/onLayoutReady/enablei18n.ts";
|
||||
import { useOfflineScanner } from "@lib/serviceFeatures/offlineScanner.ts";
|
||||
import { useRemoteConfiguration } from "@lib/serviceFeatures/remoteConfig.ts";
|
||||
import { useCheckRemoteSize } from "@lib/serviceFeatures/checkRemoteSize.ts";
|
||||
import { useRedFlagFeatures } from "./serviceFeatures/redFlag.ts";
|
||||
import { useSetupProtocolFeature } from "./serviceFeatures/setupObsidian/setupProtocol.ts";
|
||||
@@ -174,6 +175,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const curriedFeature = () => featuresInitialiser(core);
|
||||
core.services.appLifecycle.onLayoutReady.addHandler(curriedFeature);
|
||||
const setupManager = core.getModule(SetupManager);
|
||||
|
||||
useRemoteConfiguration(core);
|
||||
|
||||
useSetupProtocolFeature(core, setupManager);
|
||||
useSetupQRCodeFeature(core);
|
||||
useSetupURIFeature(core);
|
||||
|
||||
@@ -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, {
|
||||
|
||||
17
updates.md
17
updates.md
@@ -3,6 +3,23 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
|
||||
|
||||
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
|
||||
|
||||
## Unreleased 2
|
||||
|
||||
3rd April, 2026
|
||||
|
||||
As this commit is a bit of a fragile matter, I shall add a note here.
|
||||
|
||||
You know that untagged updates shall not be tested well. please be careful to use your own build. In most cases, I check that the warnings have disappeared, that the code compiles successfully without any warnings, and that it runs on the desktop.
|
||||
|
||||
### 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.
|
||||
|
||||
## Unreleased
|
||||
|
||||
2nd April, 2026
|
||||
|
||||
Reference in New Issue
Block a user