mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-11 18:21:50 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6b153c0de | ||
|
|
eca6a6e0ba | ||
|
|
ca43d96c46 | ||
|
|
112e3c8b1d | ||
|
|
d1eb105801 | ||
|
|
d5b93e89cd | ||
|
|
e96fe7cde1 | ||
|
|
68e0610f1d | ||
|
|
772b6ecf26 | ||
|
|
81dc7f604b | ||
|
|
a9c87fa52e | ||
|
|
e81f023943 | ||
|
|
7a4b76a550 | ||
|
|
f9294446ba |
2
.github/ISSUE_TEMPLATE/issue-report.md
vendored
2
.github/ISSUE_TEMPLATE/issue-report.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Issue report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
labels: 'uncategorised'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
41
.github/workflows/cli-deno-tests.yml
vendored
41
.github/workflows/cli-deno-tests.yml
vendored
@@ -17,9 +17,48 @@ permissions:
|
||||
contents: read
|
||||
|
||||
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:
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
task: ${{ fromJson(needs.prepare.outputs.task_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -64,7 +103,7 @@ jobs:
|
||||
LIVESYNC_DOCKER_MODE: native
|
||||
LIVESYNC_CLI_RETRY: 3
|
||||
run: |
|
||||
TASK="${{ github.event_name == 'workflow_dispatch' && inputs.test_task || 'test' }}"
|
||||
TASK="${{ matrix.task }}"
|
||||
echo "[INFO] Running Deno task: $TASK"
|
||||
deno task "$TASK"
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const prettierConfig = {
|
||||
tabWidth: 4,
|
||||
printWidth: 120,
|
||||
semi: true,
|
||||
endOfLine: "cr",
|
||||
endOfLine: "lf",
|
||||
...localPrettierConfig,
|
||||
};
|
||||
|
||||
|
||||
3
devs.md
3
devs.md
@@ -63,6 +63,9 @@ npm test # Run vitest tests (requires Docker services)
|
||||
|
||||
### 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)
|
||||
- Development builds auto-copy to these paths on build
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export default [
|
||||
"modules/octagonal-wheels/rollup.config.js",
|
||||
"modules/octagonal-wheels/dist/**/*",
|
||||
"src/lib/test",
|
||||
"src/lib/_tools",
|
||||
"src/lib/src/cli",
|
||||
"**/main.js",
|
||||
"src/apps/**/*",
|
||||
|
||||
4
src/apps/cli/.gitignore
vendored
4
src/apps/cli/.gitignore
vendored
@@ -3,4 +3,6 @@ test/*
|
||||
!test/*.sh
|
||||
test/test-init.local.sh
|
||||
node_modules
|
||||
.*.json
|
||||
.*.json
|
||||
*.env
|
||||
!.test.env
|
||||
@@ -92,28 +92,39 @@ livesync-cli ./my-db pull folder/note.md ./note.md
|
||||
|
||||
## 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
|
||||
# Install dependencies (ensure you are in repository root directory, not src/apps/cli)
|
||||
# due to shared dependencies with webapp and main library
|
||||
npm install
|
||||
# Build the project (ensure you are in `src/apps/cli` directory)
|
||||
npm run build
|
||||
```
|
||||
|
||||
Run the CLI:
|
||||
|
||||
```bash
|
||||
# Run with npm script (from repository root)
|
||||
npm run --silent cli -- [database-path] [command] [args...]
|
||||
Run the CLI:
|
||||
|
||||
```bash
|
||||
# Run with npm script (from repository root)
|
||||
npm run --silent cli -- [database-path] [command] [args...]
|
||||
# Run the built executable directly
|
||||
node src/apps/cli/dist/index.cjs [database-path] [command] [args...]
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
A Docker image is provided for headless / server deployments. Build from the repository root:
|
||||
### Docker
|
||||
|
||||
A Docker image is provided for headless / server deployments. Build from the repository root:
|
||||
|
||||
```bash
|
||||
docker build -f src/apps/cli/Dockerfile -t livesync-cli .
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"prebuild": "node scripts/check-submodule.mjs",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"cli": "node dist/index.cjs",
|
||||
|
||||
36
src/apps/cli/scripts/check-submodule.mjs
Normal file
36
src/apps/cli/scripts/check-submodule.mjs
Normal 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);
|
||||
9
src/apps/cli/testdeno/.test.env
Normal file
9
src/apps/cli/testdeno/.test.env
Normal 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
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"tasks": {
|
||||
"test": "deno test -A --no-check test-*.ts",
|
||||
"test:local": "deno test -A --no-check test-setup-put-cat.ts test-mirror.ts",
|
||||
"test:push-pull": "deno test -A --no-check test-push-pull.ts",
|
||||
"test:setup-put-cat": "deno test -A --no-check test-setup-put-cat.ts",
|
||||
"test:mirror": "deno test -A --no-check test-mirror.ts",
|
||||
"test:sync-two-local": "deno test -A --no-check test-sync-two-local-databases.ts",
|
||||
"test:sync-locked-remote": "deno test -A --no-check test-sync-locked-remote.ts",
|
||||
"test:p2p-host": "deno test -A --no-check test-p2p-host.ts",
|
||||
"test:p2p-peers": "deno test -A --no-check test-p2p-peers-local-relay.ts",
|
||||
"test:p2p-sync": "deno test -A --no-check test-p2p-sync.ts",
|
||||
"test:p2p-three-nodes": "deno test -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:e2e-couchdb": "deno test -A --no-check test-e2e-two-vaults-couchdb.ts",
|
||||
"test:e2e-matrix": "deno test -A --no-check test-e2e-two-vaults-matrix.ts"
|
||||
"test": "deno test --env-file=.test.env -A --no-check test-*.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 --env-file=.test.env -A --no-check test-push-pull.ts",
|
||||
"test:setup-put-cat": "deno test --env-file=.test.env -A --no-check test-setup-put-cat.ts",
|
||||
"test:mirror": "deno test --env-file=.test.env -A --no-check test-mirror.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 --env-file=.test.env -A --no-check test-sync-locked-remote.ts",
|
||||
"test:p2p-host": "deno test --env-file=.test.env -A --no-check test-p2p-host.ts",
|
||||
"test:p2p-peers": "deno test --env-file=.test.env -A --no-check test-p2p-peers-local-relay.ts",
|
||||
"test:p2p-sync": "deno test --env-file=.test.env -A --no-check test-p2p-sync.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 --env-file=.test.env -A --no-check test-p2p-upload-download-repro.ts",
|
||||
"test:e2e-couchdb": "deno test --env-file=.test.env -A --no-check test-e2e-two-vaults-couchdb.ts",
|
||||
"test:e2e-matrix": "deno test --env-file=.test.env -A --no-check test-e2e-two-vaults-matrix.ts"
|
||||
},
|
||||
"imports": {
|
||||
"@std/assert": "jsr:@std/assert@^1.0.13",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { assert } from "@std/assert";
|
||||
import { TempDir } from "./helpers/temp.ts";
|
||||
import { loadEnvFile } from "./helpers/env.ts";
|
||||
import {
|
||||
runCli,
|
||||
runCliOrFail,
|
||||
@@ -11,31 +10,29 @@ import {
|
||||
} from "./helpers/cli.ts";
|
||||
import { applyRemoteSyncSettings, initSettingsFile } from "./helpers/settings.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";
|
||||
|
||||
function requireEnv(env: Record<string, string>, key: string): string {
|
||||
const value = env[key]?.trim();
|
||||
if (!value) throw new Error(`Required env var is missing: ${key}`);
|
||||
return value;
|
||||
function requireEnv(...keys: string[]): string {
|
||||
for (const key of keys) {
|
||||
const value = Deno.env.get(key)?.trim();
|
||||
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> {
|
||||
const env = await loadEnvFile(TEST_ENV);
|
||||
const dbSuffix = `${Date.now()}-${Math.floor(Math.random() * 100000)}`;
|
||||
|
||||
const couchdbUri = remoteType === "COUCHDB" ? requireEnv(env, "hostname").replace(/\/$/, "") : "";
|
||||
const couchdbUser = remoteType === "COUCHDB" ? requireEnv(env, "username") : "";
|
||||
const couchdbPassword = remoteType === "COUCHDB" ? requireEnv(env, "password") : "";
|
||||
const dbPrefix = remoteType === "COUCHDB" ? requireEnv(env, "dbname") : "";
|
||||
const couchdbUri = remoteType === "COUCHDB" ? requireEnv("COUCHDB_URI", "hostname").replace(/\/$/, "") : "";
|
||||
const couchdbUser = remoteType === "COUCHDB" ? requireEnv("COUCHDB_USER", "username") : "";
|
||||
const couchdbPassword = remoteType === "COUCHDB" ? requireEnv("COUCHDB_PASSWORD", "password") : "";
|
||||
const dbPrefix = remoteType === "COUCHDB" ? requireEnv("COUCHDB_DBNAME", "dbname") : "";
|
||||
const dbname = remoteType === "COUCHDB" ? `${dbPrefix}-${dbSuffix}` : "";
|
||||
|
||||
const minioEndpoint = remoteType === "MINIO" ? requireEnv(env, "minioEndpoint").replace(/\/$/, "") : "";
|
||||
const minioAccessKey = remoteType === "MINIO" ? requireEnv(env, "accessKey") : "";
|
||||
const minioSecretKey = remoteType === "MINIO" ? requireEnv(env, "secretKey") : "";
|
||||
const minioBucketBase = remoteType === "MINIO" ? requireEnv(env, "bucketName") : "";
|
||||
const minioEndpoint = remoteType === "MINIO" ? requireEnv("MINIO_ENDPOINT", "minioEndpoint").replace(/\/$/, "") : "";
|
||||
const minioAccessKey = remoteType === "MINIO" ? requireEnv("MINIO_ACCESS_KEY", "accessKey") : "";
|
||||
const minioSecretKey = remoteType === "MINIO" ? requireEnv("MINIO_SECRET_KEY", "secretKey") : "";
|
||||
const minioBucketBase = remoteType === "MINIO" ? requireEnv("MINIO_BUCKET_NAME", "bucketName") : "";
|
||||
const minioBucket = remoteType === "MINIO" ? `${minioBucketBase}-${dbSuffix}` : "";
|
||||
|
||||
const passphrase = "e2e-passphrase";
|
||||
|
||||
@@ -6,30 +6,26 @@
|
||||
*/
|
||||
|
||||
import { assert, assertStringIncludes } from "@std/assert";
|
||||
import { join } from "@std/path";
|
||||
import { loadEnvFile } from "./helpers/env.ts";
|
||||
import { TempDir } from "./helpers/temp.ts";
|
||||
import { runCli } from "./helpers/cli.ts";
|
||||
import { applyCouchdbSettings, initSettingsFile } from "./helpers/settings.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";
|
||||
|
||||
function requireEnv(env: Record<string, string>, key: string): string {
|
||||
const value = env[key]?.trim();
|
||||
if (!value) {
|
||||
throw new Error(`Required env var is missing: ${key}`);
|
||||
function requireEnv(...keys: string[]): string {
|
||||
for (const key of keys) {
|
||||
const value = Deno.env.get(key)?.trim();
|
||||
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 () => {
|
||||
const env = await loadEnvFile(TEST_ENV);
|
||||
const couchdbUri = requireEnv(env, "hostname").replace(/\/$/, "");
|
||||
const couchdbUser = requireEnv(env, "username");
|
||||
const couchdbPassword = requireEnv(env, "password");
|
||||
const dbPrefix = requireEnv(env, "dbname");
|
||||
const couchdbUri = requireEnv("COUCHDB_URI", "hostname").replace(/\/$/, "");
|
||||
const couchdbUser = requireEnv("COUCHDB_USER", "username");
|
||||
const couchdbPassword = requireEnv("COUCHDB_PASSWORD", "password");
|
||||
const dbPrefix = requireEnv("COUCHDB_DBNAME", "dbname");
|
||||
const dbname = `${dbPrefix}-locked-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
|
||||
|
||||
await using workDir = await TempDir.create("livesync-cli-locked-test");
|
||||
|
||||
@@ -23,13 +23,11 @@
|
||||
* deno test -A test-sync-two-local-databases.ts
|
||||
*/
|
||||
|
||||
import { join } from "@std/path";
|
||||
import { assertEquals, assert } from "@std/assert";
|
||||
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 { startCouchdb, stopCouchdb } from "./helpers/docker.ts";
|
||||
import { loadEnvFile } from "./helpers/env.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Load configuration
|
||||
@@ -41,20 +39,7 @@ async function resolveConfig(): Promise<{
|
||||
password: string;
|
||||
baseDbname: string;
|
||||
} | null> {
|
||||
let env: Record<string, string> = {};
|
||||
|
||||
// 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 env = Deno.env.toObject();
|
||||
|
||||
const uri = (env["COUCHDB_URI"] ?? env["hostname"] ?? "").replace(/\/$/, "");
|
||||
const user = env["COUCHDB_USER"] ?? env["username"] ?? "";
|
||||
|
||||
@@ -66,6 +66,11 @@ export class DocumentHistoryModal extends Modal {
|
||||
currentDeleted = false;
|
||||
initialRev?: string;
|
||||
|
||||
// Diff navigation state
|
||||
currentDiffIndex = -1;
|
||||
diffNavContainer!: HTMLDivElement;
|
||||
diffNavIndicator!: HTMLSpanElement;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
core: LiveSyncBaseCore,
|
||||
@@ -216,6 +221,64 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.contentView.innerHTML =
|
||||
(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() {
|
||||
@@ -236,25 +299,47 @@ export class DocumentHistoryModal extends Modal {
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
});
|
||||
});
|
||||
contentEl
|
||||
.createDiv("", (e) => {
|
||||
e.createEl("label", {}, (label) => {
|
||||
label.appendChild(
|
||||
createEl("input", { type: "checkbox" }, (checkbox) => {
|
||||
if (this.showDiff) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
checkbox.addEventListener("input", (evt: any) => {
|
||||
this.showDiff = checkbox.checked;
|
||||
localStorage.setItem("ols-history-highlightdiff", this.showDiff == true ? "1" : "");
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
});
|
||||
})
|
||||
);
|
||||
label.appendText("Highlight diff");
|
||||
});
|
||||
})
|
||||
.addClass("op-info");
|
||||
const diffOptionsRow = contentEl.createDiv("");
|
||||
diffOptionsRow.addClass("op-info");
|
||||
diffOptionsRow.addClass("diff-options-row");
|
||||
|
||||
diffOptionsRow.createEl("label", {}, (label) => {
|
||||
label.appendChild(
|
||||
createEl("input", { type: "checkbox" }, (checkbox) => {
|
||||
if (this.showDiff) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
checkbox.addEventListener("input", (evt: any) => {
|
||||
this.showDiff = checkbox.checked;
|
||||
localStorage.setItem("ols-history-highlightdiff", this.showDiff == true ? "1" : "");
|
||||
this.updateDiffNavVisibility();
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
});
|
||||
})
|
||||
);
|
||||
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.addClass("op-info");
|
||||
fireAndForget(async () => await this.loadFile(this.initialRev));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
41
styles.css
41
styles.css
@@ -484,4 +484,45 @@ div.workspace-leaf-content[data-type=bases] .livesync-status {
|
||||
white-space: pre-wrap;
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user