Compare commits

...

39 Commits

Author SHA1 Message Date
vorotamoroz
56a234e6d7 chore: wrap timer functions 2026-05-31 19:03:43 +09:00
vorotamoroz
39014b2294 use coreEnvVars for some vars 2026-05-30 23:59:36 +09:00
vorotamoroz
24e6c110a3 Remove unused imports 2026-05-30 23:53:19 +09:00
vorotamoroz
f24d110552 Change type assertion 2026-05-30 23:49:32 +09:00
vorotamoroz
7189c1c05a Update dependency
set no-deprecated to warn
2026-05-30 23:42:32 +09:00
vorotamoroz
547afe9a86 remove unused eslint comment
use _fetch instead of fetch
add ignore list
2026-05-30 23:32:46 +09:00
vorotamoroz
7b5876037d Add ignores 2026-05-30 23:22:17 +09:00
vorotamoroz
b714c00644 (chore): Improve type assertion, remove unused imports 2026-05-29 04:19:58 +01:00
vorotamoroz
e14e771bfb (chore): tidied tsconfig and eslint, and some incorrect imports 2026-05-28 04:42:03 +01:00
vorotamoroz
1130bbcee8 remove unmaintained tests 2026-05-26 11:33:14 +01:00
vorotamoroz
8841ef4619 fix css duplicated props 2026-05-26 11:27:31 +01:00
vorotamoroz
45fe0b3682 chore: lint: enable cache and concurrency 2026-05-26 11:24:41 +01:00
vorotamoroz
8d3825abc9 Merge pull request #925 from vrtmrz/v0_25_70
Releasing v0.25.70
2026-05-25 18:38:04 +09:00
vorotamoroz
c5f9841b85 chore: fix grammatical error 2026-05-25 10:14:18 +01:00
vorotamoroz
d36d176d99 bump 2026-05-25 10:10:46 +01:00
vorotamoroz
38b2cf73ed Update lib (forgot to include #942) 2026-05-25 09:55:11 +01:00
vorotamoroz
40b15a6950 Merge pull request #924 from vrtmrz/p2p_add_fix_and_diag
Improvements: More diagnostic information for P2P connections
2026-05-25 17:52:37 +09:00
vorotamoroz
e312bb7640 Merge pull request #891 from SeleiXi/feat/conflict-diff-jump
feat: add diff navigation to conflict resolver
2026-05-25 17:51:49 +09:00
Ching Wing Kwok
852c0e6c13 Merge branch 'main' into feat/conflict-diff-jump 2026-05-25 15:38:21 +08:00
vorotamoroz
7c1bcf9e9b Merge pull request #889 from SeleiXi/diff-only-button
feat: add diff-only view button to document history
2026-05-25 14:00:11 +09:00
vorotamoroz
2b79bed085 Merge branch 'main' into pr/SeleiXi/889 2026-05-25 05:43:36 +01:00
vorotamoroz
6b1e0c4aa8 Merge pull request #890 from SeleiXi/feat/history-search
feat: Add document history search
2026-05-25 12:55:29 +09:00
vorotamoroz
3c3645eba4 Improved: More diagnostic information for P2P connections is now shown, including why a connection failure occurred and the current connection status. 2026-05-25 04:31:22 +01:00
SeleiXi
009cc3c87a Merge remote-tracking branch 'origin/main' into feat/conflict-diff-jump
# Conflicts:
#	src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts
2026-05-23 02:06:52 +08:00
SeleiXi
fc5fd4be94 Merge remote-tracking branch 'origin/main' into feat/history-search
# Conflicts:
#	src/lib
#	src/modules/features/DocumentHistory/DocumentHistoryModal.ts
2026-05-23 02:05:33 +08:00
SeleiXi
8ed1acf79d Merge remote-tracking branch 'origin/main' into diff-only-button
# Conflicts:
#	src/modules/features/DocumentHistory/DocumentHistoryModal.ts
2026-05-23 02:04:48 +08:00
vorotamoroz
c518223d21 Merge pull request #923 from vrtmrz/p2p_add_fix_and_diag
0.25.69: Fix P2P, add diag feature
2026-05-23 00:47:36 +09:00
vorotamoroz
caaff618e9 fix grammar 2026-05-22 16:40:40 +01:00
vorotamoroz
148aa8505e bump 2026-05-22 16:38:44 +01:00
vorotamoroz
f9a626a858 ### Fixed
- No longer the P2P passphrase mismatch causes a server shutdown.
- Settings related to P2P synchronisation are now correctly applied on start-up and no longer reverted.

### New features
- Diagnostic P2P connection stats are now available.
  - These stats indicate the number of connection trials, successes, and, failures.
2026-05-22 16:37:05 +01:00
vorotamoroz
1b8747115c Merge pull request #922 from vrtmrz/update_trystero
Releasing v0.25.68 (dependency and P2P update)
2026-05-22 19:17:22 +09:00
vorotamoroz
e739302fb9 bump 2026-05-22 11:11:18 +01:00
vorotamoroz
8f20d53f55 Update trystero, typed well. fixed some potentially problems
Weaken terser for libraries
2026-05-22 11:01:51 +01:00
vorotamoroz
fd84b0377b Update actions 2026-05-21 10:02:46 +01:00
vorotamoroz
340d416b76 Merge pull request #920 from vrtmrz/v0_25_66
Release v0.25.67
2026-05-20 19:44:41 +09:00
SeleiXi
5454e1106f feat: add diff navigation to conflict resolver 2026-05-13 00:19:56 +08:00
SeleiXi
0d9397c8b9 fix: resolve UI alignment issue for diff navigation buttons 2026-05-12 00:52:20 +08:00
SeleiXi
429a3ff1fd feat: add diff-only view button to document history 2026-05-11 23:53:07 +08:00
SeleiXi
bfff6ea7b8 feat: add document history search support 2026-05-11 22:45:42 +08:00
36 changed files with 667 additions and 1289 deletions

View File

@@ -9,6 +9,10 @@ on:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
with:
@@ -29,68 +33,20 @@ jobs:
run: |
npm ci
npm run build --if-present
# Attest
- name: Attest Plugin Artifacts
uses: actions/attest-build-provenance@v4
with:
subject-path: |
main.js
manifest.json
styles.css
# Package the required files into a zip
- name: Package
run: |
mkdir ${{ github.event.repository.name }}
cp main.js manifest.json styles.css README.md ${{ github.event.repository.name }}
zip -r ${{ github.event.repository.name }}.zip ${{ github.event.repository.name }}
# Create the release on github
# - name: Create Release
# id: create_release
# uses: actions/create-release@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# VERSION: ${{ steps.version.outputs.tag }}
# with:
# tag_name: ${{ steps.version.outputs.tag }}
# release_name: ${{ steps.version.outputs.tag }}
# draft: true
# prerelease: false
# # Upload the packaged release file
# - name: Upload zip file
# id: upload-zip
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ./${{ github.event.repository.name }}.zip
# asset_name: ${{ github.event.repository.name }}-${{ steps.version.outputs.tag }}.zip
# asset_content_type: application/zip
# # Upload the main.js
# - name: Upload main.js
# id: upload-main
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ./main.js
# asset_name: main.js
# asset_content_type: text/javascript
# # Upload the manifest.json
# - name: Upload manifest.json
# id: upload-manifest
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ./manifest.json
# asset_name: manifest.json
# asset_content_type: application/json
# # Upload the style.css
# - name: Upload styles.css
# id: upload-css
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ./styles.css
# asset_name: styles.css
# asset_content_type: text/css
- name: Create Release and Upload Assets
uses: softprops/action-gh-release@v2
with:

View File

@@ -10,7 +10,18 @@ on:
paths:
- 'src/**'
- 'test/**'
- 'lib/**'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'vite.config.ts'
- 'vitest.config*.ts'
- 'esbuild.config.mjs'
- 'eslint.config.mjs'
- '.github/workflows/unit-ci.yml'
pull_request:
paths:
- 'src/**'
- 'test/**'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'

3
.gitignore vendored
View File

@@ -30,4 +30,5 @@ cov_profile/**
coverage
src/apps/cli/dist/*
_testdata/**
utils/bench/splitResults.csv
utils/bench/splitResults.csv
.eslintcache

View File

@@ -17,7 +17,7 @@ The plugin uses a dynamic module system to reduce coupling and improve maintaina
- `coreObsidian/` - Obsidian-specific core (e.g., `ModuleFileAccessObsidian`)
- `essential/` - Required modules (e.g., `ModuleMigration`, `ModuleKeyValueDB`)
- `features/` - Optional features (e.g., `ModuleLog`, `ModuleObsidianSettings`)
- `extras/` - Development/testing tools (e.g., `ModuleDev`, `ModuleIntegratedTest`)
- `extras/` - Development/testing tools (e.g., `ModuleDev`, ~~`ModuleIntegratedTest`~~)
- **Services**: Core services (e.g., `database`, `replicator`, `storageAccess`) are registered in `ServiceHub` and accessed by modules. They provide an extension point for add new behaviour without modifying existing code.
- For example, checks before the replication can be added to the `replication.onBeforeReplicate` handler, and the handlers can be return `false` to prevent replication-starting. `vault.isTargetFile` also can be used to prevent processing specific files.
- **ServiceModule**: A new type of module that directly depends on services.

View File

@@ -6,66 +6,86 @@ import * as sveltePlugin from "eslint-plugin-svelte";
export default defineConfig([
globalIgnores([
"**/node_modules/*",
"**/jest.config.js",
// Build outputs and legacy files
"**/build",
"coverage",
"**/main.js",
"main_org.js",
"pouchdb-browser.js",
"version-bump.mjs",
"package.json",
"**/*.json",
"**/.eslintrc.js.bak",
// Files from linked dependencies (those files should not exist for most people).
"modules/octagonal-wheels/dist/**/*",
// Sub-projects (Exclude from root linting as they have different environments)
"src/apps/**/*",
"utils/**/*",
// Specific exclusions from common library (src/lib)
"src/lib/coverage",
"src/lib/browsertest",
"**/test.ts",
"**/tests.ts",
"**/**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/patches/pouchdb-utils",
"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",
// Config files and build scripts
"**/jest.config.js",
"**/rollup.config.js",
"**/esbuild.config.mjs",
"**/terser.*.mjs",
".prettierrc.*.mjs",
".prettierrc.mjs",
"*.config.mjs",
"vite.*",
"vitest.*",
// Testing files (Simplified patterns)
"test/**",
"**/*.test.ts",
"**/*.unit.spec.ts",
"**/test.ts",
"**/tests.ts",
]),
...sveltePlugin.configs["flat/base"],
...obsidianmd.configs.recommended,
{
files: ["**/*.ts"],
languageOptions: {
globals: { ...globals.browser },
globals: { ...globals.browser, "PouchDB": "readonly" },
parser: tsParser,
parserOptions: {
project: "./tsconfig.json",
},
},
rules: {
// Base rules (turned off in favour of TS specific versions or explicitly disabled).
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
"no-unused-labels": "off",
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"require-await": "off",
// TypeScript specific rules
"@typescript-eslint/no-deprecated": "warn",
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "off",
"require-await": "error",
"obsidianmd/rule-custom-message": "off", // Temporary
"obsidianmd/ui/sentence-case": "off", // Temporary
"@typescript-eslint/require-await": "warn",
"@typescript-eslint/no-misused-promises": "warn",
"@typescript-eslint/no-floating-promises": "warn",
"no-async-promise-executor": "warn",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
// General rules
"no-async-promise-executor": "warn",
"no-constant-condition": ["error", { checkLoops: false }],
// Plugin specific overrides (Pending review)
"obsidianmd/rule-custom-message": "off",
"obsidianmd/ui/sentence-case": "off",
},
},
{
@@ -77,7 +97,7 @@ export default defineConfig([
},
rules: {
"no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
"obsidianmd/no-plugin-as-component": "off", // Temporary
"obsidianmd/no-plugin-as-component": "off",
},
},
}
]);

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.25.67",
"version": "0.25.70",
"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.",
"author": "vorotamoroz",

