Compare commits

...

14 Commits

Author SHA1 Message Date
vorotamoroz
b6b153c0de Merge pull request #887 from vrtmrz/add_ignore_to_eslint
chore: Change eslint config to ignore _tools
2026-05-11 21:01:47 +09:00
vorotamoroz
eca6a6e0ba chore: Change eslint config to ignore _tools 2026-05-11 13:00:32 +01:00
vorotamoroz
ca43d96c46 Merge pull request #886 from vrtmrz/fix_prettier
Fix prettier config
2026-05-11 20:34:22 +09:00
vorotamoroz
112e3c8b1d Fix prettier config 2026-05-11 12:33:32 +01:00
vorotamoroz
d1eb105801 Merge pull request #872 from OriBoharon/make-cli-onboarding-easier
added documentaion and a hook build script to make onbaording easier when trying to build the cli app
2026-05-11 18:43:00 +09:00
vorotamoroz
d5b93e89cd Change the default issue report label from 'bug' to 'uncategorised' 2026-05-11 17:55:31 +09:00
vorotamoroz
e96fe7cde1 Merge pull request #885 from vrtmrz/tidy_file
(chore) remove obsoleted file
2026-05-11 17:52:22 +09:00
vorotamoroz
68e0610f1d (chore) remove obsoleted file 2026-05-11 09:49:32 +01:00
vorotamoroz
772b6ecf26 Merge pull request #871 from SeleiXi/feat/diff-navigation-buttons
feat: Add diff navigation buttons for Document History
2026-05-09 22:51:04 +09:00
SeleiXi
81dc7f604b feat: auto navigation to diff 2026-05-09 14:07:08 +08:00
vorotamoroz
a9c87fa52e - Add default test environment
- Fixed to use environment by APIs
- Make test parallel
2026-05-08 03:04:14 +00:00
vorotamoroz
e81f023943 Add default test env 2026-05-08 03:01:22 +00:00
bori
7a4b76a550 added documentaion and a hook build script to make onbaording easier 2026-05-02 18:51:07 +03:00
SeleiXi
f9294446ba feat: add diff block navigation to Document History modal
Add prev/next buttons to jump between diff blocks in the
Document History view. Includes position indicator and
auto-scroll with visual focus highlighting.
2026-05-02 22:18:43 +08:00
17 changed files with 307 additions and 309 deletions

View File

@@ -2,7 +2,7 @@
name: Issue report name: Issue report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: 'bug' labels: 'uncategorised'
assignees: '' assignees: ''
--- ---

View File

@@ -17,9 +17,48 @@ permissions:
contents: read contents: read
jobs: jobs:
prepare:
runs-on: ubuntu-latest
outputs:
task_matrix: ${{ steps.select.outputs.task_matrix }}
steps:
- name: Select task matrix
id: select
shell: bash
run: |
set -euo pipefail
SELECTED_TASK="${{ github.event_name == 'workflow_dispatch' && inputs.test_task || 'test' }}"
echo "[INFO] Selected task set: $SELECTED_TASK"
case "$SELECTED_TASK" in
test)
TASK_MATRIX='["test:setup-put-cat","test:mirror","test:push-pull","test:sync-two-local","test:sync-locked-remote","test:p2p-host","test:p2p-peers","test:p2p-sync","test:p2p-three-nodes","test:p2p-upload-download","test:e2e-couchdb","test:e2e-matrix"]'
;;
test:local)
TASK_MATRIX='["test:setup-put-cat","test:mirror"]'
;;
test:e2e-matrix)
TASK_MATRIX='["test:e2e-matrix"]'
;;
test:p2p-sync)
TASK_MATRIX='["test:p2p-sync"]'
;;
*)
echo "[ERROR] Unknown task set: $SELECTED_TASK" >&2
exit 1
;;
esac
echo "task_matrix=$TASK_MATRIX" >> "$GITHUB_OUTPUT"
test: test:
needs: prepare
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 60 timeout-minutes: 60
strategy:
fail-fast: false
matrix:
task: ${{ fromJson(needs.prepare.outputs.task_matrix) }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -64,7 +103,7 @@ jobs:
LIVESYNC_DOCKER_MODE: native LIVESYNC_DOCKER_MODE: native
LIVESYNC_CLI_RETRY: 3 LIVESYNC_CLI_RETRY: 3
run: | run: |
TASK="${{ github.event_name == 'workflow_dispatch' && inputs.test_task || 'test' }}" TASK="${{ matrix.task }}"
echo "[INFO] Running Deno task: $TASK" echo "[INFO] Running Deno task: $TASK"
deno task "$TASK" deno task "$TASK"

View File

@@ -13,7 +13,7 @@ const prettierConfig = {
tabWidth: 4, tabWidth: 4,
printWidth: 120, printWidth: 120,
semi: true, semi: true,
endOfLine: "cr", endOfLine: "lf",
...localPrettierConfig, ...localPrettierConfig,
}; };

