mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-02 00:37:07 +00:00
Merge pull request #896 from vrtmrz/address_community_review
Address community review
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
import builtins from "builtin-modules";
|
|
||||||
import sveltePlugin from "esbuild-svelte";
|
import sveltePlugin from "esbuild-svelte";
|
||||||
import { sveltePreprocess } from "svelte-preprocess";
|
import { sveltePreprocess } from "svelte-preprocess";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
|||||||
+60
-80
@@ -1,103 +1,83 @@
|
|||||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
|
||||||
import svelte from "eslint-plugin-svelte";
|
|
||||||
import _import from "eslint-plugin-import";
|
|
||||||
import { fixupPluginRules } from "@eslint/compat";
|
|
||||||
import tsParser from "@typescript-eslint/parser";
|
import tsParser from "@typescript-eslint/parser";
|
||||||
import path from "node:path";
|
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||||
import { fileURLToPath } from "node:url";
|
import globals from "globals";
|
||||||
import js from "@eslint/js";
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
import * as sveltePlugin from "eslint-plugin-svelte";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
export default defineConfig([
|
||||||
const __dirname = path.dirname(__filename);
|
globalIgnores([
|
||||||
const compat = new FlatCompat({
|
"**/node_modules/*",
|
||||||
baseDirectory: __dirname,
|
"**/jest.config.js",
|
||||||
recommendedConfig: js.configs.recommended,
|
"src/lib/coverage",
|
||||||
allConfig: js.configs.all,
|
"src/lib/browsertest",
|
||||||
});
|
"**/test.ts",
|
||||||
|
"**/tests.ts",
|
||||||
export default [
|
"**/**test.ts",
|
||||||
|
"**/**.test.ts",
|
||||||
|
"**/*.unit.spec.ts",
|
||||||
|
"**/esbuild.*.mjs",
|
||||||
|
"**/terser.*.mjs",
|
||||||
|
"**/node_modules",
|
||||||
|
"**/build",
|
||||||
|
"**/.eslintrc.js.bak",
|
||||||
|
"src/lib/src/patches/pouchdb-utils",
|
||||||
|
"**/esbuild.config.mjs",
|
||||||
|
"**/rollup.config.js",
|
||||||
|
"modules/octagonal-wheels/rollup.config.js",
|
||||||
|
"modules/octagonal-wheels/dist/**/*",
|
||||||
|
"src/lib/test",
|
||||||
|
"src/lib/_tools",
|
||||||
|
"src/lib/src/cli",
|
||||||
|
"**/main.js",
|
||||||
|
"src/apps/**/*",
|
||||||
|
".prettierrc.*.mjs",
|
||||||
|
".prettierrc.mjs",
|
||||||
|
"*.config.mjs",
|
||||||
|
"src/apps/**/*",
|
||||||
|
"src/lib/src/services/implements/browser/**",
|
||||||
|
"src/lib/src/services/implements/headless/**",
|
||||||
|
"src/lib/src/API",
|
||||||
|
]),
|
||||||
|
...sveltePlugin.configs["flat/base"],
|
||||||
|
...obsidianmd.configs.recommended,
|
||||||
{
|
{
|
||||||
ignores: [
|
files: ["**/*.ts"],
|
||||||
"**/node_modules/*",
|
|
||||||
"**/jest.config.js",
|
|
||||||
"src/lib/coverage",
|
|
||||||
"src/lib/browsertest",
|
|
||||||
"**/test.ts",
|
|
||||||
"**/tests.ts",
|
|
||||||
"**/**test.ts",
|
|
||||||
"**/**.test.ts",
|
|
||||||
"**/esbuild.*.mjs",
|
|
||||||
"**/terser.*.mjs",
|
|
||||||
"**/node_modules",
|
|
||||||
"**/build",
|
|
||||||
"**/.eslintrc.js.bak",
|
|
||||||
"src/lib/src/patches/pouchdb-utils",
|
|
||||||
"**/esbuild.config.mjs",
|
|
||||||
"**/rollup.config.js",
|
|
||||||
"modules/octagonal-wheels/rollup.config.js",
|
|
||||||
"modules/octagonal-wheels/dist/**/*",
|
|
||||||
"src/lib/test",
|
|
||||||
"src/lib/_tools",
|
|
||||||
"src/lib/src/cli",
|
|
||||||
"**/main.js",
|
|
||||||
"src/apps/**/*",
|
|
||||||
".prettierrc.*.mjs",
|
|
||||||
".prettierrc.mjs",
|
|
||||||
"*.config.mjs"
|
|
||||||
],
|
|
||||||
},
|
|
||||||
...compat.extends(
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended"
|
|
||||||
),
|
|
||||||
{
|
|
||||||
plugins: {
|
|
||||||
"@typescript-eslint": typescriptEslint,
|
|
||||||
svelte,
|
|
||||||
import: fixupPluginRules(_import),
|
|
||||||
},
|
|
||||||
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
|
globals: { ...globals.browser },
|
||||||
parser: tsParser,
|
parser: tsParser,
|
||||||
ecmaVersion: 5,
|
|
||||||
sourceType: "module",
|
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: ["tsconfig.json"],
|
project: "./tsconfig.json",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
args: "none",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
"no-unused-labels": "off",
|
"no-unused-labels": "off",
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
"no-prototype-builtins": "off",
|
"no-prototype-builtins": "off",
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
"require-await": "error",
|
"require-await": "error",
|
||||||
|
"obsidianmd/rule-custom-message": "off", // Temporary
|
||||||
|
"obsidianmd/ui/sentence-case": "off", // Temporary
|
||||||
"@typescript-eslint/require-await": "warn",
|
"@typescript-eslint/require-await": "warn",
|
||||||
"@typescript-eslint/no-misused-promises": "warn",
|
"@typescript-eslint/no-misused-promises": "warn",
|
||||||
"@typescript-eslint/no-floating-promises": "warn",
|
"@typescript-eslint/no-floating-promises": "warn",
|
||||||
"no-async-promise-executor": "warn",
|
"no-async-promise-executor": "warn",
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||||
|
"no-constant-condition": ["error", { checkLoops: false }],
|
||||||
"no-constant-condition": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
checkLoops: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
files: ["**/*.svelte"],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
|
||||||
|
"obsidianmd/no-plugin-as-component": "off", // Temporary
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.25.60",
|
"version": "0.25.60",
|
||||||
"minAppVersion": "0.9.12",
|
"minAppVersion": "1.7.2",
|
||||||
"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.",
|
"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",
|
"author": "vorotamoroz",
|
||||||
"authorUrl": "https://github.com/vrtmrz",
|
"authorUrl": "https://github.com/vrtmrz",
|
||||||
|
|||||||
Generated
+1271
-80
File diff suppressed because it is too large
Load Diff
+4
-7
@@ -61,8 +61,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chialab/esbuild-plugin-worker": "^0.19.0",
|
"@chialab/esbuild-plugin-worker": "^0.19.0",
|
||||||
"@eslint/compat": "^2.0.2",
|
|
||||||
"@eslint/eslintrc": "^3.3.4",
|
|
||||||
"@eslint/js": "^9.39.3",
|
"@eslint/js": "^9.39.3",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
"@tsconfig/svelte": "^5.0.8",
|
"@tsconfig/svelte": "^5.0.8",
|
||||||
@@ -84,18 +82,15 @@
|
|||||||
"@vitest/browser": "^4.1.1",
|
"@vitest/browser": "^4.1.1",
|
||||||
"@vitest/browser-playwright": "^4.1.1",
|
"@vitest/browser-playwright": "^4.1.1",
|
||||||
"@vitest/coverage-v8": "^4.1.1",
|
"@vitest/coverage-v8": "^4.1.1",
|
||||||
"builtin-modules": "5.0.0",
|
|
||||||
"dotenv": "^17.3.1",
|
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"esbuild": "0.25.0",
|
"esbuild": "0.25.0",
|
||||||
"esbuild-plugin-inline-worker": "^0.1.1",
|
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||||
"esbuild-svelte": "^0.9.4",
|
"esbuild-svelte": "^0.9.4",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "^9.39.3",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||||
"eslint-plugin-svelte": "^3.15.0",
|
"eslint-plugin-svelte": "^3.15.0",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"glob": "^13.0.6",
|
"globals": "^14.0.0",
|
||||||
"obsidian": "^1.12.3",
|
|
||||||
"playwright": "^1.58.2",
|
"playwright": "^1.58.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"postcss-load-config": "^6.0.1",
|
"postcss-load-config": "^6.0.1",
|
||||||
@@ -116,6 +111,7 @@
|
|||||||
"svelte-check": "^4.4.3",
|
"svelte-check": "^4.4.3",
|
||||||
"svelte-preprocess": "^6.0.3",
|
"svelte-preprocess": "^6.0.3",
|
||||||
"terser": "^5.39.0",
|
"terser": "^5.39.0",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
"transform-pouch": "^2.0.0",
|
"transform-pouch": "^2.0.0",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
@@ -136,6 +132,7 @@
|
|||||||
"@trystero-p2p/nostr": "^0.23.0",
|
"@trystero-p2p/nostr": "^0.23.0",
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
"commander": "^14.0.3",
|
"commander": "^14.0.3",
|
||||||
|
"obsidian": "^1.12.3",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { LOG_LEVEL_INFO } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_INFO } from "octagonal-wheels/common/logger";
|
||||||
|
import type PouchDB from "pouchdb-core";
|
||||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||||
import type { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "./lib/src/common/types";
|
import type { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "./lib/src/common/types";
|
||||||
import { __$checkInstanceBinding } from "./lib/src/dev/checks";
|
import { __$checkInstanceBinding } from "./lib/src/dev/checks";
|
||||||
@@ -123,7 +124,7 @@ export class LiveSyncBaseCore<
|
|||||||
for (const module of this.modules) {
|
for (const module of this.modules) {
|
||||||
if (module.constructor === constructor) return module as T;
|
if (module.constructor === constructor) return module as T;
|
||||||
}
|
}
|
||||||
throw new Error(`Module ${constructor} not found or not loaded.`);
|
throw new Error(`Module ${constructor.name} not found or not loaded.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,8 +161,10 @@ export class LiveSyncBaseCore<
|
|||||||
module.onBindFunction(this, this.services);
|
module.onBindFunction(this, this.services);
|
||||||
__$checkInstanceBinding(module); // Check if all functions are properly bound, and log warnings if not.
|
__$checkInstanceBinding(module); // Check if all functions are properly bound, and log warnings if not.
|
||||||
} else {
|
} else {
|
||||||
|
// module should not be never.
|
||||||
|
const moduleName = (module as unknown)?.constructor?.name ?? "unknown";
|
||||||
this.services.API.addLog(
|
this.services.API.addLog(
|
||||||
`Module ${(module as any)?.constructor?.name ?? "unknown"} does not have onBindFunction, skipping binding.`,
|
`Module ${moduleName} does not have onBindFunction, skipping binding.`,
|
||||||
LOG_LEVEL_INFO
|
LOG_LEVEL_INFO
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,12 +257,12 @@ describe("daemon command", () => {
|
|||||||
// failure 1: 30000*2=60000, failure 2: 30000*4=120000,
|
// failure 1: 30000*2=60000, failure 2: 30000*4=120000,
|
||||||
// failure 3: 30000*8=240000, failure 4: 30000*16=480000→capped, 5→cap, 6→cap
|
// failure 3: 30000*8=240000, failure 4: 30000*16=480000→capped, 5→cap, 6→cap
|
||||||
const expectedIntervals = [
|
const expectedIntervals = [
|
||||||
baseMs * 2, // after failure 1: 60000
|
baseMs * 2, // after failure 1: 60000
|
||||||
baseMs * 4, // after failure 2: 120000
|
baseMs * 4, // after failure 2: 120000
|
||||||
baseMs * 8, // after failure 3: 240000
|
baseMs * 8, // after failure 3: 240000
|
||||||
300_000, // after failure 4 (would be 480000, capped)
|
300_000, // after failure 4 (would be 480000, capped)
|
||||||
300_000, // after failure 5 (cap)
|
300_000, // after failure 5 (cap)
|
||||||
300_000, // after failure 6 (cap)
|
300_000, // after failure 6 (cap)
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const expected of expectedIntervals) {
|
for (const expected of expectedIntervals) {
|
||||||
|
|||||||
@@ -43,10 +43,13 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
|
|
||||||
// 3. Re-enable sync.
|
// 3. Re-enable sync.
|
||||||
const restoreSyncSettings = async () => {
|
const restoreSyncSettings = async () => {
|
||||||
await core.services.setting.applyPartial({
|
await core.services.setting.applyPartial(
|
||||||
...context.originalSyncSettings,
|
{
|
||||||
suspendFileWatching: false,
|
...context.originalSyncSettings,
|
||||||
}, true);
|
suspendFileWatching: false,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
// applySettings fires the full lifecycle: onSuspending → onResumed.
|
// applySettings fires the full lifecycle: onSuspending → onResumed.
|
||||||
// ModuleReplicatorCouchDB starts continuous replication on onResumed
|
// ModuleReplicatorCouchDB starts continuous replication on onResumed
|
||||||
// via fireAndForget.
|
// via fireAndForget.
|
||||||
@@ -54,10 +57,13 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
// Lifecycle events (onSuspending) may re-enable suspension flags.
|
// Lifecycle events (onSuspending) may re-enable suspension flags.
|
||||||
// Clear them explicitly after the lifecycle completes. applyPartial
|
// Clear them explicitly after the lifecycle completes. applyPartial
|
||||||
// with true is a direct store write — it does not re-trigger lifecycle.
|
// with true is a direct store write — it does not re-trigger lifecycle.
|
||||||
await core.services.setting.applyPartial({
|
await core.services.setting.applyPartial(
|
||||||
suspendFileWatching: false,
|
{
|
||||||
suspendParseReplicationResult: false,
|
suspendFileWatching: false,
|
||||||
}, true);
|
suspendParseReplicationResult: false,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
};
|
};
|
||||||
if (options.interval) {
|
if (options.interval) {
|
||||||
log(`Polling mode: syncing every ${options.interval}s`);
|
log(`Polling mode: syncing every ${options.interval}s`);
|
||||||
@@ -80,7 +86,9 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
currentIntervalMs = Math.min(baseIntervalMs * Math.pow(2, consecutiveFailures), maxIntervalMs);
|
currentIntervalMs = Math.min(baseIntervalMs * Math.pow(2, consecutiveFailures), maxIntervalMs);
|
||||||
console.error(`[Daemon] Poll error (${consecutiveFailures} consecutive):`, err);
|
console.error(`[Daemon] Poll error (${consecutiveFailures} consecutive):`, err);
|
||||||
if (consecutiveFailures >= 5) {
|
if (consecutiveFailures >= 5) {
|
||||||
console.error(`[Daemon] Warning: ${consecutiveFailures} consecutive failures, backing off to ${Math.round(currentIntervalMs / 1000)}s`);
|
console.error(
|
||||||
|
`[Daemon] Warning: ${consecutiveFailures} consecutive failures, backing off to ${Math.round(currentIntervalMs / 1000)}s`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pollTimer = setTimeout(poll, currentIntervalMs);
|
pollTimer = setTimeout(poll, currentIntervalMs);
|
||||||
@@ -99,9 +107,11 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
log("LiveSync active");
|
log("LiveSync active");
|
||||||
const currentSettings = core.services.setting.currentSettings();
|
const currentSettings = core.services.setting.currentSettings();
|
||||||
if (!currentSettings.liveSync && !currentSettings.syncOnStart) {
|
if (!currentSettings.liveSync && !currentSettings.syncOnStart) {
|
||||||
console.error("[Daemon] Warning: liveSync and syncOnStart are both disabled in settings. " +
|
console.error(
|
||||||
"No sync will occur. Set liveSync=true in your settings file for continuous sync, " +
|
"[Daemon] Warning: liveSync and syncOnStart are both disabled in settings. " +
|
||||||
"or use --interval for polling mode.");
|
"No sync will occur. Set liveSync=true in your settings file for continuous sync, " +
|
||||||
|
"or use --interval for polling mode."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,16 @@ export interface CLICommandContext {
|
|||||||
databasePath: string;
|
databasePath: string;
|
||||||
core: LiveSyncBaseCore<ServiceContext, any>;
|
core: LiveSyncBaseCore<ServiceContext, any>;
|
||||||
settingsPath: string;
|
settingsPath: string;
|
||||||
originalSyncSettings: Pick<ObsidianLiveSyncSettings, "liveSync" | "syncOnStart" | "periodicReplication" | "syncOnSave" | "syncOnEditorSave" | "syncOnFileOpen" | "syncAfterMerge">;
|
originalSyncSettings: Pick<
|
||||||
|
ObsidianLiveSyncSettings,
|
||||||
|
| "liveSync"
|
||||||
|
| "syncOnStart"
|
||||||
|
| "periodicReplication"
|
||||||
|
| "syncOnSave"
|
||||||
|
| "syncOnEditorSave"
|
||||||
|
| "syncOnFileOpen"
|
||||||
|
| "syncAfterMerge"
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VALID_COMMANDS = new Set([
|
export const VALID_COMMANDS = new Set([
|
||||||
|
|||||||
@@ -280,16 +280,13 @@ export async function main() {
|
|||||||
// chokidar's ignored option is populated when beginWatch() fires during onLoad().
|
// chokidar's ignored option is populated when beginWatch() fires during onLoad().
|
||||||
const watchEnabled = options.command === "daemon";
|
const watchEnabled = options.command === "daemon";
|
||||||
const vaultPath =
|
const vaultPath =
|
||||||
options.command === "mirror" && options.commandArgs[0]
|
options.command === "mirror" && options.commandArgs[0] ? path.resolve(options.commandArgs[0]) : databasePath;
|
||||||
? path.resolve(options.commandArgs[0])
|
|
||||||
: databasePath;
|
|
||||||
let ignoreRules: IgnoreRules | undefined;
|
let ignoreRules: IgnoreRules | undefined;
|
||||||
if (options.command === "daemon" || options.command === "mirror") {
|
if (options.command === "daemon" || options.command === "mirror") {
|
||||||
ignoreRules = new IgnoreRules(vaultPath);
|
ignoreRules = new IgnoreRules(vaultPath);
|
||||||
await ignoreRules.load();
|
await ignoreRules.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Create service context and hub
|
// Create service context and hub
|
||||||
const context = new NodeServiceContext(databasePath);
|
const context = new NodeServiceContext(databasePath);
|
||||||
const serviceHubInstance = new NodeServiceHub<NodeServiceContext>(databasePath, context);
|
const serviceHubInstance = new NodeServiceHub<NodeServiceContext>(databasePath, context);
|
||||||
|
|||||||
@@ -97,7 +97,11 @@ class CLIConverterAdapter implements IStorageEventConverterAdapter<NodeFile> {
|
|||||||
class CLIWatchAdapter implements IStorageEventWatchAdapter {
|
class CLIWatchAdapter implements IStorageEventWatchAdapter {
|
||||||
private _watcher: FSWatcher | undefined;
|
private _watcher: FSWatcher | undefined;
|
||||||
|
|
||||||
constructor(private basePath: string, private ignoreRules?: IgnoreRules, private watchEnabled: boolean = false) {}
|
constructor(
|
||||||
|
private basePath: string,
|
||||||
|
private ignoreRules?: IgnoreRules,
|
||||||
|
private watchEnabled: boolean = false
|
||||||
|
) {}
|
||||||
|
|
||||||
private _toNodeFile(filePath: string, stats: Stats | undefined): NodeFile {
|
private _toNodeFile(filePath: string, stats: Stats | undefined): NodeFile {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -60,10 +60,7 @@ describe("CLIStorageEventManagerAdapter", () => {
|
|||||||
await adapter.watch.beginWatch(handlers);
|
await adapter.watch.beginWatch(handlers);
|
||||||
|
|
||||||
expect(chokidar.watch).toHaveBeenCalledTimes(1);
|
expect(chokidar.watch).toHaveBeenCalledTimes(1);
|
||||||
expect(chokidar.watch).toHaveBeenCalledWith(
|
expect(chokidar.watch).toHaveBeenCalledWith("/base", expect.objectContaining({ ignoreInitial: true }));
|
||||||
"/base",
|
|
||||||
expect.objectContaining({ ignoreInitial: true })
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("add event produces NodeFile with correct relative path via onCreate", async () => {
|
it("add event produces NodeFile with correct relative path via onCreate", async () => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function initialiseServiceModulesCLI(
|
|||||||
core: LiveSyncBaseCore<ServiceContext, any>,
|
core: LiveSyncBaseCore<ServiceContext, any>,
|
||||||
services: InjectableServiceHub<ServiceContext>,
|
services: InjectableServiceHub<ServiceContext>,
|
||||||
ignoreRules?: IgnoreRules,
|
ignoreRules?: IgnoreRules,
|
||||||
watchEnabled: boolean = false,
|
watchEnabled: boolean = false
|
||||||
): ServiceModules {
|
): ServiceModules {
|
||||||
const storageAccessManager = new StorageAccessManager();
|
const storageAccessManager = new StorageAccessManager();
|
||||||
|
|
||||||
@@ -39,13 +39,19 @@ export function initialiseServiceModulesCLI(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// CLI-specific storage event manager
|
// CLI-specific storage event manager
|
||||||
const storageEventManager = new StorageEventManagerCLI(basePath, core, {
|
const storageEventManager = new StorageEventManagerCLI(
|
||||||
fileProcessing: services.fileProcessing,
|
basePath,
|
||||||
setting: services.setting,
|
core,
|
||||||
vaultService: services.vault,
|
{
|
||||||
storageAccessManager: storageAccessManager,
|
fileProcessing: services.fileProcessing,
|
||||||
APIService: services.API,
|
setting: services.setting,
|
||||||
}, ignoreRules, watchEnabled);
|
vaultService: services.vault,
|
||||||
|
storageAccessManager: storageAccessManager,
|
||||||
|
APIService: services.API,
|
||||||
|
},
|
||||||
|
ignoreRules,
|
||||||
|
watchEnabled
|
||||||
|
);
|
||||||
|
|
||||||
// Close the file watcher during graceful shutdown so the process can exit cleanly.
|
// Close the file watcher during graceful shutdown so the process can exit cleanly.
|
||||||
services.appLifecycle.onUnload.addHandler(async () => {
|
services.appLifecycle.onUnload.addHandler(async () => {
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ export class IgnoreRules {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith("import:")) {
|
if (trimmed.startsWith("import:")) {
|
||||||
console.error(`[IgnoreRules] Warning: unrecognised directive '${trimmed}' — only 'import: .gitignore' is supported`);
|
console.error(
|
||||||
|
`[IgnoreRules] Warning: unrecognised directive '${trimmed}' — only 'import: .gitignore' is supported`
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
this._addPattern(trimmed);
|
this._addPattern(trimmed);
|
||||||
@@ -105,7 +107,7 @@ export class IgnoreRules {
|
|||||||
if (raw.startsWith("!")) {
|
if (raw.startsWith("!")) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[IgnoreRules] Negation pattern '${raw}' is not supported. ` +
|
`[IgnoreRules] Negation pattern '${raw}' is not supported. ` +
|
||||||
`Remove it from .livesync/ignore or use a separate include/exclude file.`
|
`Remove it from .livesync/ignore or use a separate include/exclude file.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.patterns.push(this._normalisePattern(raw));
|
this.patterns.push(this._normalisePattern(raw));
|
||||||
|
|||||||
@@ -122,10 +122,7 @@ describe("IgnoreRules", () => {
|
|||||||
describe("load() with comments and blank lines", () => {
|
describe("load() with comments and blank lines", () => {
|
||||||
it("skips # comment lines and blank lines", async () => {
|
it("skips # comment lines and blank lines", async () => {
|
||||||
const vaultPath = await createVault();
|
const vaultPath = await createVault();
|
||||||
await writeIgnoreFile(
|
await writeIgnoreFile(vaultPath, "# This is a comment\n\n \n*.tmp\n# another comment\nbuild/\n");
|
||||||
vaultPath,
|
|
||||||
"# This is a comment\n\n \n*.tmp\n# another comment\nbuild/\n"
|
|
||||||
);
|
|
||||||
const rules = new IgnoreRules(vaultPath);
|
const rules = new IgnoreRules(vaultPath);
|
||||||
await rules.load();
|
await rules.load();
|
||||||
expect(rules.shouldIgnore("scratch.tmp")).toBe(true);
|
expect(rules.shouldIgnore("scratch.tmp")).toBe(true);
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ export async function runScenario(remoteType: RemoteType, encrypt: boolean): Pro
|
|||||||
const dbPrefix = remoteType === "COUCHDB" ? requireEnv("COUCHDB_DBNAME", "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("MINIO_ENDPOINT", "minioEndpoint").replace(/\/$/, "") : "";
|
const minioEndpoint =
|
||||||
|
remoteType === "MINIO" ? requireEnv("MINIO_ENDPOINT", "minioEndpoint").replace(/\/$/, "") : "";
|
||||||
const minioAccessKey = remoteType === "MINIO" ? requireEnv("MINIO_ACCESS_KEY", "accessKey") : "";
|
const minioAccessKey = remoteType === "MINIO" ? requireEnv("MINIO_ACCESS_KEY", "accessKey") : "";
|
||||||
const minioSecretKey = remoteType === "MINIO" ? requireEnv("MINIO_SECRET_KEY", "secretKey") : "";
|
const minioSecretKey = remoteType === "MINIO" ? requireEnv("MINIO_SECRET_KEY", "secretKey") : "";
|
||||||
const minioBucketBase = remoteType === "MINIO" ? requireEnv("MINIO_BUCKET_NAME", "bucketName") : "";
|
const minioBucketBase = remoteType === "MINIO" ? requireEnv("MINIO_BUCKET_NAME", "bucketName") : "";
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ function injectBanner(): import("vite").Plugin {
|
|||||||
// Insert after the shebang line if present, otherwise at the top.
|
// Insert after the shebang line if present, otherwise at the top.
|
||||||
if (chunk.code.startsWith("#!")) {
|
if (chunk.code.startsWith("#!")) {
|
||||||
const newline = chunk.code.indexOf("\n");
|
const newline = chunk.code.indexOf("\n");
|
||||||
chunk.code = chunk.code.slice(0, newline + 1) + fileReaderPolyfillBanner + chunk.code.slice(newline + 1);
|
chunk.code =
|
||||||
|
chunk.code.slice(0, newline + 1) + fileReaderPolyfillBanner + chunk.code.slice(newline + 1);
|
||||||
} else {
|
} else {
|
||||||
chunk.code = fileReaderPolyfillBanner + chunk.code;
|
chunk.code = fileReaderPolyfillBanner + chunk.code;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ async function renderHistoryList(): Promise<VaultHistoryItem[]> {
|
|||||||
|
|
||||||
const [items, lastUsedId] = await Promise.all([historyStore.getVaultHistory(), historyStore.getLastUsedVaultId()]);
|
const [items, lastUsedId] = await Promise.all([historyStore.getVaultHistory(), historyStore.getLastUsedVaultId()]);
|
||||||
|
|
||||||
listEl.innerHTML = "";
|
listEl.replaceChildren();
|
||||||
emptyEl.classList.toggle("is-hidden", items.length > 0);
|
emptyEl.classList.toggle("is-hidden", items.length > 0);
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
|||||||
+1
-1
Submodule src/lib updated: adcfe42522...f6a6c2dff0
+3
-2
@@ -1,5 +1,6 @@
|
|||||||
import { Notice, Plugin, type App, type PluginManifest } from "./deps";
|
import { getLanguage, Notice, Plugin, type App, type PluginManifest } from "./deps";
|
||||||
|
import { setGetLanguage } from "./lib/src/common/coreEnvFunctions.ts";
|
||||||
|
setGetLanguage(getLanguage);
|
||||||
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
|
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
|
||||||
import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts";
|
import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts";
|
||||||
import { ConfigSync } from "./features/ConfigSync/CmdConfigSync.ts";
|
import { ConfigSync } from "./features/ConfigSync/CmdConfigSync.ts";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TFile, Modal, App, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "../../../deps.ts";
|
import { TFile, Modal, App, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "../../../deps.ts";
|
||||||
import { getPathFromTFile, isValidPath } from "../../../common/utils.ts";
|
import { getPathFromTFile, isValidPath } from "../../../common/utils.ts";
|
||||||
import { decodeBinary, escapeStringToHTML, readString } from "../../../lib/src/string_and_binary/convert.ts";
|
import { decodeBinary, readString } from "../../../lib/src/string_and_binary/convert.ts";
|
||||||
import ObsidianLiveSyncPlugin from "../../../main.ts";
|
import ObsidianLiveSyncPlugin from "../../../main.ts";
|
||||||
import {
|
import {
|
||||||
type DocumentID,
|
type DocumentID,
|
||||||
@@ -145,22 +145,66 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareContentView(usePreformatted = true) {
|
||||||
|
this.contentView.empty();
|
||||||
|
this.contentView.toggleClass("op-pre", usePreformatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
appendTextDiff(diff: [number, string][]) {
|
||||||
|
for (const [operation, text] of diff) {
|
||||||
|
if (operation == DIFF_DELETE) {
|
||||||
|
this.contentView.createSpan({ text, cls: "history-deleted" });
|
||||||
|
} else if (operation == DIFF_EQUAL) {
|
||||||
|
this.contentView.createSpan({ text, cls: "history-normal" });
|
||||||
|
} else if (operation == DIFF_INSERT) {
|
||||||
|
this.contentView.createSpan({ text, cls: "history-added" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendImageDiff(baseSrc: string, overlaySrc?: string) {
|
||||||
|
const wrap = this.contentView.createDiv({ cls: "ls-imgdiff-wrap" });
|
||||||
|
const overlay = wrap.createDiv({ cls: "overlay" });
|
||||||
|
overlay.createEl("img", { cls: "img-base" }, (img) => {
|
||||||
|
img.src = baseSrc;
|
||||||
|
});
|
||||||
|
if (overlaySrc) {
|
||||||
|
overlay.createEl("img", { cls: "img-overlay" }, (img) => {
|
||||||
|
img.src = overlaySrc;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendDeletedNotice(usePreformatted = true) {
|
||||||
|
const notice = "(At this revision, the file has been deleted)";
|
||||||
|
if (usePreformatted) {
|
||||||
|
this.contentView.appendText(`${notice}\n`);
|
||||||
|
} else {
|
||||||
|
this.contentView.createDiv({ text: notice });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async showExactRev(rev: string) {
|
async showExactRev(rev: string) {
|
||||||
const db = this.core.localDatabase;
|
const db = this.core.localDatabase;
|
||||||
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
||||||
this.currentText = "";
|
this.currentText = "";
|
||||||
this.currentDeleted = false;
|
this.currentDeleted = false;
|
||||||
|
this.prepareContentView();
|
||||||
if (w === false) {
|
if (w === false) {
|
||||||
this.currentDeleted = true;
|
this.currentDeleted = true;
|
||||||
this.info.innerHTML = "";
|
this.info.empty();
|
||||||
this.contentView.innerHTML = `Could not read this revision<br>(${rev})`;
|
this.contentView.appendText("Could not read this revision");
|
||||||
|
this.contentView.createEl("br");
|
||||||
|
this.contentView.appendText(`(${rev})`);
|
||||||
} else {
|
} else {
|
||||||
this.currentDoc = w;
|
this.currentDoc = w;
|
||||||
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
this.info.setText(`Modified:${new Date(w.mtime).toLocaleString()}`);
|
||||||
let result = undefined;
|
|
||||||
const w1data = readDocument(w);
|
const w1data = readDocument(w);
|
||||||
this.currentDeleted = !!w.deleted;
|
this.currentDeleted = !!w.deleted;
|
||||||
// this.currentText = w1data;
|
if (typeof w1data == "string") {
|
||||||
|
this.currentText = w1data;
|
||||||
|
}
|
||||||
|
let rendered = false;
|
||||||
if (this.showDiff) {
|
if (this.showDiff) {
|
||||||
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
||||||
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
|
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
|
||||||
@@ -168,58 +212,55 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
const w2 = await db.getDBEntry(this.file, { rev: oldRev }, false, false, true);
|
const w2 = await db.getDBEntry(this.file, { rev: oldRev }, false, false, true);
|
||||||
if (w2 != false) {
|
if (w2 != false) {
|
||||||
if (typeof w1data == "string") {
|
if (typeof w1data == "string") {
|
||||||
result = "";
|
const w2data = readDocument(w2);
|
||||||
const dmp = new diff_match_patch();
|
if (typeof w2data == "string") {
|
||||||
const w2data = readDocument(w2) as string;
|
const dmp = new diff_match_patch();
|
||||||
const diff = dmp.diff_main(w2data, w1data);
|
const diff = dmp.diff_main(w2data, w1data);
|
||||||
dmp.diff_cleanupSemantic(diff);
|
dmp.diff_cleanupSemantic(diff);
|
||||||
for (const v of diff) {
|
if (this.currentDeleted) {
|
||||||
const x1 = v[0];
|
this.appendDeletedNotice();
|
||||||
const x2 = v[1];
|
|
||||||
if (x1 == DIFF_DELETE) {
|
|
||||||
result += "<span class='history-deleted'>" + escapeStringToHTML(x2) + "</span>";
|
|
||||||
} else if (x1 == DIFF_EQUAL) {
|
|
||||||
result += "<span class='history-normal'>" + escapeStringToHTML(x2) + "</span>";
|
|
||||||
} else if (x1 == DIFF_INSERT) {
|
|
||||||
result += "<span class='history-added'>" + escapeStringToHTML(x2) + "</span>";
|
|
||||||
}
|
}
|
||||||
|
this.appendTextDiff(diff);
|
||||||
|
rendered = true;
|
||||||
}
|
}
|
||||||
result = result.replace(/\n/g, "<br>");
|
|
||||||
} else if (isImage(this.file)) {
|
} else if (isImage(this.file)) {
|
||||||
const src = this.generateBlobURL("base", w1data);
|
const src = this.generateBlobURL("base", w1data);
|
||||||
const overlay = this.generateBlobURL(
|
const overlay = this.generateBlobURL(
|
||||||
"overlay",
|
"overlay",
|
||||||
readDocument(w2) as Uint8Array<ArrayBuffer>
|
readDocument(w2) as Uint8Array<ArrayBuffer>
|
||||||
);
|
);
|
||||||
result = `<div class='ls-imgdiff-wrap'>
|
this.prepareContentView(false);
|
||||||
<div class='overlay'>
|
if (this.currentDeleted) {
|
||||||
<img class='img-base' src="${src}">
|
this.appendDeletedNotice(false);
|
||||||
<img class='img-overlay' src='${overlay}'>
|
}
|
||||||
</div>
|
this.appendImageDiff(src, overlay);
|
||||||
</div>`;
|
rendered = true;
|
||||||
this.contentView.removeClass("op-pre");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result == undefined) {
|
if (!rendered) {
|
||||||
if (typeof w1data != "string") {
|
if (typeof w1data != "string") {
|
||||||
if (isImage(this.file)) {
|
if (isImage(this.file)) {
|
||||||
const src = this.generateBlobURL("base", w1data);
|
const src = this.generateBlobURL("base", w1data);
|
||||||
result = `<div class='ls-imgdiff-wrap'>
|
this.prepareContentView(false);
|
||||||
<div class='overlay'>
|
if (this.currentDeleted) {
|
||||||
<img class='img-base' src="${src}">
|
this.appendDeletedNotice(false);
|
||||||
</div>
|
}
|
||||||
</div>`;
|
this.appendImageDiff(src);
|
||||||
this.contentView.removeClass("op-pre");
|
} else {
|
||||||
|
if (this.currentDeleted) {
|
||||||
|
this.appendDeletedNotice();
|
||||||
|
}
|
||||||
|
this.contentView.appendText("Binary file");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result = escapeStringToHTML(w1data);
|
if (this.currentDeleted) {
|
||||||
|
this.appendDeletedNotice();
|
||||||
|
}
|
||||||
|
this.contentView.appendText(w1data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result == undefined) result = typeof w1data == "string" ? escapeStringToHTML(w1data) : "Binary file";
|
|
||||||
this.contentView.innerHTML =
|
|
||||||
(this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result;
|
|
||||||
}
|
}
|
||||||
// Reset diff navigation after content changes
|
// Reset diff navigation after content changes
|
||||||
this.resetDiffNavigation();
|
this.resetDiffNavigation();
|
||||||
@@ -245,8 +286,7 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
if (direction === "next") {
|
if (direction === "next") {
|
||||||
this.currentDiffIndex = (this.currentDiffIndex + 1) % diffElements.length;
|
this.currentDiffIndex = (this.currentDiffIndex + 1) % diffElements.length;
|
||||||
} else {
|
} else {
|
||||||
this.currentDiffIndex =
|
this.currentDiffIndex = this.currentDiffIndex <= 0 ? diffElements.length - 1 : this.currentDiffIndex - 1;
|
||||||
this.currentDiffIndex <= 0 ? diffElements.length - 1 : this.currentDiffIndex - 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = diffElements[this.currentDiffIndex];
|
const target = diffElements[this.currentDiffIndex];
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { App, Modal } from "../../../deps.ts";
|
import { App, Modal } from "../../../deps.ts";
|
||||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
||||||
import { CANCELLED, LEAVE_TO_SUBSEQUENT, type diff_result } from "../../../lib/src/common/types.ts";
|
import { CANCELLED, LEAVE_TO_SUBSEQUENT, type diff_result } from "../../../lib/src/common/types.ts";
|
||||||
import { escapeStringToHTML } from "../../../lib/src/string_and_binary/convert.ts";
|
|
||||||
import { delay } from "../../../lib/src/common/utils.ts";
|
import { delay } from "../../../lib/src/common/utils.ts";
|
||||||
import { eventHub } from "../../../common/events.ts";
|
import { eventHub } from "../../../common/events.ts";
|
||||||
import { globalSlipBoard } from "../../../lib/src/bureau/bureau.ts";
|
import { globalSlipBoard } from "../../../lib/src/bureau/bureau.ts";
|
||||||
@@ -44,6 +43,25 @@ export class ConflictResolveModal extends Modal {
|
|||||||
// sendValue("close-resolve-conflict:" + this.filename, false);
|
// sendValue("close-resolve-conflict:" + this.filename, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appendDiffFragment(container: HTMLDivElement, text: string, cls: string) {
|
||||||
|
const lines = text.split("\n");
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
const span = container.createSpan({ cls });
|
||||||
|
span.textContent = line;
|
||||||
|
if (index < lines.length - 1) {
|
||||||
|
container.createSpan({ cls: "ls-mark-cr" });
|
||||||
|
container.createEl("br");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
appendVersionInfo(container: HTMLDivElement, cls: string, name: string, date: string) {
|
||||||
|
const line = container.createSpan({ cls });
|
||||||
|
line.createSpan({ text: name, cls: "conflict-dev-name" });
|
||||||
|
line.appendText(`: ${date}`);
|
||||||
|
container.createEl("br");
|
||||||
|
}
|
||||||
|
|
||||||
override onOpen() {
|
override onOpen() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
// Send cancel signal for the previous merge dialogue
|
// Send cancel signal for the previous merge dialogue
|
||||||
@@ -64,25 +82,21 @@ export class ConflictResolveModal extends Modal {
|
|||||||
const div = contentEl.createDiv("");
|
const div = contentEl.createDiv("");
|
||||||
div.addClass("op-scrollable");
|
div.addClass("op-scrollable");
|
||||||
div.addClass("ls-dialog");
|
div.addClass("ls-dialog");
|
||||||
let diff = "";
|
let diffLength = 0;
|
||||||
for (const v of this.result.diff) {
|
for (const v of this.result.diff) {
|
||||||
const x1 = v[0];
|
const x1 = v[0];
|
||||||
const x2 = v[1];
|
const x2 = v[1];
|
||||||
|
diffLength += x2.length;
|
||||||
|
if (diffLength > 100 * 1024) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (x1 == DIFF_DELETE) {
|
if (x1 == DIFF_DELETE) {
|
||||||
diff +=
|
this.appendDiffFragment(div, x2, "deleted");
|
||||||
"<span class='deleted'>" +
|
div.createEl("span", { text: x2, cls: "deleted normal conflict-dev-name" });
|
||||||
escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") +
|
|
||||||
"</span>";
|
|
||||||
} else if (x1 == DIFF_EQUAL) {
|
} else if (x1 == DIFF_EQUAL) {
|
||||||
diff +=
|
this.appendDiffFragment(div, x2, "normal");
|
||||||
"<span class='normal'>" +
|
|
||||||
escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") +
|
|
||||||
"</span>";
|
|
||||||
} else if (x1 == DIFF_INSERT) {
|
} else if (x1 == DIFF_INSERT) {
|
||||||
diff +=
|
this.appendDiffFragment(div, x2, "added");
|
||||||
"<span class='added'>" +
|
|
||||||
escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") +
|
|
||||||
"</span>";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +106,8 @@ export class ConflictResolveModal extends Modal {
|
|||||||
new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : "");
|
new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : "");
|
||||||
const date2 =
|
const date2 =
|
||||||
new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : "");
|
new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : "");
|
||||||
div2.innerHTML = `<span class='deleted'><span class='conflict-dev-name'>${this.localName}</span>: ${date1}</span><br>
|
this.appendVersionInfo(div2, "deleted", this.localName, date1);
|
||||||
<span class='added'><span class='conflict-dev-name'>${this.remoteName}</span>: ${date2}</span><br>`;
|
this.appendVersionInfo(div2, "added", this.remoteName, date2);
|
||||||
contentEl.createEl("button", { text: `Use ${this.localName}` }, (e) =>
|
contentEl.createEl("button", { text: `Use ${this.localName}` }, (e) =>
|
||||||
e.addEventListener("click", () => this.sendResponse(this.result.right.rev))
|
e.addEventListener("click", () => this.sendResponse(this.result.right.rev))
|
||||||
).style.marginRight = "4px";
|
).style.marginRight = "4px";
|
||||||
@@ -108,11 +122,9 @@ export class ConflictResolveModal extends Modal {
|
|||||||
contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) =>
|
contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) =>
|
||||||
e.addEventListener("click", () => this.sendResponse(CANCELLED))
|
e.addEventListener("click", () => this.sendResponse(CANCELLED))
|
||||||
).style.marginRight = "4px";
|
).style.marginRight = "4px";
|
||||||
diff = diff.replace(/\n/g, "<br>");
|
if (diffLength > 100 * 1024) {
|
||||||
if (diff.length > 100 * 1024) {
|
div.empty();
|
||||||
div.innerText = "(Too large diff to display)";
|
div.innerText = "(Too large diff to display)";
|
||||||
} else {
|
|
||||||
div.innerHTML = diff;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,10 +43,13 @@ export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElem
|
|||||||
// tmpDiv.addClass("sls-header-button");
|
// tmpDiv.addClass("sls-header-button");
|
||||||
tmpDiv.addClass("op-warn-info");
|
tmpDiv.addClass("op-warn-info");
|
||||||
|
|
||||||
tmpDiv.innerHTML = `<p>${$msg("obsidianLiveSyncSettingTab.msgNewVersionNote")}</p><button>${$msg("obsidianLiveSyncSettingTab.optionOkReadEverything")}</button>`;
|
tmpDiv.createEl("p", { text: $msg("obsidianLiveSyncSettingTab.msgNewVersionNote") });
|
||||||
|
const readEverythingButton = tmpDiv.createEl("button", {
|
||||||
|
text: $msg("obsidianLiveSyncSettingTab.optionOkReadEverything"),
|
||||||
|
});
|
||||||
if (lastVersion > (this.editingSettings?.lastReadUpdates || 0)) {
|
if (lastVersion > (this.editingSettings?.lastReadUpdates || 0)) {
|
||||||
const informationButtonDiv = informationDivEl.appendChild(tmpDiv);
|
const informationButtonDiv = informationDivEl.appendChild(tmpDiv);
|
||||||
informationButtonDiv.querySelector("button")?.addEventListener("click", () => {
|
readEverythingButton.addEventListener("click", () => {
|
||||||
fireAndForget(async () => {
|
fireAndForget(async () => {
|
||||||
this.editingSettings.lastReadUpdates = lastVersion;
|
this.editingSettings.lastReadUpdates = lastVersion;
|
||||||
await this.saveAllDirtySettings();
|
await this.saveAllDirtySettings();
|
||||||
|
|||||||
@@ -121,13 +121,13 @@ export function paneSetup(
|
|||||||
const repo = "vrtmrz/obsidian-livesync";
|
const repo = "vrtmrz/obsidian-livesync";
|
||||||
const topPath = $msg("obsidianLiveSyncSettingTab.linkTroubleshooting");
|
const topPath = $msg("obsidianLiveSyncSettingTab.linkTroubleshooting");
|
||||||
const rawRepoURI = `https://raw.githubusercontent.com/${repo}/main`;
|
const rawRepoURI = `https://raw.githubusercontent.com/${repo}/main`;
|
||||||
this.createEl(
|
this.createEl(paneEl, "div", "", (el) => {
|
||||||
paneEl,
|
el.createEl("a", { text: $msg("obsidianLiveSyncSettingTab.linkOpenInBrowser") }, (anchor) => {
|
||||||
"div",
|
anchor.href = `https://github.com/${repo}/blob/main${topPath}`;
|
||||||
"",
|
anchor.target = "_blank";
|
||||||
(el) =>
|
anchor.rel = "noopener";
|
||||||
(el.innerHTML = `<a href='https://github.com/${repo}/blob/main${topPath}' target="_blank">${$msg("obsidianLiveSyncSettingTab.linkOpenInBrowser")}</a>`)
|
});
|
||||||
);
|
});
|
||||||
const troubleShootEl = this.createEl(paneEl, "div", {
|
const troubleShootEl = this.createEl(paneEl, "div", {
|
||||||
text: "",
|
text: "",
|
||||||
cls: "sls-troubleshoot-preview",
|
cls: "sls-troubleshoot-preview",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const checkConfig = async (
|
|||||||
Logger($msg("obsidianLiveSyncSettingTab.logCheckingDbConfig"), LOG_LEVEL_INFO);
|
Logger($msg("obsidianLiveSyncSettingTab.logCheckingDbConfig"), LOG_LEVEL_INFO);
|
||||||
let isSuccessful = true;
|
let isSuccessful = true;
|
||||||
const emptyDiv = createDiv();
|
const emptyDiv = createDiv();
|
||||||
emptyDiv.innerHTML = "<span></span>";
|
emptyDiv.createSpan();
|
||||||
checkResultDiv?.replaceChildren(...[emptyDiv]);
|
checkResultDiv?.replaceChildren(...[emptyDiv]);
|
||||||
const addResult = (msg: string, classes?: string[]) => {
|
const addResult = (msg: string, classes?: string[]) => {
|
||||||
const tmpDiv = createDiv();
|
const tmpDiv = createDiv();
|
||||||
@@ -21,7 +21,7 @@ export const checkConfig = async (
|
|||||||
if (classes) {
|
if (classes) {
|
||||||
tmpDiv.addClasses(classes);
|
tmpDiv.addClasses(classes);
|
||||||
}
|
}
|
||||||
tmpDiv.innerHTML = `${msg}`;
|
tmpDiv.textContent = msg;
|
||||||
checkResultDiv?.appendChild(tmpDiv);
|
checkResultDiv?.appendChild(tmpDiv);
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
@@ -47,9 +47,10 @@ export const checkConfig = async (
|
|||||||
if (!checkResultDiv) return;
|
if (!checkResultDiv) return;
|
||||||
const tmpDiv = createDiv();
|
const tmpDiv = createDiv();
|
||||||
tmpDiv.addClass("ob-btn-config-fix");
|
tmpDiv.addClass("ob-btn-config-fix");
|
||||||
tmpDiv.innerHTML = `<label>${title}</label><button>${$msg("obsidianLiveSyncSettingTab.btnFix")}</button>`;
|
tmpDiv.createEl("label", { text: title });
|
||||||
|
const fixButton = tmpDiv.createEl("button", { text: $msg("obsidianLiveSyncSettingTab.btnFix") });
|
||||||
const x = checkResultDiv.appendChild(tmpDiv);
|
const x = checkResultDiv.appendChild(tmpDiv);
|
||||||
x.querySelector("button")?.addEventListener("click", () => {
|
fixButton.addEventListener("click", () => {
|
||||||
fireAndForget(async () => {
|
fireAndForget(async () => {
|
||||||
Logger($msg("obsidianLiveSyncSettingTab.logCouchDbConfigSet", { title, key, value }));
|
Logger($msg("obsidianLiveSyncSettingTab.logCouchDbConfigSet", { title, key, value }));
|
||||||
const res = await requestToCouchDBWithCredentials(
|
const res = await requestToCouchDBWithCredentials(
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
const TYPE_CLOSE = "close";
|
const TYPE_CLOSE = "close";
|
||||||
type ResultType = typeof TYPE_CLOSE;
|
type ResultType = typeof TYPE_CLOSE;
|
||||||
type Props = {
|
type Props = {
|
||||||
setResult: (result: ResultType) => void;
|
setResult: (_result: ResultType) => void;
|
||||||
};
|
};
|
||||||
const { setResult }: Props = $props();
|
const { setResult }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,11 +3,22 @@ import { createServiceFeature } from "@lib/interfaces/ServiceModule";
|
|||||||
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@lib/common/rosetta";
|
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@lib/common/rosetta";
|
||||||
import { $msg, setLang } from "@lib/common/i18n";
|
import { $msg, setLang } from "@lib/common/i18n";
|
||||||
|
|
||||||
|
function tryGetLanguage() {
|
||||||
|
try {
|
||||||
|
// Note: 1.8.7+ is required. but it is 18, Feb., 2025. we want to fallback on earlier versions, so we catch the error here.
|
||||||
|
// eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||||
|
return getLanguage();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to get Obsidian language, defaulting to 'def'", e);
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const enableI18nFeature = createServiceFeature(async ({ services: { setting, API } }) => {
|
export const enableI18nFeature = createServiceFeature(async ({ services: { setting, API } }) => {
|
||||||
let isChanged = false;
|
let isChanged = false;
|
||||||
const settings = setting.currentSettings();
|
const settings = setting.currentSettings();
|
||||||
if (settings.displayLanguage == "") {
|
if (settings.displayLanguage == "") {
|
||||||
const obsidianLanguage = getLanguage();
|
const obsidianLanguage = tryGetLanguage();
|
||||||
if (
|
if (
|
||||||
SUPPORTED_I18N_LANGS.indexOf(obsidianLanguage) !== -1 && // Check if the language is supported
|
SUPPORTED_I18N_LANGS.indexOf(obsidianLanguage) !== -1 && // Check if the language is supported
|
||||||
obsidianLanguage != settings.displayLanguage // Check if the language is different from the current setting
|
obsidianLanguage != settings.displayLanguage // Check if the language is different from the current setting
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { type UseP2PReplicatorResult } from "@/lib/src/replication/trystero/UseP
|
|||||||
import { P2PLogCollector } from "@/lib/src/replication/trystero/P2PLogCollector";
|
import { P2PLogCollector } from "@/lib/src/replication/trystero/P2PLogCollector";
|
||||||
import { P2PReplicatorPaneView, VIEW_TYPE_P2P } from "@/features/P2PSync/P2PReplicator/P2PReplicatorPaneView";
|
import { P2PReplicatorPaneView, VIEW_TYPE_P2P } from "@/features/P2PSync/P2PReplicator/P2PReplicatorPaneView";
|
||||||
import type { LiveSyncCore } from "@/main";
|
import type { LiveSyncCore } from "@/main";
|
||||||
|
import type { WorkspaceLeaf } from "@/deps";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ServiceFeature: P2P Replicator lifecycle management.
|
* ServiceFeature: P2P Replicator lifecycle management.
|
||||||
@@ -43,7 +44,7 @@ export function useP2PReplicatorUI(
|
|||||||
|
|
||||||
// Register view, commands and ribbon if a view factory is provided
|
// Register view, commands and ribbon if a view factory is provided
|
||||||
const viewType = VIEW_TYPE_P2P;
|
const viewType = VIEW_TYPE_P2P;
|
||||||
const factory = (leaf: any) => {
|
const factory = (leaf: WorkspaceLeaf) => {
|
||||||
return new P2PReplicatorPaneView(leaf, core, {
|
return new P2PReplicatorPaneView(leaf, core, {
|
||||||
replicator: getReplicator(),
|
replicator: getReplicator(),
|
||||||
p2pLogCollector,
|
p2pLogCollector,
|
||||||
|
|||||||
@@ -38,10 +38,20 @@ export class ObsidianVaultAdapter implements IVaultAdapter<TFile> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(file: TFile | TFolder, force = false): Promise<void> {
|
async delete(file: TFile | TFolder, force = false): Promise<void> {
|
||||||
|
// if ("trashFile" in this.app.fileManager) {
|
||||||
|
// // eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||||
|
// return await this.app.fileManager.trashFile(file);
|
||||||
|
// }
|
||||||
|
//TODO: need fix
|
||||||
return await this.app.vault.delete(file, force);
|
return await this.app.vault.delete(file, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
async trash(file: TFile | TFolder, force = false): Promise<void> {
|
async trash(file: TFile | TFolder, force = false): Promise<void> {
|
||||||
|
// if ("trashFile" in this.app.fileManager) {
|
||||||
|
// // eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||||
|
// return await this.app.fileManager.trashFile(file);
|
||||||
|
// }
|
||||||
|
//TODO: need fix
|
||||||
return await this.app.vault.trash(file, force);
|
return await this.app.vault.trash(file, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"0.25.60": "1.7.2",
|
||||||
"1.0.1": "0.9.12",
|
"1.0.1": "0.9.12",
|
||||||
"1.0.0": "0.9.7"
|
"1.0.0": "0.9.7"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { defineConfig, mergeConfig } from "vitest/config";
|
|||||||
import { playwright } from "@vitest/browser-playwright";
|
import { playwright } from "@vitest/browser-playwright";
|
||||||
import viteConfig from "./vitest.config.common";
|
import viteConfig from "./vitest.config.common";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import dotenv from "dotenv";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { parseEnv } from "node:util";
|
||||||
import { grantClipboardPermissions, writeHandoffFile, readHandoffFile } from "./test/lib/commands";
|
import { grantClipboardPermissions, writeHandoffFile, readHandoffFile } from "./test/lib/commands";
|
||||||
|
|
||||||
// P2P test environment variables
|
// P2P test environment variables
|
||||||
@@ -22,8 +23,9 @@ import { grantClipboardPermissions, writeHandoffFile, readHandoffFile } from "./
|
|||||||
// General test options (also read from env):
|
// General test options (also read from env):
|
||||||
// ENABLE_DEBUGGER - Set to "true" to attach a debugger and pause before tests
|
// ENABLE_DEBUGGER - Set to "true" to attach a debugger and pause before tests
|
||||||
// ENABLE_UI - Set to "true" to open a visible browser window during tests
|
// ENABLE_UI - Set to "true" to open a visible browser window during tests
|
||||||
const defEnv = dotenv.config({ path: ".env" }).parsed;
|
const loadEnvFile = (path: string) => (existsSync(path) ? parseEnv(readFileSync(path, "utf-8")) : undefined);
|
||||||
const testEnv = dotenv.config({ path: ".test.env" }).parsed;
|
const defEnv = loadEnvFile(".env");
|
||||||
|
const testEnv = loadEnvFile(".test.env");
|
||||||
// Merge: dotenv files < process.env (so shell-injected vars like P2P_TEST_* take precedence)
|
// Merge: dotenv files < process.env (so shell-injected vars like P2P_TEST_* take precedence)
|
||||||
const p2pEnv: Record<string, string> = {};
|
const p2pEnv: Record<string, string> = {};
|
||||||
if (process.env.P2P_TEST_ROOM_ID) p2pEnv.P2P_TEST_ROOM_ID = process.env.P2P_TEST_ROOM_ID;
|
if (process.env.P2P_TEST_ROOM_ID) p2pEnv.P2P_TEST_ROOM_ID = process.env.P2P_TEST_ROOM_ID;
|
||||||
|
|||||||
+6
-3
@@ -2,10 +2,13 @@ import { defineConfig, mergeConfig } from "vitest/config";
|
|||||||
import { playwright } from "@vitest/browser-playwright";
|
import { playwright } from "@vitest/browser-playwright";
|
||||||
import viteConfig from "./vitest.config.common";
|
import viteConfig from "./vitest.config.common";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import dotenv from "dotenv";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { parseEnv } from "node:util";
|
||||||
import { grantClipboardPermissions, openWebPeer, closeWebPeer, acceptWebPeer } from "./test/lib/commands";
|
import { grantClipboardPermissions, openWebPeer, closeWebPeer, acceptWebPeer } from "./test/lib/commands";
|
||||||
const defEnv = dotenv.config({ path: ".env" }).parsed;
|
|
||||||
const testEnv = dotenv.config({ path: ".test.env" }).parsed;
|
const loadEnvFile = (path: string) => (existsSync(path) ? parseEnv(readFileSync(path, "utf-8")) : undefined);
|
||||||
|
const defEnv = loadEnvFile(".env");
|
||||||
|
const testEnv = loadEnvFile(".test.env");
|
||||||
const env = Object.assign({}, defEnv, testEnv);
|
const env = Object.assign({}, defEnv, testEnv);
|
||||||
const debuggerEnabled = env?.ENABLE_DEBUGGER === "true";
|
const debuggerEnabled = env?.ENABLE_DEBUGGER === "true";
|
||||||
const enableUI = env?.ENABLE_UI === "true";
|
const enableUI = env?.ENABLE_UI === "true";
|
||||||
|
|||||||
Reference in New Issue
Block a user