44
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "obsidian-livesync",
"version": "0.25.67",
"version": "0.25.70",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-livesync",
"version": "0.25.67",
"version": "0.25.70",
"license": "MIT",
"dependencies": {
"@aws-sdk/client-s3": "^3.808.0",
@@ -15,7 +15,7 @@
"@smithy/middleware-apply-body-checksum": "^4.3.9",
"@smithy/protocol-http": "^5.3.9",
"@smithy/querystring-builder": "^4.2.9",
"@trystero-p2p/nostr": "^0.23.0",
"@trystero-p2p/nostr": "^0.24.0",
"chokidar": "^4.0.0",
"commander": "^14.0.3",
"diff-match-patch": "^1.0.5",
@@ -25,10 +25,10 @@
"micromatch": "^4.0.0",
"minimatch": "^10.2.2",
"obsidian": "^1.12.3",
"octagonal-wheels": "^0.1.45",
"octagonal-wheels": "^0.1.46",
"pouchdb-adapter-leveldb": "^9.0.0",
"qrcode-generator": "^1.4.4",
"werift": "^0.22.9",
"werift": "^0.23.0",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
},
"devDependencies": {
@@ -2684,9 +2684,9 @@
}
},
"node_modules/@noble/secp256k1": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.0.0.tgz",
"integrity": "sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.1.0.tgz",
"integrity": "sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
@@ -4297,19 +4297,19 @@
"license": "MIT"
},
"node_modules/@trystero-p2p/core": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@trystero-p2p/core/-/core-0.23.0.tgz",
"integrity": "sha512-ozhtgxKDZH11Gdef0wH8xivwAE/L0/lDFvEcNFWPJWnHZlxWPPyfeonwE287ssGevQNi10vnj6x2ZcOi0n1bQQ==",
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/@trystero-p2p/core/-/core-0.24.0.tgz",
"integrity": "sha512-W5ATiflgzZLE21fN2VA3YsK2yBJEzCvhmJ/9q2Vm3QT/gcdqDpcBxsO0DYCy/wE1PBEwoB+A75eBNtGIGAPdxw==",
"license": "MIT"
},
"node_modules/@trystero-p2p/nostr": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@trystero-p2p/nostr/-/nostr-0.23.0.tgz",
"integrity": "sha512-KSqUR2c1KVfv4zeErcntuegtyKzFTzNNiitIKGD0LiKA/4H3CeTF81ROk2h+X/PNvP4mv7Gp5eVxFYwfMu4Nrg==",
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/@trystero-p2p/nostr/-/nostr-0.24.0.tgz",
"integrity": "sha512-LmsJSicsFU/rhmOWYaP/OxFl3rwGieX+q0eh0pAWUQM7IXbMu6tLC5+aAimtHitikPv9r6sck6EUTWMin8dBAw==",
"license": "MIT",
"dependencies": {
"@noble/secp256k1": "^3.0.0",
"@trystero-p2p/core": "0.23.0"
"@noble/secp256k1": "^3.1.0",
"@trystero-p2p/core": "0.24.0"
}
},
"node_modules/@tsconfig/svelte": {
@@ -11491,9 +11491,9 @@
"license": "MIT"
},
"node_modules/octagonal-wheels": {
"version": "0.1.45",
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.45.tgz",
"integrity": "sha512-gXoCrwoUIXhmu57YN4BxAtBe+JaYNJNaXaZuVjqjopwYKpH5p2mn1om6KjA22rgGPiIJFXkse2U28FFXoT3/0Q==",
"version": "0.1.46",
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.46.tgz",
"integrity": "sha512-19eB7b/WNNrZ4Xghu93f+NVJsbRiaZaIIzU1rn5shxb6SzwVBoOVkNPJdCAsONl6C1MwjaGDrPUS8CBXvPHjPg==",
"license": "MIT",
"dependencies": {
"idb": "^8.0.3"
@@ -16291,9 +16291,9 @@
"license": "BSD-2-Clause"
},
"node_modules/werift": {
"version": "0.22.9",
"resolved": "https://registry.npmjs.org/werift/-/werift-0.22.9.tgz",
"integrity": "sha512-TE9AxskSRWBMYm0MBRllfnKVXQelqC76JCvyolQyVWpmKabfY5BA/cuvkGg+71JWn3QrGih1YWtpIWGPqoxcoA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/werift/-/werift-0.23.0.tgz",
"integrity": "sha512-/WcIN5DHFG9Ri4anGOmIkp8gxBGFMWSIB/m4sfZ5CWlLfD3iMhiaAUuTBuc+KV3SY9NDmvmLtiN2uaM7k3lVzw==",
"license": "MIT",
"dependencies": {
"@fidm/x509": "^1.2.1",

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "0.25.67",
"version": "0.25.70",
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"main": "main.js",
"type": "module",
@@ -19,7 +19,7 @@
"buildVite": "npx dotenv-cli -e .env -- vite build --mode production",
"buildViteOriginal": "npx dotenv-cli -e .env -- vite build --mode original",
"buildDev": "node esbuild.config.mjs dev",
"lint": "eslint src",
"lint": "eslint --cache --concurrency auto src",
"svelte-check": "svelte-check --tsconfig ./tsconfig.json",
"tsc-check": "tsc --noEmit",
"pretty": "npm run prettyNoWrite -- --write --log-level error",
@@ -130,7 +130,7 @@
"@smithy/middleware-apply-body-checksum": "^4.3.9",
"@smithy/protocol-http": "^5.3.9",
"@smithy/querystring-builder": "^4.2.9",
"@trystero-p2p/nostr": "^0.23.0",
"@trystero-p2p/nostr": "^0.24.0",
"chokidar": "^4.0.0",
"commander": "^14.0.3",
"obsidian": "^1.12.3",
@@ -140,10 +140,10 @@
"markdown-it": "^14.1.1",
"micromatch": "^4.0.0",
"minimatch": "^10.2.2",
"octagonal-wheels": "^0.1.45",
"octagonal-wheels": "^0.1.46",
"pouchdb-adapter-leveldb": "^9.0.0",
"qrcode-generator": "^1.4.4",
"werift": "^0.22.9",
"werift": "^0.23.0",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
}
}

View File

@@ -146,7 +146,7 @@ export const _requestToCouchDBFetch = async (
contentType: "application/json",
body: JSON.stringify(body),
};
return await fetch(uri, requestParam);
return await _fetch(uri, requestParam);
};
export const _requestToCouchDB = async (
@@ -214,6 +214,7 @@ import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.cons
export { BASE_IS_NEW, EVEN, TARGET_IS_NEW };
// Why 2000? : ZIP FILE Does not have enough resolution.
import { compareMTime } from "@lib/common/utils.ts";
import { _fetch } from "@/lib/src/common/coreEnvFunctions.ts";
export { compareMTime };
function getKey(file: AnyEntry | string | UXFileInfoStub) {
const key = typeof file == "string" ? file : stripAllPrefixes(file.path);

View File

@@ -68,7 +68,7 @@ import { ConflictResolveModal } from "../../modules/features/InteractiveConflict
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "../../common/events.ts";
import { PluginDialogModal } from "./PluginDialogModal.ts";
import { $msg } from "src/lib/src/common/i18n.ts";
import { $msg } from "@/lib/src/common/i18n.ts";
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
import type { LiveSyncCore } from "../../main.ts";
@@ -564,7 +564,7 @@ export class ConfigSync extends LiveSyncCommands {
...data,
documentPath: this.getPath(wx),
files: xFiles,
} as PluginDataExDisplay;
} satisfies PluginDataExDisplay;
}
return false;
}

View File

