Add live sync visibility and Gitea updater
This commit is contained in:
@@ -5,10 +5,12 @@ Simple CouchDB Sync is an Obsidian plugin that synchronizes vault files through
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Manual synchronization command and ribbon action.
|
- Manual synchronization command and ribbon action.
|
||||||
- Optional live synchronization for file create, modify, delete, and rename events.
|
- Live synchronization for file create, modify, delete, rename events, and periodic remote pulls.
|
||||||
|
- Visible Obsidian status bar text with the latest sync result.
|
||||||
- Syncs Markdown notes and binary attachments.
|
- Syncs Markdown notes and binary attachments.
|
||||||
- Uses Obsidian `requestUrl`, so it can work on desktop and mobile, including Android.
|
- Uses Obsidian `requestUrl`, so it can work on desktop and mobile, including Android.
|
||||||
- Stores CouchDB connection settings inside the plugin settings tab.
|
- Stores CouchDB connection settings inside the plugin settings tab.
|
||||||
|
- One-button plugin update from the Gitea repository.
|
||||||
|
|
||||||
## Default Test Configuration
|
## Default Test Configuration
|
||||||
|
|
||||||
@@ -44,6 +46,7 @@ styles.css
|
|||||||
7. Enable `Simple CouchDB Sync`.
|
7. Enable `Simple CouchDB Sync`.
|
||||||
8. Open the plugin settings and verify the CouchDB server, database, username, and password.
|
8. Open the plugin settings and verify the CouchDB server, database, username, and password.
|
||||||
9. Press `Sync` in the plugin settings, or use the command `Sync vault with CouchDB`.
|
9. Press `Sync` in the plugin settings, or use the command `Sync vault with CouchDB`.
|
||||||
|
10. Keep `Live sync` enabled on every device that should receive remote changes automatically.
|
||||||
|
|
||||||
For your test vault, the target folder is:
|
For your test vault, the target folder is:
|
||||||
|
|
||||||
@@ -94,12 +97,25 @@ styles.css
|
|||||||
7. Go to Settings -> Community plugins and enable `Simple CouchDB Sync`.
|
7. Go to Settings -> Community plugins and enable `Simple CouchDB Sync`.
|
||||||
8. Open the plugin settings and enter the same CouchDB connection settings as on desktop.
|
8. Open the plugin settings and enter the same CouchDB connection settings as on desktop.
|
||||||
9. Run `Sync vault with CouchDB`.
|
9. Run `Sync vault with CouchDB`.
|
||||||
|
10. Keep `Live sync` enabled if the phone should pull changes from CouchDB automatically.
|
||||||
|
|
||||||
For easier Android updates, install an Android Git client such as Termux or a file manager with Git support, then clone the repository into the same plugin folder. After updating files, restart Obsidian so it reloads `main.js`.
|
For easier Android updates, install an Android Git client such as Termux or a file manager with Git support, then clone the repository into the same plugin folder. After updating files, restart Obsidian so it reloads `main.js`.
|
||||||
|
|
||||||
## Updating The Plugin
|
## Updating The Plugin
|
||||||
|
|
||||||
When the repository changes, update these three files in the plugin folder:
|
The plugin includes a one-button updater.
|
||||||
|
|
||||||
|
1. Open Obsidian Settings -> Community plugins -> Simple CouchDB Sync.
|
||||||
|
2. In `Plugin updates`, keep the Gitea raw URL as:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://nnootteess.dinlo.ru/dimon/simple-couchdb-sync/raw/branch/main
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Press `Check and update`.
|
||||||
|
4. Restart Obsidian, or disable and re-enable the plugin.
|
||||||
|
|
||||||
|
Manual update is also possible. When the repository changes, update these three files in the plugin folder:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
manifest.json
|
manifest.json
|
||||||
@@ -109,6 +125,15 @@ styles.css
|
|||||||
|
|
||||||
Then restart Obsidian or disable and re-enable the plugin.
|
Then restart Obsidian or disable and re-enable the plugin.
|
||||||
|
|
||||||
|
## Live Sync Behavior
|
||||||
|
|
||||||
|
Live sync must be enabled on every device where automatic updates are expected.
|
||||||
|
|
||||||
|
- When a local file changes, the plugin queues it and syncs after a short delay.
|
||||||
|
- On the configured interval, the plugin checks CouchDB for changes created on other devices.
|
||||||
|
- The status bar shows the latest result, including pushed and pulled file counts.
|
||||||
|
- If live sync is disabled, use the `Sync` button manually on each device.
|
||||||
|
|
||||||
## CouchDB Notes
|
## CouchDB Notes
|
||||||
|
|
||||||
The plugin stores one CouchDB document per vault file. Document ids are derived from file paths. Deleted files are represented by tombstone documents so deletion can propagate between devices.
|
The plugin stores one CouchDB document per vault file. Document ids are derived from file paths. Deleted files are represented by tombstone documents so deletion can propagate between devices.
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ const DEFAULT_SETTINGS = {
|
|||||||
database: "obsidian-test",
|
database: "obsidian-test",
|
||||||
username: "test",
|
username: "test",
|
||||||
password: "testpassword",
|
password: "testpassword",
|
||||||
liveSync: false,
|
liveSync: true,
|
||||||
syncIntervalSeconds: 120,
|
syncIntervalSeconds: 120,
|
||||||
syncHiddenFiles: false,
|
syncHiddenFiles: false,
|
||||||
conflictPolicy: "newest"
|
conflictPolicy: "newest",
|
||||||
|
repositoryRawUrl: "https://nnootteess.dinlo.ru/dimon/simple-couchdb-sync/raw/branch/main",
|
||||||
|
settingsVersion: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
const PLUGIN_ID = "simple-couchdb-sync";
|
const PLUGIN_ID = "simple-couchdb-sync";
|
||||||
@@ -33,7 +35,8 @@ module.exports = class SimpleCouchDbSyncPlugin extends Plugin {
|
|||||||
deviceId: data.state.deviceId || this.settings.deviceId || makeId(),
|
deviceId: data.state.deviceId || this.settings.deviceId || makeId(),
|
||||||
lastSyncStartedAt: null,
|
lastSyncStartedAt: null,
|
||||||
lastSyncFinishedAt: null,
|
lastSyncFinishedAt: null,
|
||||||
lastMessage: "Not synced yet"
|
lastMessage: "Not synced yet",
|
||||||
|
lastStats: null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.settings.deviceId) {
|
if (!this.settings.deviceId) {
|
||||||
@@ -45,6 +48,8 @@ module.exports = class SimpleCouchDbSyncPlugin extends Plugin {
|
|||||||
this.syncInProgress = false;
|
this.syncInProgress = false;
|
||||||
this.ignoreVaultEventsUntil = 0;
|
this.ignoreVaultEventsUntil = 0;
|
||||||
this.client = new CouchDbClient(this.settings);
|
this.client = new CouchDbClient(this.settings);
|
||||||
|
this.statusBarEl = this.addStatusBarItem();
|
||||||
|
this.updateStatusBar();
|
||||||
|
|
||||||
this.addRibbonIcon("refresh-cw", "Sync with CouchDB", () => this.syncNow("manual"));
|
this.addRibbonIcon("refresh-cw", "Sync with CouchDB", () => this.syncNow("manual"));
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
@@ -57,10 +62,18 @@ module.exports = class SimpleCouchDbSyncPlugin extends Plugin {
|
|||||||
name: "Push local vault to CouchDB",
|
name: "Push local vault to CouchDB",
|
||||||
callback: () => this.pushLocalVault()
|
callback: () => this.pushLocalVault()
|
||||||
});
|
});
|
||||||
|
this.addCommand({
|
||||||
|
id: "update-plugin-from-gitea",
|
||||||
|
name: "Update plugin from Gitea",
|
||||||
|
callback: () => this.updatePluginFromGitea()
|
||||||
|
});
|
||||||
|
|
||||||
this.addSettingTab(new SimpleCouchDbSyncSettingTab(this.app, this));
|
this.addSettingTab(new SimpleCouchDbSyncSettingTab(this.app, this));
|
||||||
this.registerVaultEvents();
|
this.registerVaultEvents();
|
||||||
this.configureLiveSync();
|
this.configureLiveSync();
|
||||||
|
if (this.settings.liveSync) {
|
||||||
|
window.setTimeout(() => this.syncNow("startup").catch((error) => this.showError("Startup sync failed", error)), 5000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
@@ -79,14 +92,24 @@ module.exports = class SimpleCouchDbSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (raw.settings || raw.state) {
|
if (raw.settings || raw.state) {
|
||||||
|
const settings = Object.assign({}, DEFAULT_SETTINGS, raw.settings || {});
|
||||||
|
if (!settings.settingsVersion || settings.settingsVersion < 2) {
|
||||||
|
settings.liveSync = true;
|
||||||
|
settings.settingsVersion = 2;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
settings: Object.assign({}, DEFAULT_SETTINGS, raw.settings || {}),
|
settings,
|
||||||
state: Object.assign({ knownFiles: {}, deviceId: raw.settings && raw.settings.deviceId }, raw.state || {})
|
state: Object.assign({ knownFiles: {}, deviceId: raw.settings && raw.settings.deviceId }, raw.state || {})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settings = Object.assign({}, DEFAULT_SETTINGS, raw);
|
||||||
|
if (!settings.settingsVersion || settings.settingsVersion < 2) {
|
||||||
|
settings.liveSync = true;
|
||||||
|
settings.settingsVersion = 2;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
settings: Object.assign({}, DEFAULT_SETTINGS, raw),
|
settings,
|
||||||
state: {
|
state: {
|
||||||
knownFiles: raw.knownFiles || {},
|
knownFiles: raw.knownFiles || {},
|
||||||
deviceId: raw.deviceId || makeId()
|
deviceId: raw.deviceId || makeId()
|
||||||
@@ -163,25 +186,32 @@ module.exports = class SimpleCouchDbSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
this.syncInProgress = true;
|
this.syncInProgress = true;
|
||||||
this.syncState.lastSyncStartedAt = new Date().toISOString();
|
this.syncState.lastSyncStartedAt = new Date().toISOString();
|
||||||
this.syncState.lastMessage = "Syncing...";
|
this.syncState.lastMessage = `Syncing (${source})...`;
|
||||||
|
this.updateStatusBar();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.client.reload(this.settings);
|
await this.client.reload(this.settings);
|
||||||
await this.client.ensureDatabase();
|
await this.client.ensureDatabase();
|
||||||
await this.client.ensureDesignDocument();
|
await this.client.ensureDesignDocument();
|
||||||
|
|
||||||
const pushed = await this.pushPendingLocalChanges();
|
const localFiles = this.getSyncableFiles().length;
|
||||||
const pulled = await this.pullRemoteChanges();
|
const pulled = source === "push" ? 0 : await this.pullRemoteChanges();
|
||||||
|
const pushed = await this.pushPendingLocalChanges(source === "push");
|
||||||
|
const pulledAfterPush = pushed > 0 ? await this.pullRemoteChanges() : 0;
|
||||||
|
|
||||||
this.syncState.lastSyncFinishedAt = new Date().toISOString();
|
this.syncState.lastSyncFinishedAt = new Date().toISOString();
|
||||||
this.syncState.lastMessage = `Synced. Pushed ${pushed}, pulled ${pulled}.`;
|
this.syncState.lastStats = { source, localFiles, pushed, pulled: pulled + pulledAfterPush };
|
||||||
|
this.syncState.lastMessage = `Synced ${formatTime(this.syncState.lastSyncFinishedAt)}. Files ${localFiles}, pushed ${pushed}, pulled ${pulled + pulledAfterPush}.`;
|
||||||
await this.savePluginData();
|
await this.savePluginData();
|
||||||
new Notice(source === "manual" ? this.syncState.lastMessage : "CouchDB live sync completed");
|
if (source === "manual" || pushed > 0 || pulled > 0) {
|
||||||
|
new Notice(this.syncState.lastMessage);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.syncState.lastMessage = error.message || String(error);
|
this.syncState.lastMessage = error.message || String(error);
|
||||||
this.showError("CouchDB sync failed", error);
|
this.showError("CouchDB sync failed", error);
|
||||||
} finally {
|
} finally {
|
||||||
this.syncInProgress = false;
|
this.syncInProgress = false;
|
||||||
|
this.updateStatusBar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,14 +225,19 @@ module.exports = class SimpleCouchDbSyncPlugin extends Plugin {
|
|||||||
for (const file of this.getSyncableFiles()) {
|
for (const file of this.getSyncableFiles()) {
|
||||||
this.syncQueue.set(file.path, { kind: "modify", path: file.path });
|
this.syncQueue.set(file.path, { kind: "modify", path: file.path });
|
||||||
}
|
}
|
||||||
await this.syncNow("manual");
|
await this.syncNow("push");
|
||||||
}
|
}
|
||||||
|
|
||||||
async pushPendingLocalChanges() {
|
async pushPendingLocalChanges(forceAll) {
|
||||||
let pushed = 0;
|
let pushed = 0;
|
||||||
const filesByPath = new Map(this.getSyncableFiles().map((file) => [file.path, file]));
|
const filesByPath = new Map(this.getSyncableFiles().map((file) => [file.path, file]));
|
||||||
|
|
||||||
if (this.syncQueue.size === 0) {
|
if (forceAll) {
|
||||||
|
this.syncQueue.clear();
|
||||||
|
for (const file of filesByPath.values()) {
|
||||||
|
this.syncQueue.set(file.path, { kind: "modify", path: file.path });
|
||||||
|
}
|
||||||
|
} else if (this.syncQueue.size === 0) {
|
||||||
for (const file of filesByPath.values()) {
|
for (const file of filesByPath.values()) {
|
||||||
const localDoc = await this.buildFileDocument(file);
|
const localDoc = await this.buildFileDocument(file);
|
||||||
const known = this.syncState.knownFiles[file.path];
|
const known = this.syncState.knownFiles[file.path];
|
||||||
@@ -237,6 +272,41 @@ module.exports = class SimpleCouchDbSyncPlugin extends Plugin {
|
|||||||
return pushed;
|
return pushed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updatePluginFromGitea() {
|
||||||
|
if (this.updateInProgress) {
|
||||||
|
new Notice("Plugin update is already running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateInProgress = true;
|
||||||
|
this.syncState.lastMessage = "Checking plugin update...";
|
||||||
|
this.updateStatusBar();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updater = new GiteaPluginUpdater(this);
|
||||||
|
const result = await updater.update();
|
||||||
|
this.syncState.lastMessage = result.message;
|
||||||
|
this.updateStatusBar();
|
||||||
|
new Notice(result.message, 10000);
|
||||||
|
if (result.updated) {
|
||||||
|
new Notice("Restart Obsidian or disable and enable the plugin to load the new version.", 12000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.syncState.lastMessage = error.message || String(error);
|
||||||
|
this.showError("Plugin update failed", error);
|
||||||
|
} finally {
|
||||||
|
this.updateInProgress = false;
|
||||||
|
this.updateStatusBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatusBar() {
|
||||||
|
if (!this.statusBarEl) return;
|
||||||
|
const live = this.settings && this.settings.liveSync ? "live on" : "live off";
|
||||||
|
const message = this.syncState ? this.syncState.lastMessage : "Loading";
|
||||||
|
this.statusBarEl.setText(`CouchDB Sync: ${live} | ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
async putFileDocument(doc) {
|
async putFileDocument(doc) {
|
||||||
const existing = await this.client.getDocument(doc._id);
|
const existing = await this.client.getDocument(doc._id);
|
||||||
if (existing && existing._rev) doc._rev = existing._rev;
|
if (existing && existing._rev) doc._rev = existing._rev;
|
||||||
@@ -522,6 +592,56 @@ class CouchDbClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GiteaPluginUpdater {
|
||||||
|
constructor(plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.rawUrl = trimRight(plugin.settings.repositoryRawUrl || DEFAULT_SETTINGS.repositoryRawUrl, "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
async update() {
|
||||||
|
if (!this.rawUrl) throw new Error("Gitea raw repository URL is empty");
|
||||||
|
|
||||||
|
const remoteManifest = await this.fetchText("manifest.json");
|
||||||
|
const manifest = JSON.parse(remoteManifest);
|
||||||
|
const currentVersion = this.plugin.manifest.version || "0.0.0";
|
||||||
|
const remoteVersion = manifest.version || "0.0.0";
|
||||||
|
|
||||||
|
if (compareVersions(remoteVersion, currentVersion) <= 0) {
|
||||||
|
return {
|
||||||
|
updated: false,
|
||||||
|
message: `Plugin is already up to date (${currentVersion}).`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = {
|
||||||
|
"manifest.json": remoteManifest,
|
||||||
|
"main.js": await this.fetchText("main.js"),
|
||||||
|
"styles.css": await this.fetchText("styles.css")
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const fileName of Object.keys(files)) {
|
||||||
|
await this.plugin.app.vault.adapter.write(this.plugin.manifest.dir + "/" + fileName, files[fileName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updated: true,
|
||||||
|
message: `Plugin updated from ${currentVersion} to ${remoteVersion}.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchText(fileName) {
|
||||||
|
const response = await requestUrl({
|
||||||
|
url: `${this.rawUrl}/${fileName}`,
|
||||||
|
method: "GET",
|
||||||
|
throw: false
|
||||||
|
});
|
||||||
|
if (response.status >= 400) {
|
||||||
|
throw new Error(`Could not download ${fileName}: HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SimpleCouchDbSyncSettingTab extends PluginSettingTab {
|
class SimpleCouchDbSyncSettingTab extends PluginSettingTab {
|
||||||
constructor(app, plugin) {
|
constructor(app, plugin) {
|
||||||
super(app, plugin);
|
super(app, plugin);
|
||||||
@@ -579,7 +699,7 @@ class SimpleCouchDbSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName("Live sync")
|
.setName("Live sync")
|
||||||
.setDesc("Automatically sync after vault changes and on the interval below.")
|
.setDesc("Automatically pushes local changes and pulls remote changes on every device.")
|
||||||
.addToggle((toggle) => toggle
|
.addToggle((toggle) => toggle
|
||||||
.setValue(this.plugin.settings.liveSync)
|
.setValue(this.plugin.settings.liveSync)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
@@ -589,7 +709,7 @@ class SimpleCouchDbSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName("Live sync interval")
|
.setName("Live sync interval")
|
||||||
.setDesc("Minimum 30 seconds.")
|
.setDesc("How often this device checks CouchDB for changes made on other devices. Minimum 30 seconds.")
|
||||||
.addText((text) => text
|
.addText((text) => text
|
||||||
.setPlaceholder("120")
|
.setPlaceholder("120")
|
||||||
.setValue(String(this.plugin.settings.syncIntervalSeconds))
|
.setValue(String(this.plugin.settings.syncIntervalSeconds))
|
||||||
@@ -623,10 +743,46 @@ class SimpleCouchDbSyncSettingTab extends PluginSettingTab {
|
|||||||
.setButtonText("Push")
|
.setButtonText("Push")
|
||||||
.onClick(() => this.plugin.pushLocalVault()));
|
.onClick(() => this.plugin.pushLocalVault()));
|
||||||
|
|
||||||
|
containerEl.createEl("h3", { text: "Plugin updates" });
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName("Gitea raw URL")
|
||||||
|
.setDesc("Raw branch URL used by the one-button updater.")
|
||||||
|
.addText((text) => text
|
||||||
|
.setPlaceholder(DEFAULT_SETTINGS.repositoryRawUrl)
|
||||||
|
.setValue(this.plugin.settings.repositoryRawUrl)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.repositoryRawUrl = trimRight(value.trim(), "/");
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName("Update from Gitea")
|
||||||
|
.setDesc("Downloads manifest.json, main.js, and styles.css from Gitea. Restart Obsidian after updating.")
|
||||||
|
.addButton((button) => button
|
||||||
|
.setButtonText("Check and update")
|
||||||
|
.setCta()
|
||||||
|
.onClick(() => this.plugin.updatePluginFromGitea()));
|
||||||
|
|
||||||
containerEl.createDiv({
|
containerEl.createDiv({
|
||||||
cls: "simple-couchdb-sync-status",
|
cls: "simple-couchdb-sync-status",
|
||||||
text: `Status: ${this.plugin.syncState.lastMessage}`
|
text: `Status: ${this.plugin.syncState.lastMessage}`
|
||||||
});
|
});
|
||||||
|
containerEl.createDiv({
|
||||||
|
cls: "simple-couchdb-sync-status",
|
||||||
|
text: `Live sync: ${this.plugin.settings.liveSync ? "enabled" : "disabled"}, interval ${this.plugin.settings.syncIntervalSeconds}s`
|
||||||
|
});
|
||||||
|
if (this.plugin.syncState.lastStats) {
|
||||||
|
const stats = this.plugin.syncState.lastStats;
|
||||||
|
containerEl.createDiv({
|
||||||
|
cls: "simple-couchdb-sync-status",
|
||||||
|
text: `Last sync: ${stats.source}, files ${stats.localFiles}, pushed ${stats.pushed}, pulled ${stats.pulled}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
containerEl.createDiv({
|
||||||
|
cls: "simple-couchdb-sync-status",
|
||||||
|
text: `Plugin version: ${this.plugin.manifest.version}`
|
||||||
|
});
|
||||||
containerEl.createDiv({
|
containerEl.createDiv({
|
||||||
cls: "simple-couchdb-sync-status",
|
cls: "simple-couchdb-sync-status",
|
||||||
text: `Device id: ${this.plugin.syncState.deviceId}`
|
text: `Device id: ${this.plugin.syncState.deviceId}`
|
||||||
@@ -712,3 +868,22 @@ function makeId() {
|
|||||||
function isNewer(leftIso, rightIso) {
|
function isNewer(leftIso, rightIso) {
|
||||||
return new Date(leftIso || 0).getTime() > new Date(rightIso || 0).getTime();
|
return new Date(leftIso || 0).getTime() > new Date(rightIso || 0).getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTime(iso) {
|
||||||
|
if (!iso) return "never";
|
||||||
|
return new Date(iso).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareVersions(left, right) {
|
||||||
|
const leftParts = String(left).split(".").map((part) => Number(part) || 0);
|
||||||
|
const rightParts = String(right).split(".").map((part) => Number(part) || 0);
|
||||||
|
const length = Math.max(leftParts.length, rightParts.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const diff = (leftParts[i] || 0) - (rightParts[i] || 0);
|
||||||
|
if (diff > 0) return 1;
|
||||||
|
if (diff < 0) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "simple-couchdb-sync",
|
"id": "simple-couchdb-sync",
|
||||||
"name": "Simple CouchDB Sync",
|
"name": "Simple CouchDB Sync",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"minAppVersion": "1.5.0",
|
"minAppVersion": "1.5.0",
|
||||||
"description": "Synchronize Obsidian vault files with a CouchDB database.",
|
"description": "Synchronize Obsidian vault files with a CouchDB database.",
|
||||||
"author": "Codex",
|
"author": "Codex",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "simple-couchdb-sync",
|
"name": "simple-couchdb-sync",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Obsidian plugin for simple vault synchronization through CouchDB.",
|
"description": "Obsidian plugin for simple vault synchronization through CouchDB.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user