Add live sync visibility and Gitea updater
This commit is contained in:
@@ -13,10 +13,12 @@ const DEFAULT_SETTINGS = {
|
||||
database: "obsidian-test",
|
||||
username: "test",
|
||||
password: "testpassword",
|
||||
liveSync: false,
|
||||
liveSync: true,
|
||||
syncIntervalSeconds: 120,
|
||||
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";
|
||||
@@ -33,7 +35,8 @@ module.exports = class SimpleCouchDbSyncPlugin extends Plugin {
|
||||
deviceId: data.state.deviceId || this.settings.deviceId || makeId(),
|
||||
lastSyncStartedAt: null,
|
||||
lastSyncFinishedAt: null,
|
||||
lastMessage: "Not synced yet"
|
||||
lastMessage: "Not synced yet",
|
||||
lastStats: null
|
||||
};
|
||||
|
||||
if (!this.settings.deviceId) {
|
||||
@@ -45,6 +48,8 @@ module.exports = class SimpleCouchDbSyncPlugin extends Plugin {
|
||||
this.syncInProgress = false;
|
||||
this.ignoreVaultEventsUntil = 0;
|
||||
this.client = new CouchDbClient(this.settings);
|
||||
this.statusBarEl = this.addStatusBarItem();
|
||||
this.updateStatusBar();
|
||||
|
||||
this.addRibbonIcon("refresh-cw", "Sync with CouchDB", () => this.syncNow("manual"));
|
||||
this.addCommand({
|
||||
@@ -57,10 +62,18 @@ module.exports = class SimpleCouchDbSyncPlugin extends Plugin {
|
||||
name: "Push local vault to CouchDB",
|
||||
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.registerVaultEvents();
|
||||
this.configureLiveSync();
|
||||
if (this.settings.liveSync) {
|
||||
window.setTimeout(() => this.syncNow("startup").catch((error) => this.showError("Startup sync failed", error)), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
@@ -79,14 +92,24 @@ module.exports = class SimpleCouchDbSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
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 {
|
||||
settings: Object.assign({}, DEFAULT_SETTINGS, raw.settings || {}),
|
||||
settings,
|
||||
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 {
|
||||
settings: Object.assign({}, DEFAULT_SETTINGS, raw),
|
||||
settings,
|
||||
state: {
|
||||
knownFiles: raw.knownFiles || {},
|
||||
deviceId: raw.deviceId || makeId()
|
||||
@@ -163,25 +186,32 @@ module.exports = class SimpleCouchDbSyncPlugin extends Plugin {
|
||||
|
||||
this.syncInProgress = true;
|
||||
this.syncState.lastSyncStartedAt = new Date().toISOString();
|
||||
this.syncState.lastMessage = "Syncing...";
|
||||
this.syncState.lastMessage = `Syncing (${source})...`;
|
||||
this.updateStatusBar();
|
||||
|
||||
try {
|
||||
await this.client.reload(this.settings);
|
||||
await this.client.ensureDatabase();
|
||||
await this.client.ensureDesignDocument();
|
||||
|
||||
const pushed = await this.pushPendingLocalChanges();
|
||||
const pulled = await this.pullRemoteChanges();
|
||||
const localFiles = this.getSyncableFiles().length;
|
||||
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.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();
|
||||
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) {
|
||||
this.syncState.lastMessage = error.message || String(error);
|
||||
this.showError("CouchDB sync failed", error);
|
||||
} finally {
|
||||
this.syncInProgress = false;
|
||||
this.updateStatusBar();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,14 +225,19 @@ module.exports = class SimpleCouchDbSyncPlugin extends Plugin {
|
||||
for (const file of this.getSyncableFiles()) {
|
||||
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;
|
||||
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()) {
|
||||
const localDoc = await this.buildFileDocument(file);
|
||||
const known = this.syncState.knownFiles[file.path];
|
||||
@@ -237,6 +272,41 @@ module.exports = class SimpleCouchDbSyncPlugin extends Plugin {
|
||||
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) {
|
||||
const existing = await this.client.getDocument(doc._id);
|
||||
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 {
|
||||
constructor(app, plugin) {
|
||||
super(app, plugin);
|
||||
@@ -579,7 +699,7 @@ class SimpleCouchDbSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
new Setting(containerEl)
|
||||
.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
|
||||
.setValue(this.plugin.settings.liveSync)
|
||||
.onChange(async (value) => {
|
||||
@@ -589,7 +709,7 @@ class SimpleCouchDbSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
new Setting(containerEl)
|
||||
.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
|
||||
.setPlaceholder("120")
|
||||
.setValue(String(this.plugin.settings.syncIntervalSeconds))
|
||||
@@ -623,10 +743,46 @@ class SimpleCouchDbSyncSettingTab extends PluginSettingTab {
|
||||
.setButtonText("Push")
|
||||
.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({
|
||||
cls: "simple-couchdb-sync-status",
|
||||
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({
|
||||
cls: "simple-couchdb-sync-status",
|
||||
text: `Device id: ${this.plugin.syncState.deviceId}`
|
||||
@@ -712,3 +868,22 @@ function makeId() {
|
||||
function isNewer(leftIso, rightIso) {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user