## 0.24.19

### New Feature

- Now we can generate a QR Code for transferring the configuration to another device.
This commit is contained in:
vorotamoroz
2025-03-05 11:12:00 +00:00
parent 65648683a3
commit 6049c19e8a
7 changed files with 338 additions and 138 deletions

12
package-lock.json generated
View File

@@ -19,6 +19,7 @@
"idb": "^8.0.2", "idb": "^8.0.2",
"minimatch": "^10.0.1", "minimatch": "^10.0.1",
"octagonal-wheels": "^0.1.24", "octagonal-wheels": "^0.1.24",
"qrcode-generator": "^1.4.4",
"svelte-check": "^4.1.4", "svelte-check": "^4.1.4",
"trystero": "^0.20.1", "trystero": "^0.20.1",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
@@ -9355,6 +9356,12 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/qrcode-generator": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz",
"integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==",
"license": "MIT"
},
"node_modules/querystringify": { "node_modules/querystringify": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
@@ -18189,6 +18196,11 @@
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz",
"integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==" "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="
}, },
"qrcode-generator": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz",
"integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw=="
},
"querystringify": { "querystringify": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",

View File

@@ -78,6 +78,7 @@
"idb": "^8.0.2", "idb": "^8.0.2",
"minimatch": "^10.0.1", "minimatch": "^10.0.1",
"octagonal-wheels": "^0.1.24", "octagonal-wheels": "^0.1.24",
"qrcode-generator": "^1.4.4",
"svelte-check": "^4.1.4", "svelte-check": "^4.1.4",
"trystero": "^0.20.1", "trystero": "^0.20.1",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"

View File

@@ -88,3 +88,4 @@ export const ICXHeader = "ix:";
export const FileWatchEventQueueMax = 10; export const FileWatchEventQueueMax = 10;
export const configURIBase = "obsidian://setuplivesync?settings="; export const configURIBase = "obsidian://setuplivesync?settings=";
export const configURIBaseQR = "obsidian://setuplivesync?settingsQR=";

View File

@@ -538,3 +538,119 @@ export function updatePreviousExecutionTime(key: string, timeDelta: number = 0)
} }
waitingTasks[key].leastNext = Math.max(Date.now() + timeDelta, waitingTasks[key].leastNext); waitingTasks[key].leastNext = Math.max(Date.now() + timeDelta, waitingTasks[key].leastNext);
} }
const prefixMapObject = {
s: {
1: "V",
2: "W",
3: "X",
4: "Y",
5: "Z",
},
o: {
1: "v",
2: "w",
3: "x",
4: "y",
5: "z",
},
} as Record<string, Record<number, string>>;
const decodePrefixMapObject = Object.fromEntries(
Object.entries(prefixMapObject).flatMap(([prefix, map]) =>
Object.entries(map).map(([len, char]) => [char, { prefix, len: parseInt(len) }])
)
);
const prefixMapNumber = {
n: {
1: "a",
2: "b",
3: "c",
4: "d",
5: "e",
},
N: {
1: "A",
2: "B",
3: "C",
4: "D",
5: "E",
},
} as Record<string, Record<number, string>>;
const decodePrefixMapNumber = Object.fromEntries(
Object.entries(prefixMapNumber).flatMap(([prefix, map]) =>
Object.entries(map).map(([len, char]) => [char, { prefix, len: parseInt(len) }])
)
);
export function encodeAnyArray(obj: any[]): string {
const tempArray = obj.map((v) => {
if (v == null) return "n";
if (v == false) return "f";
if (v == true) return "t";
if (v == undefined) return "u";
if (typeof v == "number") {
const b36 = v.toString(36);
const strNum = v.toString();
const expression = b36.length < strNum.length ? "N" : "n";
const encodedStr = expression == "N" ? b36 : strNum;
const len = encodedStr.length.toString(36);
const lenLen = len.length;
const prefix2 = prefixMapNumber[expression][lenLen];
return prefix2 + len + encodedStr;
}
const str = typeof v == "string" ? v : JSON.stringify(v);
const prefix = typeof v == "string" ? "s" : "o";
const length = str.length.toString(36);
const lenLen = length.length;
const prefix2 = prefixMapObject[prefix][lenLen];
return prefix2 + length + str;
});
const w = tempArray.join("");
return w;
}
const decodeMapConstant = {
u: undefined,
n: null,
f: false,
t: true,
} as Record<string, any>;
export function decodeAnyArray(str: string): any[] {
const result = [];
let i = 0;
while (i < str.length) {
const char = str[i];
i++;
if (char in decodeMapConstant) {
result.push(decodeMapConstant[char]);
continue;
}
if (char in decodePrefixMapNumber) {
const { prefix, len } = decodePrefixMapNumber[char];
const lenStr = str.substring(i, i + len);
i += len;
const radix = prefix == "N" ? 36 : 10;
const lenNum = parseInt(lenStr, 36);
const value = str.substring(i, i + lenNum);
i += lenNum;
result.push(parseInt(value, radix));
continue;
}
const { prefix, len } = decodePrefixMapObject[char];
const lenStr = str.substring(i, i + len);
i += len;
const lenNum = parseInt(lenStr, 36);
const value = str.substring(i, i + lenNum);
i += lenNum;
if (prefix == "s") {
result.push(value);
} else {
result.push(JSON.parse(value));
}
}
return result;
}

