diff --git a/README.md b/README.md index faf1841..e4e387f 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ First, get your database ready. IBM Cloudant is preferred for testing. Or you ca 1. Install the plugin on your device. 2. Configure with the remote database. - 1. Fill your server's information into the `Remote Database configuration pane`. + 1. Fill your server's information into the `Remote Database configuration` pane. 2. Enabling `End to End Encryption` is recommended. After inputting the passphrase, you have to press `Just apply`. 3. Hit `Test Database Connection` and make sure that the plugin says `Connected`. 4. Hit `Check database configuration` and make sure all tests have been passed. @@ -63,27 +63,28 @@ First, get your database ready. IBM Cloudant is preferred for testing. Or you ca 5. Back to the editor. I hope that initial scan is in the progress or done. 6. When status became stabilized (All ⏳ and 🧩 disappeared), you are ready to synchronize with the server. 7. Press the replicate icon on the Ribbon or run `Replicate now` from the Command pallet. You'll send all your data to the server. -8. Open the command palette and run `Copy setup uri`. And share copied URI to your other devices. -**IMPORTANT NOTICE: DO NOT EXPOSE THIS URI. THIS CONTAINS YOUR CREDENTIALS.** +8. Open the command palette, `Copy setup URI`, and set the passphrase to encrypt the information. Then your configuration will be copied to the clipboard. Please share copied URI with your other devices. +**IMPORTANT NOTICE: BE CAREFUL TO TREAT THIS URI. THE URI CONTAINS YOUR CREDENTIALS EVEN THOUGH NOBODY COULD READ WITHOUT THE PASSPHRASE.** ### Subsequent Devices Strongly recommend using the vault in which all files are completely synchronized including timestamps. Otherwise, some files will be corrupted if failed to resolve conflicts. To simplify, I recommend using a new empty vault. 1. Install the plug-in. -2. Open the link that you had been copied to the other device. +2. Open the link that you copied from the other device. + 1. If you are hard to open the link (i.e., in android), you can use `Open setup URI` from the command palette and paste the URI into LiveSync manually. 3. The plug-in asks you that are you sure to apply the configurations. Please answer `Yes` and the following instruction below: 1. Answer `Yes` to `Keep local DB?`. *Note: If you started with existed vault, you have to answer `No`. And `No` to `Rebuild the database?`.* 2. Answer `Yes` to `Keep remote DB?`. - 3. Answer `Yes` to `Replicate once?`. + 3. Answer `Yes` to `Unlock and replicate?`. Yes, you have to answer `Yes` to everything. Then, all your settings are copied from the first device. 4. Your notes will arrive soon. ## Something looks corrupted... -Please open the link again and Answer as below: +Please open the link again and answer as below: - If your local database looks corrupted (in other words, when your Obsidian getting weird even standalone.) - Answer `No` to `Keep local DB?` diff --git a/manifest.json b/manifest.json index 7a7519a..b70ce10 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.11.5", + "version": "0.11.6", "minAppVersion": "0.9.12", "description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "author": "vorotamoroz", diff --git a/package-lock.json b/package-lock.json index 70b8f8c..80e9bdb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.11.5", + "version": "0.11.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.11.5", + "version": "0.11.6", "license": "MIT", "dependencies": { "diff-match-patch": "^1.0.5", diff --git a/package.json b/package.json index 6292931..645d08e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.11.5", + "version": "0.11.6", "description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "main": "main.js", "type": "module", diff --git a/src/ObsidianLiveSyncSettingTab.ts b/src/ObsidianLiveSyncSettingTab.ts index 2754d34..37c5dec 100644 --- a/src/ObsidianLiveSyncSettingTab.ts +++ b/src/ObsidianLiveSyncSettingTab.ts @@ -188,7 +188,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { ); new Setting(containerRemoteDatabaseEl) .setName("End to End Encryption") - .setDesc("Encrypting contents on the remote database. If you use the plugins synchronizing feature, enabling this is recommend.") + .setDesc("Encrypt contents on the remote database. If you use the plugins synchronizing feature, enabling this is recommend.") .addToggle((toggle) => toggle.setValue(this.plugin.settings.workingEncrypt).onChange(async (value) => { this.plugin.settings.workingEncrypt = value; diff --git a/src/main.ts b/src/main.ts index a94b170..83a244e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App, FuzzySuggestModal } from "obsidian"; +import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App, FuzzySuggestModal, Setting } from "obsidian"; import { diff_match_patch } from "diff-match-patch"; import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID } from "./lib/src/types"; @@ -59,6 +59,62 @@ class PluginDialogModal extends Modal { } } } + +class InputStringDialog extends Modal { + result: string | false = false; + onSubmit: (result: string | boolean) => void; + title: string; + key: string; + placeholder: string; + isManuallyClosed = false; + + constructor(app: App, title: string, key: string, placeholder: string, onSubmit: (result: string | false) => void) { + super(app); + this.onSubmit = onSubmit; + this.title = title; + this.placeholder = placeholder; + this.key = key; + } + + onOpen() { + const { contentEl } = this; + + contentEl.createEl("h1", { text: this.title }); + + new Setting(contentEl).setName(this.key).addText((text) => + text.onChange((value) => { + this.result = value; + }) + ); + + new Setting(contentEl).addButton((btn) => + btn + .setButtonText("Ok") + .setCta() + .onClick(() => { + this.isManuallyClosed = true; + this.close(); + }) + ).addButton((btn) => + btn + .setButtonText("Cancel") + .setCta() + .onClick(() => { + this.close(); + }) + ); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + if (this.isManuallyClosed) { + this.onSubmit(this.result); + } else { + this.onSubmit(false); + } + } +} class PopoverYesNo extends FuzzySuggestModal { app: App; callback: (e: string) => void = () => { }; @@ -99,6 +155,13 @@ const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => { }); }; +const askString = (app: App, title: string, key: string, placeholder: string): Promise => { + return new Promise((res) => { + const dialog = new InputStringDialog(app, title, key, placeholder, (result) => res(result)); + dialog.open(); + }); +}; + export default class ObsidianLiveSyncPlugin extends Plugin { settings: ObsidianLiveSyncSettings; localDatabase: LocalPouchDB; @@ -241,20 +304,40 @@ export default class ObsidianLiveSyncPlugin extends Plugin { Logger(ex, LOG_LEVEL.VERBOSE); } }); + const configURIBase = "obsidian://setuplivesync?settings="; this.addCommand({ - id: "livesync-exportconfig", - name: "Copy setup uri (beta)", + id: "livesync-copysetupuri", + name: "Copy setup URI (beta)", callback: async () => { - const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(this.settings), "---")); - const uri = `obsidian://setuplivesync?settings=${encryptedSetting}`; + const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "Passphrase", ""); + if (encryptingPassphrase === false) return; + const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(this.settings), encryptingPassphrase)); + const uri = `${configURIBase}${encryptedSetting}`; await navigator.clipboard.writeText(uri); - Logger("Setup uri copied to clipboard", LOG_LEVEL.NOTICE); + Logger("Setup URI copied to clipboard", LOG_LEVEL.NOTICE); }, }); - this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => { + this.addCommand({ + id: "livesync-opensetupuri", + name: "Open setup URI (beta)", + callback: async () => { + const setupURI = await askString(this.app, "Set up manually", "Set up URI", `${configURIBase}aaaaa`); + if (setupURI === false) return; + if (!setupURI.startsWith(`${configURIBase}`)) { + Logger("Set up URI looks wrong.", LOG_LEVEL.NOTICE); + return; + } + const config = decodeURIComponent(setupURI.substring(configURIBase.length)); + console.dir(config) + await setupwizard(config); + }, + }); + const setupwizard = async (confString: string) => { try { const oldConf = JSON.parse(JSON.stringify(this.settings)); - const newconf = await JSON.parse(await decrypt(conf.settings, "---")); + const encryptingPassphrase = await askString(this.app, "Passphrase", "Passphrase for your settings", ""); + if (encryptingPassphrase === false) return; + const newconf = await JSON.parse(await decrypt(confString, encryptingPassphrase)); if (newconf) { const result = await askYesNo(this.app, "Importing LiveSync's conf, OK?"); if (result == "yes") { @@ -269,6 +352,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin { // nothing to do. so peaceful. this.settings = newSettingW; await this.saveSettings(); + const replicate = await askYesNo(this.app, "Unlock and replicate?"); + if (replicate == "yes") { + await this.replicate(true); + await this.markRemoteUnlocked(); + } Logger("Configuration loaded.", LOG_LEVEL.NOTICE); return; } @@ -313,8 +401,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin { Logger("Cancelled.", LOG_LEVEL.NOTICE); } } catch (ex) { - Logger("Couldn't parse configuration uri.", LOG_LEVEL.NOTICE); + Logger("Couldn't parse or decrypt configuration uri.", LOG_LEVEL.NOTICE); } + }; + this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => { + await setupwizard(conf.settings); }); this.addCommand({ id: "livesync-replicate", @@ -1217,14 +1308,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin { const locks = getLocks(); const pendingTask = locks.pending.length ? "\nPending: " + - Object.entries([...new Set([...locks.pending])].reduce((p, c) => ({ ...p, [c]: p[c] ?? 0 + 1 }), {} as { [key: string]: number })) + Object.entries(locks.pending.reduce((p, c) => ({ ...p, [c]: (p[c] ?? 0) + 1 }), {} as { [key: string]: number })) .map((e) => `${e[0]}${e[1] == 1 ? "" : `(${e[1]})`}`) .join(", ") : ""; const runningTask = locks.running.length ? "\nRunning: " + - Object.entries([...new Set([...locks.running])].reduce((p, c) => ({ ...p, [c]: p[c] ?? 0 + 1 }), {} as { [key: string]: number })) + Object.entries(locks.running.reduce((p, c) => ({ ...p, [c]: (p[c] ?? 0) + 1 }), {} as { [key: string]: number })) .map((e) => `${e[0]}${e[1] == 1 ? "" : `(${e[1]})`}`) .join(", ") : ""; diff --git a/styles.css b/styles.css index 005e065..7760ab3 100644 --- a/styles.css +++ b/styles.css @@ -2,28 +2,33 @@ color: var(--text-on-accent); background-color: var(--text-accent); } + .normal { color: var(--text-normal); } + .deleted { color: var(--text-on-accent); background-color: var(--text-muted); - text-decoration: line-through; } + .op-scrollable { overflow-y: scroll; /* min-height: 280px; */ max-height: 280px; user-select: text; } + .op-pre { white-space: pre-wrap; } + .op-warn { border: 1px solid salmon; padding: 2px; border-radius: 4px; } + .op-warn::before { content: "Warning"; font-weight: bold; @@ -31,11 +36,13 @@ position: relative; display: block; } + .op-warn-info { border: 1px solid rgb(255, 209, 81); padding: 2px; border-radius: 4px; } + .op-warn-info::before { content: "Notice"; font-weight: bold; @@ -43,27 +50,33 @@ position: relative; display: block; } + .syncstatusbar { -webkit-filter: grayscale(100%); filter: grayscale(100%); } + .tcenter { text-align: center; } + .sls-plugins-wrap { display: flex; flex-grow: 1; max-height: 50vh; overflow-y: scroll; } + .sls-plugins-tbl { border: 1px solid var(--background-modifier-border); width: 100%; max-height: 80%; } + .divider th { border-top: 1px solid var(--background-modifier-border); } + /* .sls-table-head{ width:50%; } @@ -75,9 +88,11 @@ .sls-btn-left { padding-right: 4px; } + .sls-btn-right { padding-left: 4px; } + .sls-hidden { display: none; } @@ -85,8 +100,9 @@ :root { --slsmessage: ""; } + .CodeMirror-wrap::before, -.cm-s-obsidian > .cm-editor::before { +.cm-s-obsidian>.cm-editor::before { content: var(--slsmessage); text-align: right; white-space: pre-wrap; @@ -105,12 +121,15 @@ .CodeMirror-wrap::before { right: 0px; } -.cm-s-obsidian > .cm-editor::before { + +.cm-s-obsidian>.cm-editor::before { right: 16px; } + .sls-setting-tab { display: none; } + div.sls-setting-menu-btn { color: var(--text-normal); background-color: var(--background-secondary-alt); @@ -131,8 +150,9 @@ div.sls-setting-menu-btn { flex-grow: 1; /* width: 100%; */ } -.sls-setting-tab:hover ~ div.sls-setting-menu-btn, -.sls-setting-tab:checked ~ div.sls-setting-menu-btn { + +.sls-setting-tab:hover~div.sls-setting-menu-btn, +.sls-setting-tab:checked~div.sls-setting-menu-btn { background-color: var(--interactive-accent); color: var(--text-on-accent); } @@ -143,14 +163,17 @@ div.sls-setting-menu-btn { /* flex-wrap: wrap; */ overflow-x: auto; } + .sls-setting-label { flex-grow: 1; display: inline-flex; justify-content: center; } + .setting-collapsed { display: none; } + .sls-plugins-tbl-buttons { text-align: right; } @@ -159,13 +182,16 @@ div.sls-setting-menu-btn { flex-grow: 0; padding: 6px 10px; } + .sls-plugins-tbl-device-head { background-color: var(--background-secondary-alt); color: var(--text-accent); } + .op-flex { display: flex; } + .op-flex input { display: inline-flex; flex-grow: 1; @@ -185,9 +211,11 @@ div.sls-setting-menu-btn { color: var(--text-on-accent); background-color: var(--text-accent); } + .history-normal { color: var(--text-normal); } + .history-deleted { color: var(--text-on-accent); background-color: var(--text-muted); @@ -197,6 +225,7 @@ div.sls-setting-menu-btn { .ob-btn-config-fix label { margin-right: 40px; } + .ob-btn-config-info { border: 1px solid salmon; padding: 2px; @@ -208,4 +237,4 @@ div.sls-setting-menu-btn { padding: 2px; margin: 1px; border-radius: 4px; -} +} \ No newline at end of file