@@ -24,6 +24,7 @@
let serverInfo = $state<P2PServerInfo | undefined>(undefined);
let replicatorStatus = $state<P2PReplicatorStatus | undefined>(undefined);
let roomSuffix = $state<string>(extractP2PRoomSuffix(core?.services.setting.currentSettings()?.P2P_roomID ?? ""));
let useDiagRTC = $state<boolean>(core?.services.setting.currentSettings()?.P2P_useDiagRTC ?? false);
async function requestServerStatus() {
await Promise.resolve(liveSyncReplicator.requestStatus());
@@ -48,6 +49,18 @@
}
}
async function toggleDiagRTC() {
if (!core) {
return;
}
const next = !useDiagRTC;
await core.services.setting.updateSettings((settings) => {
settings.P2P_useDiagRTC = next;
return settings;
}, true);
useDiagRTC = next;
}
onMount(() => {
const unsubscribe = eventHub.onEvent(EVENT_SERVER_STATUS, (status) => {
serverInfo = status;
@@ -58,6 +71,7 @@
});
const unsubscribeSettings = eventHub.onEvent(EVENT_SETTING_SAVED, (settings) => {
roomSuffix = extractP2PRoomSuffix(settings?.P2P_roomID ?? "");
useDiagRTC = settings?.P2P_useDiagRTC ?? false;
});
fireAndForget(async () => {
@@ -131,6 +145,48 @@
</button>
</div>
{/if}
{#if core}
<div class="status-item status-action diag-toggle-row">
<label class="broadcast-label" for="diag-toggle">
🕵️ Diag
</label>
<button
id="diag-toggle"
class="broadcast-button {useDiagRTC ? 'is-on' : 'is-off'}"
onclick={toggleDiagRTC}
title={useDiagRTC
? 'Diagnostic RTCPeerConnection is enabled'
: 'Use Diagnostic RTCPeerConnection for statistics'}
>
{useDiagRTC ? 'On' : 'Off'}
</button>
</div>
{/if}
{#if serverInfo}
<div class="diag-section">
<h4>Stats</h4>
<div class="diag-grid">
<div class="diag-item">
<span>Incoming:</span>
<span>{serverInfo.diag.totalNewConnections}</span>
</div>
<div class="diag-item">
<span>Connected:</span>
<span>{serverInfo.diag.totalSuccessfulConnections}</span>
</div>
<div class="diag-item">
<span>Failed:</span>
<span>{serverInfo.diag.totalFailedConnections}</span>
</div>
<div class="diag-item">
<span>Closed:</span>
<span>{serverInfo.diag.totalClosedConnections}</span>
</div>
</div>
</div>
{/if}
</div>
<style>
@@ -190,6 +246,11 @@
margin-top: 0.25rem;
}
.diag-toggle-row {
align-items: center;
margin-top: 0.25rem;
}
.broadcast-label {
font-size: 0.9rem;
color: var(--text-normal);
@@ -221,4 +282,29 @@
background-color: var(--interactive-hover);
color: var(--text-normal);
}
.diag-section {
border-top: 1px solid var(--divider-color);
margin-top: 0.75rem;
padding-top: 0.75rem;
}
.diag-section h4 {
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
font-weight: 600;
}
.diag-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 0.35rem 0.75rem;
}
.diag-item {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
gap: 0.5rem;
}
</style>

Submodule src/lib updated: c5beaa3866...faf5c55fd7

View File

@@ -12,8 +12,6 @@ import { ModuleObsidianEvents } from "./modules/essentialObsidian/ModuleObsidian
import { ModuleObsidianSettingDialogue } from "./modules/features/ModuleObsidianSettingTab.ts";
import { ModuleObsidianDocumentHistory } from "./modules/features/ModuleObsidianDocumentHistory.ts";
import { ModuleObsidianGlobalHistory } from "./modules/features/ModuleGlobalHistory.ts";
import { ModuleIntegratedTest } from "./modules/extras/ModuleIntegratedTest.ts";
import { ModuleReplicateTest } from "./modules/extras/ModuleReplicateTest.ts";
import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub.ts";
import { ObsidianServiceHub } from "./modules/services/ObsidianServiceHub.ts";
@@ -156,8 +154,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
new ModuleInteractiveConflictResolver(this, core),
new ModuleObsidianGlobalHistory(this, core),
new ModuleDev(this, core),
new ModuleReplicateTest(this, core),
new ModuleIntegratedTest(this, core),
new SetupManager(core), // this should be moved to core?
new ModuleMigration(core),
];

View File

@@ -1,6 +1,7 @@
import { ButtonComponent } from "@/deps.ts";
import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting } from "../../../deps.ts";
import { EVENT_PLUGIN_UNLOADED, eventHub } from "../../../common/events.ts";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
class AutoClosableModal extends Modal {
_closeByUnload() {
@@ -121,7 +122,7 @@ export class PopoverSelectString extends FuzzySuggestModal<string> {
this.callback = undefined;
}
override onClose(): void {
setTimeout(() => {
compatGlobal.setTimeout(() => {
if (this.callback) {
this.callback("");
this.callback = undefined;
@@ -139,7 +140,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
isManuallyClosed = false;
defaultAction: string | undefined;
timeout: number | undefined;
timer: ReturnType<typeof setInterval> | undefined = undefined;
timer: ReturnType<typeof compatGlobal.setInterval> | undefined = undefined;
defaultButtonComponent: ButtonComponent | undefined;
wideButton: boolean;
@@ -165,12 +166,12 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
this.timeout = timeout;
this.wideButton = wideButton;
if (this.timeout) {
this.timer = setInterval(() => {
this.timer = compatGlobal.setInterval(() => {
if (this.timeout === undefined) return;
this.timeout--;
if (this.timeout < 0) {
if (this.timer) {
clearInterval(this.timer);
compatGlobal.clearInterval(this.timer);
this.defaultButtonComponent?.setButtonText(`${defaultAction}`);
this.timer = undefined;
}
@@ -213,7 +214,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
if (this.timer) {
labelWrapper.empty();
labelWrapper.style.display = "none";
clearInterval(this.timer);
compatGlobal.clearInterval(this.timer);
this.timer = undefined;
this.defaultButtonComponent?.setButtonText(`${this.defaultAction}`);
}
@@ -224,7 +225,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
this.isManuallyClosed = true;
this.result = button;
if (this.timer) {
clearInterval(this.timer);
compatGlobal.clearInterval(this.timer);
this.timer = undefined;
}
this.close();
@@ -247,7 +248,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
const { contentEl } = this;
contentEl.empty();
if (this.timer) {
clearInterval(this.timer);
compatGlobal.clearInterval(this.timer);
this.timer = undefined;
}
if (this.isManuallyClosed) {

View File

@@ -27,7 +27,6 @@ export class ObsHttpHandler extends FetchHttpHandler {
this.requestTimeoutInMs = options === undefined ? undefined : options.requestTimeout;
this.reverseProxyNoSignUrl = reverseProxyNoSignUrl;
}
// eslint-disable-next-line require-await
override async handle(
request: HttpRequest,
{ abortSignal }: HttpHandlerOptions = {}

View File

@@ -13,6 +13,7 @@ import {
hiddenFilesProcessingCount,
} from "../../lib/src/mock_and_interop/stores.ts";
import type { LiveSyncCore } from "../../main.ts";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
export class ModuleObsidianEvents extends AbstractObsidianModule {
_everyOnloadStart(): Promise<boolean> {
@@ -222,9 +223,9 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
);
});
this.plugin.registerInterval(
setInterval(() => {
compatGlobal.setInterval(() => {
__tick.value++;
}, 1000) as unknown as number
}, 1000)
);
let stableCheck = 3;

View File

@@ -111,7 +111,7 @@ export class ModuleDev extends AbstractObsidianModule {
const filename = "test-create-conflict.md";
const content = `# Test create conflict\n\n`;
const w = await this.core.databaseFileAccess.store({
name: filename as FilePathWithPrefix,
name: filename,
path: filename as FilePathWithPrefix,
body: new Blob([content], { type: "text/markdown" }),
stat: {

View File

@@ -1,446 +0,0 @@
import { delay } from "octagonal-wheels/promises";
import { LOG_LEVEL_NOTICE, REMOTE_MINIO, type FilePathWithPrefix } from "src/lib/src/common/types";
import { shareRunningResult } from "octagonal-wheels/concurrency/lock";
import { AbstractObsidianModule } from "../AbstractObsidianModule";
export class ModuleIntegratedTest extends AbstractObsidianModule {
async waitFor(proc: () => Promise<boolean>, timeout = 10000): Promise<boolean> {
await delay(100);
const start = Date.now();
while (!(await proc())) {
if (timeout > 0) {
if (Date.now() - start > timeout) {
this._log(`Timeout`);
return false;
}
}
await delay(500);
}
return true;
}
waitWithReplicating(proc: () => Promise<boolean>, timeout = 10000): Promise<boolean> {
return this.waitFor(async () => {
await this.tryReplicate();
return await proc();
}, timeout);
}
async storageContentIsEqual(file: string, content: string): Promise<boolean> {
try {
const fileContent = await this.readStorageContent(file as FilePathWithPrefix);
if (fileContent === content) {
return true;
} else {
// this._log(`Content is not same \n Expected:${content}\n Actual:${fileContent}`, LOG_LEVEL_VERBOSE);
return false;
}
} catch (e) {
this._log(`Error: ${e}`);
return false;
}
}
async assert(proc: () => Promise<boolean>): Promise<boolean> {
if (!(await proc())) {
this._log(`Assertion failed`);
return false;
}
return true;
}
async __orDie(key: string, proc: () => Promise<boolean>): Promise<true> | never {
if (!(await this._test(key, proc))) {
throw new Error(`${key}`);
}
return true;
}
tryReplicate() {
if (!this.settings.liveSync) {
return shareRunningResult("replicate-test", async () => {
await this.services.replication.replicate();
});
}
}
async readStorageContent(file: FilePathWithPrefix): Promise<string | undefined> {
if (!(await this.core.storageAccess.isExistsIncludeHidden(file))) {
return undefined;
}
return await this.core.storageAccess.readHiddenFileText(file);
}
async __proceed(no: number, title: string): Promise<boolean> {
const stepFile = "_STEP.md" as FilePathWithPrefix;
const stepAckFile = "_STEP_ACK.md" as FilePathWithPrefix;
const stepContent = `Step ${no}`;
await this.services.conflict.resolveByNewest(stepFile);
await this.core.storageAccess.writeFileAuto(stepFile, stepContent);
await this.__orDie(`Wait for acknowledge ${no}`, async () => {
if (
!(await this.waitWithReplicating(async () => {
return await this.storageContentIsEqual(stepAckFile, stepContent);
}, 20000))
)
return false;
return true;
});
return true;
}
async __join(no: number, title: string): Promise<boolean> {
const stepFile = "_STEP.md" as FilePathWithPrefix;
const stepAckFile = "_STEP_ACK.md" as FilePathWithPrefix;
// const otherStepFile = `_STEP_${isLeader ? "R" : "L"}.md` as FilePathWithPrefix;
const stepContent = `Step ${no}`;
await this.__orDie(`Wait for step ${no} (${title})`, async () => {
if (
!(await this.waitWithReplicating(async () => {
return await this.storageContentIsEqual(stepFile, stepContent);
}, 20000))
)
return false;
return true;
});
await this.services.conflict.resolveByNewest(stepAckFile);
await this.core.storageAccess.writeFileAuto(stepAckFile, stepContent);
await this.tryReplicate();
return true;
}
async performStep({
step,
title,
isGameChanger,
proc,
check,
}: {
step: number;
title: string;
isGameChanger: boolean;
proc: () => Promise<any>;
check: () => Promise<boolean>;
}): Promise<boolean> {
if (isGameChanger) {
await this.__proceed(step, title);
try {
await proc();
} catch (e) {
this._log(`Error: ${e}`);
return false;
}
return await this.__orDie(`Step ${step} - ${title}`, async () => await this.waitWithReplicating(check));
} else {
return await this.__join(step, title);
}
}
// // see scenario.md
// async testLeader(testMain: (testFileName: FilePathWithPrefix) => Promise<boolean>): Promise<boolean> {
// }
// async testReceiver(testMain: (testFileName: FilePathWithPrefix) => Promise<boolean>): Promise<boolean> {
// }
async nonLiveTestRunner(
isLeader: boolean,
testMain: (testFileName: FilePathWithPrefix, isLeader: boolean) => Promise<boolean>
): Promise<boolean> {
const storage = this.core.storageAccess;
// const database = this.core.databaseFileAccess;
// const _orDie = this._orDie.bind(this);
const testCommandFile = "IT.md" as FilePathWithPrefix;
const textCommandResponseFile = "ITx.md" as FilePathWithPrefix;
let testFileName: FilePathWithPrefix;
this.addTestResult(
"-------Starting ... ",
true,
`Test as ${isLeader ? "Leader" : "Receiver"} command file ${testCommandFile}`
);
if (isLeader) {
await this.__proceed(0, "start");
}
await this.tryReplicate();
await this.performStep({
step: 0,
title: "Make sure that command File Not Exists",
isGameChanger: isLeader,
proc: async () => await storage.removeHidden(testCommandFile),
check: async () => !(await storage.isExistsIncludeHidden(testCommandFile)),
});
await this.performStep({
step: 1,
title: "Make sure that command File Not Exists On Receiver",
isGameChanger: !isLeader,
proc: async () => await storage.removeHidden(textCommandResponseFile),
check: async () => !(await storage.isExistsIncludeHidden(textCommandResponseFile)),
});
await this.performStep({
step: 2,
title: "Decide the test file name",
isGameChanger: isLeader,
proc: async () => {
testFileName = (Date.now() + "-" + Math.ceil(Math.random() * 1000) + ".md") as FilePathWithPrefix;
const testCommandFile = "IT.md" as FilePathWithPrefix;
await storage.writeFileAuto(testCommandFile, testFileName);
},
check: () => Promise.resolve(true),
});
await this.performStep({
step: 3,
title: "Wait for the command file to be arrived",
isGameChanger: !isLeader,
proc: async () => {},
check: async () => await storage.isExistsIncludeHidden(testCommandFile),
});
await this.performStep({
step: 4,
title: "Send the response file",
isGameChanger: !isLeader,
proc: async () => {
await storage.writeHiddenFileAuto(textCommandResponseFile, "!");
},
check: () => Promise.resolve(true),
});
await this.performStep({
step: 5,
title: "Wait for the response file to be arrived",
isGameChanger: isLeader,
proc: async () => {},
check: async () => await storage.isExistsIncludeHidden(textCommandResponseFile),
});
await this.performStep({
step: 6,
title: "Proceed to begin the test",
isGameChanger: isLeader,
proc: async () => {},
check: () => Promise.resolve(true),
});
await this.performStep({
step: 6,
title: "Begin the test",
isGameChanger: !false,
proc: async () => {},
check: () => {
return Promise.resolve(true);
},
});
// await this.step(0, isLeader, true);
try {
this.addTestResult("** Main------", true, ``);
if (isLeader) {
return await testMain(testFileName!, true);
} else {
const testFileName = await this.readStorageContent(testCommandFile);
this.addTestResult("testFileName", true, `Request client to use :${testFileName!}`);
return await testMain(testFileName! as FilePathWithPrefix, false);
}
} finally {
this.addTestResult("Teardown", true, `Deleting ${testFileName!}`);
await storage.removeHidden(testFileName!);
}
return true;
// Make sure the
}
async testBasic(filename: FilePathWithPrefix, isLeader: boolean): Promise<boolean> {
const storage = this.core.storageAccess;
const database = this.core.databaseFileAccess;
await this.addTestResult(
`---**Starting Basic Test**---`,
true,
`Test as ${isLeader ? "Leader" : "Receiver"} command file ${filename}`
);
// if (isLeader) {
// await this._proceed(0);
// }
// await this.tryReplicate();
await this.performStep({
step: 0,
title: "Make sure that file is not exist",
isGameChanger: !isLeader,
proc: async () => {},
check: async () => !(await storage.isExists(filename)),
});
await this.performStep({
step: 1,
title: "Write a file",
isGameChanger: isLeader,
proc: async () => await storage.writeFileAuto(filename, "Hello World"),
check: async () => await storage.isExists(filename),
});
await this.performStep({
step: 2,
title: "Make sure the file is arrived",
isGameChanger: !isLeader,
proc: async () => {},
check: async () => await storage.isExists(filename),
});
await this.performStep({
step: 3,
title: "Update to Hello World 2",
isGameChanger: isLeader,
proc: async () => await storage.writeFileAuto(filename, "Hello World 2"),
check: async () => await this.storageContentIsEqual(filename, "Hello World 2"),
});
await this.performStep({
step: 4,
title: "Make sure the modified file is arrived",
isGameChanger: !isLeader,
proc: async () => {},
check: async () => await this.storageContentIsEqual(filename, "Hello World 2"),
});
await this.performStep({
step: 5,
title: "Update to Hello World 3",
isGameChanger: !isLeader,
proc: async () => await storage.writeFileAuto(filename, "Hello World 3"),
check: async () => await this.storageContentIsEqual(filename, "Hello World 3"),
});
await this.performStep({
step: 6,
title: "Make sure the modified file is arrived",
isGameChanger: isLeader,
proc: async () => {},
check: async () => await this.storageContentIsEqual(filename, "Hello World 3"),
});
const multiLineContent = `Line1:A
Line2:B
Line3:C
Line4:D`;
await this.performStep({
step: 7,
title: "Update to Multiline",
isGameChanger: isLeader,
proc: async () => await storage.writeFileAuto(filename, multiLineContent),
check: async () => await this.storageContentIsEqual(filename, multiLineContent),
});
await this.performStep({
step: 8,
title: "Make sure the modified file is arrived",
isGameChanger: !isLeader,
proc: async () => {},
check: async () => await this.storageContentIsEqual(filename, multiLineContent),
});
// While LiveSync, possibly cannot cause the conflict.
if (!this.settings.liveSync) {
// Step 9 Make Conflict But Resolvable
const multiLineContentL = `Line1:A
Line2:B
Line3:C!
Line4:D`;
const multiLineContentC = `Line1:A
Line2:bbbbb
Line3:C
Line4:D`;
await this.performStep({
step: 9,
title: "Progress to be conflicted",
isGameChanger: isLeader,
proc: async () => {},
check: () => Promise.resolve(true),
});
await storage.writeFileAuto(filename, isLeader ? multiLineContentL : multiLineContentC);
await this.performStep({
step: 10,
title: "Update As Conflicted",
isGameChanger: !isLeader,
proc: async () => {},
check: () => Promise.resolve(true),
});
await this.performStep({
step: 10,
title: "Make sure Automatically resolved",
isGameChanger: isLeader,
proc: async () => {},
check: async () => (await database.getConflictedRevs(filename)).length === 0,
});
await this.performStep({
step: 11,
title: "Make sure Automatically resolved",
isGameChanger: !isLeader,
proc: async () => {},
check: async () => (await database.getConflictedRevs(filename)).length === 0,
});
const sensiblyMergedContent = `Line1:A
Line2:bbbbb
Line3:C!
Line4:D`;
await this.performStep({
step: 12,
title: "Make sure Sensibly Merged on Leader",
isGameChanger: isLeader,
proc: async () => {},
check: async () => await this.storageContentIsEqual(filename, sensiblyMergedContent),
});
await this.performStep({
step: 13,
title: "Make sure Sensibly Merged on Receiver",
isGameChanger: !isLeader,
proc: async () => {},
check: async () => await this.storageContentIsEqual(filename, sensiblyMergedContent),
});
}
await this.performStep({
step: 14,
title: "Delete File",
isGameChanger: isLeader,
proc: async () => {
await storage.removeHidden(filename);
},
check: async () => !(await storage.isExists(filename)),
});
await this.performStep({
step: 15,
title: "Make sure File is deleted",
isGameChanger: !isLeader,
proc: async () => {},
check: async () => !(await storage.isExists(filename)),
});
this._log(`The Basic Test has been completed`, LOG_LEVEL_NOTICE);
return true;
}
async testBasicEvent(isLeader: boolean) {
this.settings.liveSync = false;
await this.saveSettings();
await this._test("basic", async () => await this.nonLiveTestRunner(isLeader, (t, l) => this.testBasic(t, l)));
}
async testBasicLive(isLeader: boolean) {
this.settings.liveSync = true;
await this.saveSettings();
await this._test("basic", async () => await this.nonLiveTestRunner(isLeader, (t, l) => this.testBasic(t, l)));
}
async _everyModuleTestMultiDevice(): Promise<boolean> {
if (!this.settings.enableDebugTools) return Promise.resolve(true);
const isLeader = this.core.services.vault.vaultName().indexOf("recv") === -1;
this.addTestResult("-------", true, `Test as ${isLeader ? "Leader" : "Receiver"}`);
try {
this._log(`Starting Test`);
await this.testBasicEvent(isLeader);
if (this.settings.remoteType == REMOTE_MINIO) await this.testBasicLive(isLeader);
} catch (e) {
this._log(e);
this._log(`Error: ${e}`);
return Promise.resolve(false);
}
return Promise.resolve(true);
}
override onBindFunction(core: typeof this.core, services: typeof core.services): void {
services.test.testMultiDevice.addHandler(this._everyModuleTestMultiDevice.bind(this));
}
}

View File

@@ -1,590 +0,0 @@
// I intend to discontinue maintenance of this class. It seems preferable to test it externally.
import { delay } from "octagonal-wheels/promises";
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import { eventHub } from "../../common/events";
import { getWebCrypto } from "../../lib/src/mods.ts";
import { uint8ArrayToHexString } from "octagonal-wheels/binary/hex";
import { parseYaml, requestUrl, stringifyYaml } from "@/deps.ts";
import type { FilePath } from "../../lib/src/common/types.ts";
import { scheduleTask } from "octagonal-wheels/concurrency/task";
import { getFileRegExp } from "../../lib/src/common/utils.ts";
import type { LiveSyncCore } from "../../main.ts";
declare global {
interface LSEvents {
"debug-sync-status": string[];
}
}
export class ModuleReplicateTest extends AbstractObsidianModule {
testRootPath = "_test/";
testInfoPath = "_testinfo/";
get isLeader() {
return (
this.services.vault.getVaultName().indexOf("dev") >= 0 &&
this.services.vault.vaultName().indexOf("recv") < 0
);
}
get nameByKind() {
if (!this.isLeader) {
return "RECV";
} else if (this.isLeader) {
return "LEADER";
}
}
get pairName() {
if (this.isLeader) {
return "RECV";
} else if (!this.isLeader) {
return "LEADER";
}
}
watchIsSynchronised = false;
statusBarSyncStatus?: HTMLElement;
async readFileContent(file: string) {
try {
return await this.core.storageAccess.readHiddenFileText(file);
} catch {
return "";
}
}
async dumpList() {
if (this.settings.syncInternalFiles) {
this._log("Write file list (Include Hidden)");
await this.__dumpFileListIncludeHidden("files.md");
} else {
this._log("Write file list");
await this.__dumpFileList("files.md");
}
}
async _everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
if (!this.settings.enableDebugTools) return Promise.resolve(true);
await this.dumpList();
return true;
}
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
if (!this.settings.enableDebugTools) return Promise.resolve(true);
this.addCommand({
id: "dump-file-structure-normal",
name: `Dump Structure (Normal)`,
callback: () => {
void this.__dumpFileList("files.md").finally(() => {
void this.refreshSyncStatus();
});
},
});
this.addCommand({
id: "dump-file-structure-ih",
name: "Dump Structure (Include Hidden)",
callback: () => {
const d = "files.md";
void this.__dumpFileListIncludeHidden(d);
},
});
this.addCommand({
id: "dump-file-structure-auto",
name: "Dump Structure",
callback: () => {
void this.dumpList();
},
});
this.addCommand({
id: "dump-file-test",
name: `Perform Test (Dev) ${this.isLeader ? "(Leader)" : "(Recv)"}`,
callback: () => {
void this.performTestManually();
},
});
this.addCommand({
id: "watch-sync-result",
name: `Watch sync result is matched between devices`,
callback: () => {
this.watchIsSynchronised = !this.watchIsSynchronised;
void this.refreshSyncStatus();
},
});
this.app.vault.on("modify", async (file) => {
if (file.path.startsWith(this.testInfoPath)) {
await this.refreshSyncStatus();
} else {
scheduleTask("dumpStatus", 125, async () => {
await this.dumpList();
return true;
});
}
});
this.statusBarSyncStatus = this.plugin.addStatusBarItem();
return Promise.resolve(true);
}
async getSyncStatusAsText() {
const fileMine = this.testInfoPath + this.nameByKind + "/" + "files.md";
const filePair = this.testInfoPath + this.pairName + "/" + "files.md";
const mine = parseYaml(await this.readFileContent(fileMine));
const pair = parseYaml(await this.readFileContent(filePair));
const result = [] as string[];
if (mine.length != pair.length) {
result.push(`File count is different: ${mine.length} vs ${pair.length}`);
}
const filesAll = new Set([...mine.map((e: any) => e.path), ...pair.map((e: any) => e.path)]);
for (const file of filesAll) {
const mineFile = mine.find((e: any) => e.path == file);
const pairFile = pair.find((e: any) => e.path == file);
if (!mineFile || !pairFile) {
result.push(`File not found: ${file}`);
} else {
if (mineFile.size != pairFile.size) {
result.push(`Size is different: ${file} ${mineFile.size} vs ${pairFile.size}`);
}
if (mineFile.hash != pairFile.hash) {
result.push(`Hash is different: ${file} ${mineFile.hash} vs ${pairFile.hash}`);
}
}
}
eventHub.emitEvent("debug-sync-status", result);
return result.join("\n");
}
async refreshSyncStatus() {
if (this.watchIsSynchronised) {
// Normal Files
const syncStatus = await this.getSyncStatusAsText();
if (syncStatus) {
this.statusBarSyncStatus!.setText(`Sync Status: Having Error`);
this._log(`Sync Status: Having Error\n${syncStatus}`, LOG_LEVEL_INFO);
} else {
this.statusBarSyncStatus!.setText(`Sync Status: Synchronised`);
}
} else {
this.statusBarSyncStatus!.setText("");
}
}
async __dumpFileList(outFile?: string) {
if (!this.core || !this.core.storageAccess) {
this._log("No storage access", LOG_LEVEL_INFO);
return;
}
const files = await this.core.storageAccess.getFiles();
const out = [] as any[];
const webcrypto = await getWebCrypto();
for (const file of files) {
if (!(await this.services.vault.isTargetFile(file.path))) {
continue;
}
if (file.path.startsWith(this.testInfoPath)) continue;
const stat = await this.core.storageAccess.stat(file.path);
if (stat) {
const hashSrc = await this.core.storageAccess.readHiddenFileBinary(file.path);
const hash = await webcrypto.subtle.digest("SHA-1", hashSrc);
const hashStr = uint8ArrayToHexString(new Uint8Array(hash));
const item = {
path: file.path,
name: file.name,
size: stat.size,
mtime: stat.mtime,
hash: hashStr,
};
// const fileLine = `-${file.path}:${stat.size}:${stat.mtime}:${hashStr}`;
out.push(item);
}
}
out.sort((a, b) => a.path.localeCompare(b.path));
if (outFile) {
outFile = this.testInfoPath + this.nameByKind + "/" + outFile;
await this.core.storageAccess.ensureDir(outFile);
await this.core.storageAccess.writeHiddenFileAuto(outFile, stringifyYaml(out));
} else {
// console.dir(out);
}
this._log(`Dumped ${out.length} files`, LOG_LEVEL_INFO);
}
async __dumpFileListIncludeHidden(outFile?: string) {
const ignorePatterns = getFileRegExp(this.core.settings, "syncInternalFilesIgnorePatterns");
const targetPatterns = getFileRegExp(this.core.settings, "syncInternalFilesTargetPatterns");
const out = [] as any[];
const files = await this.core.storageAccess.getFilesIncludeHidden("", targetPatterns, ignorePatterns);
// console.dir(files);
const webcrypto = await getWebCrypto();
for (const file of files) {
// if (!await this.core.$$isTargetFile(file)) {
// continue;
// }
if (file.startsWith(this.testInfoPath)) continue;
const stat = await this.core.storageAccess.statHidden(file);
if (stat) {
const hashSrc = await this.core.storageAccess.readHiddenFileBinary(file);
const hash = await webcrypto.subtle.digest("SHA-1", hashSrc);
const hashStr = uint8ArrayToHexString(new Uint8Array(hash));
const item = {
path: file,
name: file.split("/").pop(),
size: stat.size,
mtime: stat.mtime,
hash: hashStr,
};
// const fileLine = `-${file.path}:${stat.size}:${stat.mtime}:${hashStr}`;
out.push(item);
}
}
out.sort((a, b) => a.path.localeCompare(b.path));
if (outFile) {
outFile = this.testInfoPath + this.nameByKind + "/" + outFile;
await this.core.storageAccess.ensureDir(outFile);
await this.core.storageAccess.writeHiddenFileAuto(outFile, stringifyYaml(out));
} else {
// console.dir(out);
}
this._log(`Dumped ${out.length} files`, LOG_LEVEL_NOTICE);
}
async collectTestFiles() {
const remoteTopDir = "https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/refs/heads/main/";
const files = [
"README.md",
"docs/adding_translations.md",
"docs/design_docs_of_journalsync.md",
"docs/design_docs_of_keep_newborn_chunks.md",
"docs/design_docs_of_prefixed_hidden_file_sync.md",
"docs/design_docs_of_sharing_tweak_value.md",
"docs/quick_setup_cn.md",
"docs/quick_setup_ja.md",
"docs/quick_setup.md",
"docs/settings_ja.md",
"docs/settings.md",
"docs/setup_cloudant_ja.md",
"docs/setup_cloudant.md",
"docs/setup_flyio.md",
"docs/setup_own_server_cn.md",
"docs/setup_own_server_ja.md",
"docs/setup_own_server.md",
"docs/tech_info_ja.md",
"docs/tech_info.md",
"docs/terms.md",
"docs/troubleshooting.md",
"images/1.png",
"images/2.png",
"images/corrupted_data.png",
"images/hatch.png",
"images/lock_pattern1.png",
"images/lock_pattern2.png",
"images/quick_setup_1.png",
"images/quick_setup_2.png",
"images/quick_setup_3.png",
"images/quick_setup_3b.png",
"images/quick_setup_4.png",
"images/quick_setup_5.png",
"images/quick_setup_6.png",
"images/quick_setup_7.png",
"images/quick_setup_8.png",
"images/quick_setup_9_1.png",
"images/quick_setup_9_2.png",
"images/quick_setup_10.png",
"images/remote_db_setting.png",
"images/write_logs_into_the_file.png",
];
for (const file of files) {
const remote = remoteTopDir + file;
const local = this.testRootPath + file;
try {
const f = (await requestUrl(remote)).arrayBuffer;
await this.core.storageAccess.ensureDir(local);
await this.core.storageAccess.writeHiddenFileAuto(local, f);
} catch (ex) {
this._log(`Could not fetch ${remote}`, LOG_LEVEL_VERBOSE);
this._log(ex, LOG_LEVEL_VERBOSE);
}
}
await this.dumpList();
}
async waitFor(proc: () => Promise<boolean>, timeout = 10000): Promise<boolean> {
await delay(100);
const start = Date.now();
while (!(await proc())) {
if (timeout > 0) {
if (Date.now() - start > timeout) {
this._log(`Timeout`);
return false;
}
}
await delay(500);
}
return true;
}
async testConflictedManually1() {
await this.services.replication.replicate();
const commonFile = `Resolve!
*****, the amazing chocolatier!!`;
if (this.isLeader) {
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", commonFile);
}
await this.services.replication.replicate();
await this.services.replication.replicate();
if (
(await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 1?", {
timeout: 30,
defaultOption: "Yes",
})) == "no"
) {
return;
}
const fileA = `Resolve to KEEP THIS
Willy Wonka, Willy Wonka, the amazing chocolatier!!`;
const fileB = `Resolve to DISCARD THIS
Charlie Bucket, Charlie Bucket, the amazing chocolatier!!`;
if (this.isLeader) {
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", fileA);
} else {
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", fileB);
}
if (
(await this.core.confirm.askYesNoDialog("Ready to check the result of Manually 1?", {
timeout: 30,
defaultOption: "Yes",
})) == "no"
) {
return;
}
await this.services.replication.replicate();
await this.services.replication.replicate();
if (
!(await this.waitFor(async () => {
await this.services.replication.replicate();
return (
(await this.__assertStorageContent(
(this.testRootPath + "wonka.md") as FilePath,
fileA,
false,
true
)) == true
);
}, 30000))
) {
return await this.__assertStorageContent((this.testRootPath + "wonka.md") as FilePath, fileA, false, true);
}
return true;
// We have to check the result
}
async testConflictedManually2() {
await this.services.replication.replicate();
const commonFile = `Resolve To concatenate
ABCDEFG`;
if (this.isLeader) {
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", commonFile);
}
await this.services.replication.replicate();
await this.services.replication.replicate();
if (
(await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 2?", {
timeout: 30,
defaultOption: "Yes",
})) == "no"
) {
return;
}
const fileA = `Resolve to Concatenate
ABCDEFGHIJKLMNOPQRSTYZ`;
const fileB = `Resolve to Concatenate
AJKLMNOPQRSTUVWXYZ`;
const concatenated = `Resolve to Concatenate
ABCDEFGHIJKLMNOPQRSTUVWXYZ`;
if (this.isLeader) {
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", fileA);
} else {
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", fileB);
}
if (
(await this.core.confirm.askYesNoDialog("Ready to test conflict Manually 2?", {
timeout: 30,
defaultOption: "Yes",
})) == "no"
) {
return;
}
await this.services.replication.replicate();
await this.services.replication.replicate();
if (
!(await this.waitFor(async () => {
await this.services.replication.replicate();
return (
(await this.__assertStorageContent(
(this.testRootPath + "concat.md") as FilePath,
concatenated,
false,
true
)) == true
);
}, 30000))
) {
return await this.__assertStorageContent(
(this.testRootPath + "concat.md") as FilePath,
concatenated,
false,
true
);
}
return true;
}
async testConflictAutomatic() {
if (this.isLeader) {
const baseDoc = `Tasks!
- [ ] Task 1
- [ ] Task 2
- [ ] Task 3
- [ ] Task 4
`;
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", baseDoc);
}
await delay(100);
await this.services.replication.replicate();
await this.services.replication.replicate();
if (
(await this.core.confirm.askYesNoDialog("Ready to test conflict?", {
timeout: 30,
defaultOption: "Yes",
})) == "no"
) {
return;
}
const mod1Doc = `Tasks!
- [ ] Task 1
- [v] Task 2
- [ ] Task 3
- [ ] Task 4
`;
const mod2Doc = `Tasks!
- [ ] Task 1
- [ ] Task 2
- [v] Task 3
- [ ] Task 4
`;
if (this.isLeader) {
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", mod1Doc);
} else {
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", mod2Doc);
}
await this.services.replication.replicate();
await this.services.replication.replicate();
await delay(1000);
if (
(await this.core.confirm.askYesNoDialog("Ready to check result?", { timeout: 30, defaultOption: "Yes" })) ==
"no"
) {
return;
}
await this.services.replication.replicate();
await this.services.replication.replicate();
const mergedDoc = `Tasks!
- [ ] Task 1
- [v] Task 2
- [v] Task 3
- [ ] Task 4
`;
return this.__assertStorageContent((this.testRootPath + "task.md") as FilePath, mergedDoc, false, true);
}
// No longer tested
async checkConflictResolution() {
this._log("Before testing conflicted files, resolve all once", LOG_LEVEL_NOTICE);
await this.services.conflict.resolveAllConflictedFilesByNewerOnes();
await this.services.conflict.resolveAllConflictedFilesByNewerOnes();
await this.services.replication.replicate();
await delay(1000);
if (!(await this.testConflictAutomatic())) {
this._log("Conflict resolution (Auto) failed", LOG_LEVEL_NOTICE);
return false;
}
if (!(await this.testConflictedManually1())) {
this._log("Conflict resolution (Manual1) failed", LOG_LEVEL_NOTICE);
return false;
}
if (!(await this.testConflictedManually2())) {
this._log("Conflict resolution (Manual2) failed", LOG_LEVEL_NOTICE);
return false;
}
return true;
}
async __assertStorageContent(
fileName: FilePath,
content: string,
inverted = false,
showResult = false
): Promise<boolean | string> {
try {
const fileContent = await this.core.storageAccess.readHiddenFileText(fileName);
let result = fileContent === content;
if (inverted) {
result = !result;
}
if (result) {
return true;
} else {
if (showResult) {
this._log(`Content is not same \n Expected:${content}\n Actual:${fileContent}`, LOG_LEVEL_VERBOSE);
}
return `Content is not same \n Expected:${content}\n Actual:${fileContent}`;
}
} catch (e) {
this._log(`Cannot assert storage content: ${e}`);
return false;
}
}
async performTestManually() {
if (!this.settings.enableDebugTools) return Promise.resolve(true);
await this.checkConflictResolution();
// await this.collectTestFiles();
}
// testResults = writable<[boolean, string, string][]>([]);
// testResults: string[] = [];
// $$addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void {
// const logLine = `${name}: ${key} ${summary ?? ""}`;
// this.testResults.update((results) => {
// results.push([result, logLine, message ?? ""]);
// return results;
// });
// }
private async _everyModuleTestMultiDevice(): Promise<boolean> {
if (!this.settings.enableDebugTools) return Promise.resolve(true);
// this.core.$$addTestResult("DevModule", "Test", true);
// return Promise.resolve(true);
await this._test("Conflict resolution", async () => await this.checkConflictResolution());
return this.testDone();
}
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
services.replication.onBeforeReplicate.addHandler(this._everyBeforeReplicate.bind(this));
services.test.testMultiDevice.addHandler(this._everyModuleTestMultiDevice.bind(this));
}
}

View File

@@ -1,17 +1,15 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import { onMount } from "svelte";
import type ObsidianLiveSyncPlugin from "../../../main.ts";
import { perf_trench } from "./tests.ts";
import { MarkdownRenderer, Notice } from "../../../deps.ts";
import type { ModuleDev } from "../ModuleDev.ts";
import { fireAndForget } from "octagonal-wheels/promises";
import { EVENT_LAYOUT_READY, eventHub } from "../../../common/events.ts";
import { writable } from "svelte/store";
export let plugin: ObsidianLiveSyncPlugin;
export let moduleDev: ModuleDev;
$: core = plugin.core;
let performanceTestResult = "";
let functionCheckResult = "";
let testRunning = false;
let prefTestResultEl: HTMLDivElement;
let isReady = false;

View File

@@ -16,6 +16,7 @@ import { fireAndForget, getDocData, readContent } from "../../../lib/src/common/
import { isPlainText, stripPrefix } from "../../../lib/src/string_and_binary/path.ts";
import { scheduleOnceIfDuplicated } from "octagonal-wheels/concurrency/lock";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
function isImage(path: string) {
const ext = path.split(".").splice(-1)[0].toLowerCase();
@@ -56,6 +57,7 @@ export class DocumentHistoryModal extends Modal {
info!: HTMLDivElement;
fileInfo!: HTMLDivElement;
showDiff = false;
diffOnly = false;
id?: DocumentID;
file: FilePathWithPrefix;
@@ -70,6 +72,15 @@ export class DocumentHistoryModal extends Modal {
currentDiffIndex = -1;
diffNavContainer!: HTMLDivElement;
diffNavIndicator!: HTMLSpanElement;
diffOnlyLabel!: HTMLLabelElement;
// Search state
searchKeyword = "";
searchResults: { rev: string; index: number; matchType: "Content" | "Diff" }[] = [];
currentSearchIndex = -1;
searchResultIndicator!: HTMLSpanElement;
searchProgressIndicator!: HTMLSpanElement;
searchTimeout: number | null = null;
constructor(
app: App,
@@ -88,9 +99,12 @@ export class DocumentHistoryModal extends Modal {
if (!file && id) {
this.file = this.services.path.id2path(id);
}
if (localStorage.getItem("ols-history-highlightdiff") == "1") {
if (this.app.loadLocalStorage("ols-history-highlightdiff") == "1") {
this.showDiff = true;
}
if (this.app.loadLocalStorage("ols-history-diffonly") == "1") {
this.diffOnly = true;
}
}
async loadFile(initialRev?: string) {
@@ -151,17 +165,48 @@ export class DocumentHistoryModal extends Modal {
}
appendTextDiff(diff: [number, string][]) {
let hasOmitted = false;
for (const [operation, text] of diff) {
if (operation == DIFF_DELETE) {
this.contentView.createSpan({ text, cls: "history-deleted" });
this.appendSearchHighlightedText(this.contentView.createSpan({ cls: "history-deleted" }), text);
hasOmitted = false;
} else if (operation == DIFF_EQUAL) {
this.contentView.createSpan({ text, cls: "history-normal" });
if (this.diffOnly) {
if (!hasOmitted) {
this.contentView.appendText("\n...\n");
hasOmitted = true;
}
} else {
this.appendSearchHighlightedText(this.contentView.createSpan({ cls: "history-normal" }), text);
}
} else if (operation == DIFF_INSERT) {
this.contentView.createSpan({ text, cls: "history-added" });
this.appendSearchHighlightedText(this.contentView.createSpan({ cls: "history-added" }), text);
hasOmitted = false;
}
}
}
appendSearchHighlightedText(container: HTMLElement, text: string) {
if (!this.searchKeyword) {
container.appendText(text);
return;
}
const escapedKeyword = this.searchKeyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(escapedKeyword, "gi");
let lastIndex = 0;
for (const match of text.matchAll(regex)) {
const index = match.index ?? 0;
if (index > lastIndex) {
container.appendText(text.slice(lastIndex, index));
}
container.createEl("mark", { text: match[0] });
lastIndex = index + match[0].length;
}
if (lastIndex < text.length) {
container.appendText(text.slice(lastIndex));
}
}
appendImageDiff(baseSrc: string, overlaySrc?: string) {
const wrap = this.contentView.createDiv({ cls: "ls-imgdiff-wrap" });
const overlay = wrap.createDiv({ cls: "overlay" });
@@ -258,7 +303,7 @@ export class DocumentHistoryModal extends Modal {
if (this.currentDeleted) {
this.appendDeletedNotice();
}
this.contentView.appendText(w1data);
this.appendSearchHighlightedText(this.contentView, w1data);
}
}
}
@@ -266,6 +311,11 @@ export class DocumentHistoryModal extends Modal {
this.resetDiffNavigation();
if (this.showDiff) {
this.navigateDiff("next");
} else if (this.searchKeyword) {
const firstMark = this.contentView.querySelector("mark");
if (firstMark) {
firstMark.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
}
@@ -293,7 +343,7 @@ export class DocumentHistoryModal extends Modal {
target.classList.add("diff-focused");
target.scrollIntoView({ behavior: "smooth", block: "center" });
this.diffNavIndicator.textContent = `${this.currentDiffIndex + 1}/${diffElements.length}`;
this.diffNavIndicator.setText(`${this.currentDiffIndex + 1}/${diffElements.length}`);
}
/**
@@ -304,9 +354,9 @@ export class DocumentHistoryModal extends Modal {
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";
this.diffNavIndicator.setText(diffElements.length > 0 ? `0/${diffElements.length}` : "\u2014");
} else {
this.diffNavIndicator.textContent = "\u2014";
this.diffNavIndicator.setText("\u2014");
}
}
this.updateDiffNavVisibility();
@@ -319,6 +369,117 @@ export class DocumentHistoryModal extends Modal {
if (this.diffNavContainer) {
this.diffNavContainer.style.display = this.showDiff ? "flex" : "none";
}
if (this.diffOnlyLabel) {
this.diffOnlyLabel.style.display = this.showDiff ? "inline-block" : "none";
}
}
/**
* Search through the last 100 revisions for the given keyword.
*/
async performSearch(keyword: string) {
this.searchKeyword = keyword;
this.searchResults = [];
this.currentSearchIndex = -1;
if (!keyword) {
this.searchResultIndicator.setText("");
this.searchProgressIndicator.setText("");
return;
}
const db = this.core.localDatabase;
const limit = 100;
const totalRevs = this.revs_info.length;
const end = Math.min(totalRevs, limit);
this.searchProgressIndicator.setText("Searching...");
const dmp = new diff_match_patch();
// 0 is the newest, higher index is older.
for (let i = 0; i < end; i++) {
const revInfo = this.revs_info[i];
const rev = revInfo.rev;
this.searchProgressIndicator.setText(`Searching ${i + 1}/${end}...`);
const doc = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
if (doc === false) continue;
const content = readDocument(doc);
if (typeof content !== "string") continue;
const keywordLower = keyword.toLocaleLowerCase();
// Search in content
if (content.toLocaleLowerCase().includes(keywordLower)) {
this.searchResults.push({ rev, index: i, matchType: "Content" });
this.updateSearchUI();
continue;
}
// Search in diff (from older version to this version)
// Older version is at i + 1
if (i < totalRevs - 1) {
const olderRev = this.revs_info[i + 1].rev;
const olderDoc = await db.getDBEntry(this.file, { rev: olderRev }, false, false, true);
if (olderDoc !== false) {
const olderContent = readDocument(olderDoc);
if (typeof olderContent === "string") {
const diffs = dmp.diff_main(olderContent, content);
let foundInDiff = false;
for (const d of diffs) {
if (
(d[0] === DIFF_INSERT || d[0] === DIFF_DELETE) &&
d[1].toLocaleLowerCase().includes(keywordLower)
) {
foundInDiff = true;
break;
}
}
if (foundInDiff) {
this.searchResults.push({ rev, index: i, matchType: "Diff" });
this.updateSearchUI();
}
}
}
}
}
this.searchProgressIndicator.setText("Done");
this.updateSearchUI();
}
updateSearchUI() {
if (this.searchResults.length === 0) {
this.searchResultIndicator.setText(this.searchKeyword ? "No matches found" : "");
} else {
const current = this.currentSearchIndex >= 0 ? this.currentSearchIndex + 1 : 0;
this.searchResultIndicator.setText(`${current}/${this.searchResults.length} matches`);
}
}
navigateSearch(direction: "prev" | "next") {
if (this.searchResults.length === 0) return;
if (direction === "next") {
this.currentSearchIndex = (this.currentSearchIndex + 1) % this.searchResults.length;
} else {
this.currentSearchIndex =
this.currentSearchIndex <= 0 ? this.searchResults.length - 1 : this.currentSearchIndex - 1;
}
const match = this.searchResults[this.currentSearchIndex];
this.range.value = `${this.revs_info.length - 1 - match.index}`;
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
this.updateSearchUI();
// If it's a diff match, make sure Highlight diff is on
if (match.matchType === "Diff" && !this.showDiff) {
// We could auto-enable it, but maybe just notify the user?
// For now, let's just let the user toggle it if they want to see the diff.
}
}
override onOpen() {
@@ -327,6 +488,42 @@ export class DocumentHistoryModal extends Modal {
contentEl.empty();
this.fileInfo = contentEl.createDiv("");
this.fileInfo.addClass("op-info");
// Search Row
const searchRow = contentEl.createDiv("");
searchRow.addClass("op-info");
searchRow.addClass("search-row");
searchRow.addClass("history-search-row");
const searchInput = searchRow.createEl("input", {
type: "text",
placeholder: "Search in history (last 100)...",
});
searchInput.addClass("history-search-input");
searchInput.addEventListener("input", () => {
if (this.searchTimeout) {
compatGlobal.clearTimeout(this.searchTimeout);
}
this.searchTimeout = compatGlobal.setTimeout(() => {
void this.performSearch(searchInput.value);
}, 500);
});
searchRow.createEl("button", { text: "\u25B2" }, (e) => {
e.title = "Previous match";
e.addEventListener("click", () => this.navigateSearch("prev"));
});
searchRow.createEl("button", { text: "\u25BC" }, (e) => {
e.title = "Next match";
e.addEventListener("click", () => this.navigateSearch("next"));
});
this.searchResultIndicator = searchRow.createEl("span", { text: "" });
this.searchResultIndicator.addClass("history-search-result-indicator");
this.searchProgressIndicator = searchRow.createEl("span", { text: "" });
this.searchProgressIndicator.addClass("history-search-progress-indicator");
const divView = contentEl.createDiv("");
divView.addClass("op-flex");
@@ -342,24 +539,43 @@ export class DocumentHistoryModal extends Modal {
const diffOptionsRow = contentEl.createDiv("");
diffOptionsRow.addClass("op-info");
diffOptionsRow.addClass("diff-options-row");
diffOptionsRow.addClass("history-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());
});
})
);
const highlightDiffContainer = diffOptionsRow.createDiv("");
highlightDiffContainer.addClass("history-highlight-diff-container");
highlightDiffContainer.createEl("label", {}, (label) => {
label.addClass("history-highlight-diff-label");
label.createEl("input", { type: "checkbox" }, (checkbox) => {
if (this.showDiff) {
checkbox.checked = true;
}
checkbox.addEventListener("input", (evt: any) => {
this.showDiff = checkbox.checked;
this.app.saveLocalStorage("ols-history-highlightdiff", this.showDiff == true ? "1" : null);
this.updateDiffNavVisibility();
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
});
});
label.appendText("Highlight diff");
});
const diffOnlyLabel = diffOptionsRow.createEl("label", {});
diffOnlyLabel.createEl("input", { type: "checkbox" }, (checkbox) => {
if (this.diffOnly) {
checkbox.checked = true;
}
checkbox.addEventListener("input", (evt: any) => {
this.diffOnly = checkbox.checked;
this.app.saveLocalStorage("ols-history-diffonly", this.diffOnly == true ? "1" : null);
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
});
});
diffOnlyLabel.appendText("Diff only");
diffOnlyLabel.addClass("diff-only-label");
diffOnlyLabel.style.display = this.showDiff ? "inline-block" : "none";
this.diffOnlyLabel = diffOnlyLabel;
// Diff navigation buttons
this.diffNavContainer = diffOptionsRow.createDiv("");
this.diffNavContainer.addClass("diff-nav");

View File

@@ -27,6 +27,9 @@ export class ConflictResolveModal extends Modal {
localName: string = "Base";
remoteName: string = "Conflicted";
offEvent?: ReturnType<typeof eventHub.onEvent>;
currentDiffIndex = -1;
diffView!: HTMLDivElement;
diffNavIndicator!: HTMLSpanElement;
constructor(app: App, filename: string, diff: diff_result, pluginPickMode?: boolean, remoteName?: string) {
super(app);
@@ -47,7 +50,7 @@ export class ConflictResolveModal extends Modal {
const lines = text.split("\n");
lines.forEach((line, index) => {
const span = container.createSpan({ cls });
span.textContent = line;
span.setText(line);
if (index < lines.length - 1) {
container.createSpan({ cls: "ls-mark-cr" });
container.createEl("br");
@@ -62,6 +65,33 @@ export class ConflictResolveModal extends Modal {
container.createEl("br");
}
navigateDiff(direction: "prev" | "next") {
const diffElements = this.diffView.querySelectorAll(".added, .deleted");
if (diffElements.length === 0) return;
const prevFocused = this.diffView.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.setText(`${this.currentDiffIndex + 1}/${diffElements.length}`);
}
resetDiffNavigation() {
this.currentDiffIndex = -1;
const diffElements = this.diffView.querySelectorAll(".added, .deleted");
this.diffNavIndicator.setText(diffElements.length > 0 ? `0/${diffElements.length}` : "\u2014");
}
override onOpen() {
const { contentEl } = this;
// Send cancel signal for the previous merge dialogue
@@ -78,10 +108,26 @@ export class ConflictResolveModal extends Modal {
// sendValue("close-resolve-conflict:" + this.filename, false);
this.titleEl.setText(this.title);
contentEl.empty();
contentEl.createEl("span", { text: this.filename });
const div = contentEl.createDiv("");
div.addClass("op-scrollable");
div.addClass("ls-dialog");
const diffOptionsRow = contentEl.createDiv("");
diffOptionsRow.addClass("diff-options-row");
diffOptionsRow.createEl("span", { text: this.filename });
const diffNavContainer = diffOptionsRow.createDiv("");
diffNavContainer.addClass("diff-nav");
diffNavContainer.createEl("button", { text: "\u25B2 Prev" }, (e) => {
e.addClass("diff-nav-btn");
e.addEventListener("click", () => this.navigateDiff("prev"));
});
diffNavContainer.createEl("button", { text: "\u25BC Next" }, (e) => {
e.addClass("diff-nav-btn");
e.addEventListener("click", () => this.navigateDiff("next"));
});
this.diffNavIndicator = diffNavContainer.createEl("span", { text: "\u2014" });
this.diffNavIndicator.addClass("diff-nav-indicator");
this.diffView = contentEl.createDiv("");
this.diffView.addClass("op-scrollable");
this.diffView.addClass("ls-dialog");
let diffLength = 0;
for (const v of this.result.diff) {
const x1 = v[0];
@@ -91,12 +137,11 @@ export class ConflictResolveModal extends Modal {
continue;
}
if (x1 == DIFF_DELETE) {
this.appendDiffFragment(div, x2, "deleted");
div.createEl("span", { text: x2, cls: "deleted normal conflict-dev-name" });
this.appendDiffFragment(this.diffView, x2, "deleted");
} else if (x1 == DIFF_EQUAL) {
this.appendDiffFragment(div, x2, "normal");
this.appendDiffFragment(this.diffView, x2, "normal");
} else if (x1 == DIFF_INSERT) {
this.appendDiffFragment(div, x2, "added");
this.appendDiffFragment(this.diffView, x2, "added");
}
}
@@ -108,24 +153,30 @@ export class ConflictResolveModal extends Modal {
new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : "");
this.appendVersionInfo(div2, "deleted", this.localName, date1);
this.appendVersionInfo(div2, "added", this.remoteName, date2);
contentEl.createEl("button", { text: `Use ${this.localName}` }, (e) =>
e.addEventListener("click", () => this.sendResponse(this.result.right.rev))
).style.marginRight = "4px";
contentEl.createEl("button", { text: `Use ${this.remoteName}` }, (e) =>
e.addEventListener("click", () => this.sendResponse(this.result.left.rev))
).style.marginRight = "4px";
contentEl.createEl("button", { text: `Use ${this.localName}` }, (e) => {
e.addClass("conflict-action-button");
e.addEventListener("click", () => this.sendResponse(this.result.right.rev));
});
contentEl.createEl("button", { text: `Use ${this.remoteName}` }, (e) => {
e.addClass("conflict-action-button");
e.addEventListener("click", () => this.sendResponse(this.result.left.rev));
});
if (!this.pluginPickMode) {
contentEl.createEl("button", { text: "Concat both" }, (e) =>
e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT))
).style.marginRight = "4px";
contentEl.createEl("button", { text: "Concat both" }, (e) => {
e.addClass("conflict-action-button");
e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT));
});
}
contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) =>
e.addEventListener("click", () => this.sendResponse(CANCELLED))
).style.marginRight = "4px";
contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) => {
e.addClass("conflict-action-button");
e.addEventListener("click", () => this.sendResponse(CANCELLED));
});
if (diffLength > 100 * 1024) {
div.empty();
div.innerText = "(Too large diff to display)";
this.diffView.empty();
this.diffView.setText("(Too large diff to display)");
}
this.resetDiffNavigation();
this.navigateDiff("next");
}
sendResponse(result: MergeDialogResult) {

View File

@@ -2,7 +2,7 @@ import { WorkspaceLeaf } from "@/deps.ts";
import LogPaneComponent from "./LogPane.svelte";
import type ObsidianLiveSyncPlugin from "../../../main.ts";
import { SvelteItemView } from "../../../common/SvelteItemView.ts";
import { $msg } from "src/lib/src/common/i18n.ts";
import { $msg } from "@lib/common/i18n.ts";
import { mount } from "svelte";
export const VIEW_TYPE_LOG = "log-log";
//Log view

View File

@@ -29,7 +29,7 @@ import { addIcon, debounce, normalizePath, Notice, stringifyYaml, type Workspace
import { LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/logger";
import { LogPaneView, VIEW_TYPE_LOG } from "./Log/LogPaneView.ts";
import { serialized } from "octagonal-wheels/concurrency/lock";
import { $msg } from "src/lib/src/common/i18n.ts";
import { $msg } from "@lib/common/i18n.ts";
import { P2PLogCollector } from "@/lib/src/replication/trystero/P2PLogCollector.ts";
import type { LiveSyncCore } from "../../main.ts";
import { LiveSyncError } from "@lib/common/LSError.ts";

View File

@@ -8,12 +8,7 @@ import {
type ValueComponent,
} from "@/deps.ts";
import { unique } from "octagonal-wheels/collection";
import {
LEVEL_ADVANCED,
LEVEL_POWER_USER,
statusDisplay,
type ConfigurationItem,
} from "../../../lib/src/common/types.ts";
import { LEVEL_ADVANCED, LEVEL_POWER_USER, statusDisplay, type ConfigurationItem } from "@lib/common/types.ts";
import { createStub, type ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
import {
type AllSettingItemKey,
@@ -23,7 +18,7 @@ import {
type AllNumericItemKey,
type AllBooleanItemKey,
} from "./settingConstants.ts";
import { $msg } from "src/lib/src/common/i18n.ts";
import { $msg } from "@lib/common/i18n.ts";
import { findAttrFromParent, wrapMemo, type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts";
export class LiveSyncSetting extends Setting {

View File

@@ -1,29 +1,16 @@
import { stringifyYaml } from "../../../deps.ts";
import {
type ObsidianLiveSyncSettings,
type FilePathWithPrefix,
type DocumentID,
LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE,
type LoadedEntry,
REMOTE_COUCHDB,
REMOTE_MINIO,
type MetaEntry,
type FilePath,
DEFAULT_SETTINGS,
} from "../../../lib/src/common/types.ts";
import {
createBlob,
getFileRegExp,
isDocContentSame,
parseHeaderValues,
readAsBlob,
} from "../../../lib/src/common/utils.ts";
import { Logger } from "../../../lib/src/common/logger.ts";
import { isCloudantURI } from "../../../lib/src/pouchdb/utils_couchdb.ts";
import { requestToCouchDBWithCredentials } from "../../../common/utils.ts";
import { addPrefix, shouldBeIgnored, stripAllPrefixes } from "../../../lib/src/string_and_binary/path.ts";
import { $msg } from "../../../lib/src/common/i18n.ts";
} from "@lib/common/types.ts";
import { createBlob, getFileRegExp, isDocContentSame, readAsBlob } from "@lib/common/utils.ts";
import { Logger } from "@lib/common/logger.ts";
import { addPrefix, shouldBeIgnored, stripAllPrefixes } from "@lib/string_and_binary/path.ts";
import { $msg } from "@lib/common/i18n.ts";
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
import {
@@ -32,14 +19,13 @@ import {
EVENT_REQUEST_RUN_DOCTOR,
EVENT_REQUEST_RUN_FIX_INCOMPLETE,
eventHub,
} from "../../../common/events.ts";
import { ICHeader, ICXHeader, PSCHeader } from "../../../common/types.ts";
import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSync.ts";
import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts";
import { generateCredentialObject } from "../../../lib/src/replication/httplib.ts";
} from "@/common/events.ts";
import { ICHeader, ICXHeader, PSCHeader } from "@/common/types.ts";
import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync.ts";
import { EVENT_REQUEST_SHOW_HISTORY } from "@/common/obsidianEvents.ts";
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
import type { PageFunctions } from "./SettingPane.ts";
import { generateReport } from "@/common/reportTool.ts";
import { isNotFoundError } from "@lib/common/utils.doc.ts";
export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void {
// const hatchWarn = this.createEl(paneEl, "div", { text: `To stop the boot up sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` });
// hatchWarn.addClass("op-warn-info");
@@ -160,14 +146,14 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
}
if (!(await addOn.storeInternalFileToDatabase(file, true))) {
Logger(
`Failed to store the file to the database (Hidden file): ${file}`,
`Failed to store the file to the database (Hidden file): ${file.path}`,
LOG_LEVEL_NOTICE
);
return;
}
}
} else {
if (!(await this.core.fileHandler.storeFileToDB(file as FilePath, true))) {
if (!(await this.core.fileHandler.storeFileToDB(file, true))) {
Logger(
`Failed to store the file to the database: ${file}`,
LOG_LEVEL_NOTICE
@@ -406,8 +392,8 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
Logger(`Converting ${docName} Failed!`, LOG_LEVEL_NOTICE);
Logger(ret, LOG_LEVEL_VERBOSE);
}
} catch (ex: any) {
if (ex?.status == 404) {
} catch (ex: unknown) {
if (isNotFoundError(ex)) {
// We can perform this safely
if ((await this.core.localDatabase.putRaw(newDoc)).ok) {
Logger(`${docName} has been converted`, LOG_LEVEL_NOTICE);

View File

@@ -150,7 +150,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
xxhash64: "xxhash64 (Fastest)",
"mixed-purejs": "PureJS fallback (Fast, W/O WebAssembly)",
sha1: "Older fallback (Slow, W/O WebAssembly)",
} as Record<HashAlgorithm, string>,
} satisfies Record<HashAlgorithm, string>,
});
this.addOnSaved("hashAlg", async () => {
await this.core.localDatabase._prepareHashFunctions();

View File

@@ -7,7 +7,7 @@ import {
REMOTE_MINIO,
REMOTE_P2P,
} from "../../lib/src/common/types.ts";
import { generatePatchObj, isObjectDifferent } from "../../lib/src/common/utils.ts";
import { isObjectDifferent } from "@lib/common/utils.ts";
import Intro from "./SetupWizard/dialogs/Intro.svelte";
import SelectMethodNewUser from "./SetupWizard/dialogs/SelectMethodNewUser.svelte";
import SelectMethodExisting from "./SetupWizard/dialogs/SelectMethodExisting.svelte";
@@ -23,6 +23,7 @@ import SetupRemoteP2P from "./SetupWizard/dialogs/SetupRemoteP2P.svelte";
import SetupRemoteE2EE from "./SetupWizard/dialogs/SetupRemoteE2EE.svelte";
import { decodeSettingsFromQRCodeData } from "../../lib/src/API/processSetting.ts";
import { AbstractModule } from "../AbstractModule.ts";
import { ConnectionStringParser } from "@lib/common/ConnectionString.ts";
/**
* User modes for onboarding and setup
@@ -194,8 +195,24 @@ export class SetupManager extends AbstractModule {
return await this.onOnboard(userMode);
}
const newSetting = { ...currentSetting, ...p2pConf } as ObsidianLiveSyncSettings;
// Apply remoteConfigurations
if (newSetting.P2P_ActiveRemoteConfigurationId) {
const id = newSetting.P2P_ActiveRemoteConfigurationId;
const merged = {
...newSetting,
...p2pConf,
} as ObsidianLiveSyncSettings;
const uri = ConnectionStringParser.serialize({ type: "p2p", settings: merged });
newSetting.remoteConfigurations[id] = {
...newSetting.remoteConfigurations[id],
uri,
isEncrypted: false,
};
newSetting.P2P_ActiveRemoteConfigurationId = id;
}
if (activate) {
newSetting.remoteType = REMOTE_P2P;
newSetting.activeConfigurationId = newSetting.P2P_ActiveRemoteConfigurationId;
}
return await this.onConfirmApplySettingsFromWizard(newSetting, userMode, activate);
}
@@ -285,9 +302,9 @@ export class SetupManager extends AbstractModule {
this._log("No changes in settings detected. Skipping applying settings from wizard.", LOG_LEVEL_NOTICE);
return true;
}
const patch = generatePatchObj(this.settings, newConf);
console.log(`Changes:`);
console.dir(patch);
// const patch = generatePatchObj(this.settings, newConf);
// console.log(`Changes:`);
// console.dir(patch);
if (!activate) {
extra();
await this.applySetting(newConf, UserMode.ExistingUser);

View File

@@ -7,9 +7,6 @@
import Options from "@/lib/src/UI/components/Options.svelte";
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
import Check from "@/lib/src/UI/components/Check.svelte";
const TYPE_USE_SETUP_URI = "use-setup-uri";
const TYPE_SCAN_QR_CODE = "scan-qr-code";
const TYPE_CONFIGURE_MANUALLY = "configure-manually";

View File

@@ -7,9 +7,6 @@
import Options from "@/lib/src/UI/components/Options.svelte";
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
import Check from "@/lib/src/UI/components/Check.svelte";
const TYPE_USE_SETUP_URI = "use-setup-uri";
const TYPE_CONFIGURE_MANUALLY = "configure-manually";
const TYPE_CANCELLED = "cancelled";

View File

@@ -13,6 +13,7 @@ import { AbstractModule } from "../AbstractModule.ts";
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub.ts";
import type { LiveSyncCore } from "../../main.ts";
import { initialiseWorkerModule } from "@lib/worker/bgWorker.ts";
import { manifestVersion, packageVersion } from "@lib/common/coreEnvVars.ts";
export class ModuleLiveSyncMain extends AbstractModule {
async _onLiveSyncReady() {
@@ -89,11 +90,6 @@ export class ModuleLiveSyncMain extends AbstractModule {
return false;
}
// this.addUIs();
//@ts-ignore
const manifestVersion: string = MANIFEST_VERSION || "0.0.0";
//@ts-ignore
const packageVersion: string = PACKAGE_VERSION || "0.0.0";
this._log($msg("moduleLiveSyncMain.logPluginVersion", { manifestVersion, packageVersion }));
await this.services.setting.loadSettings();
if (!(await this.services.appLifecycle.onSettingLoaded())) {

View File

@@ -5,9 +5,17 @@ import { ObsHttpHandler } from "../essentialObsidian/APILib/ObsHttpHandler";
import { ObsidianConfirm } from "./ObsidianConfirm";
import type { Confirm } from "@lib/interfaces/Confirm";
import { requestUrl, type RequestUrlParam } from "@/deps";
import { compatGlobal } from "@/lib/src/common/coreEnvFunctions";
// All Services will be migrated to be based on Plain Services, not Injectable Services.
// This is a migration step.
declare module "obsidian" {
interface App {
appId?: string;
isMobile?: boolean;
}
}
export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceContext> {
_customHandler: ObsHttpHandler | undefined;
_confirmInstance: Confirm;
@@ -83,8 +91,7 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
}
}
override isMobile(): boolean {
//@ts-ignore : internal API
return this.app.isMobile;
return "isMobile" in this.app ? !!this.app.isMobile : false;
}
override getAppID(): string {
return `${"appId" in this.app ? this.app.appId : ""}`;
@@ -95,7 +102,7 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
}
override getAppVersion(): string {
const navigatorString = globalThis.navigator?.userAgent ?? "";
const navigatorString = compatGlobal.navigator?.userAgent ?? "";
const match = navigatorString.match(/obsidian\/([0-9]+\.[0-9]+\.[0-9]+)/);
if (match && match.length >= 2) {
return match[1];
@@ -196,7 +203,7 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
}
override setInterval(handler: () => void, timeout: number): number {
const timerId = globalThis.setInterval(handler, timeout) as unknown as number;
const timerId = compatGlobal.setInterval(handler, timeout);
this.context.plugin.registerInterval(timerId);
return timerId;
}

View File

@@ -17,6 +17,10 @@
min-width: 5em;
}
.conflict-action-button {
margin-right: 4px;
}
.op-scrollable {
overflow-y: scroll;
/* min-height: 280px; */
@@ -167,7 +171,7 @@ body {
.sls-setting-menu-buttons {
border: 1px solid var(--sls-col-warn);
padding: 2px;
/* padding: 2px; */
margin: 1px;
border-radius: 4px;
background-image: linear-gradient(-45deg,
@@ -176,7 +180,7 @@ body {
background-size: 30px 30px;
display: flex;
flex-direction: row;
justify-content: flex-end;
/* justify-content: flex-end; */
padding: 0.5em 0.25em;
justify-content: center;
align-items: center;
@@ -311,9 +315,9 @@ body {
.sls-setting-obsolete {
background-image: linear-gradient(-45deg,
/* background-image: linear-gradient(-45deg,
var(--sls-col-warn-stripe1) 25%, var(--sls-col-warn-stripe2) 25%, var(--sls-col-warn-stripe2) 50%,
var(--sls-col-warn-stripe1) 50%, var(--sls-col-warn-stripe1) 75%, var(--sls-col-warn-stripe2) 75%, var(--sls-col-warn-stripe2));
var(--sls-col-warn-stripe1) 50%, var(--sls-col-warn-stripe1) 75%, var(--sls-col-warn-stripe2) 75%, var(--sls-col-warn-stripe2)); */
background-image: linear-gradient(-45deg,
transparent 25%, rgba(var(--background-secondary), 0.1) 25%, rgba(var(--background-secondary), 0.1) 50%, transparent 50%, transparent 75%, rgba(var(--background-secondary), 0.1) 75%, rgba(var(--background-secondary), 0.1));
background-size: 60px 60px;
@@ -521,8 +525,48 @@ div.workspace-leaf-content[data-type=bases] .livesync-status {
text-align: center;
}
.diff-only-label {
margin-left: 10px;
}
.history-search-row {
display: flex;
gap: 5px;
align-items: center;
margin-bottom: 10px;
}
.history-search-input {
flex-grow: 1;
}
.history-search-result-indicator {
font-size: 0.8em;
min-width: 80px;
}
.history-search-progress-indicator {
font-size: 0.8em;
color: var(--text-muted);
}
.history-diff-options-row {
justify-content: space-between;
}
.history-highlight-diff-container,
.history-highlight-diff-label {
display: flex;
align-items: center;
}
.history-highlight-diff-label {
gap: 4px;
}
.diff-focused {
outline: 2px solid var(--interactive-accent);
outline-offset: 1px;
border-radius: 2px;
}
}

View File

@@ -34,11 +34,11 @@ const terserOption = {
inline: false,
join_vars: true,
loops: true,
passes: 4,
passes: 1,
reduce_vars: true,
reduce_funcs: false,
arrows: true,
collapse_vars: true,
collapse_vars: false,
comparisons: true,
//@ts-ignore
lhs_constants: true,

View File

@@ -2,7 +2,6 @@
"extends": "@tsconfig/svelte/tsconfig.json",
"inlineSourceMap": true,
"compilerOptions": {
"baseUrl": ".",
"module": "ESNext",
"target": "ES2018",
"allowJs": true,
@@ -20,8 +19,8 @@
"strictBindCallApply": true,
"strictFunctionTypes": true,
"paths": {
"@/*": ["src/*"],
"@lib/*": ["src/lib/src/*"]
"@/*": ["./src/*"],
"@lib/*": ["./src/lib/src/*"]
}
},
"include": ["**/*.ts", "test/**/*.test.ts", "**/*.unit.spec.ts"],

View File

@@ -3,6 +3,45 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
## 0.25.70
25th May, 2026
### New features
- Diff dialogue now has great tools to navigate and understand the differences, including:
- A checkbox to toggle the visibility of collapsed identical sections, making it easier to focus on the actual differences (PR #889).
- A search feature to find specific text in past revisions, and navigate revisions with search results highlighted in the dialogue (PR #890).
- Conflict resolution dialogue now has a navigation feature to jump between conflicts (PR #891).
Thank you so much to @SeleiXi for implementing these features!
### Improved
- More diagnostic information for P2P connections is now shown, including why a connection failure occurred and the current connection status.
## 0.25.69
22nd May, 2026
### Fixed
- No longer does the P2P passphrase mismatch cause a server shutdown.
- Settings related to P2P synchronisation are now correctly applied on start-up and no longer reverted.
### New features
- Diagnostic P2P connection stats are now available.
- These stats indicate the number of connection trials, successes, and failures.
## 0.25.68
22nd May, 2026
### Improved
- P2P connections have improved slightly
- Upgrade to `trystero` v0.24.0, and fixes event handler assignment. This should fix some edge cases where P2P connections fail to establish or messages are not properly handled.
- Weaken terser options to avoid potential issues with minification that could cause runtime errors in some environments.
## ~~0.25.66~~ 0.25.67
20th May, 2026