View File

@@ -63,6 +63,9 @@ npm test # Run vitest tests (requires Docker services)
### Environment Setup ### Environment Setup
- Clone with submodules: `git clone --recurse-submodules <repository-url>`
- If you already cloned without them, run: `git submodule update --init --recursive`
- The shared common library is provided by the `src/lib` submodule, and builds will fail if it is missing
- Create `.env` file with `PATHS_TEST_INSTALL` pointing to test vault plug-in directories (`:` separated on Unix, `;` on Windows) - Create `.env` file with `PATHS_TEST_INSTALL` pointing to test vault plug-in directories (`:` separated on Unix, `;` on Windows)
- Development builds auto-copy to these paths on build - Development builds auto-copy to these paths on build

View File

@@ -38,6 +38,7 @@ export default [
"modules/octagonal-wheels/rollup.config.js", "modules/octagonal-wheels/rollup.config.js",
"modules/octagonal-wheels/dist/**/*", "modules/octagonal-wheels/dist/**/*",
"src/lib/test", "src/lib/test",
"src/lib/_tools",
"src/lib/src/cli", "src/lib/src/cli",
"**/main.js", "**/main.js",
"src/apps/**/*", "src/apps/**/*",

View File

@@ -3,4 +3,6 @@ test/*
!test/*.sh !test/*.sh
test/test-init.local.sh test/test-init.local.sh
node_modules node_modules
.*.json .*.json
*.env
!.test.env

View File

@@ -92,28 +92,39 @@ livesync-cli ./my-db pull folder/note.md ./note.md
## Installation ## Installation
### Build from source ### Build from source
```bash
# Clone with submodules, because the shared core lives in src/lib
git clone --recurse-submodules <repository-url>
cd obsidian-livesync
# If you already cloned without submodules, run this once instead
git submodule update --init --recursive
# Install dependencies from the repository root
npm install
# Build the CLI from its package directory
cd src/apps/cli
npm run build
```
If `src/lib` is missing, `npm run build` now stops early with a targeted message
instead of a low-level Vite `ENOENT` error.
```bash Run the CLI:
# Install dependencies (ensure you are in repository root directory, not src/apps/cli)
# due to shared dependencies with webapp and main library ```bash
npm install # Run with npm script (from repository root)
# Build the project (ensure you are in `src/apps/cli` directory) npm run --silent cli -- [database-path] [command] [args...]
npm run build
```
Run the CLI:
```bash
# Run with npm script (from repository root)
npm run --silent cli -- [database-path] [command] [args...]
# Run the built executable directly # Run the built executable directly
node src/apps/cli/dist/index.cjs [database-path] [command] [args...] node src/apps/cli/dist/index.cjs [database-path] [command] [args...]
``` ```
### Docker ### Docker
A Docker image is provided for headless / server deployments. Build from the repository root: A Docker image is provided for headless / server deployments. Build from the repository root:
```bash ```bash
docker build -f src/apps/cli/Dockerfile -t livesync-cli . docker build -f src/apps/cli/Dockerfile -t livesync-cli .

View File

@@ -6,6 +6,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"prebuild": "node scripts/check-submodule.mjs",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"cli": "node dist/index.cjs", "cli": "node dist/index.cjs",

View File

@@ -0,0 +1,36 @@
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
const cliDir = process.cwd();
const repoRoot = path.resolve(cliDir, "../../..");
const requiredFiles = [
path.join(repoRoot, "src/lib/src/common/types.ts"),
];
const missingFiles = requiredFiles.filter((filePath) => !fs.existsSync(filePath));
if (missingFiles.length === 0) {
process.exit(0);
}
console.error("[CLI Build Error] Required shared sources were not found.");
console.error("This repository uses Git submodules, and the CLI depends on src/lib.");
console.error("");
console.error("Missing file(s):");
for (const filePath of missingFiles) {
console.error(` - ${path.relative(repoRoot, filePath)}`);
}
console.error("");
console.error("Initialize submodules, then retry the CLI build:");
console.error(" git submodule update --init --recursive");
console.error("");
console.error("For a fresh clone, prefer:");
console.error(" git clone --recurse-submodules <repository-url>");
console.error("");
console.error("Then run:");
console.error(" npm install");
console.error(" cd src/apps/cli");
console.error(" npm run build");
process.exit(1);

View File

@@ -0,0 +1,9 @@
hostname=http://127.0.0.1:5989/
dbname=livesync-test-db-ci
username=admin
password=testpassword
minioEndpoint=http://127.0.0.1:9000
accessKey=minioadmin
secretKey=minioadmin
bucketName=livesync-test-bucket-ci
LIVESYNC_TEST_TEE=1

View File

@@ -1,19 +1,19 @@
{ {
"tasks": { "tasks": {
"test": "deno test -A --no-check test-*.ts", "test": "deno test --env-file=.test.env -A --no-check test-*.ts",
"test:local": "deno test -A --no-check test-setup-put-cat.ts test-mirror.ts", "test:local": "deno test --env-file=.test.env -A --no-check test-setup-put-cat.ts test-mirror.ts",
"test:push-pull": "deno test -A --no-check test-push-pull.ts", "test:push-pull": "deno test --env-file=.test.env -A --no-check test-push-pull.ts",
"test:setup-put-cat": "deno test -A --no-check test-setup-put-cat.ts", "test:setup-put-cat": "deno test --env-file=.test.env -A --no-check test-setup-put-cat.ts",
"test:mirror": "deno test -A --no-check test-mirror.ts", "test:mirror": "deno test --env-file=.test.env -A --no-check test-mirror.ts",
"test:sync-two-local": "deno test -A --no-check test-sync-two-local-databases.ts", "test:sync-two-local": "deno test --env-file=.test.env -A --no-check test-sync-two-local-databases.ts",
"test:sync-locked-remote": "deno test -A --no-check test-sync-locked-remote.ts", "test:sync-locked-remote": "deno test --env-file=.test.env -A --no-check test-sync-locked-remote.ts",
"test:p2p-host": "deno test -A --no-check test-p2p-host.ts", "test:p2p-host": "deno test --env-file=.test.env -A --no-check test-p2p-host.ts",
"test:p2p-peers": "deno test -A --no-check test-p2p-peers-local-relay.ts", "test:p2p-peers": "deno test --env-file=.test.env -A --no-check test-p2p-peers-local-relay.ts",
"test:p2p-sync": "deno test -A --no-check test-p2p-sync.ts", "test:p2p-sync": "deno test --env-file=.test.env -A --no-check test-p2p-sync.ts",
"test:p2p-three-nodes": "deno test -A --no-check test-p2p-three-nodes-conflict.ts", "test:p2p-three-nodes": "deno test --env-file=.test.env -A --no-check test-p2p-three-nodes-conflict.ts",
"test:p2p-upload-download": "deno test -A --no-check test-p2p-upload-download-repro.ts", "test:p2p-upload-download": "deno test --env-file=.test.env -A --no-check test-p2p-upload-download-repro.ts",
"test:e2e-couchdb": "deno test -A --no-check test-e2e-two-vaults-couchdb.ts", "test:e2e-couchdb": "deno test --env-file=.test.env -A --no-check test-e2e-two-vaults-couchdb.ts",
"test:e2e-matrix": "deno test -A --no-check test-e2e-two-vaults-matrix.ts" "test:e2e-matrix": "deno test --env-file=.test.env -A --no-check test-e2e-two-vaults-matrix.ts"
}, },
"imports": { "imports": {
"@std/assert": "jsr:@std/assert@^1.0.13", "@std/assert": "jsr:@std/assert@^1.0.13",

View File

@@ -1,6 +1,5 @@
import { assert } from "@std/assert"; import { assert } from "@std/assert";
import { TempDir } from "./helpers/temp.ts"; import { TempDir } from "./helpers/temp.ts";
import { loadEnvFile } from "./helpers/env.ts";
import { import {
runCli, runCli,
runCliOrFail, runCliOrFail,
@@ -11,31 +10,29 @@ import {
} from "./helpers/cli.ts"; } from "./helpers/cli.ts";
import { applyRemoteSyncSettings, initSettingsFile } from "./helpers/settings.ts"; import { applyRemoteSyncSettings, initSettingsFile } from "./helpers/settings.ts";
import { startCouchdb, startMinio, stopCouchdb, stopMinio } from "./helpers/docker.ts"; import { startCouchdb, startMinio, stopCouchdb, stopMinio } from "./helpers/docker.ts";
import { join } from "@std/path";
const TEST_ENV = join(import.meta.dirname!, "..", ".test.env");
type RemoteType = "COUCHDB" | "MINIO"; type RemoteType = "COUCHDB" | "MINIO";
function requireEnv(env: Record<string, string>, key: string): string { function requireEnv(...keys: string[]): string {
const value = env[key]?.trim(); for (const key of keys) {
if (!value) throw new Error(`Required env var is missing: ${key}`); const value = Deno.env.get(key)?.trim();
return value; if (value) return value;
}
throw new Error(`Required env var is missing: ${keys.join(" or ")}`);
} }
export async function runScenario(remoteType: RemoteType, encrypt: boolean): Promise<void> { export async function runScenario(remoteType: RemoteType, encrypt: boolean): Promise<void> {
const env = await loadEnvFile(TEST_ENV);
const dbSuffix = `${Date.now()}-${Math.floor(Math.random() * 100000)}`; const dbSuffix = `${Date.now()}-${Math.floor(Math.random() * 100000)}`;
const couchdbUri = remoteType === "COUCHDB" ? requireEnv(env, "hostname").replace(/\/$/, "") : ""; const couchdbUri = remoteType === "COUCHDB" ? requireEnv("COUCHDB_URI", "hostname").replace(/\/$/, "") : "";
const couchdbUser = remoteType === "COUCHDB" ? requireEnv(env, "username") : ""; const couchdbUser = remoteType === "COUCHDB" ? requireEnv("COUCHDB_USER", "username") : "";
const couchdbPassword = remoteType === "COUCHDB" ? requireEnv(env, "password") : ""; const couchdbPassword = remoteType === "COUCHDB" ? requireEnv("COUCHDB_PASSWORD", "password") : "";
const dbPrefix = remoteType === "COUCHDB" ? requireEnv(env, "dbname") : ""; const dbPrefix = remoteType === "COUCHDB" ? requireEnv("COUCHDB_DBNAME", "dbname") : "";
const dbname = remoteType === "COUCHDB" ? `${dbPrefix}-${dbSuffix}` : ""; const dbname = remoteType === "COUCHDB" ? `${dbPrefix}-${dbSuffix}` : "";
const minioEndpoint = remoteType === "MINIO" ? requireEnv(env, "minioEndpoint").replace(/\/$/, "") : ""; const minioEndpoint = remoteType === "MINIO" ? requireEnv("MINIO_ENDPOINT", "minioEndpoint").replace(/\/$/, "") : "";
const minioAccessKey = remoteType === "MINIO" ? requireEnv(env, "accessKey") : ""; const minioAccessKey = remoteType === "MINIO" ? requireEnv("MINIO_ACCESS_KEY", "accessKey") : "";
const minioSecretKey = remoteType === "MINIO" ? requireEnv(env, "secretKey") : ""; const minioSecretKey = remoteType === "MINIO" ? requireEnv("MINIO_SECRET_KEY", "secretKey") : "";
const minioBucketBase = remoteType === "MINIO" ? requireEnv(env, "bucketName") : ""; const minioBucketBase = remoteType === "MINIO" ? requireEnv("MINIO_BUCKET_NAME", "bucketName") : "";
const minioBucket = remoteType === "MINIO" ? `${minioBucketBase}-${dbSuffix}` : ""; const minioBucket = remoteType === "MINIO" ? `${minioBucketBase}-${dbSuffix}` : "";
const passphrase = "e2e-passphrase"; const passphrase = "e2e-passphrase";

View File

@@ -6,30 +6,26 @@
*/ */
import { assert, assertStringIncludes } from "@std/assert"; import { assert, assertStringIncludes } from "@std/assert";
import { join } from "@std/path";
import { loadEnvFile } from "./helpers/env.ts";
import { TempDir } from "./helpers/temp.ts"; import { TempDir } from "./helpers/temp.ts";
import { runCli } from "./helpers/cli.ts"; import { runCli } from "./helpers/cli.ts";
import { applyCouchdbSettings, initSettingsFile } from "./helpers/settings.ts"; import { applyCouchdbSettings, initSettingsFile } from "./helpers/settings.ts";
import { createCouchdbDatabase, startCouchdb, stopCouchdb, updateCouchdbDoc } from "./helpers/docker.ts"; import { createCouchdbDatabase, startCouchdb, stopCouchdb, updateCouchdbDoc } from "./helpers/docker.ts";
const TEST_ENV = join(import.meta.dirname!, "..", ".test.env");
const MILESTONE_DOC = "_local/obsydian_livesync_milestone"; const MILESTONE_DOC = "_local/obsydian_livesync_milestone";
function requireEnv(env: Record<string, string>, key: string): string { function requireEnv(...keys: string[]): string {
const value = env[key]?.trim(); for (const key of keys) {
if (!value) { const value = Deno.env.get(key)?.trim();
throw new Error(`Required env var is missing: ${key}`); if (value) return value;
} }
return value; throw new Error(`Required env var is missing: ${keys.join(" or ")}`);
} }
Deno.test("sync: actionable error against locked remote DB", async () => { Deno.test("sync: actionable error against locked remote DB", async () => {
const env = await loadEnvFile(TEST_ENV); const couchdbUri = requireEnv("COUCHDB_URI", "hostname").replace(/\/$/, "");
const couchdbUri = requireEnv(env, "hostname").replace(/\/$/, ""); const couchdbUser = requireEnv("COUCHDB_USER", "username");
const couchdbUser = requireEnv(env, "username"); const couchdbPassword = requireEnv("COUCHDB_PASSWORD", "password");
const couchdbPassword = requireEnv(env, "password"); const dbPrefix = requireEnv("COUCHDB_DBNAME", "dbname");
const dbPrefix = requireEnv(env, "dbname");
const dbname = `${dbPrefix}-locked-${Date.now()}-${Math.floor(Math.random() * 100000)}`; const dbname = `${dbPrefix}-locked-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
await using workDir = await TempDir.create("livesync-cli-locked-test"); await using workDir = await TempDir.create("livesync-cli-locked-test");

View File

@@ -23,13 +23,11 @@
* deno test -A test-sync-two-local-databases.ts * deno test -A test-sync-two-local-databases.ts
*/ */
import { join } from "@std/path";
import { assertEquals, assert } from "@std/assert"; import { assertEquals, assert } from "@std/assert";
import { TempDir } from "./helpers/temp.ts"; import { TempDir } from "./helpers/temp.ts";
import { CLI_DIR, runCliOrFail, jsonFieldIsNa } from "./helpers/cli.ts"; import { runCliOrFail, jsonFieldIsNa } from "./helpers/cli.ts";
import { applyCouchdbSettings, initSettingsFile } from "./helpers/settings.ts"; import { applyCouchdbSettings, initSettingsFile } from "./helpers/settings.ts";
import { startCouchdb, stopCouchdb } from "./helpers/docker.ts"; import { startCouchdb, stopCouchdb } from "./helpers/docker.ts";
import { loadEnvFile } from "./helpers/env.ts";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Load configuration // Load configuration
@@ -41,20 +39,7 @@ async function resolveConfig(): Promise<{
password: string; password: string;
baseDbname: string; baseDbname: string;
} | null> { } | null> {
let env: Record<string, string> = {}; const env = Deno.env.toObject();
// 1. Explicit environment variables take priority
if (Deno.env.get("COUCHDB_URI")) {
env = Object.fromEntries(Deno.env.toObject());
} else {
// 2. TEST_ENV_FILE env var
const envFile = Deno.env.get("TEST_ENV_FILE") ?? join(CLI_DIR, ".test.env");
try {
env = await loadEnvFile(envFile);
} catch {
return null; // no config available — skip
}
}
const uri = (env["COUCHDB_URI"] ?? env["hostname"] ?? "").replace(/\/$/, ""); const uri = (env["COUCHDB_URI"] ?? env["hostname"] ?? "").replace(/\/$/, "");
const user = env["COUCHDB_USER"] ?? env["username"] ?? ""; const user = env["COUCHDB_USER"] ?? env["username"] ?? "";

View File

@@ -66,6 +66,11 @@ export class DocumentHistoryModal extends Modal {
currentDeleted = false; currentDeleted = false;
initialRev?: string; initialRev?: string;
// Diff navigation state
currentDiffIndex = -1;
diffNavContainer!: HTMLDivElement;
diffNavIndicator!: HTMLSpanElement;
constructor( constructor(
app: App, app: App,
core: LiveSyncBaseCore, core: LiveSyncBaseCore,
@@ -216,6 +221,64 @@ export class DocumentHistoryModal extends Modal {
this.contentView.innerHTML = this.contentView.innerHTML =
(this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result; (this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result;
} }
// Reset diff navigation after content changes
this.resetDiffNavigation();
if (this.showDiff) {
this.navigateDiff("next");
}
}
/**
* Navigate to the previous or next diff block in the content view.
* Only effective when diff highlighting is enabled.
*/
navigateDiff(direction: "prev" | "next") {
const diffElements = this.contentView.querySelectorAll(".history-added, .history-deleted");
if (diffElements.length === 0) return;
// Remove previous focus highlight
const prevFocused = this.contentView.querySelector(".diff-focused");
if (prevFocused) {
prevFocused.classList.remove("diff-focused");
}
if (direction === "next") {
this.currentDiffIndex = (this.currentDiffIndex + 1) % diffElements.length;
} else {
this.currentDiffIndex =
this.currentDiffIndex <= 0 ? diffElements.length - 1 : this.currentDiffIndex - 1;
}
const target = diffElements[this.currentDiffIndex];
target.classList.add("diff-focused");
target.scrollIntoView({ behavior: "smooth", block: "center" });
this.diffNavIndicator.textContent = `${this.currentDiffIndex + 1}/${diffElements.length}`;
}
/**
* Reset the diff navigation index and update the indicator.
*/
resetDiffNavigation() {
this.currentDiffIndex = -1;
if (this.diffNavIndicator) {
if (this.showDiff) {
const diffElements = this.contentView.querySelectorAll(".history-added, .history-deleted");
this.diffNavIndicator.textContent = diffElements.length > 0 ? `0/${diffElements.length}` : "\u2014";
} else {
this.diffNavIndicator.textContent = "\u2014";
}
}
this.updateDiffNavVisibility();
}
/**
* Show or hide the diff navigation buttons based on the showDiff state.
*/
updateDiffNavVisibility() {
if (this.diffNavContainer) {
this.diffNavContainer.style.display = this.showDiff ? "flex" : "none";
}
} }
override onOpen() { override onOpen() {
@@ -236,25 +299,47 @@ export class DocumentHistoryModal extends Modal {
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs()); void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
}); });
}); });
contentEl const diffOptionsRow = contentEl.createDiv("");
.createDiv("", (e) => { diffOptionsRow.addClass("op-info");
e.createEl("label", {}, (label) => { diffOptionsRow.addClass("diff-options-row");
label.appendChild(
createEl("input", { type: "checkbox" }, (checkbox) => { diffOptionsRow.createEl("label", {}, (label) => {
if (this.showDiff) { label.appendChild(
checkbox.checked = true; createEl("input", { type: "checkbox" }, (checkbox) => {
} if (this.showDiff) {
checkbox.addEventListener("input", (evt: any) => { checkbox.checked = true;
this.showDiff = checkbox.checked; }
localStorage.setItem("ols-history-highlightdiff", this.showDiff == true ? "1" : ""); checkbox.addEventListener("input", (evt: any) => {
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs()); this.showDiff = checkbox.checked;
}); localStorage.setItem("ols-history-highlightdiff", this.showDiff == true ? "1" : "");
}) this.updateDiffNavVisibility();
); void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
label.appendText("Highlight diff"); });
}); })
}) );
.addClass("op-info"); label.appendText("Highlight diff");
});
// Diff navigation buttons
this.diffNavContainer = diffOptionsRow.createDiv("");
this.diffNavContainer.addClass("diff-nav");
this.diffNavContainer.style.display = this.showDiff ? "flex" : "none";
this.diffNavContainer.createEl("button", { text: "\u25B2 Prev" }, (e) => {
e.addClass("diff-nav-btn");
e.addEventListener("click", () => {
this.navigateDiff("prev");
});
});
this.diffNavContainer.createEl("button", { text: "\u25BC Next" }, (e) => {
e.addClass("diff-nav-btn");
e.addEventListener("click", () => {
this.navigateDiff("next");
});
});
this.diffNavIndicator = this.diffNavContainer.createEl("span", { text: "\u2014" });
this.diffNavIndicator.addClass("diff-nav-indicator");
this.info = contentEl.createDiv(""); this.info = contentEl.createDiv("");
this.info.addClass("op-info"); this.info.addClass("op-info");
fireAndForget(async () => await this.loadFile(this.initialRev)); fireAndForget(async () => await this.loadFile(this.initialRev));

View File

@@ -1,208 +0,0 @@
import { type ObsidianLiveSyncSettings, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types.ts";
import { configURIBase } from "../../common/types.ts";
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.js";
import { fireAndForget } from "../../lib/src/common/utils.ts";
import {
EVENT_REQUEST_COPY_SETUP_URI,
EVENT_REQUEST_OPEN_P2P_SETTINGS,
EVENT_REQUEST_OPEN_SETUP_URI,
EVENT_REQUEST_SHOW_SETUP_QR,
eventHub,
} from "../../common/events.ts";
import { $msg } from "../../lib/src/common/i18n.ts";
// import { performDoctorConsultation, RebuildOptions } from "@/lib/src/common/configForDoc.ts";
import type { LiveSyncCore } from "../../main.ts";
import {
encodeQR,
encodeSettingsToQRCodeData,
encodeSettingsToSetupURI,
OutputFormat,
} from "../../lib/src/API/processSetting.ts";
import { SetupManager, UserMode } from "./SetupManager.ts";
import { AbstractModule } from "../AbstractModule.ts";
export class ModuleSetupObsidian extends AbstractModule {
private _setupManager!: SetupManager;
private _everyOnload(): Promise<boolean> {
this._setupManager = this.core.getModule(SetupManager);
try {
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
if (conf.settings) {
await this._setupManager.onUseSetupURI(
UserMode.Unknown,
`${configURIBase}${encodeURIComponent(conf.settings)}`
);
} else if (conf.settingsQR) {
await this._setupManager.decodeQR(conf.settingsQR);
}
});
} catch (e) {
this._log(
"Failed to register protocol handler. This feature may not work in some environments.",
LOG_LEVEL_NOTICE
);
this._log(e, LOG_LEVEL_VERBOSE);
}
this.addCommand({
id: "livesync-setting-qr",
name: "Show settings as a QR code",
callback: () => fireAndForget(this.encodeQR()),
});
this.addCommand({
id: "livesync-copysetupuri",
name: "Copy settings as a new setup URI",
callback: () => fireAndForget(this.command_copySetupURI()),
});
this.addCommand({
id: "livesync-copysetupuri-short",
name: "Copy settings as a new setup URI (With customization sync)",
callback: () => fireAndForget(this.command_copySetupURIWithSync()),
});
this.addCommand({
id: "livesync-copysetupurifull",
name: "Copy settings as a new setup URI (Full)",
callback: () => fireAndForget(this.command_copySetupURIFull()),
});
this.addCommand({
id: "livesync-opensetupuri",
name: "Use the copied setup URI (Formerly Open setup URI)",
callback: () => fireAndForget(this.command_openSetupURI()),
});
eventHub.onEvent(EVENT_REQUEST_OPEN_SETUP_URI, () => fireAndForget(() => this.command_openSetupURI()));
eventHub.onEvent(EVENT_REQUEST_COPY_SETUP_URI, () => fireAndForget(() => this.command_copySetupURI()));
eventHub.onEvent(EVENT_REQUEST_SHOW_SETUP_QR, () => fireAndForget(() => this.encodeQR()));
eventHub.onEvent(EVENT_REQUEST_OPEN_P2P_SETTINGS, () =>
fireAndForget(() => {
return this._setupManager.onP2PManualSetup(UserMode.Update, this.settings, false);
})
);
return Promise.resolve(true);
}
async encodeQR() {
const settingString = encodeSettingsToQRCodeData(this.settings);
const codeSVG = encodeQR(settingString, OutputFormat.SVG);
if (codeSVG == "") {
return "";
}
const msg = $msg("Setup.QRCode", { qr_image: codeSVG });
await this.core.confirm.confirmWithMessage("Settings QR Code", msg, ["OK"], "OK");
return await Promise.resolve(codeSVG);
}
async askEncryptingPassphrase(): Promise<string | false> {
const encryptingPassphrase = await this.core.confirm.askString(
"Encrypt your settings",
"The passphrase to encrypt the setup URI",
"",
true
);
return encryptingPassphrase;
}
async command_copySetupURI(stripExtra = true) {
const encryptingPassphrase = await this.askEncryptingPassphrase();
if (encryptingPassphrase === false) return;
const encryptedURI = await encodeSettingsToSetupURI(
this.settings,
encryptingPassphrase,
[...((stripExtra ? ["pluginSyncExtendedSetting"] : []) as (keyof ObsidianLiveSyncSettings)[])],
true
);
if (await this.services.UI.promptCopyToClipboard("Setup URI", encryptedURI)) {
this._log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
}
// await navigator.clipboard.writeText(encryptedURI);
}
async command_copySetupURIFull() {
const encryptingPassphrase = await this.askEncryptingPassphrase();
if (encryptingPassphrase === false) return;
const encryptedURI = await encodeSettingsToSetupURI(this.settings, encryptingPassphrase, [], false);
await navigator.clipboard.writeText(encryptedURI);
this._log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
}
async command_copySetupURIWithSync() {
await this.command_copySetupURI(false);
}
async command_openSetupURI() {
await this._setupManager.onUseSetupURI(UserMode.Unknown);
}
// TODO: Where to implement these?
// async askSyncWithRemoteConfig(tryingSettings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
// const buttons = {
// fetch: $msg("Setup.FetchRemoteConf.Buttons.Fetch"),
// no: $msg("Setup.FetchRemoteConf.Buttons.Skip"),
// } as const;
// const fetchRemoteConf = await this.core.confirm.askSelectStringDialogue(
// $msg("Setup.FetchRemoteConf.Message"),
// Object.values(buttons),
// { defaultAction: buttons.fetch, timeout: 0, title: $msg("Setup.FetchRemoteConf.Title") }
// );
// if (fetchRemoteConf == buttons.no) {
// return tryingSettings;
// }
// const newSettings = JSON.parse(JSON.stringify(tryingSettings)) as ObsidianLiveSyncSettings;
// const remoteConfig = await this.services.tweakValue.fetchRemotePreferred(newSettings);
// if (remoteConfig) {
// this._log("Remote configuration found.", LOG_LEVEL_NOTICE);
// const resultSettings = {
// ...DEFAULT_SETTINGS,
// ...tryingSettings,
// ...remoteConfig,
// } satisfies ObsidianLiveSyncSettings;
// return resultSettings;
// } else {
// this._log("Remote configuration not applied.", LOG_LEVEL_NOTICE);
// return {
// ...DEFAULT_SETTINGS,
// ...tryingSettings,
// } satisfies ObsidianLiveSyncSettings;
// }
// }
// async askPerformDoctor(
// tryingSettings: ObsidianLiveSyncSettings
// ): Promise<{ settings: ObsidianLiveSyncSettings; shouldRebuild: boolean; isModified: boolean }> {
// const buttons = {
// yes: $msg("Setup.Doctor.Buttons.Yes"),
// no: $msg("Setup.Doctor.Buttons.No"),
// } as const;
// const performDoctor = await this.core.confirm.askSelectStringDialogue(
// $msg("Setup.Doctor.Message"),
// Object.values(buttons),
// { defaultAction: buttons.yes, timeout: 0, title: $msg("Setup.Doctor.Title") }
// );
// if (performDoctor == buttons.no) {
// return { settings: tryingSettings, shouldRebuild: false, isModified: false };
// }
// const newSettings = JSON.parse(JSON.stringify(tryingSettings)) as ObsidianLiveSyncSettings;
// const { settings, shouldRebuild, isModified } = await performDoctorConsultation(this.core, newSettings, {
// localRebuild: RebuildOptions.AutomaticAcceptable, // Because we are in the setup wizard, we can skip the confirmation.
// remoteRebuild: RebuildOptions.SkipEvenIfRequired,
// activateReason: "New settings from URI",
// });
// if (isModified) {
// this._log("Doctor has fixed some issues!", LOG_LEVEL_NOTICE);
// return {
// settings,
// shouldRebuild,
// isModified,
// };
// } else {
// this._log("Doctor detected no issues!", LOG_LEVEL_NOTICE);
// return { settings: tryingSettings, shouldRebuild: false, isModified: false };
// }
// }
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this));
}
}

View File

@@ -484,4 +484,45 @@ div.workspace-leaf-content[data-type=bases] .livesync-status {
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
}
/* Diff navigation */
.diff-options-row {
display: flex;
align-items: center;
gap: 8px;
}
.diff-nav {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
}
.diff-nav-btn {
padding: 2px 8px;
font-size: 0.85em;
cursor: pointer;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background-color: var(--background-secondary);
color: var(--text-normal);
}
.diff-nav-btn:hover {
background-color: var(--background-modifier-hover);
}
.diff-nav-indicator {
font-size: 0.85em;
color: var(--text-muted);
min-width: 3em;
text-align: center;
}
.diff-focused {
outline: 2px solid var(--interactive-accent);
outline-offset: 1px;
border-radius: 2px;
} }