mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-12-13 09:45:56 +00:00
Implemented:
- Encrypting setup URI by passphrse. (Note: You have to make the setup URI again) Fixed: - Setup procedure fixed. - Status text fixed. - Documentation fixed.
This commit is contained in:
13
README.md
13
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?`
|
||||
|
||||
@@ -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",
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
113
src/main.ts
113
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<string> {
|
||||
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<string | false> => {
|
||||
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(", ")
|
||||
: "";
|
||||
|
||||
41
styles.css
41
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user