Submodule src/lib updated: b8e4fa6b9e...a5d21afb61

View File

@@ -1,22 +1,34 @@
import { import {
type ObsidianLiveSyncSettings, type ObsidianLiveSyncSettings,
DEFAULT_SETTINGS, DEFAULT_SETTINGS,
KeyIndexOfSettings,
LOG_LEVEL_NOTICE, LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE, LOG_LEVEL_VERBOSE,
} from "../../lib/src/common/types.ts"; } from "../../lib/src/common/types.ts";
import { configURIBase } from "../../common/types.ts"; import { configURIBase, configURIBaseQR } from "../../common/types.ts";
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.js"; // import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.js";
import { decrypt, encrypt } from "../../lib/src/encryption/e2ee_v2.ts"; import { decrypt, encrypt } from "../../lib/src/encryption/e2ee_v2.ts";
import { fireAndForget } from "../../lib/src/common/utils.ts"; import { fireAndForget } from "../../lib/src/common/utils.ts";
import { EVENT_REQUEST_COPY_SETUP_URI, EVENT_REQUEST_OPEN_SETUP_URI, eventHub } from "../../common/events.ts"; import { EVENT_REQUEST_COPY_SETUP_URI, EVENT_REQUEST_OPEN_SETUP_URI, eventHub } from "../../common/events.ts";
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { decodeAnyArray, encodeAnyArray } from "../../common/utils.ts";
import qrcode from "qrcode-generator";
import { $msg } from "../../lib/src/common/i18n.ts";
export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsidianModule { export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsidianModule {
$everyOnload(): Promise<boolean> { $everyOnload(): Promise<boolean> {
this.registerObsidianProtocolHandler( this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
"setuplivesync", if (conf.settings) {
async (conf: any) => await this.setupWizard(conf.settings) await this.setupWizard(conf.settings);
); } else if (conf.settingsQR) {
await this.decodeQR(conf.settingsQR);
}
});
this.addCommand({
id: "livesync-setting-qr",
name: "Show settings as a QR code",
callback: () => fireAndForget(this.encodeQR()),
});
this.addCommand({ this.addCommand({
id: "livesync-copysetupuri", id: "livesync-copysetupuri",
@@ -44,7 +56,45 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
eventHub.onEvent(EVENT_REQUEST_COPY_SETUP_URI, () => fireAndForget(() => this.command_copySetupURI())); eventHub.onEvent(EVENT_REQUEST_COPY_SETUP_URI, () => fireAndForget(() => this.command_copySetupURI()));
return Promise.resolve(true); return Promise.resolve(true);
} }
async encodeQR() {
const settingArr = [];
const fullIndexes = Object.entries(KeyIndexOfSettings) as [keyof ObsidianLiveSyncSettings, number][];
for (const [settingKey, index] of fullIndexes) {
const settingValue = this.settings[settingKey];
settingArr[index] = settingValue;
}
const w = encodeAnyArray(settingArr);
// console.warn(w.length)
// console.warn(w);
// const j = decodeAnyArray(w);
// console.warn(j);
// console.warn(`is equal: ${isObjectDifferent(settingArr, j)}`);
const qr = qrcode(0, "L");
const uri = `${configURIBaseQR}${encodeURIComponent(w)}`;
qr.addData(uri);
qr.make();
const img = qr.createSvgTag(3);
const msg = $msg("Setup.QRCode", { qr_image: img });
await this.core.confirm.confirmWithMessage("Settings QR Code", msg, ["OK"], "OK");
return await Promise.resolve(w);
}
async decodeQR(qr: string) {
const settingArr = decodeAnyArray(qr);
// console.warn(settingArr);
const fullIndexes = Object.entries(KeyIndexOfSettings) as [keyof ObsidianLiveSyncSettings, number][];
const newSettings = { ...DEFAULT_SETTINGS } as ObsidianLiveSyncSettings;
for (const [settingKey, index] of fullIndexes) {
if (index >= settingArr.length) {
// Possibly a new setting added.
continue;
}
const settingValue = settingArr[index];
//@ts-ignore
newSettings[settingKey] = settingValue;
}
console.warn(newSettings);
await this.applySettingWizard(this.settings, newSettings, "QR Code");
}
async command_copySetupURI(stripExtra = true) { async command_copySetupURI(stripExtra = true) {
const encryptingPassphrase = await this.core.confirm.askString( const encryptingPassphrase = await this.core.confirm.askString(
"Encrypt your settings", "Encrypt your settings",
@@ -74,7 +124,7 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
const encryptedSetting = encodeURIComponent( const encryptedSetting = encodeURIComponent(
await encrypt(JSON.stringify(setting), encryptingPassphrase, false) await encrypt(JSON.stringify(setting), encryptingPassphrase, false)
); );
const uri = `${configURIBase}${encryptedSetting}`; const uri = `${configURIBase}${encryptedSetting} `;
await navigator.clipboard.writeText(uri); await navigator.clipboard.writeText(uri);
this._log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); this._log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
} }
@@ -95,7 +145,7 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
const encryptedSetting = encodeURIComponent( const encryptedSetting = encodeURIComponent(
await encrypt(JSON.stringify(setting), encryptingPassphrase, false) await encrypt(JSON.stringify(setting), encryptingPassphrase, false)
); );
const uri = `${configURIBase}${encryptedSetting}`; const uri = `${configURIBase}${encryptedSetting} `;
await navigator.clipboard.writeText(uri); await navigator.clipboard.writeText(uri);
this._log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); this._log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
} }
@@ -103,30 +153,22 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
await this.command_copySetupURI(false); await this.command_copySetupURI(false);
} }
async command_openSetupURI() { async command_openSetupURI() {
const setupURI = await this.core.confirm.askString("Easy setup", "Set up URI", `${configURIBase}aaaaa`); const setupURI = await this.core.confirm.askString("Easy setup", "Set up URI", `${configURIBase} aaaaa`);
if (setupURI === false) return; if (setupURI === false) return;
if (!setupURI.startsWith(`${configURIBase}`)) { if (!setupURI.startsWith(`${configURIBase}`)) {
this._log("Set up URI looks wrong.", LOG_LEVEL_NOTICE); this._log("Set up URI looks wrong.", LOG_LEVEL_NOTICE);
return; return;
} }
const config = decodeURIComponent(setupURI.substring(configURIBase.length)); const config = decodeURIComponent(setupURI.substring(configURIBase.length));
console.dir(config);
await this.setupWizard(config); await this.setupWizard(config);
} }
async setupWizard(confString: string) { async applySettingWizard(
try { oldConf: ObsidianLiveSyncSettings,
const oldConf = JSON.parse(JSON.stringify(this.settings)); newConf: ObsidianLiveSyncSettings,
const encryptingPassphrase = await this.core.confirm.askString( method = "Setup URI"
"Passphrase", ) {
"The passphrase to decrypt your setup URI",
"",
true
);
if (encryptingPassphrase === false) return;
const newConf = await JSON.parse(await decrypt(confString, encryptingPassphrase, false));
if (newConf) {
const result = await this.core.confirm.askYesNoDialog( const result = await this.core.confirm.askYesNoDialog(
"Importing Configuration from the Setup-URI. Are you sure to proceed?", "Importing Configuration from the " + method + ". Are you sure to proceed ? ",
{} {}
); );
if (result == "yes") { if (result == "yes") {
@@ -138,7 +180,7 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
newSettingW.configPassphraseStore = ""; newSettingW.configPassphraseStore = "";
newSettingW.encryptedPassphrase = ""; newSettingW.encryptedPassphrase = "";
newSettingW.encryptedCouchDBConnection = ""; newSettingW.encryptedCouchDBConnection = "";
newSettingW.additionalSuffixOfDatabaseName = `${"appId" in this.app ? this.app.appId : ""}`; newSettingW.additionalSuffixOfDatabaseName = `${"appId" in this.app ? this.app.appId : ""} `;
const setupJustImport = "Don't sync anything, just apply the settings."; const setupJustImport = "Don't sync anything, just apply the settings.";
const setupAsNew = "This is a new client - sync everything from the remote server."; const setupAsNew = "This is a new client - sync everything from the remote server.";
const setupAsMerge = "This is an existing client - merge existing files with the server."; const setupAsMerge = "This is an existing client - merge existing files with the server.";
@@ -164,10 +206,12 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
} else if (setupType == setupAsNew) { } else if (setupType == setupAsNew) {
this.core.settings = newSettingW; this.core.settings = newSettingW;
this.core.$$clearUsedPassphrase(); this.core.$$clearUsedPassphrase();
await this.core.saveSettings();
await this.core.rebuilder.$fetchLocal(); await this.core.rebuilder.$fetchLocal();
} else if (setupType == setupAsMerge) { } else if (setupType == setupAsMerge) {
this.core.settings = newSettingW; this.core.settings = newSettingW;
this.core.$$clearUsedPassphrase(); this.core.$$clearUsedPassphrase();
await this.core.saveSettings();
await this.core.rebuilder.$fetchLocal(true); await this.core.rebuilder.$fetchLocal(true);
} else if (setupType == setupAgain) { } else if (setupType == setupAgain) {
const confirm = const confirm =
@@ -182,6 +226,7 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
return; return;
} }
this.core.settings = newSettingW; this.core.settings = newSettingW;
await this.core.saveSettings();
this.core.$$clearUsedPassphrase(); this.core.$$clearUsedPassphrase();
await this.core.rebuilder.$rebuildEverything(); await this.core.rebuilder.$rebuildEverything();
} else if (setupType == setupManually) { } else if (setupType == setupManually) {
@@ -250,8 +295,26 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
} }
} }
} }
this._log("Configuration loaded.", LOG_LEVEL_NOTICE);
} else {
this._log("Cancelled", LOG_LEVEL_NOTICE);
this.core.settings = oldConf;
return;
} }
}
async setupWizard(confString: string) {
try {
const oldConf = JSON.parse(JSON.stringify(this.settings));
const encryptingPassphrase = await this.core.confirm.askString(
"Passphrase",
"The passphrase to decrypt your setup URI",
"",
true
);
if (encryptingPassphrase === false) return;
const newConf = await JSON.parse(await decrypt(confString, encryptingPassphrase, false));
if (newConf) {
await this.applySettingWizard(oldConf, newConf);
this._log("Configuration loaded.", LOG_LEVEL_NOTICE); this._log("Configuration loaded.", LOG_LEVEL_NOTICE);
} else { } else {
this._log("Cancelled.", LOG_LEVEL_NOTICE); this._log("Cancelled.", LOG_LEVEL_NOTICE);

View File

@@ -437,3 +437,10 @@ span.ls-mark-cr::after {
.sls-dialogue-note-countdown { .sls-dialogue-note-countdown {
font-size: 0.8em; font-size: 0.8em;
} }
.sls-qr {
display: flex;
justify-content: center;
align-items: center;
max-width: max-content;
}