From 6475d32769fe92f49efe0eb9b3f0d7df41227b94 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 17 Jul 2026 02:36:21 +0000 Subject: [PATCH 001/117] docs: define common library package boundary --- ...2026_07_common_library_package_boundary.md | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 docs/adr/2026_07_common_library_package_boundary.md diff --git a/docs/adr/2026_07_common_library_package_boundary.md b/docs/adr/2026_07_common_library_package_boundary.md new file mode 100644 index 00000000..26bc0f80 --- /dev/null +++ b/docs/adr/2026_07_common_library_package_boundary.md @@ -0,0 +1,275 @@ +# Architectural Decision Record: Package the Common Library Behind Explicit Host Boundaries + +## Status + +Proposed + +## Context + +Self-hosted LiveSync currently consumes `livesync-commonlib` as the `src/lib` Git submodule. TypeScript, Vite, Vitest, the CLI, Webapp, and WebPeer resolve `@lib/*` directly to the submodule's TypeScript source. The common-library repository has no package manifest, standalone build, export map, or self-contained test command. Its tests and dependency versions are consequently supplied by the Self-hosted LiveSync repository. + +Source archives do not populate a Git submodule. Self-hosted LiveSync therefore also commits generated declarations under `_types` and resolves `@lib/*` to those declarations as a fallback. This makes one logical dependency appear in the plug-in repository twice: once as a submodule checkout and once as generated declarations. Release preparation must regenerate and commit the fallback, and repository scanners can report generated lint directives as if they were maintained plug-in source. + +The Obsidian community scanner currently inspects repository source beyond the plug-in entry point. Reports include `src/apps/cli`, `src/apps/webapp`, `src/apps/webpeer`, and generated `_types`. Moving the common library from `src/lib` to another source directory or a workspace package inside the Self-hosted LiveSync repository would therefore preserve the scan surface. It could remove the submodule fallback, but it would not create the same dependency boundary as consuming a published npm package. + +The common library is already a substantial shared domain layer. At the time of this decision it contains 270 non-test TypeScript or Svelte source files. At Self-hosted LiveSync revision `e114f66fb2b7c6f3fec2d53701f6638d2557e606`, source outside `src/lib` uses 180 distinct raw `@lib/*` specifiers across plug-in, application, and test source. Normalising optional TypeScript source suffixes leaves 140 migration paths. The plug-in contributes most of those imports, but the CLI, Webapp, and WebPeer also depend on the library. Treating every existing deep path as a permanent public API would make future refactoring impractical. + +The intended external use is not new: + +- [commonlib issue #1](https://github.com/vrtmrz/livesync-commonlib/issues/1) has requested an npm package and an established API since 2022; +- [Self-hosted LiveSync issue #87](https://github.com/vrtmrz/obsidian-livesync/issues/87) asks for a client with list, get, submit, and delete operations; +- [commonlib issue #10](https://github.com/vrtmrz/livesync-commonlib/issues/10) asks for an API which lets another application add Markdown files to the CouchDB data model; and +- [commonlib issue #13](https://github.com/vrtmrz/livesync-commonlib/issues/13) records the integration cost of consuming a Git submodule and compiling its source. + +Existing external consumers use custom TypeScript aliases, loaders, and host stubs to reach `DirectFileManipulator` and other deep modules. The present source shape therefore imposes real integration work without supplying a stable contract. + +The library is close to being self-contained, but it crosses its boundary in a small number of important places: + +- four files depend on Self-hosted LiveSync's event hub or event identifiers; +- `KeyValueDBService` imports the host's concrete database-opening function; +- `ObsidianServiceContext` imports the plug-in class and Obsidian types from the parent repository; and +- `coreEnvFunctions` imports an Obsidian function type even though the module is intended to be host-neutral. + +The library also mixes domain logic with host presentation: + +- Svelte dialogue mounting depends on Svelte, LiveSync service context, application-lifecycle events, translation, and cancellation policy in one implementation; +- Obsidian context and setup helpers remain below the common-library directory; +- browser dialogue shims and LiveSync Svelte components are shipped alongside headless replication code; and +- the translation implementation statically imports the complete generated catalogue. The generated message modules and JSON catalogues account for approximately 1.5 MB of source and generated data, even when a headless consumer does not require them. + +The root `src/index.ts` currently exports only `DirectFileManipulator` and its options. `DirectFileManipulator` is useful evidence for a future SDK, but it is not yet a sufficient stable façade: initialisation starts from its constructor, enumeration remains unfinished, watch ownership and failure semantics are not documented, and conflict and concurrency semantics are not defined as a public guarantee. + +The present process model also assumes one main LiveSync instance. Event dispatch, translation state, environment configuration, offline-scan state, worker pools, synchronisation-parameter handlers, diagnostic counters, and some compatibility caches are module-scoped. Some of these are safe process-wide facilities, while others can allow two client instances to influence each other. Publishing a package makes concurrent clients in one process a supported possibility, so this state must be classified rather than carried across accidentally. + +## Decision + +Maintain `livesync-commonlib` as the authoritative independent repository and publish its compiled output as a scoped, pre-1.0 npm package. `@vrtmrz/livesync-commonlib` is the working package name; the final name must be checked before the first publication. + +Self-hosted LiveSync, its CLI, Webapp, WebPeer, and external tools will consume the compiled package and declarations through package export maps. They must not compile the common library's source through a path alias. The package lock records the exact resolved artefact used to build a plug-in release. + +The common-library repository may become a small workspace if independently useful artefacts emerge, but it does not move into the Fancy Kit repository or the Self-hosted LiveSync plug-in repository. This preserves its domain ownership, existing history, issues, and independent release cadence while making the plug-in repository a package consumer. + +The first package release is an infrastructure and compatibility release, not a declaration that every internal service is stable. It exposes: + +- a small documented root API; +- named, task-oriented public subpaths; +- explicitly marked compatibility subpaths required to migrate current Self-hosted LiveSync imports; and +- no unrestricted source-directory wildcard as a permanent contract. + +Compatibility subpaths may be exported during the migration, but they remain pre-1.0 and are documented as internal. New external integrations should use the high-level client façade rather than reproduce Self-hosted LiveSync's internal Service Hub composition. + +### Domain ownership + +The common library continues to own behaviour which defines the Self-hosted LiveSync data and synchronisation model: + +- document, metadata, chunk, setting, and protocol types; +- path and identifier encoding; +- chunk splitting, hashing, compression, encryption, and content reconstruction; +- PouchDB-facing data access and replication primitives; +- CouchDB, Object Storage, and P2P replication domain logic; +- conflict, chunk-delivery, storage-event, and replication managers; +- platform-neutral storage and service contracts; +- headless composition; and +- the future high-level client façade. + +The Self-hosted LiveSync repository owns plug-in and product integration: + +- `ObsidianServiceContext` and every reference to the plug-in class, `App`, `Plugin`, or Obsidian lifecycle; +- Setup Wizard components and LiveSync-specific presentation policy; +- Obsidian menus, notices, settings panes, and dialogue composition; +- plug-in event wiring which is not part of the replication protocol; and +- the concrete initialisation of injected environment, translation, storage, key-value database, and UI capabilities. + +Browser-only implementations may remain in the common-library repository only when they implement a documented host-neutral contract used by more than one browser consumer. LiveSync-specific Webapp and WebPeer composition remains application code even if it later moves out of the plug-in repository. + +Neutral utilities which do not express LiveSync data, storage, replication, or UI policy may move to `octagonal-wheels` after more than one consumer proves the abstraction. Reusable Obsidian adapters may move to `@vrtmrz/obsidian-plugin-kit`. Neither package becomes an owner of LiveSync domain behaviour. + +### Dependency inversion + +The package must contain no `@/` import and no direct Obsidian import. The current reverse dependencies are removed as follows: + +- common protocol and service events live with their common-library contracts; +- host-only event reactions are registered by Self-hosted LiveSync at its composition root; +- `KeyValueDBService` receives an `openKeyValueDatabase` factory through constructor or service dependencies; +- `ObsidianServiceContext` moves to Self-hosted LiveSync; +- the language getter uses a local function type rather than importing the Obsidian declaration; and +- browser globals, fetch, crypto, timers, storage, and document access are obtained through explicit host capabilities where behaviour varies by runtime. + +Temporary package-level configuration is acceptable where converting every call site in one change would be unsafe, but configuration must be instance-scoped wherever multiple clients can coexist. Importing the package must not patch `HTMLElement`, `SVGElement`, or another global prototype. Webapp compatibility patches belong in Webapp bootstrap code. + +Every mutable module-level value is classified as one of: + +- immutable or safely shared process infrastructure; +- an explicitly keyed cache with bounded ownership and disposal; or +- client state which moves behind an instance-owned service or dependency. + +In particular, event subscriptions, translation selection, offline-scan maps and timers, synchronisation-parameter handlers, and database transformation policy must not leak between independent client instances. A high-level client owns an explicit asynchronous disposal path which removes subscriptions, cancels work it owns, and releases its instance state. + +### Translation resources + +Translation lookup is separated from the generated catalogue. Core services receive a narrow translator capability, or a service-context equivalent, instead of importing a process-global `$msg` implementation which statically owns every language. + +The first migration keeps language resources in the common-library repository but exposes them through an optional package subpath such as `@vrtmrz/livesync-commonlib/language`. Importing core replication or the headless client does not load the catalogue. Self-hosted LiveSync imports the catalogue and installs the translator at its composition root. + +This is initially one versioned npm package rather than two independently versioned packages. Message identifiers and their generated catalogue change together, so an immediate package split would add version coordination before the dependency direction is proven. A later `@vrtmrz/livesync-language` package is permitted when independent use or release cadence justifies it. Its dependency must point towards message contracts or core, never from core towards the catalogue. + +### Svelte dialogue hosting + +The present Svelte dialogue implementation is split into three responsibilities: + +1. an Obsidian `Modal` host which owns content, title, close, one-shot settlement, and disposal; +2. a Svelte adapter which mounts and unmounts a component in that host; and +3. Self-hosted LiveSync policy which supplies service context, reacts to plug-in unload, requires explicit cancellation in selected workflows, translates messages, and styles Setup Wizard content. + +Fancy Kit is an appropriate owner for the first responsibility when the contract is useful beyond this migration. The Kit API should be framework-neutral: it accepts a mount callback, exposes typed `resolve`, `cancel`, `close`, and `setTitle` controls, and accepts a disposer returned by the callback. It returns a `Promise` which settles once, and it owns safe-area, viewport, focus, and Obsidian Modal lifecycle guarantees. + +Fancy Kit does not add Svelte to its core dependencies for this migration. Self-hosted LiveSync initially owns the small Svelte adapter which calls Svelte's `mount` and `unmount` through the framework-neutral host. The browser dialogue host also remains outside the Obsidian plug-in kit. If a second Obsidian plug-in needs the same Svelte adapter, it can be promoted to an optional Kit subpath with an optional Svelte peer dependency, or to a separate `@vrtmrz/obsidian-svelte-kit` package. That promotion must preserve the existing `sideEffects: false` and focused-import guarantees for consumers which do not use Svelte. + +Self-hosted LiveSync's `openWithExplicitCancel` retry policy, application service context, and Setup Wizard components do not move to Fancy Kit. + +Arbitrary component mounting is not added to the neutral `UiInteractions` contract. A component instance is a framework and host integration detail rather than a portable interaction request which a generic driver can serialise or answer. A LiveSync workflow which needs a hosted component defines and receives its own narrow, typed dialogue capability; Self-hosted LiveSync composes that capability from the Kit Modal host and its local Svelte adapter. + +### Package artefacts + +The common-library package publishes compiled ESM and generated declaration files. Consumers do not receive raw TypeScript as the runtime entry point. The package must provide: + +- an explicit `exports` map; +- declaration maps where they remain useful for debugging; +- browser and Node entry points where implementations differ; +- accurate `sideEffects` metadata; +- explicit runtime, peer, and optional dependencies; +- no unresolved `@lib/*`, `@/*`, Vite query, or source `.ts` import in published JavaScript or declarations; +- an `npm pack` contents check; and +- clean-install consumer fixtures for Node, a browser bundle, and Self-hosted LiveSync. + +Svelte source, worker query imports, AWS SDK adapters, PouchDB adapters, and Node-only crypto must not leak through the root entry point. Optional feature subpaths may carry their own heavier dependency surface. + +### Barrels and export surfaces + +The migration does not prohibit every barrel. A small root entry point or task-oriented subpath entry point is an intentional package contract when it has one documented responsibility, uses explicit named exports, and does not load unrelated implementations or optional dependencies. The root client entry point, a focused RPC entry point, and type-only storage-adapter entry points are examples which may remain after review. + +An existing barrel or forwarding façade is removed when the migration provides a clearer import and the barrel: + +- aggregates unrelated domains or exposes implementation layout as API; +- hides a platform, UI framework, worker, database adapter, or another optional dependency; +- makes side effects or tree-shaking behaviour difficult to determine; +- merely re-exports another package without adding a LiveSync-owned contract; or +- exists only to preserve the present `@lib/*` source alias or Service Hub composition. + +In particular, the broad `common/types` barrel is retained only as a temporary compatibility path while focused settings, model, protocol, and path contracts are extracted. The `InjectableServices` forwarding barrel is removed in favour of explicit compatibility imports. Forwarding exports for `octagonal-wheels` facilities are removed where consumers can import the owning package directly; mixed modules such as conversion and utility modules are split when this prevents neutral dependencies from being presented as LiveSync-owned API. + +Every retained barrel must correspond to an explicit `exports` entry, list named exports rather than use an unrestricted wildcard, and have a packed-consumer or bundle test proving that unrelated optional code is not loaded. Removing a barrel is not sufficient reason to expose every underlying file as a public subpath. + +## Migration Plan + +### Phase 0: Record and enforce the boundary + +- Add a package-boundary check which rejects `@/` and direct Obsidian imports in common-library production source. +- Record the current Self-hosted LiveSync `@lib/*` import inventory and classify each path as public, compatibility-only, host-owned, or obsolete. +- Inventory mutable module-level state and add a two-client isolation test before promising a public client API. +- Add standalone test and type-check configuration to the common-library repository while retaining downstream Self-hosted LiveSync CI. +- Add a packed-consumer fixture before changing Self-hosted LiveSync resolution. + +### Phase 1: Remove host leaks + +- Move `ObsidianServiceContext` and `setupObsidian` presentation code to Self-hosted LiveSync. +- Inject the key-value database factory and host event reactions. +- Replace the Obsidian language type import with a local contract. +- Move global DOM compatibility mutation to the browser application bootstrap. +- Introduce translator injection and detach core imports from the full catalogue. +- Split the generic dialogue host, Svelte adapter, and LiveSync policy without changing visible behaviour. + +### Phase 2: Build the package + +- Add the package manifest, compiled ESM build, declarations, export map, and package documentation. +- Keep the root export deliberately small. +- Remove accidental and forwarding barrels where focused imports are clearer; retain only reviewed package entry points with explicit named exports. +- Add temporary compatibility subpaths required by the classified 140-path migration inventory. +- Test Node, browser, worker, PouchDB, Object Storage, P2P, and headless entry points independently. +- Verify that installing a core entry does not pull Svelte UI or language catalogue code into a representative bundle. + +### Phase 3: Publish and validate a pre-release + +- Publish an immutable pre-1.0 version to a pre-release npm dist-tag. +- Verify its package name, provenance, checksum, export map, and packed files. +- Run common-library tests against the packed artefact rather than the source checkout. +- Run downstream Self-hosted LiveSync type checks, unit tests, integration tests, CLI tests, Webapp tests, WebPeer tests, production builds, and focused real-Obsidian E2E against the exact package version. +- Validate at least one external consumer which currently uses a submodule or custom loader. + +### Phase 4: Convert Self-hosted LiveSync + +- Replace `@lib/*` source aliases with package imports. +- Remove the `src/lib` submodule, `_types`, `tsconfig.types.json`, `generate-types.mjs`, and release-workflow steps which regenerate fallback declarations. +- Move common-library i18n tooling out of Self-hosted LiveSync release preparation. +- Keep application-owned source under `src/apps` until a separate decision moves an application. +- Run the community scanner's branch or commit preview before releasing the converted plug-in. + +### Phase 5: Stabilise the SDK + +- Design a high-level asynchronous client around explicit `create`, `list`, `get`, `put`, `delete`, `watch`, and `close` lifecycles. +- Specify path normalisation, encryption negotiation, conflict handling, conditional writes, deletion, history, and resource disposal before declaring the façade stable. +- Adapt `DirectFileManipulator` behind that façade or deprecate it; do not treat its current constructor and deep dependencies as the final API. +- Narrow or remove compatibility-only export paths as Self-hosted LiveSync imports migrate to documented package modules. + +## Scanner and Repository Consequences + +Consuming the npm package is expected to remove the common-library source and generated `_types` from the plug-in repository's community scan, because package dependencies are treated as dependencies rather than maintained plug-in source. This expectation must be verified with the scanner preview before removing the old integration. + +The change does not hide or resolve warnings in Self-hosted LiveSync-owned source. The scanner will continue to inspect the plug-in, CLI, Webapp, and WebPeer while those applications remain in the repository. Moving those applications to a LiveSync-family application repository may be considered after the package boundary is stable, but it is not part of this decision because the CLI and Webapp still import shared plug-in composition code. + +Package extraction also does not resolve release-asset attestation verification, unsupported release assets, declarative settings migration, or other scanner findings which are independent of source ownership. + +## Alternatives Rejected + +### Move the common library into the Self-hosted LiveSync monorepo + +This would permit atomic source changes and remove the Git submodule, but the community scanner already examines non-plug-in application source. A source workspace would remain in scope, and every library directive and platform dependency would be attributed to the plug-in repository. It would also make independent consumers depend on the plug-in repository's release cadence. + +### Move the common library into Fancy Kit + +Fancy Kit owns reusable framework-neutral interactions, Obsidian adapters, test infrastructure, and neutral utilities. Replication protocols, chunk and metadata formats, PouchDB composition, conflict rules, and LiveSync storage policy are a different domain. Moving them would obscure ownership and make general plug-in tooling carry Self-hosted LiveSync release concerns. + +### Publish the current source tree without changing boundaries + +This would expose accidental deep imports, global mutations, Svelte and browser code, Node-only modules, and parent-repository imports. Consumers would still need source aliases and bundler-specific behaviour, while maintainers would be unable to distinguish public API from implementation. + +### Split every concern into a separate npm package immediately + +Core, language, UI, browser, Node, P2P, Object Storage, and SDK packages could make dependency graphs precise, but the present source has not yet proven those release boundaries. Starting with one compiled package and explicit optional subpaths allows measurement without creating a coordinated release matrix. Additional packages remain possible after their contracts and consumers are demonstrated. + +## Verification + +The migration is complete only when: + +- common-library production source has no parent-repository or Obsidian dependency; +- the package builds and tests from a standalone clean checkout; +- `npm pack` contains only intended compiled artefacts and documentation; +- packed Node and browser consumers resolve only exported paths; +- a headless client does not bundle Svelte or the full language catalogue; +- retained entry-point barrels do not load unrelated optional implementations, and no removed barrel is replaced by unrestricted deep exports; +- two clients with different database, encryption, language, and lifecycle settings can coexist without sharing mutable client state; +- the Fancy Kit Modal host proves one-shot settlement, cancellation, disposer execution, focus, viewport containment, safe-area containment, and touch-target layout in its own unit and real-Obsidian Harness tests; +- Self-hosted LiveSync verifies its Svelte adapter and workflow policy through injected tests, with a focused composition smoke test when the adapter is first adopted rather than duplicating every Kit-owned device test; +- Self-hosted LiveSync no longer has `src/lib`, `_types`, or `@lib/*` source aliases; +- the CLI, Webapp, WebPeer, plug-in build, unit tests, integration tests, and focused real-Obsidian E2E pass against the reviewed package artefact; +- common-library downstream CI records and tests the exact Self-hosted LiveSync ref; +- a community scanner preview confirms the expected change in source findings; and +- an external consumer can replace its submodule or custom loader with the documented package API. + +## Consequences + +- The common library gains an independently consumable and testable artefact while retaining its domain ownership and history. +- Self-hosted LiveSync release archives no longer need generated fallback declarations for an absent submodule. +- Community scanning can distinguish plug-in source from a reviewed dependency, subject to preview verification. +- Changes which span the package and plug-in require coordinated package and downstream validation rather than one atomic source commit. +- Temporary compatibility exports increase the first package surface, but their pre-1.0 status and explicit classification prevent them from becoming silent permanent contracts. +- Translation, Svelte, and platform dependencies become optional composition concerns rather than root-package side effects. +- Fancy Kit may gain a generally useful typed Obsidian Modal host without acquiring LiveSync policy or a mandatory Svelte dependency. + +## Open Questions Before Acceptance + +- Confirm the published package name and whether the first version should be `0.1.0` or an earlier bootstrap version. +- Confirm whether the language catalogue begins as an optional subpath, as recommended here, or as a separately versioned package. +- Define the minimum conflict and conditional-write guarantees required by the first public high-level client. +- Decide which current deep imports can migrate directly to public subpaths and which require temporary compatibility exports. +- Confirm whether another Fancy Kit consumer needs the framework-neutral Modal host before it is added to the Kit, or whether LiveSync should pilot the contract first. From a721d3b602bd40e1fc481ef1ad37522ce55cfb22 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 17 Jul 2026 11:13:16 +0000 Subject: [PATCH 002/117] refactor: consume Commonlib as a package --- .eslintrc | 4 +- .github/workflows/cli-deno-tests.yml | 10 - .github/workflows/cli-docker.yml | 2 - .github/workflows/cli-e2e.yml | 4 +- .github/workflows/cli-p2p-compose-smoke.yml | 2 - .github/workflows/finalise-release.yml | 1 - .github/workflows/harness-ci.yml | 4 +- .github/workflows/prepare-release.yml | 11 +- .github/workflows/release.yml | 1 - .github/workflows/unit-ci.yml | 4 - .gitignore | 4 +- .gitmodules | 3 - AGENTS.md | 2 +- CONTRIBUTING.md | 14 +- _types/src/LiveSyncBaseCore.d.ts | 136 - _types/src/common/KeyValueDB.d.ts | 5 - _types/src/common/KeyValueDBv2.d.ts | 26 - _types/src/common/PeriodicProcessor.d.ts | 14 - _types/src/common/SvelteItemView.d.ts | 11 - _types/src/common/events.d.ts | 38 - _types/src/common/obsidianEvents.d.ts | 16 - _types/src/common/reportTool.d.ts | 14 - _types/src/common/stores.d.ts | 5 - _types/src/common/types.d.ts | 48 - _types/src/common/utils.d.ts | 61 - _types/src/deps.d.ts | 8 - .../features/ConfigSync/CmdConfigSync.d.ts | 147 - .../ConfigSync/PluginDialogModal.d.ts | 13 - .../HiddenFileCommon/JsonResolveModal.d.ts | 21 - .../HiddenFileSync/CmdHiddenFileSync.d.ts | 154 - .../configureHiddenFileSyncMode.d.ts | 11 - _types/src/features/LiveSyncCommands.d.ts | 36 - .../CmdLocalDatabaseMainte.d.ts | 59 - .../maintenancePrerequisites.d.ts | 15 - .../P2POpenReplicationModal.d.ts | 23 - .../P2PReplicator/P2PReplicationUI.d.ts | 24 - .../P2PReplicator/P2PReplicatorPaneView.d.ts | 30 - .../P2PServerStatusPaneView.d.ts | 21 - .../lib/src/API/DirectFileManipulator.d.ts | 4 - _types/src/lib/src/API/processSetting.d.ts | 43 - .../src/ContentSplitter/ContentSplitter.d.ts | 30 - .../ContentSplitter/ContentSplitterBase.d.ts | 53 - .../ContentSplitterRabinKarp.d.ts | 11 - .../ContentSplitter/ContentSplitterV1.d.ts | 11 - .../ContentSplitter/ContentSplitterV2.d.ts | 11 - .../src/ContentSplitter/ContentSplitters.d.ts | 14 - _types/src/lib/src/UI/svelteDialog.d.ts | 4 - _types/src/lib/src/bureau/bureau.d.ts | 9 - .../src/lib/src/common/ConnectionString.d.ts | 32 - _types/src/lib/src/common/LSError.d.ts | 50 - _types/src/lib/src/common/configForDoc.d.ts | 79 - .../src/lib/src/common/coreEnvFunctions.d.ts | 23 - _types/src/lib/src/common/coreEnvVars.d.ts | 5 - _types/src/lib/src/common/i18n.d.ts | 20 - _types/src/lib/src/common/logger.d.ts | 4 - .../common/messages/combinedMessages.dev.d.ts | 9 - .../messages/combinedMessages.prod.d.ts | 9266 ----------------- _types/src/lib/src/common/messages/de.d.ts | 299 - _types/src/lib/src/common/messages/def.d.ts | 1121 -- _types/src/lib/src/common/messages/es.d.ts | 691 -- _types/src/lib/src/common/messages/fr.d.ts | 584 -- _types/src/lib/src/common/messages/he.d.ts | 585 -- _types/src/lib/src/common/messages/ja.d.ts | 840 -- _types/src/lib/src/common/messages/ko.d.ts | 802 -- _types/src/lib/src/common/messages/ru.d.ts | 860 -- _types/src/lib/src/common/messages/zh-tw.d.ts | 386 - _types/src/lib/src/common/messages/zh.d.ts | 1095 -- .../src/lib/src/common/models/auth.type.d.ts | 43 - .../src/lib/src/common/models/db.const.d.ts | 22 - .../lib/src/common/models/db.definition.d.ts | 58 - _types/src/lib/src/common/models/db.type.d.ts | 144 - .../src/common/models/diff.definition.d.ts | 18 - .../src/common/models/fileaccess.const.d.ts | 9 - .../src/common/models/fileaccess.type.d.ts | 74 - .../lib/src/common/models/redflag.const.d.ts | 34 - .../lib/src/common/models/setting.const.d.ts | 51 - .../common/models/setting.const.defaults.d.ts | 5 - .../models/setting.const.preferred.d.ts | 7 - .../src/common/models/setting.const.qr.d.ts | 4 - .../lib/src/common/models/setting.type.d.ts | 902 -- .../common/models/shared.const.behabiour.d.ts | 30 - .../lib/src/common/models/shared.const.d.ts | 7 - .../common/models/shared.const.symbols.d.ts | 11 - .../models/shared.definition.configNames.d.ts | 38 - .../src/common/models/shared.definition.d.ts | 24 - .../src/common/models/shared.type.util.d.ts | 10 - .../src/common/models/sync.definition.d.ts | 20 - .../src/common/models/tweak.definition.d.ts | 193 - _types/src/lib/src/common/rosetta.d.ts | 44 - .../src/lib/src/common/settingConstants.d.ts | 217 - _types/src/lib/src/common/typeUtils.d.ts | 19 - _types/src/lib/src/common/types.d.ts | 109 - _types/src/lib/src/common/utils.d.ts | 131 - _types/src/lib/src/common/utils.doc.d.ts | 13 - _types/src/lib/src/common/utils.object.d.ts | 4 - _types/src/lib/src/common/utils.patch.d.ts | 10 - _types/src/lib/src/common/utils.type.d.ts | 3 - _types/src/lib/src/dataobject/StoredMap.d.ts | 14 - _types/src/lib/src/dev/checks.d.ts | 7 - .../src/lib/src/encryption/encryptHKDF.d.ts | 5 - .../lib/src/encryption/stringEncryption.d.ts | 29 - _types/src/lib/src/events/coreEvents.d.ts | 47 - _types/src/lib/src/hub/hub.d.ts | 10 - _types/src/lib/src/index.d.ts | 3 - .../src/interfaces/AsyncActivityRunner.d.ts | 20 - _types/src/lib/src/interfaces/Confirm.d.ts | 19 - .../src/interfaces/DatabaseFileAccess.d.ts | 16 - .../lib/src/interfaces/DatabaseRebuilder.d.ts | 15 - .../src/lib/src/interfaces/FileHandler.d.ts | 15 - .../lib/src/interfaces/KeyValueDatabase.d.ts | 11 - .../src/lib/src/interfaces/ServiceModule.d.ts | 61 - .../src/lib/src/interfaces/StorageAccess.d.ts | 46 - .../src/interfaces/StorageEventManager.d.ts | 18 - .../src/lib/src/managers/ChangeManager.d.ts | 63 - .../managers/ChunkDeliveryCoordinator.d.ts | 53 - _types/src/lib/src/managers/ChunkFetcher.d.ts | 46 - _types/src/lib/src/managers/ChunkManager.d.ts | 4 - .../src/lib/src/managers/ConflictManager.d.ts | 39 - .../managers/EntryManager/EntryManager.d.ts | 39 - .../EntryManager/EntryManagerImpls.d.ts | 89 - .../src/managers/HashManager/HashManager.d.ts | 62 - .../managers/HashManager/HashManagerCore.d.ts | 116 - .../HashManager/PureJSHashManager.d.ts | 79 - .../HashManager/XXHashHashManager.d.ts | 98 - .../lib/src/managers/LayeredChunkManager.d.ts | 55 - .../LayeredChunkManager/ArrivalWaitLayer.d.ts | 32 - .../LayeredChunkManager/CacheLayer.d.ts | 61 - .../ChunkLayerInterfaces.d.ts | 39 - .../DatabaseReadLayer.d.ts | 16 - .../DatabaseWriteLayer.d.ts | 13 - .../LayeredChunkManager/HotPackLayer.d.ts | 11 - .../managers/LayeredChunkManager/types.d.ts | 41 - .../lib/src/managers/LiveSyncManagers.d.ts | 51 - .../lib/src/managers/StorageEventManager.d.ts | 137 - .../managers/StorageProcessingManager.d.ts | 16 - .../IStorageEventConverterAdapter.d.ts | 18 - .../adapters/IStorageEventManagerAdapter.d.ts | 20 - .../IStorageEventPersistenceAdapter.d.ts | 17 - .../adapters/IStorageEventStatusAdapter.d.ts | 15 - .../IStorageEventTypeGuardAdapter.d.ts | 18 - .../adapters/IStorageEventWatchAdapter.d.ts | 23 - .../src/lib/src/managers/adapters/index.d.ts | 8 - .../src/lib/src/mock_and_interop/stores.d.ts | 23 - .../src/lib/src/mock_and_interop/wrapper.d.ts | 9 - _types/src/lib/src/mods.d.ts | 3 - .../lib/src/pouchdb/LiveSyncDBFunctions.d.ts | 16 - .../src/lib/src/pouchdb/LiveSyncLocalDB.d.ts | 195 - .../src/lib/src/pouchdb/ReplicatorShim.d.ts | 53 - .../src/lib/src/pouchdb/StreamingFetch.d.ts | 21 - .../StreamingFetch.integration.spec.d.ts | 3 - _types/src/lib/src/pouchdb/chunks.d.ts | 40 - _types/src/lib/src/pouchdb/compress.d.ts | 13 - _types/src/lib/src/pouchdb/encryption.d.ts | 22 - _types/src/lib/src/pouchdb/negotiation.d.ts | 11 - .../src/lib/src/pouchdb/pouchdb-browser.d.ts | 4 - _types/src/lib/src/pouchdb/pouchdb-http.d.ts | 4 - _types/src/lib/src/pouchdb/pouchdb-test.d.ts | 4 - _types/src/lib/src/pouchdb/utils_couchdb.d.ts | 6 - .../LiveSyncAbstractReplicator.d.ts | 69 - .../src/replication/SyncParamsHandler.d.ts | 37 - .../couchdb/LiveSyncReplicator.d.ts | 87 - _types/src/lib/src/replication/httplib.d.ts | 77 - .../replication/journal/JournalSyncCore.d.ts | 72 - .../replication/journal/JournalSyncTypes.d.ts | 11 - .../journal/LiveSyncJournalReplicator.d.ts | 42 - .../journal/LiveSyncJournalReplicatorEnv.d.ts | 5 - .../objectstore/JournalStorageAdapter.d.ts | 17 - .../objectstore/MinioStorageAdapter.d.ts | 23 - .../MinioStorageAdapter.integration.spec.d.ts | 3 - .../trystero/LiveSyncTrysteroReplicator.d.ts | 92 - .../replication/trystero/P2PLogCollector.d.ts | 9 - .../trystero/P2PReplicatorBase.d.ts | 21 - .../trystero/P2PReplicatorCore.d.ts | 14 - .../trystero/P2PReplicatorPaneCommon.d.ts | 43 - .../src/replication/trystero/ProxiedDB.d.ts | 15 - .../trystero/TrysteroReplicator.d.ts | 153 - .../trystero/TrysteroReplicatorP2PClient.d.ts | 21 - .../TrysteroReplicatorP2PConnection.d.ts | 4 - .../trystero/TrysteroReplicatorP2PServer.d.ts | 119 - .../trystero/UseP2PReplicatorResult.d.ts | 13 - .../trystero/addP2PEventHandlers.d.ts | 36 - .../src/replication/trystero/rpcCompat.d.ts | 3 - .../lib/src/replication/trystero/types.d.ts | 91 - .../trystero/useP2PReplicatorCommands.d.ts | 9 - .../trystero/useP2PReplicatorFeature.d.ts | 19 - _types/src/lib/src/rpc/RpcRoom.d.ts | 26 - _types/src/lib/src/rpc/RpcSession.d.ts | 11 - _types/src/lib/src/rpc/chunking.d.ts | 13 - _types/src/lib/src/rpc/errors.d.ts | 10 - _types/src/lib/src/rpc/index.d.ts | 8 - .../lib/src/rpc/pouchdb/RpcPouchDBProxy.d.ts | 86 - .../lib/src/rpc/pouchdb/RpcPouchDBServer.d.ts | 18 - .../transports/DiagRTCPeerConnections.d.ts | 26 - .../DiagRTCPeerConnections.types.d.ts | 66 - .../DiagRTCPeerConnections.utils.d.ts | 35 - .../src/rpc/transports/TrysteroTransport.d.ts | 218 - .../lib/src/rpc/transports/trysteroUtils.d.ts | 5 - _types/src/lib/src/rpc/types.d.ts | 77 - .../src/serviceFeatures/checkRemoteSize.d.ts | 30 - .../src/serviceFeatures/offlineScanner.d.ts | 130 - .../prepareDatabaseForUse.d.ts | 22 - .../lib/src/serviceFeatures/remoteConfig.d.ts | 48 - .../serviceFeatures/setupObsidian/qrCode.d.ts | 6 - .../setupObsidian/setupUri.d.ts | 9 - .../serviceFeatures/setupObsidian/types.d.ts | 4 - .../lib/src/serviceFeatures/targetFilter.d.ts | 26 - .../src/serviceModules/FileAccessBase.d.ts | 95 - .../src/lib/src/serviceModules/Rebuilder.d.ts | 91 - .../ServiceDatabaseFileAccessBase.d.ts | 39 - .../serviceModules/ServiceFileAccessBase.d.ts | 66 - .../ServiceFileHandlerBase.d.ts | 51 - .../src/serviceModules/ServiceModuleBase.d.ts | 12 - .../adapters/IConversionAdapter.d.ts | 17 - .../adapters/IFileSystemAdapter.d.ts | 49 - .../serviceModules/adapters/IPathAdapter.d.ts | 17 - .../adapters/IStorageAdapter.d.ts | 58 - .../adapters/ITypeGuardAdapter.d.ts | 16 - .../adapters/IVaultAdapter.d.ts | 53 - .../src/serviceModules/adapters/index.d.ts | 8 - .../src/lib/src/services/BrowserServices.d.ts | 9 - .../lib/src/services/HeadlessServices.d.ts | 11 - .../lib/src/services/InjectableServices.d.ts | 3 - _types/src/lib/src/services/ServiceHub.d.ts | 83 - .../src/lib/src/services/base/APIService.d.ts | 95 - .../services/base/AppLifecycleService.d.ts | 134 - .../lib/src/services/base/ConfigService.d.ts | 9 - .../src/services/base/ConflictService.d.ts | 56 - .../lib/src/services/base/ControlService.d.ts | 56 - .../services/base/DatabaseEventService.d.ts | 40 - .../src/services/base/DatabaseService.d.ts | 39 - .../services/base/FileProcessingService.d.ts | 27 - .../src/lib/src/services/base/IService.d.ts | 286 - .../src/services/base/KeyValueDBService.d.ts | 42 - .../lib/src/services/base/PathService.d.ts | 40 - .../lib/src/services/base/RemoteService.d.ts | 59 - .../src/services/base/ReplicationService.d.ts | 94 - .../src/services/base/ReplicatorService.d.ts | 68 - .../lib/src/services/base/ServiceBase.d.ts | 8 - .../lib/src/services/base/SettingService.d.ts | 114 - .../lib/src/services/base/TestService.d.ts | 30 - .../src/services/base/TweakValueService.d.ts | 42 - .../services/base/UnresolvedErrorManager.d.ts | 15 - .../lib/src/services/base/VaultService.d.ts | 71 - .../services/implements/base/UIService.d.ts | 28 - .../implements/browser/BrowserAPIService.d.ts | 53 - .../implements/browser/BrowserConfirm.d.ts | 23 - .../browser/BrowserDatabaseService.d.ts | 9 - .../implements/browser/BrowserUIService.d.ts | 19 - .../browser/ConfigServiceBrowserCompat.d.ts | 18 - .../src/services/implements/browser/Menu.d.ts | 28 - .../browser/ui/renderMessageMarkdown.d.ts | 3 - .../headless/HeadlessAPIService.d.ts | 54 - .../headless/HeadlessDatabaseService.d.ts | 9 - .../injectable/InjectableAPIService.d.ts | 9 - .../InjectableAppLifecycleService.d.ts | 13 - .../injectable/InjectableConflictService.d.ts | 13 - .../InjectableDatabaseEventService.d.ts | 6 - .../InjectableFileProcessingService.d.ts | 6 - .../injectable/InjectablePathService.d.ts | 14 - .../injectable/InjectableRemoteService.d.ts | 6 - .../InjectableReplicationService.d.ts | 6 - .../InjectableReplicatorService.d.ts | 6 - .../injectable/InjectableServiceHub.d.ts | 73 - .../injectable/InjectableServices.d.ts | 38 - .../injectable/InjectableSettingService.d.ts | 13 - .../injectable/InjectableTestService.d.ts | 7 - .../InjectableTweakValueService.d.ts | 17 - .../injectable/InjectableVaultService.d.ts | 11 - .../obsidian/ObsidianServiceContext.d.ts | 11 - .../lib/src/services/lib/HandlerUtils.d.ts | 376 - _types/src/lib/src/services/lib/logUtils.d.ts | 15 - .../lib/src/services/lib/remoteActivity.d.ts | 15 - .../src/lib/src/string_and_binary/chunks.d.ts | 12 - .../lib/src/string_and_binary/convert.d.ts | 10 - .../src/lib/src/string_and_binary/hash.d.ts | 4 - .../src/lib/src/string_and_binary/path.d.ts | 37 - _types/src/lib/src/worker/bg.common.d.ts | 4 - _types/src/lib/src/worker/bg.worker.d.ts | 3 - .../lib/src/worker/bg.worker.encryption.d.ts | 9 - .../lib/src/worker/bg.worker.splitting.d.ts | 8 - _types/src/lib/src/worker/bgWorker.d.ts | 26 - .../lib/src/worker/bgWorker.encryption.d.ts | 24 - _types/src/lib/src/worker/bgWorker.mock.d.ts | 41 - .../lib/src/worker/bgWorker.splitting.d.ts | 29 - _types/src/lib/src/worker/universalTypes.d.ts | 65 - _types/src/main.d.ts | 22 - .../ObsidianStorageEventManagerAdapter.d.ts | 66 - .../managers/StorageEventManagerObsidian.d.ts | 15 - _types/src/modules/AbstractModule.d.ts | 28 - .../src/modules/AbstractObsidianModule.d.ts | 11 - .../modules/core/ModulePeriodicProcess.d.ts | 16 - _types/src/modules/core/ModuleReplicator.d.ts | 25 - .../modules/core/ModuleReplicatorCouchDB.d.ts | 11 - .../modules/core/ModuleReplicatorMinIO.d.ts | 10 - .../core/ReplicateResultProcessor.d.ts | 116 - .../coreFeatures/ModuleConflictChecker.d.ts | 15 - .../coreFeatures/ModuleConflictResolver.d.ts | 19 - .../ModuleResolveMismatchedTweaks.d.ts | 36 - .../modules/coreObsidian/UILib/dialogs.d.ts | 56 - .../coreObsidian/storageLib/utilObsidian.d.ts | 11 - .../modules/essential/ModuleBasicMenu.d.ts | 8 - .../modules/essential/ModuleMigration.d.ts | 15 - .../APILib/ObsHttpHandler.d.ts | 18 - .../ModuleObsidianEvents.d.ts | 33 - .../essentialObsidian/ModuleObsidianMenu.d.ts | 8 - .../DocumentHistory/DocumentHistoryModal.d.ts | 71 - .../GlobalHistory/GlobalHistoryView.d.ts | 20 - .../ConflictResolveModal.d.ts | 35 - .../src/modules/features/Log/LogPaneView.d.ts | 20 - .../modules/features/ModuleGlobalHistory.d.ts | 8 - .../ModuleInteractiveConflictResolver.d.ts | 13 - _types/src/modules/features/ModuleLog.d.ts | 50 - .../ModuleObsidianDocumentHistory.d.ts | 11 - .../ModuleObsidianSettingAsMarkdown.d.ts | 24 - .../features/ModuleObsidianSettingTab.d.ts | 12 - .../features/RemoteActivityStatus.d.ts | 16 - .../SettingDialogue/LiveSyncSetting.d.ts | 55 - .../ObsidianLiveSyncSettingTab.d.ts | 104 - .../SettingDialogue/PaneAdvanced.d.ts | 5 - .../SettingDialogue/PaneChangeLog.d.ts | 4 - .../PaneCustomisationSync.d.ts | 5 - .../features/SettingDialogue/PaneGeneral.d.ts | 5 - .../features/SettingDialogue/PaneHatch.d.ts | 5 - .../SettingDialogue/PaneMaintenance.d.ts | 5 - .../features/SettingDialogue/PanePatches.d.ts | 5 - .../SettingDialogue/PanePowerUsers.d.ts | 5 - .../SettingDialogue/PaneRemoteConfig.d.ts | 5 - .../SettingDialogue/PaneSelector.d.ts | 5 - .../features/SettingDialogue/PaneSetup.d.ts | 5 - .../SettingDialogue/PaneSyncSettings.d.ts | 5 - .../features/SettingDialogue/SettingPane.d.ts | 38 - .../features/SettingDialogue/SveltePanel.d.ts | 36 - .../SettingDialogue/remoteConfigBuffer.d.ts | 4 - .../SettingDialogue/settingConstants.d.ts | 3 - .../SettingDialogue/settingUtils.d.ts | 59 - _types/src/modules/features/SetupManager.d.ts | 121 - .../SetupWizard/dialogs/setupDialogTypes.d.ts | 51 - .../modules/features/StatusBarDisplay.d.ts | 17 - .../src/modules/main/ModuleLiveSyncMain.d.ts | 11 - .../modules/services/ObsidianAPIService.d.ts | 41 - .../services/ObsidianAppLifecycleService.d.ts | 14 - .../src/modules/services/ObsidianConfirm.d.ts | 26 - .../services/ObsidianDatabaseService.d.ts | 8 - .../modules/services/ObsidianPathService.d.ts | 13 - .../modules/services/ObsidianServiceHub.d.ts | 8 - .../modules/services/ObsidianServices.d.ts | 36 - .../services/ObsidianSettingService.d.ts | 13 - .../modules/services/ObsidianUIService.d.ts | 19 - .../services/ObsidianVaultService.d.ts | 17 - .../onLayoutReady/enablei18n.d.ts | 3 - _types/src/serviceFeatures/redFlag.d.ts | 59 - .../serviceFeatures/redFlag.simpleFetch.d.ts | 19 - .../setupObsidian/setupManagerHandlers.d.ts | 8 - .../setupObsidian/setupProtocol.d.ts | 8 - .../serviceFeatures/useP2PReplicatorUI.d.ts | 19 - .../serviceModules/DatabaseFileAccess.d.ts | 6 - .../serviceModules/FileAccessObsidian.d.ts | 12 - _types/src/serviceModules/FileHandler.d.ts | 5 - .../ObsidianConversionAdapter.d.ts | 12 - .../ObsidianFileSystemAdapter.d.ts | 31 - .../ObsidianPathAdapter.d.ts | 12 - .../ObsidianStorageAdapter.d.ts | 26 - .../ObsidianTypeGuardAdapter.d.ts | 11 - .../ObsidianVaultAdapter.d.ts | 23 - .../serviceModules/ServiceFileAccessImpl.d.ts | 6 - _types/src/types.d.ts | 26 - devs.md | 51 +- docs/adding_translations.md | 16 +- ...2026_07_common_library_package_boundary.md | 44 +- esbuild.config.mjs | 4 + eslint.config.common.mjs | 1 - eslint.config.mjs | 12 - generate-types.mjs | 24 - package-lock.json | 95 +- package.json | 22 +- src/LiveSyncBaseCore.ts | 38 +- src/apps/_test/storageAdapterContract.ts | 2 +- src/apps/browser/BrowserConfirm.ts | 107 + src/apps/browser/BrowserMenu.ts | 71 + .../browser/BrowserSvelteDialogManager.ts | 79 + src/apps/browser/LiveSyncBrowserUIService.ts | 25 + .../createLiveSyncBrowserServiceHub.ts | 33 + ...eateLiveSyncBrowserServiceHub.unit.spec.ts | 28 + src/apps/browser/ui/MenuItemView.svelte | 51 + src/apps/browser/ui/MenuSeparatorView.svelte | 10 + src/apps/browser/ui/MenuView.svelte | 89 + src/apps/browser/ui/MessageBox.svelte | 127 + src/apps/browser/ui/TextInputBox.svelte | 122 + src/apps/browser/ui/renderMessageMarkdown.ts | 21 + .../ui/renderMessageMarkdown.unit.spec.ts | 27 + src/apps/cli/README.md | 9 +- .../cli/adapters/NodeConversionAdapter.ts | 6 +- .../cli/adapters/NodeFileSystemAdapter.ts | 8 +- src/apps/cli/adapters/NodePathAdapter.ts | 6 +- src/apps/cli/adapters/NodeStorageAdapter.ts | 127 - .../adapters/NodeStorageAdapter.unit.spec.ts | 5 +- src/apps/cli/adapters/NodeTypeGuardAdapter.ts | 2 +- src/apps/cli/adapters/NodeTypes.ts | 2 +- src/apps/cli/adapters/NodeVaultAdapter.ts | 6 +- .../adapters/NodeVaultAdapter.unit.spec.ts | 24 +- .../cli/commands/daemonCommand.unit.spec.ts | 8 +- src/apps/cli/commands/p2p.ts | 16 +- src/apps/cli/commands/runCommand.ts | 28 +- src/apps/cli/commands/runCommand.unit.spec.ts | 12 +- src/apps/cli/commands/types.ts | 4 +- src/apps/cli/commands/utils.ts | 2 +- src/apps/cli/commands/utils.unit.spec.ts | 2 +- src/apps/cli/entrypoint.ts | 2 +- src/apps/cli/main.ts | 19 +- .../managers/CLIStorageEventManagerAdapter.ts | 12 +- ...CLIStorageEventManagerAdapter.unit.spec.ts | 2 +- .../cli/managers/StorageEventManagerCLI.ts | 6 +- src/apps/cli/node-compat.ts | 11 - src/apps/cli/package.json | 4 +- src/apps/cli/scripts/check-submodule.mjs | 36 - .../cli/serviceModules/CLIServiceModules.ts | 13 +- .../cli/serviceModules/DatabaseFileAccess.ts | 4 +- src/apps/cli/serviceModules/FileAccessCLI.ts | 2 +- src/apps/cli/serviceModules/IgnoreRules.ts | 2 +- .../serviceModules/IgnoreRules.unit.spec.ts | 4 +- .../serviceModules/ServiceFileAccessImpl.ts | 2 +- .../cli/services/NodeKeyValueDBService.ts | 18 +- src/apps/cli/services/NodeLocalStorage.ts | 4 +- .../services/NodeLocalStorage.unit.spec.ts | 4 +- src/apps/cli/services/NodeServiceContext.ts | 10 + .../services/NodeServiceContext.unit.spec.ts | 33 + src/apps/cli/services/NodeServiceHub.ts | 100 +- src/apps/cli/services/NodeSettingService.ts | 15 +- src/apps/cli/test/test-setup-put-cat-linux.sh | 7 +- src/apps/cli/testdeno/helpers/settings.ts | 13 +- src/apps/cli/tsconfig.json | 3 +- src/apps/cli/vite.config.ts | 24 +- src/apps/storagePath.ts | 28 - .../webapp/adapters/FSAPIConversionAdapter.ts | 4 +- .../webapp/adapters/FSAPIFileSystemAdapter.ts | 10 +- src/apps/webapp/adapters/FSAPIPathAdapter.ts | 4 +- .../webapp/adapters/FSAPIStorageAdapter.ts | 225 - .../adapters/FSAPIStorageAdapter.unit.spec.ts | 6 +- .../webapp/adapters/FSAPITypeGuardAdapter.ts | 2 +- src/apps/webapp/adapters/FSAPITypes.ts | 2 +- src/apps/webapp/adapters/FSAPIVaultAdapter.ts | 4 +- src/apps/webapp/bootstrap.ts | 2 +- src/apps/webapp/main.ts | 27 +- .../FSAPIStorageEventManagerAdapter.ts | 12 +- .../managers/StorageEventManagerFSAPI.ts | 4 +- src/apps/webapp/playwright.config.ts | 4 +- .../serviceModules/DatabaseFileAccess.ts | 4 +- .../serviceModules/FSAPIServiceModules.ts | 13 +- .../webapp/serviceModules/FileAccessFSAPI.ts | 2 +- .../serviceModules/ServiceFileAccessImpl.ts | 2 +- src/apps/webapp/test-entry.ts | 13 +- src/apps/webapp/test/e2e.spec.ts | 10 +- src/apps/webapp/tsconfig.json | 3 +- src/apps/webapp/vaultSelector.ts | 2 +- src/apps/webapp/vite.config.ts | 9 +- src/apps/webpeer/package.json | 4 - src/apps/webpeer/src/CommandsShim.ts | 4 +- src/apps/webpeer/src/P2PReplicatorShim.ts | 71 +- src/apps/webpeer/src/SyncMain.svelte | 7 +- src/apps/webpeer/src/UITest.svelte | 4 +- src/apps/webpeer/src/main.ts | 2 +- src/apps/webpeer/src/uitest.ts | 2 +- src/apps/webpeer/tsconfig.app.json | 3 +- src/apps/webpeer/tsconfig.node.json | 3 +- src/apps/webpeer/vite.config.ts | 3 +- src/common/KeyValueDB.ts | 2 +- src/common/KeyValueDBv2.ts | 4 +- src/common/PeriodicProcessor.ts | 2 +- src/common/events.ts | 13 +- src/common/obsidianEvents.ts | 2 +- src/common/reportTool.ts | 18 +- src/common/translation.ts | 6 + src/common/types.ts | 8 +- src/common/utils.ts | 29 +- src/deps.ts | 2 +- src/features/ConfigSync/CmdConfigSync.ts | 20 +- src/features/ConfigSync/PluginCombo.svelte | 6 +- src/features/ConfigSync/PluginPane.svelte | 4 +- .../HiddenFileCommon/JsonResolveModal.ts | 4 +- .../HiddenFileCommon/JsonResolvePane.svelte | 6 +- .../HiddenFileSync/CmdHiddenFileSync.ts | 14 +- src/features/LiveSyncCommands.ts | 6 +- .../CmdLocalDatabaseMainte.ts | 12 +- .../CmdLocalDatabaseMainte.unit.spec.ts | 2 +- .../maintenancePrerequisites.ts | 2 +- .../P2PReplicator/P2POpenReplicationModal.ts | 2 +- .../P2POpenReplicationPane.svelte | 10 +- .../P2PSync/P2PReplicator/P2PReplicationUI.ts | 6 +- .../P2PReplicator/P2PReplicatorPane.svelte | 20 +- .../P2PReplicatorPaneController.ts | 17 + .../P2PReplicator/P2PReplicatorPaneView.ts | 8 +- .../P2PReplicator/P2PServerStatusCard.svelte | 12 +- .../P2PReplicator/P2PServerStatusPane.svelte | 16 +- .../P2PReplicator/P2PServerStatusPaneView.ts | 2 +- .../P2PReplicator/PeerStatusRow.svelte | 8 +- src/lib | 1 - src/main.ts | 31 +- .../ObsidianStorageEventManagerAdapter.ts | 12 +- src/managers/StorageEventManagerObsidian.ts | 4 +- src/modules/AbstractModule.ts | 12 +- src/modules/core/ModuleReplicator.ts | 23 +- .../core/ModuleReplicator.unit.spec.ts | 8 +- src/modules/core/ModuleReplicatorCouchDB.ts | 6 +- src/modules/core/ModuleReplicatorMinIO.ts | 6 +- src/modules/core/ReplicateResultProcessor.ts | 10 +- .../coreFeatures/ModuleConflictChecker.ts | 4 +- .../coreFeatures/ModuleConflictResolver.ts | 12 +- .../ModuleConflictResolver.unit.spec.ts | 2 +- .../ModuleResolveMismatchedTweaks.ts | 10 +- ...ModuleResolveMismatchedTweaks.unit.spec.ts | 2 +- src/modules/coreObsidian/UILib/dialogs.ts | 2 +- .../coreObsidian/storageLib/utilObsidian.ts | 6 +- src/modules/essential/ModuleMigration.ts | 17 +- .../APILib/ObsHttpHandler.ts | 2 +- .../essentialObsidian/ModuleObsidianEvents.ts | 6 +- .../ModuleObsidianEvents.unit.spec.ts | 2 +- .../essentialObsidian/ModuleObsidianMenu.ts | 4 +- src/modules/extras/ModuleDev.ts | 4 +- .../DocumentHistory/DocumentHistoryModal.ts | 14 +- .../GlobalHistory/GlobalHistory.svelte | 6 +- .../ConflictResolveModal.ts | 6 +- src/modules/features/Log/LogPane.svelte | 8 +- src/modules/features/Log/LogPaneView.ts | 2 +- .../ModuleInteractiveConflictResolver.ts | 2 +- src/modules/features/ModuleLog.ts | 24 +- .../features/ModuleObsidianDocumentHistory.ts | 2 +- .../ModuleObsidianSettingAsMarkdown.ts | 6 +- .../features/SettingDialogue/InfoPanel.svelte | 2 +- .../SettingDialogue/LiveSyncSetting.ts | 4 +- .../MultipleRegExpControl.svelte | 4 +- .../ObsidianLiveSyncSettingTab.ts | 20 +- .../features/SettingDialogue/PaneAdvanced.ts | 2 +- .../features/SettingDialogue/PaneChangeLog.ts | 4 +- .../features/SettingDialogue/PaneGeneral.ts | 6 +- .../features/SettingDialogue/PaneHatch.ts | 12 +- .../SettingDialogue/PaneMaintenance.ts | 8 +- .../features/SettingDialogue/PanePatches.ts | 8 +- .../SettingDialogue/PanePowerUsers.ts | 2 +- .../SettingDialogue/PaneRemoteConfig.ts | 14 +- .../features/SettingDialogue/PaneSelector.ts | 4 +- .../features/SettingDialogue/PaneSetup.ts | 6 +- .../SettingDialogue/PaneSyncSettings.ts | 6 +- .../features/SettingDialogue/SettingPane.ts | 4 +- .../SettingDialogue/remoteConfigBuffer.ts | 4 +- .../remoteConfigBuffer.unit.spec.ts | 2 +- .../SettingDialogue/settingConstants.ts | 2 +- .../features/SettingDialogue/settingUtils.ts | 8 +- .../SettingDialogue/utilFixCouchDBSetting.ts | 16 +- src/modules/features/SetupManager.ts | 8 +- .../features/SetupManager.unit.spec.ts | 10 +- .../dialogs/FetchEverything.svelte | 22 +- .../features/SetupWizard/dialogs/Intro.svelte | 16 +- .../dialogs/OutroAskUserMode.svelte | 16 +- .../dialogs/OutroExistingUser.svelte | 12 +- .../SetupWizard/dialogs/OutroNewUser.svelte | 12 +- .../dialogs/PanelCouchDBCheck.svelte | 6 +- .../dialogs/RebuildEverything.svelte | 22 +- .../SetupWizard/dialogs/ScanQRCode.svelte | 10 +- .../dialogs/SelectMethodExisting.svelte | 16 +- .../dialogs/SelectMethodNewUser.svelte | 16 +- .../SetupWizard/dialogs/SetupRemote.svelte | 14 +- .../dialogs/SetupRemoteBucket.svelte | 22 +- .../dialogs/SetupRemoteCouchDB.svelte | 24 +- .../dialogs/SetupRemoteE2EE.svelte | 22 +- .../SetupWizard/dialogs/SetupRemoteP2P.svelte | 32 +- .../SetupWizard/dialogs/UseSetupURI.svelte | 20 +- .../SetupWizard/dialogs/setupDialogTypes.ts | 2 +- .../SetupWizard/dialogs/utilCheckCouchDB.ts | 16 +- src/modules/features/StatusBarDisplay.ts | 2 +- src/modules/main/ModuleLiveSyncMain.ts | 16 +- .../services/LiveSyncUI/DialogHost.svelte | 162 + .../LiveSyncUI/components/Check.svelte | 53 + .../LiveSyncUI/components/Decision.svelte | 24 + .../LiveSyncUI/components/DialogHeader.svelte | 41 + .../LiveSyncUI/components/ExtraItems.svelte | 17 + .../LiveSyncUI/components/Guidance.svelte | 21 + .../LiveSyncUI/components/InfoNote.svelte | 87 + .../LiveSyncUI/components/InfoTable.svelte | 74 + .../LiveSyncUI/components/InputRow.svelte | 15 + .../LiveSyncUI/components/Instruction.svelte | 10 + .../LiveSyncUI/components/Option.svelte | 81 + .../LiveSyncUI/components/Options.svelte | 14 + .../LiveSyncUI/components/Password.svelte | 34 + .../LiveSyncUI/components/Question.svelte | 26 + .../components/UserDecisions.svelte | 12 + .../dialogues/DialogueToCopy.svelte | 59 + .../services/LiveSyncUI/svelteDialog.ts | 14 + src/modules/services/ObsidianAPIService.ts | 8 +- .../services/ObsidianAppLifecycleService.ts | 4 +- src/modules/services/ObsidianConfirm.ts | 6 +- .../services/ObsidianDatabaseService.ts | 4 +- src/modules/services/ObsidianPathService.ts | 6 +- .../services/ObsidianServiceContext.ts | 19 + .../ObsidianServiceContext.unit.spec.ts | 27 + src/modules/services/ObsidianServiceHub.ts | 11 +- src/modules/services/ObsidianServices.ts | 24 +- .../services/ObsidianSettingService.ts | 15 +- src/modules/services/ObsidianUIService.ts | 15 +- src/modules/services/ObsidianVaultService.ts | 6 +- src/modules/services/SvelteDialogObsidian.ts | 6 +- src/rabinKarpBom.unit.spec.ts | 2 +- .../onLayoutReady/enablei18n.ts | 6 +- src/serviceFeatures/redFlag.simpleFetch.ts | 10 +- src/serviceFeatures/redFlag.ts | 16 +- src/serviceFeatures/redFlag.unit.spec.ts | 18 +- src/serviceFeatures/setupObsidian/qrCode.ts | 75 + .../setupObsidian/qrCode.unit.spec.ts | 117 + .../setupObsidian/setupManagerHandlers.ts | 15 +- .../setupManagerHandlers.unit.spec.ts | 10 +- .../setupObsidian/setupProtocol.ts | 10 +- src/serviceFeatures/setupObsidian/setupUri.ts | 73 + .../setupObsidian/setupUri.unit.spec.ts | 159 + src/serviceFeatures/setupObsidian/types.ts | 3 + src/serviceFeatures/useP2PReplicatorUI.ts | 10 +- .../useP2PReplicatorUI.unit.spec.ts | 2 + src/serviceModules/DatabaseFileAccess.ts | 6 +- src/serviceModules/FileAccessObsidian.ts | 2 +- src/serviceModules/FileHandler.ts | 4 +- .../ObsidianConversionAdapter.ts | 4 +- .../ObsidianFileSystemAdapter.ts | 4 +- .../FileSystemAdapters/ObsidianPathAdapter.ts | 4 +- .../ObsidianStorageAdapter.ts | 6 +- .../ObsidianTypeGuardAdapter.ts | 2 +- .../ObsidianVaultAdapter.ts | 6 +- src/serviceModules/ServiceFileAccessImpl.ts | 2 +- src/types.ts | 10 +- test/contracts/serviceContext.ts | 75 + test/e2e-obsidian/README.md | 16 +- test/e2e-obsidian/runner/liveSyncWorkflow.ts | 74 + test/e2e-obsidian/scripts/smoke.ts | 9 + test/harness/harness.ts | 6 +- test/lib/commands.ts | 2 +- test/lib/ui.ts | 2 +- test/lib/util.ts | 2 +- test/suite/db_common.ts | 4 +- test/suite/onlylocaldb.test.ts | 4 +- test/suite/sync.senario.basic.ts | 4 +- test/suite/sync.single.test.ts | 2 +- test/suite/sync.test.ts | 2 +- test/suite/sync_common.ts | 6 +- test/suite/variables.ts | 4 +- test/suitep2p/sync_common_p2p.ts | 6 +- test/suitep2p/syncp2p.p2p-down.test.ts | 4 +- test/suitep2p/syncp2p.p2p-up.test.ts | 4 +- test/suitep2p/syncp2p.test.ts | 2 +- test/unit/dialog.test.ts | 4 +- test/utils/dummyfile.ts | 2 +- tsconfig.json | 3 +- tsconfig.types.json | 26 - updates.md | 9 + utils/bench/splitPiecesRabinKarp.ts | 12 +- utils/release-process.unit.spec.ts | 17 +- utilsdeno/README.md | 20 +- utilsdeno/normalise-imports.ts | 33 +- utilsdeno/refactor-cli-node-imports.ts | 132 - utilsdeno/refactor-globals.ts | 5 +- utilsdeno/refactor-import-utils.ts | 187 - utilsdeno/refactor-imports.ts | 155 - utilsdeno/refactor-styles.ts | 1 - utilsdeno/types-add-ignore.ts | 223 - vite.config.ts | 1 - vitest.config.common.ts | 1 - vitest.config.rpc-unit.ts | 36 - vitest.config.ts | 4 +- vitest.config.unit.ts | 6 +- 665 files changed, 3438 insertions(+), 31592 deletions(-) delete mode 100644 .gitmodules delete mode 100644 _types/src/LiveSyncBaseCore.d.ts delete mode 100644 _types/src/common/KeyValueDB.d.ts delete mode 100644 _types/src/common/KeyValueDBv2.d.ts delete mode 100644 _types/src/common/PeriodicProcessor.d.ts delete mode 100644 _types/src/common/SvelteItemView.d.ts delete mode 100644 _types/src/common/events.d.ts delete mode 100644 _types/src/common/obsidianEvents.d.ts delete mode 100644 _types/src/common/reportTool.d.ts delete mode 100644 _types/src/common/stores.d.ts delete mode 100644 _types/src/common/types.d.ts delete mode 100644 _types/src/common/utils.d.ts delete mode 100644 _types/src/deps.d.ts delete mode 100644 _types/src/features/ConfigSync/CmdConfigSync.d.ts delete mode 100644 _types/src/features/ConfigSync/PluginDialogModal.d.ts delete mode 100644 _types/src/features/HiddenFileCommon/JsonResolveModal.d.ts delete mode 100644 _types/src/features/HiddenFileSync/CmdHiddenFileSync.d.ts delete mode 100644 _types/src/features/HiddenFileSync/configureHiddenFileSyncMode.d.ts delete mode 100644 _types/src/features/LiveSyncCommands.d.ts delete mode 100644 _types/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.d.ts delete mode 100644 _types/src/features/LocalDatabaseMainte/maintenancePrerequisites.d.ts delete mode 100644 _types/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.d.ts delete mode 100644 _types/src/features/P2PSync/P2PReplicator/P2PReplicationUI.d.ts delete mode 100644 _types/src/features/P2PSync/P2PReplicator/P2PReplicatorPaneView.d.ts delete mode 100644 _types/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.d.ts delete mode 100644 _types/src/lib/src/API/DirectFileManipulator.d.ts delete mode 100644 _types/src/lib/src/API/processSetting.d.ts delete mode 100644 _types/src/lib/src/ContentSplitter/ContentSplitter.d.ts delete mode 100644 _types/src/lib/src/ContentSplitter/ContentSplitterBase.d.ts delete mode 100644 _types/src/lib/src/ContentSplitter/ContentSplitterRabinKarp.d.ts delete mode 100644 _types/src/lib/src/ContentSplitter/ContentSplitterV1.d.ts delete mode 100644 _types/src/lib/src/ContentSplitter/ContentSplitterV2.d.ts delete mode 100644 _types/src/lib/src/ContentSplitter/ContentSplitters.d.ts delete mode 100644 _types/src/lib/src/UI/svelteDialog.d.ts delete mode 100644 _types/src/lib/src/bureau/bureau.d.ts delete mode 100644 _types/src/lib/src/common/ConnectionString.d.ts delete mode 100644 _types/src/lib/src/common/LSError.d.ts delete mode 100644 _types/src/lib/src/common/configForDoc.d.ts delete mode 100644 _types/src/lib/src/common/coreEnvFunctions.d.ts delete mode 100644 _types/src/lib/src/common/coreEnvVars.d.ts delete mode 100644 _types/src/lib/src/common/i18n.d.ts delete mode 100644 _types/src/lib/src/common/logger.d.ts delete mode 100644 _types/src/lib/src/common/messages/combinedMessages.dev.d.ts delete mode 100644 _types/src/lib/src/common/messages/combinedMessages.prod.d.ts delete mode 100644 _types/src/lib/src/common/messages/de.d.ts delete mode 100644 _types/src/lib/src/common/messages/def.d.ts delete mode 100644 _types/src/lib/src/common/messages/es.d.ts delete mode 100644 _types/src/lib/src/common/messages/fr.d.ts delete mode 100644 _types/src/lib/src/common/messages/he.d.ts delete mode 100644 _types/src/lib/src/common/messages/ja.d.ts delete mode 100644 _types/src/lib/src/common/messages/ko.d.ts delete mode 100644 _types/src/lib/src/common/messages/ru.d.ts delete mode 100644 _types/src/lib/src/common/messages/zh-tw.d.ts delete mode 100644 _types/src/lib/src/common/messages/zh.d.ts delete mode 100644 _types/src/lib/src/common/models/auth.type.d.ts delete mode 100644 _types/src/lib/src/common/models/db.const.d.ts delete mode 100644 _types/src/lib/src/common/models/db.definition.d.ts delete mode 100644 _types/src/lib/src/common/models/db.type.d.ts delete mode 100644 _types/src/lib/src/common/models/diff.definition.d.ts delete mode 100644 _types/src/lib/src/common/models/fileaccess.const.d.ts delete mode 100644 _types/src/lib/src/common/models/fileaccess.type.d.ts delete mode 100644 _types/src/lib/src/common/models/redflag.const.d.ts delete mode 100644 _types/src/lib/src/common/models/setting.const.d.ts delete mode 100644 _types/src/lib/src/common/models/setting.const.defaults.d.ts delete mode 100644 _types/src/lib/src/common/models/setting.const.preferred.d.ts delete mode 100644 _types/src/lib/src/common/models/setting.const.qr.d.ts delete mode 100644 _types/src/lib/src/common/models/setting.type.d.ts delete mode 100644 _types/src/lib/src/common/models/shared.const.behabiour.d.ts delete mode 100644 _types/src/lib/src/common/models/shared.const.d.ts delete mode 100644 _types/src/lib/src/common/models/shared.const.symbols.d.ts delete mode 100644 _types/src/lib/src/common/models/shared.definition.configNames.d.ts delete mode 100644 _types/src/lib/src/common/models/shared.definition.d.ts delete mode 100644 _types/src/lib/src/common/models/shared.type.util.d.ts delete mode 100644 _types/src/lib/src/common/models/sync.definition.d.ts delete mode 100644 _types/src/lib/src/common/models/tweak.definition.d.ts delete mode 100644 _types/src/lib/src/common/rosetta.d.ts delete mode 100644 _types/src/lib/src/common/settingConstants.d.ts delete mode 100644 _types/src/lib/src/common/typeUtils.d.ts delete mode 100644 _types/src/lib/src/common/types.d.ts delete mode 100644 _types/src/lib/src/common/utils.d.ts delete mode 100644 _types/src/lib/src/common/utils.doc.d.ts delete mode 100644 _types/src/lib/src/common/utils.object.d.ts delete mode 100644 _types/src/lib/src/common/utils.patch.d.ts delete mode 100644 _types/src/lib/src/common/utils.type.d.ts delete mode 100644 _types/src/lib/src/dataobject/StoredMap.d.ts delete mode 100644 _types/src/lib/src/dev/checks.d.ts delete mode 100644 _types/src/lib/src/encryption/encryptHKDF.d.ts delete mode 100644 _types/src/lib/src/encryption/stringEncryption.d.ts delete mode 100644 _types/src/lib/src/events/coreEvents.d.ts delete mode 100644 _types/src/lib/src/hub/hub.d.ts delete mode 100644 _types/src/lib/src/index.d.ts delete mode 100644 _types/src/lib/src/interfaces/AsyncActivityRunner.d.ts delete mode 100644 _types/src/lib/src/interfaces/Confirm.d.ts delete mode 100644 _types/src/lib/src/interfaces/DatabaseFileAccess.d.ts delete mode 100644 _types/src/lib/src/interfaces/DatabaseRebuilder.d.ts delete mode 100644 _types/src/lib/src/interfaces/FileHandler.d.ts delete mode 100644 _types/src/lib/src/interfaces/KeyValueDatabase.d.ts delete mode 100644 _types/src/lib/src/interfaces/ServiceModule.d.ts delete mode 100644 _types/src/lib/src/interfaces/StorageAccess.d.ts delete mode 100644 _types/src/lib/src/interfaces/StorageEventManager.d.ts delete mode 100644 _types/src/lib/src/managers/ChangeManager.d.ts delete mode 100644 _types/src/lib/src/managers/ChunkDeliveryCoordinator.d.ts delete mode 100644 _types/src/lib/src/managers/ChunkFetcher.d.ts delete mode 100644 _types/src/lib/src/managers/ChunkManager.d.ts delete mode 100644 _types/src/lib/src/managers/ConflictManager.d.ts delete mode 100644 _types/src/lib/src/managers/EntryManager/EntryManager.d.ts delete mode 100644 _types/src/lib/src/managers/EntryManager/EntryManagerImpls.d.ts delete mode 100644 _types/src/lib/src/managers/HashManager/HashManager.d.ts delete mode 100644 _types/src/lib/src/managers/HashManager/HashManagerCore.d.ts delete mode 100644 _types/src/lib/src/managers/HashManager/PureJSHashManager.d.ts delete mode 100644 _types/src/lib/src/managers/HashManager/XXHashHashManager.d.ts delete mode 100644 _types/src/lib/src/managers/LayeredChunkManager.d.ts delete mode 100644 _types/src/lib/src/managers/LayeredChunkManager/ArrivalWaitLayer.d.ts delete mode 100644 _types/src/lib/src/managers/LayeredChunkManager/CacheLayer.d.ts delete mode 100644 _types/src/lib/src/managers/LayeredChunkManager/ChunkLayerInterfaces.d.ts delete mode 100644 _types/src/lib/src/managers/LayeredChunkManager/DatabaseReadLayer.d.ts delete mode 100644 _types/src/lib/src/managers/LayeredChunkManager/DatabaseWriteLayer.d.ts delete mode 100644 _types/src/lib/src/managers/LayeredChunkManager/HotPackLayer.d.ts delete mode 100644 _types/src/lib/src/managers/LayeredChunkManager/types.d.ts delete mode 100644 _types/src/lib/src/managers/LiveSyncManagers.d.ts delete mode 100644 _types/src/lib/src/managers/StorageEventManager.d.ts delete mode 100644 _types/src/lib/src/managers/StorageProcessingManager.d.ts delete mode 100644 _types/src/lib/src/managers/adapters/IStorageEventConverterAdapter.d.ts delete mode 100644 _types/src/lib/src/managers/adapters/IStorageEventManagerAdapter.d.ts delete mode 100644 _types/src/lib/src/managers/adapters/IStorageEventPersistenceAdapter.d.ts delete mode 100644 _types/src/lib/src/managers/adapters/IStorageEventStatusAdapter.d.ts delete mode 100644 _types/src/lib/src/managers/adapters/IStorageEventTypeGuardAdapter.d.ts delete mode 100644 _types/src/lib/src/managers/adapters/IStorageEventWatchAdapter.d.ts delete mode 100644 _types/src/lib/src/managers/adapters/index.d.ts delete mode 100644 _types/src/lib/src/mock_and_interop/stores.d.ts delete mode 100644 _types/src/lib/src/mock_and_interop/wrapper.d.ts delete mode 100644 _types/src/lib/src/mods.d.ts delete mode 100644 _types/src/lib/src/pouchdb/LiveSyncDBFunctions.d.ts delete mode 100644 _types/src/lib/src/pouchdb/LiveSyncLocalDB.d.ts delete mode 100644 _types/src/lib/src/pouchdb/ReplicatorShim.d.ts delete mode 100644 _types/src/lib/src/pouchdb/StreamingFetch.d.ts delete mode 100644 _types/src/lib/src/pouchdb/StreamingFetch.integration.spec.d.ts delete mode 100644 _types/src/lib/src/pouchdb/chunks.d.ts delete mode 100644 _types/src/lib/src/pouchdb/compress.d.ts delete mode 100644 _types/src/lib/src/pouchdb/encryption.d.ts delete mode 100644 _types/src/lib/src/pouchdb/negotiation.d.ts delete mode 100644 _types/src/lib/src/pouchdb/pouchdb-browser.d.ts delete mode 100644 _types/src/lib/src/pouchdb/pouchdb-http.d.ts delete mode 100644 _types/src/lib/src/pouchdb/pouchdb-test.d.ts delete mode 100644 _types/src/lib/src/pouchdb/utils_couchdb.d.ts delete mode 100644 _types/src/lib/src/replication/LiveSyncAbstractReplicator.d.ts delete mode 100644 _types/src/lib/src/replication/SyncParamsHandler.d.ts delete mode 100644 _types/src/lib/src/replication/couchdb/LiveSyncReplicator.d.ts delete mode 100644 _types/src/lib/src/replication/httplib.d.ts delete mode 100644 _types/src/lib/src/replication/journal/JournalSyncCore.d.ts delete mode 100644 _types/src/lib/src/replication/journal/JournalSyncTypes.d.ts delete mode 100644 _types/src/lib/src/replication/journal/LiveSyncJournalReplicator.d.ts delete mode 100644 _types/src/lib/src/replication/journal/LiveSyncJournalReplicatorEnv.d.ts delete mode 100644 _types/src/lib/src/replication/journal/objectstore/JournalStorageAdapter.d.ts delete mode 100644 _types/src/lib/src/replication/journal/objectstore/MinioStorageAdapter.d.ts delete mode 100644 _types/src/lib/src/replication/journal/objectstore/MinioStorageAdapter.integration.spec.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/LiveSyncTrysteroReplicator.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/P2PLogCollector.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/P2PReplicatorBase.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/P2PReplicatorCore.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/P2PReplicatorPaneCommon.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/ProxiedDB.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/TrysteroReplicator.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/TrysteroReplicatorP2PClient.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/TrysteroReplicatorP2PConnection.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/TrysteroReplicatorP2PServer.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/UseP2PReplicatorResult.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/addP2PEventHandlers.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/rpcCompat.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/types.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/useP2PReplicatorCommands.d.ts delete mode 100644 _types/src/lib/src/replication/trystero/useP2PReplicatorFeature.d.ts delete mode 100644 _types/src/lib/src/rpc/RpcRoom.d.ts delete mode 100644 _types/src/lib/src/rpc/RpcSession.d.ts delete mode 100644 _types/src/lib/src/rpc/chunking.d.ts delete mode 100644 _types/src/lib/src/rpc/errors.d.ts delete mode 100644 _types/src/lib/src/rpc/index.d.ts delete mode 100644 _types/src/lib/src/rpc/pouchdb/RpcPouchDBProxy.d.ts delete mode 100644 _types/src/lib/src/rpc/pouchdb/RpcPouchDBServer.d.ts delete mode 100644 _types/src/lib/src/rpc/transports/DiagRTCPeerConnections.d.ts delete mode 100644 _types/src/lib/src/rpc/transports/DiagRTCPeerConnections.types.d.ts delete mode 100644 _types/src/lib/src/rpc/transports/DiagRTCPeerConnections.utils.d.ts delete mode 100644 _types/src/lib/src/rpc/transports/TrysteroTransport.d.ts delete mode 100644 _types/src/lib/src/rpc/transports/trysteroUtils.d.ts delete mode 100644 _types/src/lib/src/rpc/types.d.ts delete mode 100644 _types/src/lib/src/serviceFeatures/checkRemoteSize.d.ts delete mode 100644 _types/src/lib/src/serviceFeatures/offlineScanner.d.ts delete mode 100644 _types/src/lib/src/serviceFeatures/prepareDatabaseForUse.d.ts delete mode 100644 _types/src/lib/src/serviceFeatures/remoteConfig.d.ts delete mode 100644 _types/src/lib/src/serviceFeatures/setupObsidian/qrCode.d.ts delete mode 100644 _types/src/lib/src/serviceFeatures/setupObsidian/setupUri.d.ts delete mode 100644 _types/src/lib/src/serviceFeatures/setupObsidian/types.d.ts delete mode 100644 _types/src/lib/src/serviceFeatures/targetFilter.d.ts delete mode 100644 _types/src/lib/src/serviceModules/FileAccessBase.d.ts delete mode 100644 _types/src/lib/src/serviceModules/Rebuilder.d.ts delete mode 100644 _types/src/lib/src/serviceModules/ServiceDatabaseFileAccessBase.d.ts delete mode 100644 _types/src/lib/src/serviceModules/ServiceFileAccessBase.d.ts delete mode 100644 _types/src/lib/src/serviceModules/ServiceFileHandlerBase.d.ts delete mode 100644 _types/src/lib/src/serviceModules/ServiceModuleBase.d.ts delete mode 100644 _types/src/lib/src/serviceModules/adapters/IConversionAdapter.d.ts delete mode 100644 _types/src/lib/src/serviceModules/adapters/IFileSystemAdapter.d.ts delete mode 100644 _types/src/lib/src/serviceModules/adapters/IPathAdapter.d.ts delete mode 100644 _types/src/lib/src/serviceModules/adapters/IStorageAdapter.d.ts delete mode 100644 _types/src/lib/src/serviceModules/adapters/ITypeGuardAdapter.d.ts delete mode 100644 _types/src/lib/src/serviceModules/adapters/IVaultAdapter.d.ts delete mode 100644 _types/src/lib/src/serviceModules/adapters/index.d.ts delete mode 100644 _types/src/lib/src/services/BrowserServices.d.ts delete mode 100644 _types/src/lib/src/services/HeadlessServices.d.ts delete mode 100644 _types/src/lib/src/services/InjectableServices.d.ts delete mode 100644 _types/src/lib/src/services/ServiceHub.d.ts delete mode 100644 _types/src/lib/src/services/base/APIService.d.ts delete mode 100644 _types/src/lib/src/services/base/AppLifecycleService.d.ts delete mode 100644 _types/src/lib/src/services/base/ConfigService.d.ts delete mode 100644 _types/src/lib/src/services/base/ConflictService.d.ts delete mode 100644 _types/src/lib/src/services/base/ControlService.d.ts delete mode 100644 _types/src/lib/src/services/base/DatabaseEventService.d.ts delete mode 100644 _types/src/lib/src/services/base/DatabaseService.d.ts delete mode 100644 _types/src/lib/src/services/base/FileProcessingService.d.ts delete mode 100644 _types/src/lib/src/services/base/IService.d.ts delete mode 100644 _types/src/lib/src/services/base/KeyValueDBService.d.ts delete mode 100644 _types/src/lib/src/services/base/PathService.d.ts delete mode 100644 _types/src/lib/src/services/base/RemoteService.d.ts delete mode 100644 _types/src/lib/src/services/base/ReplicationService.d.ts delete mode 100644 _types/src/lib/src/services/base/ReplicatorService.d.ts delete mode 100644 _types/src/lib/src/services/base/ServiceBase.d.ts delete mode 100644 _types/src/lib/src/services/base/SettingService.d.ts delete mode 100644 _types/src/lib/src/services/base/TestService.d.ts delete mode 100644 _types/src/lib/src/services/base/TweakValueService.d.ts delete mode 100644 _types/src/lib/src/services/base/UnresolvedErrorManager.d.ts delete mode 100644 _types/src/lib/src/services/base/VaultService.d.ts delete mode 100644 _types/src/lib/src/services/implements/base/UIService.d.ts delete mode 100644 _types/src/lib/src/services/implements/browser/BrowserAPIService.d.ts delete mode 100644 _types/src/lib/src/services/implements/browser/BrowserConfirm.d.ts delete mode 100644 _types/src/lib/src/services/implements/browser/BrowserDatabaseService.d.ts delete mode 100644 _types/src/lib/src/services/implements/browser/BrowserUIService.d.ts delete mode 100644 _types/src/lib/src/services/implements/browser/ConfigServiceBrowserCompat.d.ts delete mode 100644 _types/src/lib/src/services/implements/browser/Menu.d.ts delete mode 100644 _types/src/lib/src/services/implements/browser/ui/renderMessageMarkdown.d.ts delete mode 100644 _types/src/lib/src/services/implements/headless/HeadlessAPIService.d.ts delete mode 100644 _types/src/lib/src/services/implements/headless/HeadlessDatabaseService.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectableAPIService.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectableAppLifecycleService.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectableConflictService.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectableDatabaseEventService.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectableFileProcessingService.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectablePathService.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectableRemoteService.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectableReplicationService.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectableReplicatorService.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectableServiceHub.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectableServices.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectableSettingService.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectableTestService.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectableTweakValueService.d.ts delete mode 100644 _types/src/lib/src/services/implements/injectable/InjectableVaultService.d.ts delete mode 100644 _types/src/lib/src/services/implements/obsidian/ObsidianServiceContext.d.ts delete mode 100644 _types/src/lib/src/services/lib/HandlerUtils.d.ts delete mode 100644 _types/src/lib/src/services/lib/logUtils.d.ts delete mode 100644 _types/src/lib/src/services/lib/remoteActivity.d.ts delete mode 100644 _types/src/lib/src/string_and_binary/chunks.d.ts delete mode 100644 _types/src/lib/src/string_and_binary/convert.d.ts delete mode 100644 _types/src/lib/src/string_and_binary/hash.d.ts delete mode 100644 _types/src/lib/src/string_and_binary/path.d.ts delete mode 100644 _types/src/lib/src/worker/bg.common.d.ts delete mode 100644 _types/src/lib/src/worker/bg.worker.d.ts delete mode 100644 _types/src/lib/src/worker/bg.worker.encryption.d.ts delete mode 100644 _types/src/lib/src/worker/bg.worker.splitting.d.ts delete mode 100644 _types/src/lib/src/worker/bgWorker.d.ts delete mode 100644 _types/src/lib/src/worker/bgWorker.encryption.d.ts delete mode 100644 _types/src/lib/src/worker/bgWorker.mock.d.ts delete mode 100644 _types/src/lib/src/worker/bgWorker.splitting.d.ts delete mode 100644 _types/src/lib/src/worker/universalTypes.d.ts delete mode 100644 _types/src/main.d.ts delete mode 100644 _types/src/managers/ObsidianStorageEventManagerAdapter.d.ts delete mode 100644 _types/src/managers/StorageEventManagerObsidian.d.ts delete mode 100644 _types/src/modules/AbstractModule.d.ts delete mode 100644 _types/src/modules/AbstractObsidianModule.d.ts delete mode 100644 _types/src/modules/core/ModulePeriodicProcess.d.ts delete mode 100644 _types/src/modules/core/ModuleReplicator.d.ts delete mode 100644 _types/src/modules/core/ModuleReplicatorCouchDB.d.ts delete mode 100644 _types/src/modules/core/ModuleReplicatorMinIO.d.ts delete mode 100644 _types/src/modules/core/ReplicateResultProcessor.d.ts delete mode 100644 _types/src/modules/coreFeatures/ModuleConflictChecker.d.ts delete mode 100644 _types/src/modules/coreFeatures/ModuleConflictResolver.d.ts delete mode 100644 _types/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.d.ts delete mode 100644 _types/src/modules/coreObsidian/UILib/dialogs.d.ts delete mode 100644 _types/src/modules/coreObsidian/storageLib/utilObsidian.d.ts delete mode 100644 _types/src/modules/essential/ModuleBasicMenu.d.ts delete mode 100644 _types/src/modules/essential/ModuleMigration.d.ts delete mode 100644 _types/src/modules/essentialObsidian/APILib/ObsHttpHandler.d.ts delete mode 100644 _types/src/modules/essentialObsidian/ModuleObsidianEvents.d.ts delete mode 100644 _types/src/modules/essentialObsidian/ModuleObsidianMenu.d.ts delete mode 100644 _types/src/modules/features/DocumentHistory/DocumentHistoryModal.d.ts delete mode 100644 _types/src/modules/features/GlobalHistory/GlobalHistoryView.d.ts delete mode 100644 _types/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.d.ts delete mode 100644 _types/src/modules/features/Log/LogPaneView.d.ts delete mode 100644 _types/src/modules/features/ModuleGlobalHistory.d.ts delete mode 100644 _types/src/modules/features/ModuleInteractiveConflictResolver.d.ts delete mode 100644 _types/src/modules/features/ModuleLog.d.ts delete mode 100644 _types/src/modules/features/ModuleObsidianDocumentHistory.d.ts delete mode 100644 _types/src/modules/features/ModuleObsidianSettingAsMarkdown.d.ts delete mode 100644 _types/src/modules/features/ModuleObsidianSettingTab.d.ts delete mode 100644 _types/src/modules/features/RemoteActivityStatus.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/LiveSyncSetting.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/PaneAdvanced.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/PaneChangeLog.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/PaneCustomisationSync.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/PaneGeneral.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/PaneHatch.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/PaneMaintenance.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/PanePatches.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/PanePowerUsers.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/PaneRemoteConfig.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/PaneSelector.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/PaneSetup.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/PaneSyncSettings.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/SettingPane.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/SveltePanel.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/remoteConfigBuffer.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/settingConstants.d.ts delete mode 100644 _types/src/modules/features/SettingDialogue/settingUtils.d.ts delete mode 100644 _types/src/modules/features/SetupManager.d.ts delete mode 100644 _types/src/modules/features/SetupWizard/dialogs/setupDialogTypes.d.ts delete mode 100644 _types/src/modules/features/StatusBarDisplay.d.ts delete mode 100644 _types/src/modules/main/ModuleLiveSyncMain.d.ts delete mode 100644 _types/src/modules/services/ObsidianAPIService.d.ts delete mode 100644 _types/src/modules/services/ObsidianAppLifecycleService.d.ts delete mode 100644 _types/src/modules/services/ObsidianConfirm.d.ts delete mode 100644 _types/src/modules/services/ObsidianDatabaseService.d.ts delete mode 100644 _types/src/modules/services/ObsidianPathService.d.ts delete mode 100644 _types/src/modules/services/ObsidianServiceHub.d.ts delete mode 100644 _types/src/modules/services/ObsidianServices.d.ts delete mode 100644 _types/src/modules/services/ObsidianSettingService.d.ts delete mode 100644 _types/src/modules/services/ObsidianUIService.d.ts delete mode 100644 _types/src/modules/services/ObsidianVaultService.d.ts delete mode 100644 _types/src/serviceFeatures/onLayoutReady/enablei18n.d.ts delete mode 100644 _types/src/serviceFeatures/redFlag.d.ts delete mode 100644 _types/src/serviceFeatures/redFlag.simpleFetch.d.ts delete mode 100644 _types/src/serviceFeatures/setupObsidian/setupManagerHandlers.d.ts delete mode 100644 _types/src/serviceFeatures/setupObsidian/setupProtocol.d.ts delete mode 100644 _types/src/serviceFeatures/useP2PReplicatorUI.d.ts delete mode 100644 _types/src/serviceModules/DatabaseFileAccess.d.ts delete mode 100644 _types/src/serviceModules/FileAccessObsidian.d.ts delete mode 100644 _types/src/serviceModules/FileHandler.d.ts delete mode 100644 _types/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.d.ts delete mode 100644 _types/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.d.ts delete mode 100644 _types/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.d.ts delete mode 100644 _types/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.d.ts delete mode 100644 _types/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.d.ts delete mode 100644 _types/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.d.ts delete mode 100644 _types/src/serviceModules/ServiceFileAccessImpl.d.ts delete mode 100644 _types/src/types.d.ts delete mode 100644 generate-types.mjs create mode 100644 src/apps/browser/BrowserConfirm.ts create mode 100644 src/apps/browser/BrowserMenu.ts create mode 100644 src/apps/browser/BrowserSvelteDialogManager.ts create mode 100644 src/apps/browser/LiveSyncBrowserUIService.ts create mode 100644 src/apps/browser/createLiveSyncBrowserServiceHub.ts create mode 100644 src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts create mode 100644 src/apps/browser/ui/MenuItemView.svelte create mode 100644 src/apps/browser/ui/MenuSeparatorView.svelte create mode 100644 src/apps/browser/ui/MenuView.svelte create mode 100644 src/apps/browser/ui/MessageBox.svelte create mode 100644 src/apps/browser/ui/TextInputBox.svelte create mode 100644 src/apps/browser/ui/renderMessageMarkdown.ts create mode 100644 src/apps/browser/ui/renderMessageMarkdown.unit.spec.ts delete mode 100644 src/apps/cli/adapters/NodeStorageAdapter.ts delete mode 100644 src/apps/cli/node-compat.ts delete mode 100644 src/apps/cli/scripts/check-submodule.mjs create mode 100644 src/apps/cli/services/NodeServiceContext.ts create mode 100644 src/apps/cli/services/NodeServiceContext.unit.spec.ts delete mode 100644 src/apps/storagePath.ts delete mode 100644 src/apps/webapp/adapters/FSAPIStorageAdapter.ts create mode 100644 src/common/translation.ts create mode 100644 src/features/P2PSync/P2PReplicator/P2PReplicatorPaneController.ts delete mode 160000 src/lib create mode 100644 src/modules/services/LiveSyncUI/DialogHost.svelte create mode 100644 src/modules/services/LiveSyncUI/components/Check.svelte create mode 100644 src/modules/services/LiveSyncUI/components/Decision.svelte create mode 100644 src/modules/services/LiveSyncUI/components/DialogHeader.svelte create mode 100644 src/modules/services/LiveSyncUI/components/ExtraItems.svelte create mode 100644 src/modules/services/LiveSyncUI/components/Guidance.svelte create mode 100644 src/modules/services/LiveSyncUI/components/InfoNote.svelte create mode 100644 src/modules/services/LiveSyncUI/components/InfoTable.svelte create mode 100644 src/modules/services/LiveSyncUI/components/InputRow.svelte create mode 100644 src/modules/services/LiveSyncUI/components/Instruction.svelte create mode 100644 src/modules/services/LiveSyncUI/components/Option.svelte create mode 100644 src/modules/services/LiveSyncUI/components/Options.svelte create mode 100644 src/modules/services/LiveSyncUI/components/Password.svelte create mode 100644 src/modules/services/LiveSyncUI/components/Question.svelte create mode 100644 src/modules/services/LiveSyncUI/components/UserDecisions.svelte create mode 100644 src/modules/services/LiveSyncUI/dialogues/DialogueToCopy.svelte create mode 100644 src/modules/services/LiveSyncUI/svelteDialog.ts create mode 100644 src/modules/services/ObsidianServiceContext.ts create mode 100644 src/modules/services/ObsidianServiceContext.unit.spec.ts create mode 100644 src/serviceFeatures/setupObsidian/qrCode.ts create mode 100644 src/serviceFeatures/setupObsidian/qrCode.unit.spec.ts create mode 100644 src/serviceFeatures/setupObsidian/setupUri.ts create mode 100644 src/serviceFeatures/setupObsidian/setupUri.unit.spec.ts create mode 100644 src/serviceFeatures/setupObsidian/types.ts create mode 100644 test/contracts/serviceContext.ts delete mode 100644 tsconfig.types.json delete mode 100644 utilsdeno/refactor-cli-node-imports.ts delete mode 100644 utilsdeno/refactor-import-utils.ts delete mode 100644 utilsdeno/refactor-imports.ts delete mode 100644 utilsdeno/types-add-ignore.ts delete mode 100644 vitest.config.rpc-unit.ts diff --git a/.eslintrc b/.eslintrc index f55fe989..7f98b4a6 100644 --- a/.eslintrc +++ b/.eslintrc @@ -20,8 +20,6 @@ "ignorePatterns": [ "**/node_modules/*", "**/jest.config.js", - "src/lib/coverage", - "src/lib/browsertest", "**/test.ts", "**/tests.ts", "**/**test.ts", @@ -56,4 +54,4 @@ } ] } -} \ No newline at end of file +} diff --git a/.github/workflows/cli-deno-tests.yml b/.github/workflows/cli-deno-tests.yml index afcf60e2..4df20bc5 100644 --- a/.github/workflows/cli-deno-tests.yml +++ b/.github/workflows/cli-deno-tests.yml @@ -8,9 +8,6 @@ on: paths: - '.github/workflows/cli-deno-tests.yml' - 'src/apps/cli/**' - - 'src/lib/src/API/processSetting.ts' - - 'src/lib/src/replication/trystero/**' - - 'src/lib/src/rpc/**' - 'test/bench-network/**' - 'package.json' - 'package-lock.json' @@ -18,9 +15,6 @@ on: paths: - '.github/workflows/cli-deno-tests.yml' - 'src/apps/cli/**' - - 'src/lib/src/API/processSetting.ts' - - 'src/lib/src/replication/trystero/**' - - 'src/lib/src/rpc/**' - 'test/bench-network/**' - 'package.json' - 'package-lock.json' @@ -99,8 +93,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Setup Node.js uses: actions/setup-node@v4 @@ -168,8 +160,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Show Docker versions run: | diff --git a/.github/workflows/cli-docker.yml b/.github/workflows/cli-docker.yml index f5204400..7496d8c7 100644 --- a/.github/workflows/cli-docker.yml +++ b/.github/workflows/cli-docker.yml @@ -47,8 +47,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Derive image tag id: meta diff --git a/.github/workflows/cli-e2e.yml b/.github/workflows/cli-e2e.yml index 17388606..6b3fcc4b 100644 --- a/.github/workflows/cli-e2e.yml +++ b/.github/workflows/cli-e2e.yml @@ -23,8 +23,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Setup Node.js uses: actions/setup-node@v4 @@ -64,4 +62,4 @@ jobs: working-directory: src/apps/cli run: | bash ./util/couchdb-stop.sh >/dev/null 2>&1 || true - bash ./util/minio-stop.sh >/dev/null 2>&1 || true \ No newline at end of file + bash ./util/minio-stop.sh >/dev/null 2>&1 || true diff --git a/.github/workflows/cli-p2p-compose-smoke.yml b/.github/workflows/cli-p2p-compose-smoke.yml index 882c8592..4b62bbd0 100644 --- a/.github/workflows/cli-p2p-compose-smoke.yml +++ b/.github/workflows/cli-p2p-compose-smoke.yml @@ -39,8 +39,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Show Docker versions run: | diff --git a/.github/workflows/finalise-release.yml b/.github/workflows/finalise-release.yml index 5e4d5dbf..b9e9e337 100644 --- a/.github/workflows/finalise-release.yml +++ b/.github/workflows/finalise-release.yml @@ -41,7 +41,6 @@ jobs: with: ref: ${{ steps.branch.outputs.name }} fetch-depth: 0 - submodules: recursive - name: Use Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/harness-ci.yml b/.github/workflows/harness-ci.yml index c9fff4a1..bf60e281 100644 --- a/.github/workflows/harness-ci.yml +++ b/.github/workflows/harness-ci.yml @@ -23,8 +23,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Setup Node.js uses: actions/setup-node@v4 @@ -65,4 +63,4 @@ jobs: if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }} - name: Stop test services (Nostr Relay + WebPeer) run: npm run test:docker-p2p:stop - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suitep2p/' }} \ No newline at end of file + if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suitep2p/' }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index f9d4908f..782ce451 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -37,7 +37,6 @@ jobs: with: ref: ${{ inputs.base_branch }} fetch-depth: 0 - submodules: recursive - name: Use Node.js uses: actions/setup-node@v4 @@ -45,11 +44,6 @@ jobs: node-version: "24.x" cache: npm - - name: Use Deno - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - name: Install dependencies run: npm ci @@ -78,9 +72,8 @@ jobs: git switch -c "${BRANCH}" npm version "${VERSION}" --no-git-tag-version node utils/release-notes.mjs prepare "${VERSION}" - npm run build:lib:types - git add package.json package-lock.json manifest.json versions.json updates.md src/apps/cli/package.json src/apps/webpeer/package.json src/apps/webapp/package.json _types + git add package.json package-lock.json manifest.json versions.json updates.md src/apps/cli/package.json src/apps/webpeer/package.json src/apps/webapp/package.json git diff --cached --check git commit -m "Releasing ${VERSION}" git push --set-upstream origin "${BRANCH}" @@ -104,7 +97,7 @@ jobs: - [ ] Review and polish \`updates.md\` - [ ] Confirm the release date - - [ ] Confirm \`manifest.json\`, \`versions.json\`, workspace package versions, and generated \`_types\` + - [ ] Confirm \`manifest.json\`, \`versions.json\`, workspace package versions, and the locked Commonlib package version - [ ] Confirm CI has passed - [ ] Run the finalise release workflow with this PR's fixed head SHA - [ ] Confirm the draft GitHub Release assets and the published CLI image diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d39e5ca1..cf7e87c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,6 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - submodules: recursive ref: ${{ inputs.tag }} - name: Use Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/unit-ci.yml b/.github/workflows/unit-ci.yml index 5d173f33..7bc53deb 100644 --- a/.github/workflows/unit-ci.yml +++ b/.github/workflows/unit-ci.yml @@ -56,8 +56,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Setup Node.js uses: actions/setup-node@v4 @@ -85,8 +83,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.gitignore b/.gitignore index c443c8ed..f883e1a3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,8 @@ cov_profile/** coverage src/apps/cli/dist/* +src/apps/webapp/playwright-report/ +src/apps/webapp/test-results/ _testdata/** utils/bench/splitResults.csv -.eslintcache \ No newline at end of file +.eslintcache diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ea943b62..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "src/lib"] - path = src/lib - url = https://github.com/vrtmrz/livesync-commonlib diff --git a/AGENTS.md b/AGENTS.md index 36c9df53..8438f951 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,7 +45,7 @@ Always adhere to the following stylistic and spelling rules: - **Fast Setup (Simple Fetch)** is the preferred flow for initial replication on secondary devices. It utilises stream-based replication for high speed and delays local file reflection to suppress temporary synchronisation warnings. - **Flag files** (such as `redflag.md`, `redflag2.md`, and `redflag3.md`) at the root of the vault control the boot-up sequence and trigger automated fetch/rebuild tasks. 3. **Subrepositories**: - - The directory [src/lib](file:///Users/vorotamoroz/dev/js/obsidian-livesync/src/lib) is a subrepository (Git submodule) pointing to the shared library `livesync-commonlib`. Do not make modifications inside this directory without careful consideration, as changes affect the shared library. + - Treat `@vrtmrz/livesync-commonlib` as an external, authoritative package. Make Commonlib changes in its repository, validate the packed artefact and downstream LiveSync consumer, and update the exact dependency here. Do not recreate a `src/lib` source mirror or generated `_types` fallback. 4. **Application Directories**: - The directory [src/apps](file:///Users/vorotamoroz/dev/js/obsidian-livesync/src/apps) contains independent application modules: - `cli`: A Command Line Interface application. Tests specifically for the CLI (both unit and End-to-End tests) are located and executed within [src/apps/cli](file:///Users/vorotamoroz/dev/js/obsidian-livesync/src/apps/cli) using its local `package.json` scripts. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 487bc4fc..e5f5e83d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,13 +6,9 @@ Thank you for your interest in contributing to Self-hosted LiveSync! We welcome To set up the development environment, please follow these steps: -1. Clone the repository recursively to ensure all Git submodules are loaded: +1. Clone the repository: ```bash - git clone --recursive https://github.com/vrtmrz/obsidian-livesync - ``` - If you have already cloned the repository without submodules, run the following command: - ```bash - git submodule update --init --recursive + git clone https://github.com/vrtmrz/obsidian-livesync ``` 2. Install the package dependencies: @@ -25,7 +21,7 @@ To set up the development environment, please follow these steps: npm run build ``` -For a more comprehensive guide on development workflows, testing configurations, and subrepos, please refer to [devs.md](devs.md). +For a more comprehensive guide on development workflows, testing configurations, and the Commonlib dependency, please refer to [devs.md](devs.md). ## Guidelines for Contributions @@ -61,9 +57,9 @@ For a detailed list of vocabulary conventions and terms, please refer to [docs/t To add or update translations, please refer to [docs/adding_translations.md](docs/adding_translations.md) for detailed instructions. -### 4. Git Submodules +### 4. Commonlib changes -The `src/lib` directory is a Git submodule pointing to the shared library `livesync-commonlib`. If you wish to propose changes to the shared library, do not modify `src/lib` directly. Instead, please submit a separate pull request to the [livesync-commonlib repository](https://github.com/vrtmrz/livesync-commonlib). +Shared synchronisation behaviour is provided by the `@vrtmrz/livesync-commonlib` package. If you wish to change that library, submit a separate pull request to the [livesync-commonlib repository](https://github.com/vrtmrz/livesync-commonlib), validate its packed artefact, then update the locked dependency in this repository. Do not add a source mirror or generated fallback declarations to this repository. ## License diff --git a/_types/src/LiveSyncBaseCore.d.ts b/_types/src/LiveSyncBaseCore.d.ts deleted file mode 100644 index ee2dcf9d..00000000 --- a/_types/src/LiveSyncBaseCore.d.ts +++ /dev/null @@ -1,136 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; -import type { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "@lib/common/types"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess"; -import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder"; -import type { IFileHandler } from "@lib/interfaces/FileHandler"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess"; -import type { LiveSyncLocalDBEnv } from "@lib/pouchdb/LiveSyncLocalDB"; -import type { LiveSyncCouchDBReplicatorEnv } from "@lib/replication/couchdb/LiveSyncReplicator"; -import type { CheckPointInfo } from "@lib/replication/journal/JournalSyncTypes"; -import type { LiveSyncJournalReplicatorEnv } from "@lib/replication/journal/LiveSyncJournalReplicatorEnv"; -import type { LiveSyncReplicatorEnv } from "@lib/replication/LiveSyncAbstractReplicator"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices"; -import { AbstractModule } from "./modules/AbstractModule"; -import type { ServiceModules } from "@lib/interfaces/ServiceModule"; -import type { Constructor } from "@lib/common/utils.type"; -export declare class LiveSyncBaseCore implements LiveSyncLocalDBEnv, LiveSyncReplicatorEnv, LiveSyncJournalReplicatorEnv, LiveSyncCouchDBReplicatorEnv, HasSettings { - addOns: TCommands[]; - /** - * register an add-onn to the plug-in. - * Add-ons are features that are not essential to the core functionality of the plugin, - * @param addOn - */ - private _registerAddOn; - /** - * Get an add-on by its class name. Returns undefined if not found. - * @param cls - * @returns - */ - getAddOn(cls: string): T | undefined; - constructor(serviceHub: InjectableServiceHub, serviceModuleInitialiser: (core: LiveSyncBaseCore, serviceHub: InjectableServiceHub) => ServiceModules, extraModuleInitialiser: (core: LiveSyncBaseCore) => AbstractModule[], addOnsInitialiser: (core: LiveSyncBaseCore) => TCommands[], featuresInitialiser: (core: LiveSyncBaseCore) => void); - /** - * The service hub for managing all services. - */ - _services: InjectableServiceHub | undefined; - get services(): InjectableServiceHub; - /** - * Service Modules - */ - protected _serviceModules: ServiceModules; - get serviceModules(): ServiceModules; - /** - * The modules of the plug-in. Modules are responsible for specific features or functionalities of the plug-in, such as file handling, conflict resolution, replication, etc. - */ - private modules; - /** - * Get a module by its class. Throws an error if not found. - * Mostly used for getting SetupManager. - * @param constructor - * @returns - */ - getModule(constructor: Constructor): T; - /** - * Register a module to the plug-in. - * @param module The module to register. - */ - private _registerModule; - registerModules(extraModules?: AbstractModule[]): void; - /** - * Bind module functions to services. - */ - bindModuleFunctions(): void; - /** - * @obsolete Use services.UI.confirm instead. The confirm function to show a confirmation dialog to the user. - */ - get confirm(): Confirm; - /** - * @obsolete Use services.setting.currentSettings instead. The current settings of the plug-in. - */ - get settings(): ObsidianLiveSyncSettings; - /** - * @obsolete Use services.setting.settings instead. Set the settings of the plug-in. - */ - set settings(value: ObsidianLiveSyncSettings); - /** - * @obsolete Use services.setting.currentSettings instead. Get the settings of the plug-in. - * @returns The current settings of the plug-in. - */ - getSettings(): ObsidianLiveSyncSettings; - /** - * @obsolete Use services.database.localDatabase instead. The local database instance. - */ - get localDatabase(): import("@lib/pouchdb/LiveSyncLocalDB").LiveSyncLocalDB; - /** - * @obsolete Use services.database.localDatabase instead. Get the PouchDB database instance. Note that this is not the same as the local database instance, which is a wrapper around the PouchDB database. - * @returns The PouchDB database instance. - */ - getDatabase(): PouchDB.Database; - /** - * @obsolete Use services.keyValueDB.simpleStore instead. A simple key-value store for storing non-file data, such as checkpoints, sync status, etc. - */ - get simpleStore(): SimpleStore; - /** - * @obsolete Use services.replication.getActiveReplicator instead. Get the active replicator instance. Note that there can be multiple replicators, but only one can be active at a time. - */ - get replicator(): import("@lib/replication/LiveSyncAbstractReplicator").LiveSyncAbstractReplicator; - /** - * @obsolete Use services.keyValueDB.kvDB instead. Get the key-value database instance. This is used for storing large data that cannot be stored in the simple store, such as file metadata, etc. - */ - get kvDB(): import("./lib/src/interfaces/KeyValueDatabase").KeyValueDatabase; - /** - * Storage Accessor for handling file operations. - * @obsolete Use serviceModules.storageAccess instead. - */ - get storageAccess(): StorageAccess; - /** - * Database File Accessor for handling file operations related to the database, such as exporting the database, importing from a file, etc. - * @obsolete Use serviceModules.databaseFileAccess instead. - */ - get databaseFileAccess(): DatabaseFileAccess; - /** - * File Handler for handling file operations related to replication, such as resolving conflicts, applying changes from replication, etc. - * @obsolete Use serviceModules.fileHandler instead. - */ - get fileHandler(): IFileHandler; - /** - * Rebuilder for handling database rebuilding operations. - * @obsolete Use serviceModules.rebuilder instead. - */ - get rebuilder(): Rebuilder; - /** - * Initialise ServiceFeatures. - * (Please refer `serviceFeatures` for more details) - */ - initialiseServiceFeatures(): void; -} -export interface IMinimumLiveSyncCommands { - onunload(): void; - onload(): void | Promise; - constructor: { - name: string; - }; -} diff --git a/_types/src/common/KeyValueDB.d.ts b/_types/src/common/KeyValueDB.d.ts deleted file mode 100644 index 4658cdba..00000000 --- a/_types/src/common/KeyValueDB.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase.ts"; -export { OpenKeyValueDatabase } from "./KeyValueDBv2.ts"; -export declare const _OpenKeyValueDatabase: (dbKey: string) => Promise; diff --git a/_types/src/common/KeyValueDBv2.d.ts b/_types/src/common/KeyValueDBv2.d.ts deleted file mode 100644 index cf86dab1..00000000 --- a/_types/src/common/KeyValueDBv2.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase"; -import { type IDBPDatabase } from "idb"; -export declare function OpenKeyValueDatabase(dbKey: string): Promise; -export declare class IDBKeyValueDatabase implements KeyValueDatabase { - protected _dbPromise: Promise> | null; - protected dbKey: string; - protected storeKey: string; - protected _isDestroyed: boolean; - protected destroyedPromise: Promise | null; - get isDestroyed(): boolean; - get ensuredDestroyed(): Promise; - getIsReady(): Promise; - protected ensureDB(): Promise>; - protected closeDB(setDestroyed?: boolean): Promise; - get DB(): Promise>; - constructor(dbKey: string); - get(key: IDBValidKey): Promise; - set(key: IDBValidKey, value: U): Promise; - del(key: IDBValidKey): Promise; - clear(): Promise; - keys(query?: IDBValidKey | IDBKeyRange, count?: number): Promise; - close(): Promise; - destroy(): Promise; -} diff --git a/_types/src/common/PeriodicProcessor.d.ts b/_types/src/common/PeriodicProcessor.d.ts deleted file mode 100644 index 0f291d06..00000000 --- a/_types/src/common/PeriodicProcessor.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -type PeriodicProcessorHost = NecessaryServices<"API" | "control", never>; -export declare class PeriodicProcessor { - _process: () => Promise; - _timer?: number; - _core: PeriodicProcessorHost; - constructor(core: PeriodicProcessorHost, process: () => Promise); - process(): Promise; - enable(interval: number): void; - disable(): void; -} -export {}; diff --git a/_types/src/common/SvelteItemView.d.ts b/_types/src/common/SvelteItemView.d.ts deleted file mode 100644 index f32c6632..00000000 --- a/_types/src/common/SvelteItemView.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ItemView } from "@/deps.ts"; -import { type mount } from "svelte"; -export declare abstract class SvelteItemView extends ItemView { - abstract instantiateComponent(target: HTMLElement): ReturnType | Promise>; - component?: ReturnType; - onOpen(): Promise; - _dismountComponent(): Promise; - onClose(): Promise; -} diff --git a/_types/src/common/events.d.ts b/_types/src/common/events.d.ts deleted file mode 100644 index 514419c9..00000000 --- a/_types/src/common/events.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { eventHub } from "@lib/hub/hub"; -export declare const EVENT_PLUGIN_LOADED = "plugin-loaded"; -export declare const EVENT_PLUGIN_UNLOADED = "plugin-unloaded"; -export declare const EVENT_FILE_SAVED = "file-saved"; -export declare const EVENT_LEAF_ACTIVE_CHANGED = "leaf-active-changed"; -export declare const EVENT_REQUEST_OPEN_SETTINGS = "request-open-settings"; -export declare const EVENT_REQUEST_OPEN_SETTING_WIZARD = "request-open-setting-wizard"; -export declare const EVENT_REQUEST_OPEN_SETUP_URI = "request-open-setup-uri"; -export declare const EVENT_REQUEST_COPY_SETUP_URI = "request-copy-setup-uri"; -export declare const EVENT_REQUEST_SHOW_SETUP_QR = "request-show-setup-qr"; -export declare const EVENT_REQUEST_RELOAD_SETTING_TAB = "reload-setting-tab"; -export declare const EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG = "request-open-plugin-sync-dialog"; -export declare const EVENT_REQUEST_RUN_DOCTOR = "request-run-doctor"; -export declare const EVENT_REQUEST_RUN_FIX_INCOMPLETE = "request-run-fix-incomplete"; -export declare const EVENT_ANALYSE_DB_USAGE = "analyse-db-usage"; -export declare const EVENT_REQUEST_PERFORM_GC_V3 = "request-perform-gc-v3"; -declare global { - interface LSEvents { - [EVENT_PLUGIN_LOADED]: undefined; - [EVENT_PLUGIN_UNLOADED]: undefined; - [EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG]: undefined; - [EVENT_REQUEST_OPEN_SETTINGS]: undefined; - [EVENT_REQUEST_OPEN_SETTING_WIZARD]: undefined; - [EVENT_REQUEST_RELOAD_SETTING_TAB]: undefined; - [EVENT_LEAF_ACTIVE_CHANGED]: undefined; - [EVENT_REQUEST_OPEN_SETUP_URI]: undefined; - [EVENT_REQUEST_COPY_SETUP_URI]: undefined; - [EVENT_REQUEST_SHOW_SETUP_QR]: undefined; - [EVENT_REQUEST_RUN_DOCTOR]: string; - [EVENT_REQUEST_RUN_FIX_INCOMPLETE]: undefined; - [EVENT_ANALYSE_DB_USAGE]: undefined; - [EVENT_REQUEST_PERFORM_GC_V3]: undefined; - } -} -export * from "@lib/events/coreEvents.ts"; -export { eventHub }; diff --git a/_types/src/common/obsidianEvents.d.ts b/_types/src/common/obsidianEvents.d.ts deleted file mode 100644 index 53316f35..00000000 --- a/_types/src/common/obsidianEvents.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { TFile } from "@/deps"; -import type { FilePathWithPrefix, LoadedEntry } from "@lib/common/types"; -export declare const EVENT_REQUEST_SHOW_HISTORY = "show-history"; -declare global { - interface LSEvents { - [EVENT_REQUEST_SHOW_HISTORY]: { - file: TFile; - fileOnDB: LoadedEntry; - } | { - file: FilePathWithPrefix; - fileOnDB: LoadedEntry; - }; - } -} diff --git a/_types/src/common/reportTool.d.ts b/_types/src/common/reportTool.d.ts deleted file mode 100644 index 9e67a767..00000000 --- a/_types/src/common/reportTool.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type"; -import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore"; -export declare function generateReport(settings: ObsidianLiveSyncSettings, core: LiveSyncBaseCore): Promise<{ - obsidianInfo: { - navigator: string; - fileSystem: string; - }; - responseConfig: Record; - pluginConfig: ObsidianLiveSyncSettings; - manifestVersion: string; - packageVersion: string; -}>; diff --git a/_types/src/common/stores.d.ts b/_types/src/common/stores.d.ts deleted file mode 100644 index f49554d9..00000000 --- a/_types/src/common/stores.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { PersistentMap } from "octagonal-wheels/dataobject/PersistentMap"; -export declare let sameChangePairs: PersistentMap; -export declare function initializeStores(vaultName: string): void; diff --git a/_types/src/common/types.d.ts b/_types/src/common/types.d.ts deleted file mode 100644 index 02d6a40c..00000000 --- a/_types/src/common/types.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type PluginManifest, TFile } from "@/deps.ts"; -import { type DatabaseEntry, type EntryBody, type FilePath } from "@lib/common/types.ts"; -export type { CacheData, FileEventItem } from "@lib/common/types.ts"; -export interface PluginDataEntry extends DatabaseEntry { - deviceVaultName: string; - mtime: number; - manifest: PluginManifest; - mainJs: string; - manifestJson: string; - styleCss?: string; - dataJson?: string; - _conflicts?: string[]; - type: "plugin"; -} -export interface PluginList { - [key: string]: PluginDataEntry[]; -} -export interface DevicePluginList { - [key: string]: PluginDataEntry; -} -export declare const PERIODIC_PLUGIN_SWEEP = 60; -export interface InternalFileInfo { - path: FilePath; - mtime: number; - ctime: number; - size: number; - deleted?: boolean; -} -export interface FileInfo { - path: FilePath; - mtime: number; - ctime: number; - size: number; - deleted?: boolean; - file: TFile; -} -export type queueItem = { - entry: EntryBody; - missingChildren: string[]; - timeout?: number; - done?: boolean; - warned?: boolean; -}; -export declare const FileWatchEventQueueMax = 10; -export { configURIBase, configURIBaseQR } from "@lib/common/types.ts"; -export { CHeader, PSCHeader, PSCHeaderEnd, ICHeader, ICHeaderEnd, ICHeaderLength, ICXHeader, } from "@lib/common/models/fileaccess.const.ts"; diff --git a/_types/src/common/utils.d.ts b/_types/src/common/utils.d.ts deleted file mode 100644 index 3e334a4c..00000000 --- a/_types/src/common/utils.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { TAbstractFile } from "@/deps.ts"; -import { type AnyEntry, type CouchDBCredentials, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix, type UXFileInfo, type UXFileInfoStub } from "@lib/common/types.ts"; -export { ICHeader, ICXHeader } from "./types.ts"; -import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase.ts"; -export { scheduleTask, cancelTask, cancelAllTasks } from "octagonal-wheels/concurrency/task"; -export declare function path2id(filename: FilePathWithPrefix | FilePath, obfuscatePassphrase: string | false, caseInsensitive: boolean): Promise; -export declare function id2path(id: DocumentID, entry?: EntryHasPath): FilePathWithPrefix; -export declare function getPathFromTFile(file: TAbstractFile): FilePath; -import { isInternalFile, getPathFromUXFileInfo, getStoragePathFromUXFileInfo, getDatabasePathFromUXFileInfo } from "@lib/common/typeUtils.ts"; -export { isInternalFile, getPathFromUXFileInfo, getStoragePathFromUXFileInfo, getDatabasePathFromUXFileInfo }; -export declare function memoObject(key: string, obj: T): T; -export declare function memoIfNotExist(key: string, func: () => T | Promise): Promise; -export declare function retrieveMemoObject(key: string): T | false; -export declare function disposeMemoObject(key: string): void; -export declare function isValidPath(filename: string): boolean; -export declare function trimPrefix(target: string, prefix: string): string; -export { isInternalMetadata, id2InternalMetadataId, isChunk, isCustomisationSyncMetadata, isPluginMetadata, stripInternalMetadataPrefix, } from "@lib/common/typeUtils.ts"; -export declare const _requestToCouchDBFetch: (baseUri: string, username: string, password: string, path?: string, body?: unknown, method?: string) => Promise; -export declare const _requestToCouchDB: (baseUri: string, credentials: CouchDBCredentials, origin: string, path?: string, body?: unknown, method?: string, customHeaders?: Record) => Promise; -/** - * @deprecated Use requestToCouchDBWithCredentials instead. - */ -export declare const requestToCouchDB: (baseUri: string, username: string, password: string, origin?: string, key?: string, body?: string, method?: string, customHeaders?: Record) => Promise; -export declare function requestToCouchDBWithCredentials(baseUri: string, credentials: CouchDBCredentials, origin?: string, key?: string, body?: string, method?: string, customHeaders?: Record): Promise; -import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols.ts"; -export { BASE_IS_NEW, EVEN, TARGET_IS_NEW }; -import { compareMTime } from "@lib/common/utils.ts"; -export { compareMTime }; -export declare function markChangesAreSame(file: AnyEntry | string | UXFileInfoStub, mtime1: number, mtime2: number): true | undefined; -export declare function unmarkChanges(file: AnyEntry | string | UXFileInfoStub): void; -export declare function isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | string, mtimes: number[]): typeof EVEN | undefined; -export declare function compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN; -export type MemoOption = { - key: string; - forceUpdate?: boolean; - validator?: (context: Map) => boolean; -}; -export declare function useMemo({ key, forceUpdate, validator }: MemoOption, updateFunc: (context: Map, prev: T) => T): T; -export declare function useStatic(key: string): { - value: T | undefined; -}; -export declare function useStatic(key: string, initial: T): { - value: T; -}; -export declare function disposeMemo(key: string): void; -export declare function disposeAllMemo(): void; -export declare function getLogLevel(showNotice: boolean): 32 | 64; -export type MapLike = { - set(key: K, value: V): Map; - clear(): void; - delete(key: K): boolean; - get(key: K): V | undefined; - has(key: K): boolean; - keys: () => IterableIterator; - get size(): number; -}; -export declare function autosaveCache(db: KeyValueDatabase, mapKey: string): Promise>; -export declare function onlyInNTimes(n: number, proc: (progress: number) => unknown): () => void; -export { displayRev } from "@lib/common/utils.ts"; diff --git a/_types/src/deps.d.ts b/_types/src/deps.d.ts deleted file mode 100644 index 5bf2cb25..00000000 --- a/_types/src/deps.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type FilePath } from "@lib/common/types.ts"; -export { addIcon, App, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginSettingTab, requestUrl, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder, parseYaml, ItemView, WorkspaceLeaf, Menu, request, getLanguage, ButtonComponent, TextComponent, ToggleComponent, DropdownComponent, Component, } from "obsidian"; -export type { DataWriteOptions, PluginManifest, RequestUrlParam, RequestUrlResponse, MarkdownFileInfo, ListedFiles, ValueComponent, Stat, Command, ViewCreator, } from "obsidian"; -declare const normalizePath: (from: T) => T; -export { normalizePath }; -export { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch"; diff --git a/_types/src/features/ConfigSync/CmdConfigSync.d.ts b/_types/src/features/ConfigSync/CmdConfigSync.d.ts deleted file mode 100644 index 30c5eb94..00000000 --- a/_types/src/features/ConfigSync/CmdConfigSync.d.ts +++ /dev/null @@ -1,147 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type PluginManifest } from "@/deps.ts"; -import type { EntryDoc, LoadedEntry, FilePathWithPrefix, FilePath, AnyEntry } from "@lib/common/types.ts"; -import { LiveSyncCommands } from "@/features/LiveSyncCommands.ts"; -import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts"; -import { QueueProcessor } from "octagonal-wheels/concurrency/processor"; -import type ObsidianLiveSyncPlugin from "@/main.ts"; -import { PluginDialogModal } from "./PluginDialogModal.ts"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts"; -import type { LiveSyncCore } from "@/main.ts"; -declare global { - interface OPTIONAL_SYNC_FEATURES { - DISABLE: "DISABLE"; - CUSTOMIZE: "CUSTOMIZE"; - DISABLE_CUSTOM: "DISABLE_CUSTOM"; - } -} -export declare const pluginList: import("svelte/store").Writable; -export declare const pluginIsEnumerating: import("svelte/store").Writable; -export declare const pluginV2Progress: import("svelte/store").Writable; -export type PluginDataExFile = { - filename: string; - data: string[]; - mtime: number; - size: number; - version?: string; - hash?: string; - displayName?: string; -}; -export interface IPluginDataExDisplay { - documentPath: FilePathWithPrefix; - category: string; - name: string; - term: string; - displayName?: string; - files: (LoadedEntryPluginDataExFile | PluginDataExFile)[]; - version?: string; - mtime: number; -} -export type PluginDataExDisplay = { - documentPath: FilePathWithPrefix; - category: string; - name: string; - term: string; - displayName?: string; - files: PluginDataExFile[]; - version?: string; - mtime: number; -}; -type LoadedEntryPluginDataExFile = LoadedEntry & PluginDataExFile; -export declare const pluginManifests: Map; -export declare const pluginManifestStore: import("svelte/store").Writable>; -export declare class PluginDataExDisplayV2 { - documentPath: FilePathWithPrefix; - category: string; - term: string; - files: LoadedEntryPluginDataExFile[]; - name: string; - confKey: string; - constructor(data: IPluginDataExDisplay); - setFile(file: LoadedEntryPluginDataExFile): Promise; - deleteFile(filename: string): void; - _displayName: string | undefined; - _version: string | undefined; - applyLoadedManifest(): void; - get displayName(): string; - get version(): string | undefined; - get mtime(): number; -} -export type PluginDataEx = { - documentPath?: FilePathWithPrefix; - category: string; - name: string; - displayName?: string; - term: string; - files: PluginDataExFile[]; - version?: string; - mtime: number; -}; -export declare class ConfigSync extends LiveSyncCommands { - constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore); - get configDir(): string; - get kvDB(): import("../../lib/src/interfaces/KeyValueDatabase.ts").KeyValueDatabase; - get useV2(): boolean; - get useSyncPluginEtc(): boolean; - isThisModuleEnabled(): boolean; - pluginDialog?: PluginDialogModal; - periodicPluginSweepProcessor: PeriodicProcessor; - pluginList: IPluginDataExDisplay[]; - showPluginSyncModal(): void; - hidePluginSyncModal(): void; - onunload(): void; - addRibbonIcon: (icon: string, title: string, callback: (evt: MouseEvent) => unknown) => HTMLElement; - onload(): void; - getFileCategory(filePath: string): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | ""; - isTargetPath(filePath: string): boolean; - private _everyOnDatabaseInitialized; - _everyBeforeReplicate(showNotice: boolean): Promise; - _everyOnResumeProcess(): Promise; - _everyAfterResumeProcess(): Promise; - reloadPluginList(showMessage: boolean): Promise; - loadPluginData(path: FilePathWithPrefix): Promise; - pluginScanProcessor: QueueProcessor; - pluginScanProcessorV2: QueueProcessor; - filenameToUnifiedKey(path: string, termOverRide?: string): FilePathWithPrefix; - filenameWithUnifiedKey(path: string, termOverRide?: string): FilePathWithPrefix; - unifiedKeyPrefixOfTerminal(termOverRide?: string): FilePathWithPrefix; - parseUnifiedPath(unifiedPath: FilePathWithPrefix): { - category: string; - device: string; - key: string; - filename: string; - pathV1: FilePathWithPrefix; - }; - loadedManifest_mTime: Map; - createPluginDataExFileV2(unifiedPathV2: FilePathWithPrefix, loaded?: LoadedEntry): Promise; - createPluginDataFromV2(unifiedPathV2: FilePathWithPrefix): PluginDataExDisplayV2 | undefined; - updatingV2Count: number; - updatePluginListV2(showMessage: boolean, unifiedFilenameWithKey: FilePathWithPrefix): Promise; - migrateV1ToV2(showMessage: boolean, entry: AnyEntry): Promise; - updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise; - compareUsingDisplayData(dataA: IPluginDataExDisplay, dataB: IPluginDataExDisplay, compareEach?: boolean): Promise; - applyDataV2(data: PluginDataExDisplayV2, content?: string): Promise; - applyData(data: IPluginDataExDisplay, content?: string): Promise; - deleteData(data: PluginDataEx): Promise; - _anyModuleParsedReplicationResultItem(docs: PouchDB.Core.ExistingDocument): Promise; - _everyRealizeSettingSyncMode(): Promise; - recentProcessedInternalFiles: string[]; - makeEntryFromFile(path: FilePath): Promise; - storeCustomisationFileV2(path: FilePath, term: string, force?: boolean): Promise; - storeCustomizationFiles(path: FilePath, termOverRide?: string): Promise; - _anyProcessOptionalFileEvent(path: FilePath): Promise; - watchVaultRawEventsAsync(path: FilePath): Promise; - scanAllConfigFiles(showMessage: boolean): Promise; - deleteConfigOnDatabase(prefixedFileName: FilePathWithPrefix, forceWrite?: boolean): Promise; - scanInternalFiles(): Promise; - private _allAskUsingOptionalSyncFeature; - private __askHiddenFileConfiguration; - _anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise; - private _allSuspendExtraSync; - private _allConfigureOptionalSyncFeature; - configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES): Promise; - getFiles(path: string, lastDepth: number): Promise; - onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void; -} -export {}; diff --git a/_types/src/features/ConfigSync/PluginDialogModal.d.ts b/_types/src/features/ConfigSync/PluginDialogModal.d.ts deleted file mode 100644 index d0fcf679..00000000 --- a/_types/src/features/ConfigSync/PluginDialogModal.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { mount } from "svelte"; -import { App, Modal } from "@/deps.ts"; -import ObsidianLiveSyncPlugin from "@/main.ts"; -export declare class PluginDialogModal extends Modal { - plugin: ObsidianLiveSyncPlugin; - component: ReturnType | undefined; - isOpened(): boolean; - constructor(app: App, plugin: ObsidianLiveSyncPlugin); - onOpen(): void; - onClose(): void; -} diff --git a/_types/src/features/HiddenFileCommon/JsonResolveModal.d.ts b/_types/src/features/HiddenFileCommon/JsonResolveModal.d.ts deleted file mode 100644 index 992829d4..00000000 --- a/_types/src/features/HiddenFileCommon/JsonResolveModal.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { App, Modal } from "@/deps.ts"; -import { type FilePath, type LoadedEntry } from "@lib/common/types.ts"; -import { mount } from "svelte"; -export declare class JsonResolveModal extends Modal { - filename: FilePath; - callback?: (keepRev?: string, mergedStr?: string) => Promise; - docs: LoadedEntry[]; - component?: ReturnType; - nameA: string; - nameB: string; - defaultSelect: string; - keepOrder: boolean; - hideLocal: boolean; - title: string; - constructor(app: App, filename: FilePath, docs: LoadedEntry[], callback: (keepRev?: string, mergedStr?: string) => Promise, nameA?: string, nameB?: string, defaultSelect?: string, keepOrder?: boolean, hideLocal?: boolean, title?: string); - UICallback(keepRev?: string, mergedStr?: string): Promise; - onOpen(): void; - onClose(): void; -} diff --git a/_types/src/features/HiddenFileSync/CmdHiddenFileSync.d.ts b/_types/src/features/HiddenFileSync/CmdHiddenFileSync.d.ts deleted file mode 100644 index 805773a2..00000000 --- a/_types/src/features/HiddenFileSync/CmdHiddenFileSync.d.ts +++ /dev/null @@ -1,154 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type LoadedEntry, type FilePathWithPrefix, type FilePath, type DocumentID, type UXFileInfo, type UXStat, type MetaEntry, type UXDataWriteOptions } from "@lib/common/types.ts"; -import { type InternalFileInfo } from "@/common/types.ts"; -import { type CustomRegExp } from "@lib/common/utils.ts"; -import { type MapLike } from "@/common/utils.ts"; -import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts"; -import { LiveSyncCommands } from "@/features/LiveSyncCommands.ts"; -import { QueueProcessor } from "octagonal-wheels/concurrency/processor"; -import type { LiveSyncCore } from "@/main.ts"; -type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce"; -declare global { - interface OPTIONAL_SYNC_FEATURES { - FETCH: "FETCH"; - OVERWRITE: "OVERWRITE"; - MERGE: "MERGE"; - DISABLE: "DISABLE"; - DISABLE_HIDDEN: "DISABLE_HIDDEN"; - } -} -export declare class HiddenFileSync extends LiveSyncCommands { - isThisModuleEnabled(): boolean; - periodicInternalFileScanProcessor: PeriodicProcessor; - get kvDB(): import("../../lib/src/interfaces/KeyValueDatabase.ts").KeyValueDatabase; - getConflictedDoc(path: FilePathWithPrefix, rev: string): Promise; - onunload(): void; - onload(): void; - private _everyOnDatabaseInitialized; - _everyBeforeReplicate(showNotice: boolean): Promise; - private _everyOnloadAfterLoadSettings; - updateSettingCache(): void; - isReady(): boolean; - performStartupScan(showNotice: boolean): Promise; - _everyOnResumeProcess(): Promise; - _everyRealizeSettingSyncMode(): Promise; - _anyProcessOptionalFileEvent(path: FilePath): Promise; - _anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise; - _anyProcessOptionalSyncFiles(doc: LoadedEntry): Promise; - loadFileWithInfo(path: FilePath): Promise; - _fileInfoLastProcessed: MapLike; - _fileInfoLastKnown: MapLike; - _databaseInfoLastProcessed: MapLike; - statToKey(stat: UXStat | null): string; - docToKey(doc: LoadedEntry | MetaEntry): string; - fileToStatKey(file: FilePath, stat?: UXStat | null): Promise; - updateLastProcessedFile(file: FilePath, keySrc: string | UXStat): void; - updateLastProcessedAsActualFile(file: FilePath, stat?: UXStat | null): Promise; - resetLastProcessedFile(targetFiles: FilePath[] | false): void; - getLastProcessedFileMTime(file: FilePath): number; - getLastProcessedFileKey(file: FilePath): string | undefined; - getLastProcessedDatabaseKey(file: FilePath): string | undefined; - updateLastProcessedDatabase(file: FilePath, keySrc: string | MetaEntry | LoadedEntry): void; - updateLastProcessed(path: FilePath, db: MetaEntry | LoadedEntry, stat: UXStat): void; - updateLastProcessedDeletion(path: FilePath, db: MetaEntry | LoadedEntry | false): void; - ensureDir(path: FilePath): Promise; - writeFile(path: FilePath, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise; - __removeFile(path: FilePath): Promise<"OK" | "ALREADY" | false>; - triggerEvent(path: FilePath): Promise; - updateLastProcessedAsActualDatabase(file: FilePath, doc?: MetaEntry | LoadedEntry | null | false): Promise; - resetLastProcessedDatabase(targetFiles: FilePath[] | false): void; - adoptCurrentStorageFilesAsProcessed(targetFiles: FilePath[] | false): Promise; - adoptCurrentDatabaseFilesAsProcessed(targetFiles: FilePath[] | false): Promise; - semaphore: import("octagonal-wheels/concurrency/semaphore_v2").SemaphoreObject; - serializedForEvent(file: FilePath, fn: () => Promise): Promise; - useStorageFiles(files: FilePath[], showNotice?: boolean, onlyNew?: boolean): Promise; - trackScannedStorageChanges(processFiles: FilePath[], showNotice?: boolean, onlyNew?: boolean, forceWriteAll?: boolean, includeDeleted?: boolean): Promise; - scanAllStorageChanges(showNotice?: boolean, onlyNew?: boolean, forceWriteAll?: boolean, includeDeleted?: boolean): Promise; - /** - * check the file is changed or not, and if changed, process it. - */ - trackStorageFileModification(path: FilePath, onlyNew?: boolean, forceWrite?: boolean, includeDeleted?: boolean): Promise; - pendingConflictChecks: Set; - queueConflictCheck(path: FilePathWithPrefix): void; - finishConflictCheck(path: FilePathWithPrefix): void; - requeueConflictCheck(path: FilePathWithPrefix): void; - resolveConflictOnInternalFiles(): Promise; - resolveByNewerEntry(id: DocumentID, path: FilePathWithPrefix, currentDoc: MetaEntry, currentRev: string, conflictedRev: string): Promise; - conflictResolutionProcessor: QueueProcessor; - showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise; - getDocProps(doc: LoadedEntry): { - id: DocumentID; - rev: string | undefined; - revDisplay: string; - prefixedPath: FilePathWithPrefix; - path: FilePath; - isDeleted: boolean; - shortenedId: string; - shortenedPath: string; - }; - processReplicationResult(doc: LoadedEntry): Promise; - cacheFileRegExps: Map; - /** - * Parses the regular expression settings for hidden file synchronization. - * @returns An object containing the ignore and target filters. - */ - parseRegExpSettings(): { - ignoreFilter: CustomRegExp[]; - targetFilter: CustomRegExp[]; - }; - /** - * Checks if the target file path matches the defined patterns. - */ - isTargetFileInPatterns(path: string): boolean; - cacheCustomisationSyncIgnoredFiles: Map; - /** - * Gets the list of files ignored for customization synchronization. - * @returns An array of ignored file paths (lowercase). - */ - getCustomisationSynchronizationIgnoredFiles(): string[]; - /** - * Checks if the given path is not ignored by customization synchronization. - * @param path The file path to check. - * @returns True if the path is not ignored; otherwise, false. - */ - isNotIgnoredByCustomisationSync(path: string): boolean; - isHiddenFileSyncHandlingPath(path: FilePath): boolean; - isTargetFile(path: FilePath): Promise; - trackScannedDatabaseChange(processFiles: MetaEntry[], showNotice?: boolean, onlyNew?: boolean, forceWriteAll?: boolean, includeDeletion?: boolean): Promise; - applyOfflineChanges(showNotice: boolean): Promise; - scanAllDatabaseChanges(showNotice?: boolean, onlyNew?: boolean, forceWriteAll?: boolean, includeDeletion?: boolean): Promise; - useDatabaseFiles(files: MetaEntry[], showNotice?: boolean, onlyNew?: boolean): Promise; - trackDatabaseFileModification(path: FilePath, headerLine: string, preventDoubleProcess?: boolean, onlyNew?: boolean, meta?: MetaEntry | false, includeDeletion?: boolean): Promise; - queuedNotificationFiles: Set; - notifyConfigChange(): void; - queueNotification(key: FilePath): void; - rebuildMerging(showNotice: boolean, targetFiles?: FilePath[] | false): Promise; - rebuildFromStorage(showNotice: boolean, targetFiles?: FilePath[] | false, onlyNew?: boolean): Promise; - getAllDatabaseFiles(): Promise; - rebuildFromDatabase(showNotice: boolean, targetFiles?: FilePath[] | false, onlyNew?: boolean): Promise; - initialiseInternalFileSync(direction: SyncDirection, showMessage: boolean, targetFilesSrc?: string[] | false): Promise; - __loadBaseSaveData(file: FilePath, includeContent?: boolean): Promise; - storeInternalFileToDatabase(file: InternalFileInfo | UXFileInfo, forceWrite?: boolean): Promise; - deleteInternalFileOnDatabase(filenameSrc: FilePath, forceWrite?: boolean): Promise; - extractInternalFileFromDatabase(storageFilePath: FilePath, force?: boolean, metaEntry?: MetaEntry | LoadedEntry, preventDoubleProcess?: boolean, onlyNew?: boolean, includeDeletion?: boolean): Promise; - __checkIsNeedToWriteFile(storageFilePath: FilePath, content: string | ArrayBuffer): Promise; - __writeFile(storageFilePath: FilePath, fileOnDB: LoadedEntry, force: boolean): Promise; - __deleteFile(storageFilePath: FilePath): Promise; - private _allAskUsingOptionalSyncFeature; - private __askHiddenFileConfiguration; - private _allSuspendExtraSync; - private _allConfigureOptionalSyncFeature; - configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES): Promise; - scanInternalFileNames(): Promise; - scanInternalFiles(): Promise; - getFiles(path: string, checkFunction: (path: FilePath) => Promise | boolean): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} -export {}; diff --git a/_types/src/features/HiddenFileSync/configureHiddenFileSyncMode.d.ts b/_types/src/features/HiddenFileSync/configureHiddenFileSyncMode.d.ts deleted file mode 100644 index 9c5fe849..00000000 --- a/_types/src/features/HiddenFileSync/configureHiddenFileSyncMode.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -type HiddenFileSyncDirection = "pullForce" | "pushForce" | "safe"; -type ConfigureHiddenFileSyncHandlers = { - disable: () => Promise; - enable: () => Promise; - initialise: (direction: HiddenFileSyncDirection) => Promise; -}; -export type ConfigureHiddenFileSyncResult = "ignored" | "disabled" | "enabled"; -export declare function configureHiddenFileSyncMode(mode: keyof OPTIONAL_SYNC_FEATURES, handlers: ConfigureHiddenFileSyncHandlers): Promise; -export {}; diff --git a/_types/src/features/LiveSyncCommands.d.ts b/_types/src/features/LiveSyncCommands.d.ts deleted file mode 100644 index acc3e4ef..00000000 --- a/_types/src/features/LiveSyncCommands.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type AnyEntry, type DocumentID, type FilePath, type FilePathWithPrefix, type LOG_LEVEL } from "@lib/common/types.ts"; -import type ObsidianLiveSyncPlugin from "@/main.ts"; -import type { LiveSyncCore } from "@/main.ts"; -import { createInstanceLogFunction } from "@lib/services/lib/logUtils.ts"; -export declare abstract class LiveSyncCommands { - /** - * @deprecated This class is deprecated. Please use core - */ - plugin: ObsidianLiveSyncPlugin; - core: LiveSyncCore; - get app(): import("obsidian").App; - get settings(): import("@lib/common/types.ts").ObsidianLiveSyncSettings; - get localDatabase(): import("../lib/src/pouchdb/LiveSyncLocalDB").LiveSyncLocalDB; - get services(): import("../lib/src/services/InjectableServices").InjectableServiceHub; - path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise; - getPath(entry: AnyEntry): FilePathWithPrefix; - constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore); - abstract onunload(): void; - abstract onload(): void | Promise; - _isMainReady(): boolean; - _isMainSuspended(): boolean; - _isDatabaseReady(): boolean; - _log: ReturnType; - _verbose: (msg: unknown, key?: string) => void; - _info: (msg: unknown, key?: string) => void; - _notice: (msg: unknown, key?: string) => void; - _progress: (prefix?: string, level?: LOG_LEVEL) => { - log: (msg: string) => void; - once: (msg: string) => void; - done: (msg?: string) => void; - }; - _debug: (msg: unknown, key?: string) => void; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.d.ts b/_types/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.d.ts deleted file mode 100644 index d249ec1d..00000000 --- a/_types/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type DocumentID, type EntryDoc, type EntryLeaf } from "@lib/common/types"; -import { LiveSyncCommands } from "@/features/LiveSyncCommands"; -type ChunkID = DocumentID; -type NoteDocumentID = DocumentID; -type Rev = string; -type ChunkUsageMap = Map>>; -export declare class LocalDatabaseMaintenance extends LiveSyncCommands { - onunload(): void; - onload(): void | Promise; - allChunks(includeDeleted?: boolean): Promise<{ - used: Set; - existing: Map; - }>; - get database(): PouchDB.Database; - clearHash(): void; - confirm(title: string, message: string, affirmative?: string, negative?: string): Promise; - ensureAvailable(operationName: string): Promise; - /** - * Resurrect deleted chunks that are still used in the database. - */ - resurrectChunks(): Promise; - /** - * Commit deletion of files that are marked as deleted. - * This method makes the deletion permanent, and the files will not be recovered. - * After this, chunks that are used in the deleted files become ready for compaction. - */ - commitFileDeletion(): Promise; - /** - * Commit deletion of chunks that are not used in the database. - * This method makes the deletion permanent, and the chunks will not be recovered if the database run compaction. - * After this, the database can shrink the database size by compaction. - * It is recommended to compact the database after this operation (History should be kept once before compaction). - */ - commitChunkDeletion(): Promise; - /** - * Compact the database. - * This method removes all deleted chunks that are not used in the database. - * Make sure all devices are synchronized before running this method. - */ - markUnusedChunks(): Promise; - removeUnusedChunks(): Promise; - scanUnusedChunks(): Promise<{ - chunkSet: Set; - chunkUsageMap: ChunkUsageMap; - unusedSet: Set; - }>; - /** - * Track changes in the database and update the chunk usage map for garbage collection. - * Note that this only able to perform without Fetch chunks on demand. - */ - trackChanges(fromStart?: boolean, showNotice?: boolean): Promise; - performGC(showingNotice?: boolean): Promise; - analyseDatabase(): Promise; - compactDatabase(): Promise; - gcv3(): Promise; -} -export {}; diff --git a/_types/src/features/LocalDatabaseMainte/maintenancePrerequisites.d.ts b/_types/src/features/LocalDatabaseMainte/maintenancePrerequisites.d.ts deleted file mode 100644 index 34ac1b74..00000000 --- a/_types/src/features/LocalDatabaseMainte/maintenancePrerequisites.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettings } from "@lib/common/types"; -type MaintenancePrerequisiteSettings = Pick; -type MaintenancePrerequisiteOptions = { - operationName: string; - settings: MaintenancePrerequisiteSettings; - askSelectStringDialogue: (message: string, buttons: readonly ["Apply and continue", "Cancel"], options: { - title: string; - defaultAction: "Cancel"; - }) => Promise<"Apply and continue" | "Cancel" | false | undefined>; - applyPartial: (settings: Partial, saveImmediately?: boolean) => Promise; -}; -export declare function ensureLocalDatabaseMaintenancePrerequisites({ operationName, settings, askSelectStringDialogue, applyPartial, }: MaintenancePrerequisiteOptions): Promise; -export {}; diff --git a/_types/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.d.ts b/_types/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.d.ts deleted file mode 100644 index da6f4994..00000000 --- a/_types/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { App, Modal } from "@/deps.ts"; -import { mount } from "svelte"; -import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator"; -export type P2POpenReplicationModalCallback = { - onSync: (peerId: string) => Promise; - onSyncAndClose: (peerId: string) => Promise; -}; -export declare class P2POpenReplicationModal extends Modal { - liveSyncReplicator: LiveSyncTrysteroReplicator; - callback?: P2POpenReplicationModalCallback; - component?: ReturnType; - showResult: boolean; - title: string; - onClosed?: () => void; - rebuildMode: boolean; - constructor(app: App, liveSyncReplicator: LiveSyncTrysteroReplicator, callback?: P2POpenReplicationModalCallback, showResult?: boolean, title?: string, onClosed?: () => void, rebuildMode?: boolean); - onSync(peerId: string): Promise; - onSyncAndClose(peerId: string): Promise; - onOpen(): void; - onClose(): void; -} diff --git a/_types/src/features/P2PSync/P2PReplicator/P2PReplicationUI.d.ts b/_types/src/features/P2PSync/P2PReplicator/P2PReplicationUI.d.ts deleted file mode 100644 index 03f6f04c..00000000 --- a/_types/src/features/P2PSync/P2PReplicator/P2PReplicationUI.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { App } from "@/deps.ts"; -import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator"; -/** - * Creates an openReplicationUI factory for Obsidian environments. - * Returns a per-replicator closure that opens the P2P Replication modal - * and performs bidirectional sync (pull then push on success). - * - * Usage: - * const factory = createOpenReplicationUI(app); - * useP2PReplicatorFeature(core, factory); - */ -export declare function createOpenReplicationUI(app: App): (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise; -/** - * Creates an openRebuildUI factory for Obsidian environments. - * Opens the P2P Replication modal in "rebuild" mode — one-way pull only, - * with setOnSetup / clearOnSetup bracketing the replicateFrom call. - * - * Usage: - * const factory = createOpenRebuildUI(app); - * useP2PReplicatorFeature(core, createOpenReplicationUI(app), factory); - */ -export declare function createOpenRebuildUI(app: App): (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise; diff --git a/_types/src/features/P2PSync/P2PReplicator/P2PReplicatorPaneView.d.ts b/_types/src/features/P2PSync/P2PReplicator/P2PReplicatorPaneView.d.ts deleted file mode 100644 index c46f105c..00000000 --- a/_types/src/features/P2PSync/P2PReplicator/P2PReplicatorPaneView.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { Menu, WorkspaceLeaf } from "@/deps.ts"; -import { SvelteItemView } from "@/common/SvelteItemView.ts"; -import { type PeerStatus } from "@lib/replication/trystero/P2PReplicatorPaneCommon.ts"; -import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts"; -import type { P2PPaneParams } from "@lib/replication/trystero/UseP2PReplicatorResult"; -export declare const VIEW_TYPE_P2P = "p2p-replicator"; -export declare class P2PReplicatorPaneView extends SvelteItemView { - core: LiveSyncBaseCore; - private _p2pResult; - icon: string; - title: string; - navigation: boolean; - getIcon(): string; - get replicator(): import("../../../lib/src/replication/trystero/LiveSyncTrysteroReplicator").LiveSyncTrysteroReplicator; - replicateFrom(peer: PeerStatus): Promise; - replicateTo(peer: PeerStatus): Promise; - getRemoteConfig(peer: PeerStatus): Promise; - toggleProp(peer: PeerStatus, prop: "syncOnConnect" | "watchOnConnect" | "syncOnReplicationCommand"): Promise; - m?: Menu; - constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, p2pResult: P2PPaneParams); - getViewType(): string; - getDisplayText(): string; - onClose(): Promise; - instantiateComponent(target: HTMLElement): { - $on?(type: string, callback: (e: any) => void): () => void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - $set?(props: Partial>): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - } & Record; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -} diff --git a/_types/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.d.ts b/_types/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.d.ts deleted file mode 100644 index 7c3c2ecf..00000000 --- a/_types/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { WorkspaceLeaf } from "@/deps.ts"; -import { SvelteItemView } from "@/common/SvelteItemView.ts"; -import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts"; -import type { P2PPaneParams } from "@lib/replication/trystero/UseP2PReplicatorResult"; -export declare const VIEW_TYPE_P2P_SERVER_STATUS = "p2p-server-status"; -export declare class P2PServerStatusPaneView extends SvelteItemView { - core: LiveSyncBaseCore; - private _p2pResult; - icon: string; - navigation: boolean; - constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, p2pResult: P2PPaneParams); - getIcon(): string; - getViewType(): string; - getDisplayText(): string; - instantiateComponent(target: HTMLElement): { - $on?(type: string, callback: (e: any) => void): () => void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - $set?(props: Partial>): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - } & Record; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -} diff --git a/_types/src/lib/src/API/DirectFileManipulator.d.ts b/_types/src/lib/src/API/DirectFileManipulator.d.ts deleted file mode 100644 index 72c0c858..00000000 --- a/_types/src/lib/src/API/DirectFileManipulator.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export { DirectFileManipulator } from "./DirectFileManipulatorV2.ts"; -export type { DirectFileManipulatorOptions } from "./DirectFileManipulatorV2.ts"; diff --git a/_types/src/lib/src/API/processSetting.d.ts b/_types/src/lib/src/API/processSetting.d.ts deleted file mode 100644 index 6cfe623c..00000000 --- a/_types/src/lib/src/API/processSetting.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ObsidianLiveSyncSettings } from "@lib/common/types"; -/** - * Encode settings to a tiny array to encode in QRCode, - * Due to size limitation of QR code, we encode settings as an array instead of object. - * @param settings settings to encode - */ -export declare function encodeSettingsToQRCodeData(settings: ObsidianLiveSyncSettings): string; -/** - * Decode settings from QR code data string - * @param qr data string from QR code - * @returns Decoded settings - */ -export declare function decodeSettingsFromQRCodeData(qr: string): ObsidianLiveSyncSettings; -export declare const enum OutputFormat { - SVG = 0, - ASCII = 1 -} -export interface SplitQRCodeData { - total: number; - parts: string[]; -} -/** - * Encode setting string to QR code in specified format - * @param settingString Setting string to encode - * @param format Output format - */ -export declare function encodeQR(settingString: string, format: OutputFormat): string | SplitQRCodeData; -type ErasureProperties = keyof ObsidianLiveSyncSettings; -/** - * Generate setup URI with encrypted settings - * @param settingString Settings to encode - * @param passphrase Passphrase to encrypt the settings - * @param removeProperties Properties to remove from the settings - * Means these properties will not be included in the generated setup URI, - * See also necessaryErasureProperties for properties that will always be removed. - * @param skipDefaultValue Whether to skip default values - * @returns Generated setup URI - */ -export declare function encodeSettingsToSetupURI(settingString: ObsidianLiveSyncSettings, passphrase: string, removeProperties?: ErasureProperties[], skipDefaultValue?: boolean): Promise; -export declare function decodeSettingsFromSetupURI(uri: string, passphrase: string): Promise; -export {}; diff --git a/_types/src/lib/src/ContentSplitter/ContentSplitter.d.ts b/_types/src/lib/src/ContentSplitter/ContentSplitter.d.ts deleted file mode 100644 index 62dbe999..00000000 --- a/_types/src/lib/src/ContentSplitter/ContentSplitter.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** - * Content-Splitter for Self-hosted LiveSync. - * Splits content into manageable chunks for efficient storage and synchronisation. - */ -import { type FilePathWithPrefix } from "@lib/common/types.ts"; -import type { ISettingService } from "@lib/services/base/IService.ts"; -/** - * ContentSplitter interface for splitting content into chunks. - */ -export type SplitOptions = { - blob: Blob; - path: FilePathWithPrefix; - pieceSize: number; - plainSplit: boolean; - minimumChunkSize: number; - useWorker: boolean; - useSegmenter: boolean; -}; -/** - * The maximum size, in bytes, of a document to be processed by the content splitter in the foreground. - */ -export declare const MAX_CHUNKS_SIZE_ON_UI = 1024; -/** - * Options for the content splitter. - */ -export type ContentSplitterOptions = { - settingService: ISettingService; -}; diff --git a/_types/src/lib/src/ContentSplitter/ContentSplitterBase.d.ts b/_types/src/lib/src/ContentSplitter/ContentSplitterBase.d.ts deleted file mode 100644 index c950ce4d..00000000 --- a/_types/src/lib/src/ContentSplitter/ContentSplitterBase.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type SavingEntry } from "@lib/common/types.ts"; -import { type ContentSplitterOptions, type SplitOptions } from "./ContentSplitter.ts"; -export declare abstract class ContentSplitterCore { - /** - * Options for the content splitter. - * These settings include the chunk splitter version and other configurations. - */ - options: ContentSplitterOptions; - /** - * Task for initialising the content splitter. - * This ensures that the splitter is initialised before any operations are performed. - */ - initialised: Promise | undefined; - /** - * Constructor for the content splitter core. - * @param params Content splitter options - */ - constructor(params: ContentSplitterOptions); - /** - * Initialise the content splitter with the provided options. - * @param options Content splitter options - */ - abstract initialise(options: ContentSplitterOptions): Promise; - /** - * Split the content of the loaded entry into chunks. - * @param entry The loaded entry to be split into chunks - */ - abstract splitContent(entry: SavingEntry): Promise | Generator>; -} -export declare abstract class ContentSplitterBase extends ContentSplitterCore { - initialise(_options: ContentSplitterOptions): Promise; - /** - * Check whether the content splitter is available for the given settings. - * @param setting Content splitter options - * @returns True if the content splitter is available; false otherwise - */ - static isAvailableFor(setting: ContentSplitterOptions): boolean; - /** - * Process the content and split it into chunks. - * @param options Blob content to be split into chunks - */ - abstract processSplit(options: SplitOptions): Promise | Generator>; - getParamsFor(entry: SavingEntry): SplitOptions; - /** - * Split the content of the loaded entry into chunks. - * This method waits for the initialisation task to complete before proceeding. - * @param entry The loaded entry to be split into chunks - * @returns A generator that yields the split chunks - */ - splitContent(entry: SavingEntry): Promise | Generator>; -} diff --git a/_types/src/lib/src/ContentSplitter/ContentSplitterRabinKarp.d.ts b/_types/src/lib/src/ContentSplitter/ContentSplitterRabinKarp.d.ts deleted file mode 100644 index a2b430be..00000000 --- a/_types/src/lib/src/ContentSplitter/ContentSplitterRabinKarp.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ContentSplitterOptions, SplitOptions } from "./ContentSplitter.ts"; -import { ContentSplitterBase } from "./ContentSplitterBase.ts"; -/** - * Rabin-Karp content splitter for efficient chunking - */ -export declare class ContentSplitterRabinKarp extends ContentSplitterBase { - static isAvailableFor(setting: ContentSplitterOptions): boolean; - processSplit(options: SplitOptions): Promise | Generator>; -} diff --git a/_types/src/lib/src/ContentSplitter/ContentSplitterV1.d.ts b/_types/src/lib/src/ContentSplitter/ContentSplitterV1.d.ts deleted file mode 100644 index 0030cfcf..00000000 --- a/_types/src/lib/src/ContentSplitter/ContentSplitterV1.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ContentSplitterOptions, SplitOptions } from "./ContentSplitter"; -import { ContentSplitterBase } from "./ContentSplitterBase"; -/** - * Legacy content splitter for version 1. - */ -export declare class ContentSplitterV1 extends ContentSplitterBase { - static isAvailableFor(setting: ContentSplitterOptions): boolean; - processSplit(options: SplitOptions): Promise | Generator>; -} diff --git a/_types/src/lib/src/ContentSplitter/ContentSplitterV2.d.ts b/_types/src/lib/src/ContentSplitter/ContentSplitterV2.d.ts deleted file mode 100644 index 2712a361..00000000 --- a/_types/src/lib/src/ContentSplitter/ContentSplitterV2.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ContentSplitterOptions, SplitOptions } from "./ContentSplitter.ts"; -import { ContentSplitterBase } from "./ContentSplitterBase.ts"; -/** - * Content splitter for version 2, which supports segmenter-based splitting. - */ -export declare class ContentSplitterV2 extends ContentSplitterBase { - static isAvailableFor(setting: ContentSplitterOptions): boolean; - processSplit(options: SplitOptions): Promise | Generator>; -} diff --git a/_types/src/lib/src/ContentSplitter/ContentSplitters.d.ts b/_types/src/lib/src/ContentSplitter/ContentSplitters.d.ts deleted file mode 100644 index 4b42efc2..00000000 --- a/_types/src/lib/src/ContentSplitter/ContentSplitters.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { SavingEntry } from "@lib/common/types"; -import type { ContentSplitterOptions } from "./ContentSplitter"; -import { ContentSplitterCore, type ContentSplitterBase } from "./ContentSplitterBase"; -/** - * ContentSplitter class that manages the active content splitter based on the provided settings. - */ -export declare class ContentSplitter extends ContentSplitterCore { - _activeSplitter: ContentSplitterBase; - constructor(options: ContentSplitterOptions); - initialise(options: ContentSplitterOptions): Promise; - splitContent(entry: SavingEntry): Promise | Generator>; -} diff --git a/_types/src/lib/src/UI/svelteDialog.d.ts b/_types/src/lib/src/UI/svelteDialog.d.ts deleted file mode 100644 index ec821394..00000000 --- a/_types/src/lib/src/UI/svelteDialog.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type { HasSetResult, HasGetInitialData, ComponentHasResult, GuestDialogProps, DialogSvelteComponentBaseProps, DialogControlBase, } from "@lib/services/implements/base/SvelteDialog.ts"; -export { CONTEXT_DIALOG_CONTROLS, setupDialogContext, getDialogContext, SvelteDialogManagerBase, } from "@lib/services/implements/base/SvelteDialog.ts"; diff --git a/_types/src/lib/src/bureau/bureau.d.ts b/_types/src/lib/src/bureau/bureau.d.ts deleted file mode 100644 index c1d5ec9e..00000000 --- a/_types/src/lib/src/bureau/bureau.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type SlipBoard } from "octagonal-wheels/bureau/SlipBoard"; -declare global { - interface Slips extends LSSlips { - _dummy: undefined; - } -} -export declare const globalSlipBoard: SlipBoard; diff --git a/_types/src/lib/src/common/ConnectionString.d.ts b/_types/src/lib/src/common/ConnectionString.d.ts deleted file mode 100644 index 46d8dbf9..00000000 --- a/_types/src/lib/src/common/ConnectionString.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { CouchDBConnection, BucketSyncSetting, P2PConnectionInfo } from "./models/setting.type"; -export type RemoteConfigurationResult = { - type: "couchdb"; - settings: CouchDBConnection; -} | { - type: "s3"; - settings: BucketSyncSetting; -} | { - type: "p2p"; - settings: P2PConnectionInfo; -} | { - type: "webdav"; - settings: never; -}; -export declare class ConnectionStringParser { - /** - * Restore settings from URI - */ - static parse(uriString: string): RemoteConfigurationResult; - /** - * 設定からURIを生成する - */ - static serialize(config: RemoteConfigurationResult): string; - private static parseCouchDB; - private static serializeCouchDB; - private static parseS3; - private static serializeS3; - private static parseP2P; - private static serializeP2P; -} diff --git a/_types/src/lib/src/common/LSError.d.ts b/_types/src/lib/src/common/LSError.d.ts deleted file mode 100644 index 1068b139..00000000 --- a/_types/src/lib/src/common/LSError.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { Constructor } from "@lib/common/utils.type"; -interface ErrorWithCause extends Error { - cause?: unknown; -} -/** - * Error class for Self-hosted LiveSync errors. - * This class extends the base LiveSyncError class and provides additional context for errors related to LiveSync operations. - * It includes a name property and a cause property to capture the original error. - * The status property returns the HTTP status code if available, defaulting to 500 for internal server errors. - * The class also includes static methods to check whether an error is caused by a specific error class. - */ -export declare class LiveSyncError extends Error implements ErrorWithCause { - name: string; - cause?: Error | object | string; - overrideStatus?: number; - /** - * Returns the HTTP status code associated with the error, if available. - * If the error has a status property, it returns that; otherwise, it defaults to 500 (Internal Server Error). - * @returns {number} The HTTP status code. - */ - get status(): number; - /** - * Constructs a new LiveSyncError instance. - * @param message The error message to be displayed. - */ - constructor(message: string, options?: { - cause?: unknown; - status?: number; - }); - /** - * Determines whether an error is caused by a specific error class. - * @param error The error to examine. - * @param errorClass The error class to compare against. - * @returns True if the error is caused by the specified error class; otherwise, false. - * @example - * LiveSyncError.isCausedBy(someSyncParamsFetchError, SyncParamsNotFoundError); // Returns true if the error is caused by SyncParamsNotFoundError; this is usually represented as SyncParamsFetchError at the uppermost layer. - */ - static isCausedBy(error: unknown, errorClass: Constructor): boolean; - /** - * Creates a new instance of the error class from an existing error. - * @param error The error to wrap. - * @returns A new instance of the error class with the original error's message and stack trace. - */ - static fromError(this: T, error: unknown): InstanceType; -} -export declare class LiveSyncFatalError extends LiveSyncError { -} -export {}; diff --git a/_types/src/lib/src/common/configForDoc.d.ts b/_types/src/lib/src/common/configForDoc.d.ts deleted file mode 100644 index 47f531bb..00000000 --- a/_types/src/lib/src/common/configForDoc.d.ts +++ /dev/null @@ -1,79 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { Confirm } from "@lib/interfaces/Confirm"; -import { type ObsidianLiveSyncSettings } from "./types"; -declare enum ConditionType { - PLATFORM_CASE_INSENSITIVE = "platform-case-insensitive", - PLATFORM_CASE_SENSITIVE = "platform-case-sensitive", - REMOTE_CASE_SENSITIVE = "remote-case-sensitive" -} -export declare enum RuleLevel { - Must = 0, - Necessary = 1, - Recommended = 2, - Optional = 3 -} -type BaseRule = { - level?: RuleLevel; - requireRebuild?: boolean; - requireRebuildLocal?: boolean; - recommendRebuild?: boolean; - reason?: string; - reasonFunc?: (settings: Partial) => string; - condition?: ConditionType[]; - detectionFunc?: (settings: Partial) => boolean; - value?: TValue; - valueDisplay?: string; - valueDisplayFunc?: (settings: Partial) => string; - obsoleteValues?: TValue[]; -}; -type NumberRuleExact = BaseRule<"number", number> & {}; // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type -type NumberRuleRange = BaseRule<"number", number> & { - min?: number; - max?: number; - step?: number; -}; -type StringRangeRule = BaseRule<"string", string> & { - minLength?: number; - maxLength?: number; - regexp?: string; -}; -type StringRule = BaseRule<"string", string> & {}; // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type -type BooleanRule = BaseRule<"boolean", boolean> & {}; // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type -export type RuleForType = T extends number ? NumberRuleExact | NumberRuleRange : T extends string ? StringRule | StringRangeRule : T extends boolean ? BooleanRule : never; -type DoctorCheckSettings = Omit, "remoteConfigurations" | "pluginSyncExtendedSetting">; -export type DoctorRegulation = { - version: string; - rules: { - [P in keyof DoctorCheckSettings]: RuleForType; - }; -}; -export declare const DoctorRegulationV0_24_16: DoctorRegulation; -export declare const DoctorRegulationV0_24_30: DoctorRegulation; -export declare const DoctorRegulationV0_25_0: DoctorRegulation; -export declare const DoctorRegulationV0_25_27: DoctorRegulation; -export declare const DoctorRegulation: DoctorRegulation; -export declare function checkUnsuitableValues(setting: Partial, regulation?: DoctorRegulation): DoctorRegulation; -export declare const RebuildOptions: { - readonly AutomaticAcceptable: 0; - readonly ConfirmIfRequired: 1; - readonly SkipEvenIfRequired: 2; -}; -export type RebuildOptionsType = (typeof RebuildOptions)[keyof typeof RebuildOptions]; -export type DoctorOptions = { - localRebuild: RebuildOptionsType; - remoteRebuild: RebuildOptionsType; - activateReason?: string; - forceRescan?: boolean; -}; -export type DoctorResult = { - settings: ObsidianLiveSyncSettings; - shouldRebuild: boolean; - shouldRebuildLocal: boolean; - isModified: boolean; -}; -export type HasConfirm = { - confirm: Confirm; -}; -export declare function performDoctorConsultation(env: HasConfirm, settings: ObsidianLiveSyncSettings, { localRebuild, remoteRebuild, activateReason, forceRescan, }: DoctorOptions): Promise; -export {}; diff --git a/_types/src/lib/src/common/coreEnvFunctions.d.ts b/_types/src/lib/src/common/coreEnvFunctions.d.ts deleted file mode 100644 index b3a2ce4c..00000000 --- a/_types/src/lib/src/common/coreEnvFunctions.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { getLanguage as ObsidianGetLanguage } from "obsidian"; -export declare function setGetLanguage(func: typeof ObsidianGetLanguage): void; -export declare function getLanguage(): string; -export declare const compatGlobal: typeof window; -export type CompatTimeoutHandle = ReturnType | number; -export type CompatIntervalHandle = ReturnType | number; -/** - * A wrapper around the global fetch function to ensure compatibility across different environments. - * In Obsidian, they recommend using their own requestUrl for better performance and reliability. - * However, at least for now, requestUrl cannot handle multiple concurrent requests, which causes - * problems for synchronise lively. So we will use the global fetch for now. - * If the situation changes in the future, change this function to use requestUrl. - * @param {RequestInfo} input The resource that you wish to fetch. Can be either a string or a Request object. - * @param {RequestInit} [init] An options object containing any custom settings that you want to apply to the request. - * @returns {Promise} A Promise that resolves to the Response to that request, whether it is successful or not. - */ -export declare const _fetch: { - (input: RequestInfo | URL, init?: RequestInit): Promise; - (input: RequestInfo | URL, init?: RequestInit): Promise; -} & typeof fetch; -export declare const _activeDocument: Document; diff --git a/_types/src/lib/src/common/coreEnvVars.d.ts b/_types/src/lib/src/common/coreEnvVars.d.ts deleted file mode 100644 index 2b2f7f2a..00000000 --- a/_types/src/lib/src/common/coreEnvVars.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -declare const manifestVersion: string; -declare const packageVersion: string; -export { manifestVersion, packageVersion }; diff --git a/_types/src/lib/src/common/i18n.d.ts b/_types/src/lib/src/common/i18n.d.ts deleted file mode 100644 index 39779329..00000000 --- a/_types/src/lib/src/common/i18n.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { AllMessageKeys, I18N_LANGS } from "./rosetta"; -import type { TaggedType } from "./types"; -export declare let currentLang: I18N_LANGS; -export declare function getResolvedLang(lang?: I18N_LANGS): I18N_LANGS; -export declare function isAutoDisplayLanguage(lang: I18N_LANGS): boolean; -export declare function __getMissingTranslations(): string[]; -export declare function __onMissingTranslation(callback: (key: string) => void): void; -export declare function setLang(lang: I18N_LANGS): void; -export declare function $t(message: string, lang?: I18N_LANGS): string; -export declare function translateIfAvailable(message: string, lang?: I18N_LANGS): string; -/** - * TagFunction to Automatically translate. - * @param strings - * @param values - * @returns - */ -export declare function $f(strings: TemplateStringsArray, ...values: string[]): string; -export declare function $msg(key: T, params?: Record, lang?: I18N_LANGS): TaggedType; diff --git a/_types/src/lib/src/common/logger.d.ts b/_types/src/lib/src/common/logger.d.ts deleted file mode 100644 index ae7dd437..00000000 --- a/_types/src/lib/src/common/logger.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export * from "octagonal-wheels/common/logger"; -export type * from "octagonal-wheels/common/logger"; diff --git a/_types/src/lib/src/common/messages/combinedMessages.dev.d.ts b/_types/src/lib/src/common/messages/combinedMessages.dev.d.ts deleted file mode 100644 index 97e9a730..00000000 --- a/_types/src/lib/src/common/messages/combinedMessages.dev.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { PartialMessages as def } from "./def.ts"; -import { type MESSAGE } from "@lib/common/rosetta.ts"; -type MessageKeys = keyof typeof def.def; -export declare const allMessages: { - [key: string]: MESSAGE; -}; -export { type MessageKeys }; diff --git a/_types/src/lib/src/common/messages/combinedMessages.prod.d.ts b/_types/src/lib/src/common/messages/combinedMessages.prod.d.ts deleted file mode 100644 index 8ece8ed9..00000000 --- a/_types/src/lib/src/common/messages/combinedMessages.prod.d.ts +++ /dev/null @@ -1,9266 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const allMessages: { - readonly "(Active)": { - readonly def: "(Active)"; - readonly es: "(Activo)"; - readonly ja: "(有効)"; - readonly ko: "(활성)"; - readonly ru: "(Активна)"; - readonly zh: "(已启用)"; - readonly "zh-tw": "(已啟用)"; - }; - readonly "(BETA) Always overwrite with a newer file": { - readonly def: "(BETA) Always overwrite with a newer file"; - readonly es: "(BETA) Sobrescribir siempre con archivo más nuevo"; - readonly fr: "(BÊTA) Toujours écraser avec un fichier plus récent"; - readonly he: "(BETA) תמיד לדרוס עם קובץ חדש יותר"; - readonly ja: "(ベータ機能) 常に新しいファイルで上書きする"; - readonly ko: "(베타) 항상 새로운 파일로 덮어쓰기"; - readonly ru: "(БЕТА) Всегда перезаписывать более новым файлом"; - readonly zh: "始终使用更新的文件覆盖(测试版)"; - }; - readonly "(Beta) Use ignore files": { - readonly def: "(Beta) Use ignore files"; - readonly es: "(Beta) Usar archivos de ignorar"; - readonly fr: "(Bêta) Utiliser les fichiers d'exclusion"; - readonly he: "(בטא) שימוש בקבצי התעלמות"; - readonly ja: "(ベータ機能) 除外ファイル(ignore)の使用"; - readonly ko: "(베타) 제외 규칙 파일 사용"; - readonly ru: "(Бета) Использовать файлы игнорирования"; - readonly zh: "(测试版)使用忽略文件"; - }; - readonly "(Days passed, 0 to disable automatic-deletion)": { - readonly def: "(Days passed, 0 to disable automatic-deletion)"; - readonly es: "(Días transcurridos, 0 para desactivar)"; - readonly fr: "(Jours écoulés, 0 pour désactiver la suppression automatique)"; - readonly he: "(ימים שעברו; 0 לביטול מחיקה אוטומטית)"; - readonly ja: "(経過日数、0で自動削除を無効化)"; - readonly ko: "(지난 일수, 0으로 설정하면 자동 삭제 비활성화)"; - readonly ru: "(Дней прошло, 0 для отключения автоматического удаления)"; - readonly zh: "(已过天数,0为禁用自动删除)"; - }; - readonly "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": { - readonly def: "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended."; - readonly es: "(Ej: Leer chunks online) Lee chunks directamente en línea. Aumente tamaño de chunks personalizados"; - readonly fr: "(ex. Lire les fragments en ligne) Si cette option est activée, LiveSync lit les fragments directement en ligne au lieu de les répliquer localement. L'augmentation de la taille personnalisée des fragments est recommandée."; - readonly he: "(לדוגמה: קריאת נתחים אונליין) אם אפשרות זו מופעלת, LiveSync קורא נתחים ישירות מהשרת מבלי לשכפל אותם מקומית. מומלץ להגדיל את גודל הנתח המותאם אישית."; - readonly ja: "(例: チャンクをオンラインで読む) このオプションを有効にすると、LiveSyncはチャンクをローカルに複製せず、直接オンラインで読み込みます。カスタムチャンクサイズを増やすことをお勧めします。"; - readonly ko: "(예: 청크를 원격에서 읽음) 이 옵션을 활성화하면, LiveSync는 청크를 로컬에 복제하지 않고 원격에서 직접 읽습니다. 커스텀 청크 크기를 키우는 것을 권장합니다."; - readonly ru: "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended."; - readonly zh: "(例如,在线读取块)如果启用此选项,LiveSync 将直接在线读取块,而不是在本地复制块。建议增加自定义块大小"; - }; - readonly "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": { - readonly def: "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used."; - readonly es: "(MB) Saltar cambios en archivos locales/remotos mayores a este tamaño. Si se reduce, se usará versión nueva"; - readonly fr: "(Mo) Si cette valeur est définie, les modifications des fichiers locaux et distants plus grands que cette taille seront ignorées. Si le fichier redevient plus petit, une version plus récente sera utilisée."; - readonly he: "(MB) אם ערך זה מוגדר, שינויים בקבצים מקומיים ומרוחקים הגדולים מגודל זה יידלגו. אם הקובץ יקטן שוב, ייעשה שימוש בגרסה החדשה יותר."; - readonly ja: "(MB) この値を設定すると、これより大きいサイズのローカルファイルやリモートファイルの変更はスキップされます。ファイルが再び小さくなった場合は、新しいものが使用されます。"; - readonly ko: "(MB) 이 값이 설정되면, 이보다 큰 로컬 및 원격 파일의 변경 사항은 건너뜁니다. 파일이 다시 작아지면 더 새로운 파일이 사용됩니다."; - readonly ru: "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used."; - readonly zh: "(MB)如果设置了此项,大于此大小的本地和远程文件的更改将被跳过。如果文件再次变小,将使用更新的文件"; - }; - readonly "(Mega chars)": { - readonly def: "(Mega chars)"; - readonly es: "(Millones de caracteres)"; - readonly fr: "(Méga caractères)"; - readonly he: "(מגה תווים)"; - readonly ja: "(メガ文字)"; - readonly ko: "(메가 문자)"; - readonly ru: "(Мега символов)"; - readonly zh: "(百万字符)"; - }; - readonly "(Not recommended) If set, credentials will be stored in the file.": { - readonly def: "(Not recommended) If set, credentials will be stored in the file."; - readonly es: "(No recomendado) Almacena credenciales en el archivo"; - readonly fr: "(Non recommandé) Si activé, les identifiants seront stockés dans le fichier."; - readonly he: "(לא מומלץ) אם מוגדר, פרטי הגישה יישמרו בקובץ."; - readonly ja: "(非推奨) 設定した場合、認証情報がファイルに保存されます。"; - readonly ko: "(권장하지 않음) 설정한 경우 자격 증명이 파일에 저장됩니다."; - readonly ru: "(Not recommended) If set, credentials will be stored in the file."; - readonly zh: "(不建议)如果设置,凭据将存储在文件中"; - }; - readonly "(Obsolete) Use an old adapter for compatibility": { - readonly def: "(Obsolete) Use an old adapter for compatibility"; - readonly es: "(Obsoleto) Usar adaptador antiguo"; - readonly fr: "(Obsolète) Utiliser un ancien adaptateur pour la compatibilité"; - readonly he: "(מיושן) שימוש במתאם ישן לתאימות לאחור"; - readonly ja: "(廃止済み)古いアダプターを互換性のために利用"; - readonly ko: "(사용 중단) 호환성을 위해 이전 어댑터 사용"; - readonly ru: "(Устарело) Использовать старый адаптер для совместимости"; - readonly zh: "(已弃用)为兼容性使用旧适配器"; - }; - readonly "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": { - readonly def: "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files."; - readonly es: "(RegExp) Déjelo vacío para sincronizar todos los archivos. Defina un filtro como expresión regular para limitar los archivos que se sincronizan."; - readonly ja: "(正規表現)空欄で全ファイルを同期します。正規表現を指定すると、同期対象のファイルを絞り込めます。"; - readonly ko: "(정규식) 비워 두면 모든 파일을 동기화합니다. 정규식을 지정하면 동기화할 파일을 제한할 수 있습니다."; - readonly ru: "(RegExp) Оставьте пустым, чтобы синхронизировать все файлы. Укажите регулярное выражение, чтобы ограничить синхронизируемые файлы."; - readonly zh: "(正则表达式)留空表示同步所有文件。可设置正则表达式来限制需要同步的文件。"; - readonly "zh-tw": "(正則表示式)留空即同步所有檔案。設定正則表示式可限制要同步的檔案。"; - }; - readonly "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": { - readonly def: "(RegExp) If this is set, any changes to local and remote files that match this will be skipped."; - readonly es: "(RegExp) Si se establece, se omitirá cualquier cambio en archivos locales y remotos que coincida con este patrón."; - readonly ja: "(正規表現)設定すると、これに一致するローカル/リモートファイルの変更はすべてスキップされます。"; - readonly ko: "(정규식) 설정하면 이 패턴과 일치하는 로컬 및 원격 파일 변경은 모두 건너뜁니다."; - readonly ru: "(RegExp) Если задано, любые изменения локальных и удалённых файлов, соответствующих этому шаблону, будут пропускаться."; - readonly zh: "(正则表达式)如果已设置,则所有匹配此模式的本地和远端文件变更都会被跳过。"; - readonly "zh-tw": "(正則表示式)若已設定,所有符合此模式的本機與遠端檔案變更都會被略過。"; - }; - readonly "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": { - readonly def: "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch."; - readonly es: "(Seleccione esto si ya utiliza la sincronización en otro ordenador o teléfono). Esta opción es adecuada si desea añadir este dispositivo a una configuración de LiveSync existente。"; - readonly ja: "(別の PC やスマートフォンですでに同期を利用している場合に選択してください。)この端末を既存の LiveSync 構成に追加する場合に適しています。"; - readonly ko: "(다른 컴퓨터나 스마트폰에서 이미 동기화를 사용 중인 경우 선택하세요.) 이 장치를 기존 LiveSync 구성에 추가하려는 경우에 적합합니다。"; - readonly ru: "(Выберите этот вариант, если вы уже используете синхронизацию на другом компьютере или смартфоне.) Он подходит, если вы хотите добавить это устройство к уже существующей конфигурации LiveSync。"; - readonly zh: "(如果你已经在另一台电脑或手机上使用同步,请选择此项。)此选项适合将当前设备加入现有 LiveSync 配置的用户。"; - readonly "zh-tw": "(如果你已經在另一台電腦或手機上使用同步,請選擇此項。)此選項適合將目前裝置加入既有 LiveSync 設定的使用者。"; - }; - readonly "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": { - readonly def: "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch."; - readonly es: "(Seleccione esto si está configurando este dispositivo como el primer dispositivo de sincronización). Esta opción es adecuada si es nuevo en LiveSync y desea configurarlo desde cero。"; - readonly ja: "(この端末を最初の同期端末として設定する場合に選択してください。)LiveSync を初めて利用し、最初から設定したい場合に適しています。"; - readonly ko: "(이 장치를 첫 번째 동기화 장치로 설정하는 경우 선택하세요.) LiveSync를 처음 사용하며 처음부터 설정하려는 경우에 적합합니다。"; - readonly ru: "(Выберите этот вариант, если настраиваете это устройство как первое устройство синхронизации.) Он подходит, если вы впервые используете LiveSync и хотите настроить всё с нуля。"; - readonly zh: "(如果你正在将此设备配置为第一台同步设备,请选择此项。)此选项适合初次使用 LiveSync,并希望从头开始配置的用户。"; - readonly "zh-tw": "(如果你正在將此裝置設定為第一台同步裝置,請選擇此項。)此選項適合初次使用 LiveSync,並希望從頭開始設定的使用者。"; - }; - readonly "> [!INFO]- The connected devices have been detected as follows:\n${devices}": { - readonly def: "> [!INFO]- The connected devices have been detected as follows:\n${devices}"; - readonly ja: "> [!INFO]- 次の接続済みデバイスが検出されました:\n${devices}"; - readonly ko: "> [!INFO]- 다음 연결된 기기가 감지되었습니다:\n${devices}"; - readonly ru: "> [!INFO]- Обнаружены следующие подключённые устройства:\n${devices}"; - readonly zh: "> [!INFO]- 已检测到以下已连接设备:\n${devices}"; - readonly "zh-tw": "> [!INFO]- 已偵測到以下已連線裝置:\n${devices}"; - }; - readonly "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": { - readonly def: "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration."; - readonly es: "Un URI de configuración es una única cadena de texto que contiene la dirección del servidor y los datos de autenticación. Si el script de instalación de su servidor generó un URI, usarlo proporciona una configuración sencilla y segura。"; - readonly ja: "Setup URI は、サーバーアドレスと認証情報を含む 1 本の文字列です。サーバーのインストールスクリプトで生成された URI がある場合は、それを使うと簡単かつ安全に設定できます。"; - readonly ko: "설정 URI는 서버 주소와 인증 정보를 포함한 단일 문자열입니다. 서버 설치 스크립트가 URI를 생성했다면 이를 사용하면 간단하고 안전하게 구성할 수 있습니다。"; - readonly ru: "Setup URI — это одна строка текста, содержащая адрес сервера и данные аутентификации. Если URI был создан скриптом установки сервера, его использование обеспечивает простую и безопасную настройку。"; - readonly zh: "Setup URI 是一段包含服务器地址与认证信息的文本。如果服务器安装脚本已经生成了 URI,使用它可以更简单且更安全地完成配置。"; - readonly "zh-tw": "Setup URI 是一段包含伺服器位址與驗證資訊的文字。如果伺服器安裝腳本已經產生 URI,使用它可以更簡單且更安全地完成設定。"; - }; - readonly "Access Key": { - readonly def: "Access Key"; - readonly es: "Clave de acceso"; - readonly fr: "Clé d'accès"; - readonly he: "מפתח גישה"; - readonly ja: "アクセスキー"; - readonly ko: "액세스 키"; - readonly ru: "Ключ доступа"; - readonly zh: "访问密钥"; - }; - readonly Activate: { - readonly def: "Activate"; - readonly es: "Activar"; - readonly ja: "有効化"; - readonly ko: "활성화"; - readonly ru: "Активировать"; - readonly zh: "启用"; - readonly "zh-tw": "啟用"; - }; - readonly "Active Remote Configuration": { - readonly def: "Active Remote Configuration"; - readonly fr: "Configuration distante active"; - readonly he: "תצורת שרת מרוחק פעיל"; - readonly ru: "Активная удалённая конфигурация"; - readonly zh: "生效中的远程配置"; - readonly "zh-tw": "目前啟用的遠端設定"; - }; - readonly "Add default patterns": { - readonly def: "Add default patterns"; - readonly es: "Añadir patrones predeterminados"; - readonly ja: "デフォルトパターンを追加"; - readonly ko: "기본 패턴 추가"; - readonly ru: "Добавить шаблоны по умолчанию"; - readonly zh: "添加默认模式"; - readonly "zh-tw": "新增預設模式"; - }; - readonly "Add new connection": { - readonly def: "Add new connection"; - readonly es: "Añadir conexión"; - readonly ja: "接続を追加"; - readonly ko: "연결 추가"; - readonly ru: "Добавить подключение"; - readonly zh: "新增连接"; - readonly "zh-tw": "新增連線"; - }; - readonly "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": { - readonly def: "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection."; - readonly ja: "すべてのデバイスで進捗値が同じです(${progress})。デバイスは同期されているようなので、Garbage Collection を続行できます。"; - readonly ko: "모든 기기의 진행 값이 동일합니다(${progress}). 기기들이 동기화된 것으로 보이므로 Garbage Collection을 진행할 수 있습니다."; - readonly ru: "У всех устройств одинаковое значение прогресса (${progress}). Похоже, ваши устройства синхронизированы, и можно продолжать Garbage Collection."; - readonly zh: "所有设备的进度值均相同(${progress})。看起来你的设备已经同步,可以继续执行垃圾回收。"; - readonly "zh-tw": "所有裝置的進度值均相同(${progress})。看起來你的裝置已同步,可以繼續執行垃圾回收。"; - }; - readonly "Always prompt merge conflicts": { - readonly def: "Always prompt merge conflicts"; - readonly es: "Siempre preguntar en conflictos"; - readonly fr: "Toujours demander pour les conflits de fusion"; - readonly he: "תמיד להציג בקשת אישור לקונפליקטי מיזוג"; - readonly ja: "常に競合は手動で解決する"; - readonly ko: "항상 병합 충돌 알림"; - readonly ru: "Всегда запрашивать разрешение конфликтов слияния"; - readonly zh: "始终提示合并冲突"; - readonly "zh-tw": "總是提示合併衝突"; - }; - readonly Analyse: { - readonly def: "Analyse"; - readonly fr: "Analyser"; - readonly he: "ניתוח"; - readonly ru: "Анализировать"; - readonly zh: "立即分析"; - readonly "zh-tw": "分析"; - }; - readonly "Analyse database usage": { - readonly def: "Analyse database usage"; - readonly fr: "Analyser l'utilisation de la base de données"; - readonly he: "ניתוח שימוש במסד נתונים"; - readonly ja: "データベース使用状況を分析"; - readonly ko: "데이터베이스 사용량 분석"; - readonly ru: "Анализ использования базы данных"; - readonly zh: "分析数据库使用情况"; - readonly "zh-tw": "分析資料庫使用情況"; - }; - readonly "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": { - readonly def: "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like."; - readonly fr: "Analyser l'utilisation de la base de données et générer un rapport TSV pour un diagnostic personnel. Vous pouvez coller le rapport généré dans le tableur de votre choix."; - readonly he: "נתח שימוש במסד הנתונים וצור דו\"ח TSV לאבחון עצמי. ניתן להדביק את הדו\"ח שנוצר בכל גיליון אלקטרוני."; - readonly ja: "データベース使用状況を分析し、自分で診断できるよう TSV レポートを生成します。生成したレポートは任意のスプレッドシートに貼り付けて確認できます。"; - readonly ko: "데이터베이스 사용량을 분석하고 직접 진단할 수 있도록 TSV 보고서를 생성합니다. 생성된 보고서는 원하는 스프레드시트에 붙여 넣어 확인할 수 있습니다."; - readonly ru: "Проанализируйте использование базы данных и создайте TSV-отчёт для самостоятельной диагностики. Полученный отчёт можно вставить в любую удобную для вас таблицу."; - readonly zh: "分析数据库使用情况并生成 TSV 报告以供您自行诊断。您可以将生成的报告粘贴到您喜欢的任何电子表格中。"; - readonly "zh-tw": "分析資料庫使用情況並產生 TSV 報告,方便你自行診斷。你可以將產生的報告貼到任何慣用的試算表中查看。"; - }; - readonly "Apply Latest Change if Conflicting": { - readonly def: "Apply Latest Change if Conflicting"; - readonly es: "Aplicar último cambio en conflictos"; - readonly fr: "Appliquer la dernière modification en cas de conflit"; - readonly he: "החל שינוי אחרון בעת קונפליקט"; - readonly ja: "競合がある場合は最新の変更を適用する"; - readonly ko: "충돌 시 최신 변경 사항 적용"; - readonly ru: "Применить последнее изменение при конфликте"; - readonly zh: "如果冲突则应用最新更改"; - readonly "zh-tw": "發生衝突時套用最新變更"; - }; - readonly "Apply preset configuration": { - readonly def: "Apply preset configuration"; - readonly es: "Aplicar configuración predefinida"; - readonly fr: "Appliquer une configuration prédéfinie"; - readonly he: "החל תצורה קבועה מראש"; - readonly ja: "プリセットを適用する"; - readonly ko: "프리셋 구성 적용"; - readonly ru: "Применить предустановленную конфигурацию"; - readonly zh: "应用预设配置"; - readonly "zh-tw": "套用預設配置"; - }; - readonly "Ask a passphrase at every launch": { - readonly def: "Ask a passphrase at every launch"; - readonly es: "Solicitar la frase de contraseña en cada inicio"; - readonly ja: "起動のたびにパスフレーズを確認"; - readonly ko: "시작할 때마다 암호문구 묻기"; - readonly ru: "Запрашивать парольную фразу при каждом запуске"; - readonly zh: "每次启动时询问密码短语"; - readonly "zh-tw": "每次啟動時都詢問密語"; - }; - readonly "Automatically Sync all files when opening Obsidian.": { - readonly def: "Automatically Sync all files when opening Obsidian."; - readonly es: "Sincronizar automáticamente todos los archivos al abrir Obsidian"; - readonly fr: "Synchroniser automatiquement tous les fichiers à l'ouverture d'Obsidian."; - readonly he: "סנכרן את כל הקבצים אוטומטית עם פתיחת Obsidian."; - readonly ja: "Obsidian起動時にすべてのファイルを自動同期します。"; - readonly ko: "Obsidian을 열 때 모든 파일을 자동으로 동기화합니다."; - readonly ru: "Автоматически синхронизировать все файлы при открытии Obsidian."; - readonly zh: "打开 Obsidian 时自动同步所有文件"; - readonly "zh-tw": "開啟 Obsidian 時自動同步所有檔案。"; - }; - readonly Back: { - readonly def: "Back"; - readonly es: "Volver"; - readonly ja: "戻る"; - readonly ko: "뒤로"; - readonly ru: "Назад"; - readonly zh: "返回"; - readonly "zh-tw": "返回"; - }; - readonly "Back to non-configured": { - readonly def: "Back to non-configured"; - readonly es: "Volver a no configurado"; - readonly ja: "未設定状態に戻す"; - readonly ko: "미구성 상태로 되돌리기"; - readonly ru: "Вернуть в состояние без настройки"; - readonly zh: "恢复为未配置状态"; - readonly "zh-tw": "恢復為未設定狀態"; - }; - readonly "Batch database update": { - readonly def: "Batch database update"; - readonly es: "Actualización por lotes de BD"; - readonly fr: "Mise à jour groupée de la base de données"; - readonly he: "עדכון אצווה למסד נתונים"; - readonly ja: "データベースのバッチ更新"; - readonly ko: "일괄 데이터베이스 업데이트"; - readonly ru: "Пакетное обновление базы данных"; - readonly zh: "批量数据库更新"; - readonly "zh-tw": "批次更新資料庫"; - }; - readonly "Batch limit": { - readonly def: "Batch limit"; - readonly es: "Límite de lotes"; - readonly fr: "Limite de lot"; - readonly he: "מגבלת אצווה"; - readonly ja: "バッチの上限"; - readonly ko: "일괄 제한"; - readonly ru: "Пакетный лимит"; - readonly zh: "批量限制"; - readonly "zh-tw": "批次上限"; - }; - readonly "Batch size": { - readonly def: "Batch size"; - readonly es: "Tamaño de lote"; - readonly fr: "Taille de lot"; - readonly he: "גודל אצווה"; - readonly ja: "バッチ容量"; - readonly ko: "일괄 크기"; - readonly ru: "Размер пакета"; - readonly zh: "批量大小"; - readonly "zh-tw": "批次大小"; - }; - readonly "Batch size of on-demand fetching": { - readonly def: "Batch size of on-demand fetching"; - readonly es: "Tamaño de lote para obtención bajo demanda"; - readonly fr: "Taille de lot pour la récupération à la demande"; - readonly he: "גודל אצווה במשיכה לפי דרישה"; - readonly ja: "オンデマンド取得のバッチサイズ"; - readonly ko: "필요 시 가져올 청크 묶음 크기"; - readonly ru: "Размер пакета при запросе по требованию"; - readonly zh: "按需获取的批量大小"; - readonly "zh-tw": "按需抓取的批次大小"; - }; - readonly "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": { - readonly def: "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this."; - readonly es: "Antes de v0.17.16 usábamos adaptador antiguo. Nuevo adaptador requiere reconstruir BD local. Desactive cuando pueda"; - readonly fr: "Avant la version v0.17.16, nous utilisions un ancien adaptateur pour la base de données locale. Le nouvel adaptateur est désormais recommandé. Cependant, il nécessite une reconstruction de la base locale. Veuillez désactiver cette option lorsque vous aurez suffisamment de temps. Si elle reste activée, il vous sera également demandé de la désactiver lors de la récupération depuis la base distante."; - readonly he: "לפני גרסה 0.17.16, השתמשנו במתאם ישן למסד הנתונים המקומי. כעת המתאם החדש מועדף. עם זאת, הדבר מצריך בנייה מחדש של מסד הנתונים המקומי. אנא כבה אפשרות זו כשיש לך זמן פנוי. אם תשאיר אותה מופעלת, גם בעת משיכה ממסד הנתונים המרוחק, תתבקש לכבות אותה."; - readonly ja: "v0.17.6までは古いアダプターをローカル用のデータベースに使用していましたが、現在は新しいアダプターを推奨しています。しかし、新しいアダプターに変更するにはローカルデータベースの再構築が必要です。有効のままにしておくと、リモートデータベースからフェッチする場合に、この設定を無効にするかの質問が表示されます。"; - readonly ko: "v0.17.16 이전에는 로컬 데이터베이스에 이전 어댑터를 사용했습니다. 이제는 새로운 어댑터를 권장합니다. 하지만 로컬 데이터베이스 재구축이 필요합니다. 충분한 시간이 있을 때 이 토글을 비활성화해 주세요. 활성화된 상태로 두면 원격 데이터베이스에서 가져올 때도 이를 비활성화하라는 메시지가 나타납니다."; - readonly ru: "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this."; - readonly zh: "在 v0.17.16 之前,我们使用旧适配器作为本地数据库。现在首选新适配器。但是,它需要重建本地数据库。请在有足够时间时禁用此开关。如果保持启用状态,并且在从远程数据库获取时,系统将要求您禁用此开关。"; - }; - readonly "Bucket Name": { - readonly def: "Bucket Name"; - readonly es: "Nombre del bucket"; - readonly fr: "Nom du bucket"; - readonly he: "שם דלי (Bucket)"; - readonly ja: "バケット名"; - readonly ko: "버킷 이름"; - readonly ru: "Имя бакета"; - readonly zh: "存储桶名称"; - readonly "zh-tw": "儲存桶名稱"; - }; - readonly Cancel: { - readonly def: "Cancel"; - readonly es: "Cancelar"; - readonly ja: "キャンセル"; - readonly ko: "취소"; - readonly ru: "Отмена"; - readonly zh: "取消"; - readonly "zh-tw": "取消"; - }; - readonly "Cancel Garbage Collection": { - readonly def: "Cancel Garbage Collection"; - readonly ja: "Garbage Collection をキャンセル"; - readonly ko: "Garbage Collection 취소"; - readonly ru: "Отменить Garbage Collection"; - readonly zh: "取消垃圾回收"; - readonly "zh-tw": "取消垃圾回收"; - }; - readonly "Changing this setting requires migrating existing data (a bit time may be taken) and restarting Obsidian. Please make sure to back up your data before proceeding.": { - readonly def: "Changing this setting requires migrating existing data (a bit time may be taken) and restarting Obsidian. Please make sure to back up your data before proceeding."; - }; - readonly Check: { - readonly def: "Check"; - readonly fr: "Vérifier"; - readonly he: "בדוק"; - readonly ru: "Проверить"; - readonly zh: "立即检查"; - readonly "zh-tw": "檢查"; - }; - readonly "Check and convert non-path-obfuscated files": { - readonly def: "Check and convert non-path-obfuscated files"; - readonly es: "Comprobar y convertir archivos sin ofuscación de ruta"; - readonly ja: "パス難読化されていないファイルを確認して変換"; - readonly ko: "경로 난독화되지 않은 파일 검사 및 변환"; - readonly ru: "Проверить и преобразовать файлы без обфускации пути"; - readonly zh: "检查并转换未进行路径混淆的文件"; - readonly "zh-tw": "檢查並轉換未進行路徑混淆的檔案"; - }; - readonly "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": { - readonly def: "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary."; - readonly es: "Comprueba los documentos que aún no se hayan convertido a identificadores con ruta ofuscada y conviértelos si es necesario."; - readonly ja: "まだパス難読化 ID に変換されていないドキュメントを確認し、必要に応じて変換します。"; - readonly ko: "아직 경로 난독화 ID로 변환되지 않은 문서를 확인하고 필요하면 변환합니다."; - readonly ru: "Проверяет документы, которые ещё не были преобразованы в path-obfuscated ID, и при необходимости преобразует их."; - readonly zh: "检查尚未转换为路径混淆 ID 的文档,并在需要时将其转换。"; - readonly "zh-tw": "檢查尚未轉換為路徑混淆 ID 的文件,並在需要時進行轉換。"; - }; - readonly "cmdConfigSync.showCustomizationSync": { - readonly def: "Show Customization sync"; - readonly es: "Mostrar sincronización de personalización"; - readonly fr: "Afficher la synchronisation de personnalisation"; - readonly he: "הצג סנכרון התאמה אישית"; - readonly ja: "カスタマイズ同期を表示"; - readonly ko: "사용자 설정 동기화 표시"; - readonly ru: "Показать синхронизацию настроек"; - readonly zh: "显示自定义同步"; - readonly "zh-tw": "顯示自訂同步"; - }; - readonly "Comma separated `.gitignore, .dockerignore`": { - readonly def: "Comma separated `.gitignore, .dockerignore`"; - readonly es: "Separados por comas: `.gitignore, .dockerignore`"; - readonly fr: "Séparés par des virgules `.gitignore, .dockerignore`"; - readonly he: "רשימה מופרדת בפסיקים `.gitignore, .dockerignore`"; - readonly ja: "カンマ区切り `.gitignore, .dockerignore`"; - readonly ko: "쉼표로 구분된 `.gitignore, .dockerignore`"; - readonly ru: "Через запятую `.gitignore, .dockerignore`"; - readonly zh: "用逗号分隔,例如 `.gitignore, .dockerignore`"; - }; - readonly "Compaction in progress on remote database...": { - readonly def: "Compaction in progress on remote database..."; - readonly ja: "リモートデータベースでコンパクションを実行中です..."; - readonly ko: "원격 데이터베이스에서 압축을 진행 중입니다..."; - readonly ru: "Выполняется компакция удалённой базы данных..."; - readonly zh: "正在远程数据库上执行压缩..."; - readonly "zh-tw": "正在遠端資料庫上執行壓縮..."; - }; - readonly "Compaction on remote database completed successfully.": { - readonly def: "Compaction on remote database completed successfully."; - readonly ja: "リモートデータベースでのコンパクションが正常に完了しました。"; - readonly ko: "원격 데이터베이스 압축이 성공적으로 완료되었습니다."; - readonly ru: "Компакция удалённой базы данных успешно завершена."; - readonly zh: "远程数据库压缩已成功完成。"; - readonly "zh-tw": "遠端資料庫壓縮已成功完成。"; - }; - readonly "Compaction on remote database failed.": { - readonly def: "Compaction on remote database failed."; - readonly ja: "リモートデータベースでのコンパクションに失敗しました。"; - readonly ko: "원격 데이터베이스 압축에 실패했습니다."; - readonly ru: "Компакция удалённой базы данных завершилась ошибкой."; - readonly zh: "远程数据库压缩失败。"; - readonly "zh-tw": "遠端資料庫壓縮失敗。"; - }; - readonly "Compaction on remote database timed out.": { - readonly def: "Compaction on remote database timed out."; - readonly ja: "リモートデータベースでのコンパクションがタイムアウトしました。"; - readonly ko: "원격 데이터베이스 압축 시간이 초과되었습니다."; - readonly ru: "Время ожидания компакции удалённой базы данных истекло."; - readonly zh: "远程数据库压缩超时。"; - readonly "zh-tw": "遠端資料庫壓縮逾時。"; - }; - readonly "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": { - readonly def: "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep."; - readonly es: "Compara el contenido de los archivos entre la base de datos local y el almacenamiento. Si no coinciden, se te preguntará cuál deseas conservar."; - readonly ja: "ローカルデータベースとストレージ上のファイル内容を比較します。一致しない場合は、どちらを残すか選択できます。"; - readonly ko: "로컬 데이터베이스와 저장소 간의 파일 내용을 비교합니다. 일치하지 않으면 어떤 쪽을 유지할지 묻게 됩니다."; - readonly ru: "Сравнивает содержимое файлов между локальной базой данных и хранилищем. Если они не совпадут, вам предложат выбрать, какую версию сохранить."; - readonly zh: "比较本地数据库与存储中的文件内容;如果不一致,你将被询问要保留哪一份。"; - readonly "zh-tw": "比較本機資料庫與儲存空間中的檔案內容;若不一致,系統會詢問你要保留哪一份。"; - }; - readonly "Compatibility (Conflict Behaviour)": { - readonly def: "Compatibility (Conflict Behaviour)"; - readonly es: "Compatibilidad (comportamiento de conflictos)"; - readonly ja: "互換性(競合時の挙動)"; - readonly ko: "호환성 (충돌 동작)"; - readonly ru: "Совместимость (поведение при конфликтах)"; - readonly zh: "兼容性(冲突行为)"; - readonly "zh-tw": "相容性(衝突行為)"; - }; - readonly "Compatibility (Database structure)": { - readonly def: "Compatibility (Database structure)"; - readonly es: "Compatibilidad (estructura de la base de datos)"; - readonly ja: "互換性(データベース構造)"; - readonly ko: "호환성 (데이터베이스 구조)"; - readonly ru: "Совместимость (структура базы данных)"; - readonly zh: "兼容性(数据库结构)"; - readonly "zh-tw": "相容性(資料庫結構)"; - }; - readonly "Compatibility (Internal API Usage)": { - readonly def: "Compatibility (Internal API Usage)"; - readonly es: "Compatibilidad (uso de la API interna)"; - readonly ja: "互換性(内部 API の利用)"; - readonly ko: "호환성 (내부 API 사용)"; - readonly ru: "Совместимость (использование внутреннего API)"; - readonly zh: "兼容性(内部 API 使用)"; - readonly "zh-tw": "相容性(內部 API 使用)"; - }; - readonly "Compatibility (Metadata)": { - readonly def: "Compatibility (Metadata)"; - readonly es: "Compatibilidad (metadatos)"; - readonly ja: "互換性(メタデータ)"; - readonly ko: "호환성 (메타데이터)"; - readonly ru: "Совместимость (метаданные)"; - readonly zh: "兼容性(元数据)"; - readonly "zh-tw": "相容性(中繼資料)"; - }; - readonly "Compatibility (Remote Database)": { - readonly def: "Compatibility (Remote Database)"; - readonly es: "Compatibilidad (base de datos remota)"; - readonly ja: "互換性(リモートデータベース)"; - readonly ko: "호환성 (원격 데이터베이스)"; - readonly ru: "Совместимость (удалённая база данных)"; - readonly zh: "兼容性(远端数据库)"; - readonly "zh-tw": "相容性(遠端資料庫)"; - }; - readonly "Compatibility (Trouble addressed)": { - readonly def: "Compatibility (Trouble addressed)"; - readonly es: "Compatibilidad (problemas corregidos)"; - readonly ja: "互換性(対処済みの問題)"; - readonly ko: "호환성 (문제 대응)"; - readonly ru: "Совместимость (исправленные проблемы)"; - readonly zh: "兼容性(问题修复)"; - readonly "zh-tw": "相容性(問題修復)"; - }; - readonly "Compute revisions for chunks": { - readonly def: "Compute revisions for chunks"; - readonly fr: "Calculer les révisions pour les fragments"; - readonly he: "חשב גרסאות לנתחים"; - readonly ja: "チャンクの修正(リビジョン)を計算"; - readonly ko: "청크에 대한 리비전 계산"; - readonly ru: "Вычислять ревизии для чанков"; - readonly zh: "为 chunks 计算修订版本(以前的行为)"; - }; - readonly "Configuration Encryption": { - readonly def: "Configuration Encryption"; - readonly es: "Cifrado de configuración"; - readonly ja: "設定の暗号化"; - readonly ko: "구성 암호화"; - readonly ru: "Шифрование конфигурации"; - readonly zh: "配置加密"; - readonly "zh-tw": "設定加密"; - }; - readonly Configure: { - readonly def: "Configure"; - readonly es: "Configurar"; - readonly ja: "設定"; - readonly ko: "설정"; - readonly ru: "Настроить"; - readonly zh: "配置"; - readonly "zh-tw": "設定"; - }; - readonly "Configure And Change Remote": { - readonly def: "Configure And Change Remote"; - readonly es: "Configurar y cambiar remoto"; - readonly ja: "リモートを設定して切り替える"; - readonly ko: "원격 구성 및 변경"; - readonly ru: "Настроить и переключить удалённое хранилище"; - readonly zh: "配置并切换远端"; - readonly "zh-tw": "設定並切換遠端"; - }; - readonly "Configure E2EE": { - readonly def: "Configure E2EE"; - readonly es: "Configurar E2EE"; - readonly ja: "E2EE を設定"; - readonly ko: "E2EE 구성"; - readonly ru: "Настроить сквозное шифрование"; - readonly zh: "配置 E2EE"; - readonly "zh-tw": "設定 E2EE"; - }; - readonly "Configure Remote": { - readonly def: "Configure Remote"; - readonly es: "Configurar remoto"; - readonly ja: "リモートを設定"; - readonly ko: "원격 구성"; - readonly ru: "Настроить удалённое хранилище"; - readonly zh: "配置远端"; - readonly "zh-tw": "設定遠端"; - }; - readonly "Configure the same server information as your other devices again, manually, very advanced users only.": { - readonly def: "Configure the same server information as your other devices again, manually, very advanced users only."; - readonly es: "Configure manualmente la misma información del servidor que en sus otros dispositivos. Solo para usuarios muy avanzados。"; - readonly ja: "他の端末と同じサーバー情報を手動で再入力します。上級者向けの方法です。"; - readonly ko: "다른 장치와 동일한 서버 정보를 다시 수동으로 입력합니다. 고급 사용자 전용입니다。"; - readonly ru: "Снова вручную укажите те же параметры сервера, что и на других устройствах. Только для очень опытных пользователей。"; - readonly zh: "手动重新输入与你其他设备相同的服务器信息。仅适合高级用户。"; - readonly "zh-tw": "手動重新輸入與其他裝置相同的伺服器資訊。僅適合進階使用者。"; - }; - readonly "Connection Method": { - readonly def: "Connection Method"; - readonly es: "Método de conexión"; - readonly ja: "接続方法"; - readonly ko: "연결 방법"; - readonly ru: "Способ подключения"; - readonly zh: "连接方式"; - readonly "zh-tw": "連線方式"; - }; - readonly "Continue to CouchDB setup": { - readonly def: "Continue to CouchDB setup"; - readonly es: "Continuar con la configuración de CouchDB"; - readonly ja: "CouchDB 設定へ進む"; - readonly ko: "CouchDB 설정으로 계속"; - readonly ru: "Перейти к настройке CouchDB"; - readonly zh: "继续进行 CouchDB 设置"; - readonly "zh-tw": "繼續進行 CouchDB 設定"; - }; - readonly "Continue to Peer-to-Peer only setup": { - readonly def: "Continue to Peer-to-Peer only setup"; - readonly es: "Continuar con la configuración solo Peer-to-Peer"; - readonly ja: "Peer-to-Peer 専用設定へ進む"; - readonly ko: "Peer-to-Peer 전용 설정으로 계속"; - readonly ru: "Перейти к настройке только Peer-to-Peer"; - readonly zh: "继续进行仅 Peer-to-Peer 设置"; - readonly "zh-tw": "繼續進行僅 Peer-to-Peer 設定"; - }; - readonly "Continue to S3/MinIO/R2 setup": { - readonly def: "Continue to S3/MinIO/R2 setup"; - readonly es: "Continuar con la configuración de S3/MinIO/R2"; - readonly ja: "S3/MinIO/R2 設定へ進む"; - readonly ko: "S3/MinIO/R2 설정으로 계속"; - readonly ru: "Перейти к настройке S3/MinIO/R2"; - readonly zh: "继续进行 S3/MinIO/R2 设置"; - readonly "zh-tw": "繼續進行 S3/MinIO/R2 設定"; - }; - readonly Copy: { - readonly def: "Copy"; - readonly es: "Copiar"; - readonly ja: "コピー"; - readonly ko: "복사"; - readonly ru: "Копировать"; - readonly zh: "复制"; - readonly "zh-tw": "複製"; - }; - readonly "Copy Report to clipboard": { - readonly def: "Copy Report to clipboard"; - readonly fr: "Copier le rapport dans le presse-papiers"; - readonly he: "העתק דו\"ח ללוח"; - readonly ja: "レポートをクリップボードにコピー"; - readonly ko: "보고서를 클립보드에 복사"; - readonly ru: "Копировать отчёт в буфер обмена"; - readonly zh: "将报告复制到剪贴板"; - readonly "zh-tw": "將報告複製到剪貼簿"; - }; - readonly "CouchDB Connection Tweak": { - readonly def: "CouchDB Connection Tweak"; - readonly es: "Ajustes de conexión de CouchDB"; - readonly ja: "CouchDB 接続の調整"; - readonly ko: "CouchDB 연결 조정"; - readonly ru: "Настройки подключения CouchDB"; - readonly zh: "CouchDB 连接调优"; - readonly "zh-tw": "CouchDB 連線調校"; - }; - readonly "Cross-platform": { - readonly def: "Cross-platform"; - readonly es: "Multiplataforma"; - readonly ja: "クロスプラットフォーム"; - readonly ko: "크로스 플랫폼"; - readonly ru: "Кроссплатформенные"; - readonly zh: "跨平台"; - readonly "zh-tw": "跨平台"; - }; - readonly "Current adapter: {adapter}": { - readonly def: "Current adapter: {adapter}"; - readonly es: "Adaptador actual: {adapter}"; - readonly ja: "現在のアダプター: {adapter}"; - readonly ko: "현재 어댑터: {adapter}"; - readonly ru: "Текущий адаптер: {adapter}"; - readonly zh: "当前适配器:{adapter}"; - readonly "zh-tw": "目前的適配器:{adapter}"; - }; - readonly "Customization Sync": { - readonly def: "Customization Sync"; - readonly es: "Sincronización de personalización"; - readonly ja: "カスタマイズ同期"; - readonly ko: "사용자 지정 동기화"; - readonly ru: "Синхронизация настроек"; - readonly zh: "自定义同步"; - readonly "zh-tw": "自訂同步"; - }; - readonly "Customization Sync (Beta3)": { - readonly def: "Customization Sync (Beta3)"; - readonly es: "Sincronización de personalización (Beta3)"; - readonly ja: "カスタマイズ同期 (Beta3)"; - readonly ko: "사용자 지정 동기화 (Beta3)"; - readonly ru: "Синхронизация настроек (Beta3)"; - readonly zh: "自定义同步(Beta3)"; - readonly "zh-tw": "自訂同步(Beta3)"; - }; - readonly "Data Compression": { - readonly def: "Data Compression"; - readonly es: "Compresión de datos"; - readonly fr: "Compression des données"; - readonly he: "דחיסת נתונים"; - readonly ja: "データ圧縮"; - readonly ko: "데이터 압축"; - readonly ru: "Сжатие данных"; - readonly zh: "数据压缩"; - readonly "zh-tw": "資料壓縮"; - }; - readonly "Database -> Storage": { - readonly def: "Database -> Storage"; - readonly "zh-tw": "資料庫 -> 儲存空間"; - }; - readonly "Database Adapter": { - readonly def: "Database Adapter"; - readonly es: "Adaptador de base de datos"; - readonly ja: "データベースアダプター"; - readonly ko: "데이터베이스 어댑터"; - readonly ru: "Адаптер базы данных"; - readonly zh: "数据库适配器"; - readonly "zh-tw": "資料庫適配器"; - }; - readonly "Database Name": { - readonly def: "Database Name"; - readonly es: "Nombre de la base de datos"; - readonly fr: "Nom de la base de données"; - readonly he: "שם מסד נתונים"; - readonly ja: "データベース名"; - readonly ko: "데이터베이스 이름"; - readonly ru: "Имя базы данных"; - readonly zh: "数据库名称"; - readonly "zh-tw": "資料庫名稱"; - }; - readonly "Database suffix": { - readonly def: "Database suffix"; - readonly es: "Sufijo de base de datos"; - readonly fr: "Suffixe de la base de données"; - readonly he: "סיומת מסד נתונים"; - readonly ja: "データベースの接尾辞(suffix)"; - readonly ko: "데이터베이스 접미사"; - readonly ru: "Суффикс базы данных"; - readonly zh: "数据库后缀"; - readonly "zh-tw": "資料庫後綴"; - }; - readonly Default: { - readonly def: "Default"; - readonly es: "Predeterminado"; - readonly ja: "デフォルト"; - readonly ko: "기본값"; - readonly ru: "По умолчанию"; - readonly zh: "默认"; - readonly "zh-tw": "預設"; - }; - readonly "Delay conflict resolution of inactive files": { - readonly def: "Delay conflict resolution of inactive files"; - readonly es: "Retrasar resolución de conflictos en archivos inactivos"; - readonly fr: "Différer la résolution des conflits pour les fichiers inactifs"; - readonly he: "עכב פתרון קונפליקטים לקבצים לא פעילים"; - readonly ja: "非アクティブなファイルは、競合解決を先送りする"; - readonly ko: "비활성 파일의 충돌 해결 지연"; - readonly ru: "Отложить разрешение конфликтов для неактивных файлов"; - readonly zh: "推迟解决不活动文件"; - readonly "zh-tw": "延後處理非活動檔案的衝突"; - }; - readonly "Delay merge conflict prompt for inactive files.": { - readonly def: "Delay merge conflict prompt for inactive files."; - readonly es: "Retrasar aviso de fusión para archivos inactivos"; - readonly fr: "Différer l'invite de conflit de fusion pour les fichiers inactifs."; - readonly he: "עכב הצגת בקשת מיזוג לקבצים לא פעילים."; - readonly ja: "非アクティブなファイルの競合解決のプロンプトの表示を遅延させる"; - readonly ko: "비활성 파일의 병합 충돌 프롬프트 지연."; - readonly ru: "Отложить запрос конфликта слияния для неактивных файлов."; - readonly zh: "推迟手动解决不活动文件"; - readonly "zh-tw": "延後顯示非活動檔案的合併衝突提示。"; - }; - readonly Delete: { - readonly def: "Delete"; - readonly es: "Eliminar"; - readonly ja: "削除"; - readonly ko: "삭제"; - readonly ru: "Удалить"; - readonly zh: "删除"; - readonly "zh-tw": "刪除"; - }; - readonly "Delete all customization sync data": { - readonly def: "Delete all customization sync data"; - readonly es: "Eliminar todos los datos de sincronización de personalización"; - readonly ja: "カスタマイズ同期データをすべて削除"; - readonly ko: "모든 사용자 정의 동기화 데이터 삭제"; - readonly ru: "Удалить все данные синхронизации настроек"; - readonly zh: "删除所有自定义同步数据"; - readonly "zh-tw": "刪除所有自訂同步資料"; - }; - readonly "Delete all data on the remote server.": { - readonly def: "Delete all data on the remote server."; - readonly es: "Eliminar todos los datos del servidor remoto."; - readonly ja: "リモートサーバー上のすべてのデータを削除します。"; - readonly ko: "원격 서버의 모든 데이터를 삭제합니다."; - readonly ru: "Удалить все данные на удалённом сервере."; - readonly zh: "删除远端服务器上的所有数据。"; - readonly "zh-tw": "刪除遠端伺服器上的所有資料。"; - }; - readonly "Delete local database to reset or uninstall Self-hosted LiveSync": { - readonly def: "Delete local database to reset or uninstall Self-hosted LiveSync"; - readonly es: "Eliminar la base de datos local para restablecer o desinstalar Self-hosted LiveSync"; - readonly ja: "Self-hosted LiveSync をリセットまたはアンインストールするため、ローカルデータベースを削除"; - readonly ko: "Self-hosted LiveSync를 초기화하거나 제거하기 위해 로컬 데이터베이스를 삭제"; - readonly ru: "Удалить локальную базу данных, чтобы сбросить или удалить Self-hosted LiveSync"; - readonly zh: "删除本地数据库以重置或卸载 Self-hosted LiveSync"; - readonly "zh-tw": "刪除本機資料庫以重設或解除安裝 Self-hosted LiveSync"; - }; - readonly "Delete old metadata of deleted files on start-up": { - readonly def: "Delete old metadata of deleted files on start-up"; - readonly es: "Borrar metadatos viejos al iniciar"; - readonly fr: "Supprimer les anciennes métadonnées des fichiers effacés au démarrage"; - readonly he: "מחק מטה-נתונים ישנים של קבצים שנמחקו בעת הפעלה"; - readonly ja: "削除済みデータのメタデータをクリーンナップする"; - readonly ko: "시작 시 삭제된 파일의 오래된 메타데이터 삭제"; - readonly ru: "Удалять старые метаданные удалённых файлов при запуске"; - readonly zh: "启动时删除已删除文件的旧元数据"; - readonly "zh-tw": "啟動時刪除已刪除檔案的舊中繼資料"; - }; - readonly "Delete Remote Configuration": { - readonly def: "Delete Remote Configuration"; - readonly es: "Eliminar configuración remota"; - readonly ja: "リモート設定を削除"; - readonly ko: "원격 구성 삭제"; - readonly ru: "Удалить удалённую конфигурацию"; - readonly zh: "删除远端配置"; - readonly "zh-tw": "刪除遠端設定"; - }; - readonly "Delete remote configuration '{name}'?": { - readonly def: "Delete remote configuration '{name}'?"; - readonly es: "¿Eliminar la configuración remota '{name}'?"; - readonly ja: "リモート設定 '{name}' を削除しますか?"; - readonly ko: "'{name}' 원격 구성을 삭제할까요?"; - readonly ru: "Удалить удалённую конфигурацию '{name}'?"; - readonly zh: "要删除远端配置“{name}”吗?"; - readonly "zh-tw": "要刪除遠端設定「{name}」嗎?"; - }; - readonly desktop: { - readonly def: "desktop"; - readonly es: "equipo de escritorio"; - readonly ja: "デスクトップ"; - readonly ko: "데스크톱"; - readonly ru: "рабочий стол"; - readonly zh: "桌面设备"; - readonly "zh-tw": "桌面裝置"; - }; - readonly Developer: { - readonly def: "Developer"; - readonly es: "Desarrollador"; - readonly ja: "開発者"; - readonly ko: "개발자"; - readonly ru: "Разработчик"; - readonly zh: "开发者"; - readonly "zh-tw": "開發者"; - }; - readonly Device: { - readonly def: "Device"; - readonly ja: "デバイス"; - readonly ko: "기기"; - readonly ru: "Устройство"; - readonly zh: "设备"; - readonly "zh-tw": "裝置"; - }; - readonly "Device name": { - readonly def: "Device name"; - readonly es: "Nombre del dispositivo"; - readonly fr: "Nom de l'appareil"; - readonly he: "שם מכשיר"; - readonly ja: "デバイス名"; - readonly ko: "기기 이름"; - readonly ru: "Имя устройства"; - readonly zh: "设备名称"; - readonly "zh-tw": "裝置名稱"; - }; - readonly "Device Setup Method": { - readonly def: "Device Setup Method"; - readonly es: "Método de configuración del dispositivo"; - readonly ja: "端末の設定方法"; - readonly ko: "장치 설정 방법"; - readonly ru: "Способ настройки устройства"; - readonly zh: "设备设置方式"; - readonly "zh-tw": "裝置設定方式"; - }; - readonly "dialog.yourLanguageAvailable": { - readonly def: "Self-hosted LiveSync had translations for your language, so the %{Display language} setting was enabled.\n\nNote: Not all messages are translated. We are waiting for your contributions!\nNote 2: If you create an Issue, **please revert to Default** and then take screenshots, messages and logs. This can be done in the setting dialogue.\nMay you find it easy to use!"; - readonly fr: "Self-hosted LiveSync dispose d'une traduction pour votre langue, le paramètre %{Display language} a donc été activé.\n\nNote : Tous les messages ne sont pas traduits. Nous attendons vos contributions !\nNote 2 : Si vous créez un ticket, **veuillez revenir à Par défaut** puis prendre des captures d'écran, messages et journaux. Cela peut être fait dans la boîte de dialogue des paramètres.\nBonne utilisation !"; - readonly he: "ל-Self-hosted LiveSync יש תרגום לשפתך, ולכן הגדרת %{Display language} הופעלה.\n\nהערה: לא כל ההודעות מתורגמות. אנחנו ממתינים לתרומותיך!\nהערה 2: אם אתה פותח Issue, **אנא חזור ל-%{lang-def}** ואז צלם צילומי מסך, הודעות ויומנים. ניתן לעשות זאת בדיאלוג ההגדרות.\nנקווה שתמצא/י את הפלאגין נוח לשימוש!"; - readonly ja: "Self-hosted LiveSync に設定されている言語の翻訳がありましたので、インターフェースの表示言語が適用されました。\n\n注意: 全てのメッセージは翻訳されていません。あなたの貢献をお待ちしています!\nGithubにIssueを作成する際には、 インターフェースの表示言語 を一旦 Default に戻してから、スクショやメッセージ、ログを収集してください。これは設定から変更できます。\n\n便利に使用できれば幸いです。"; - readonly ko: "Self-hosted LiveSync에서 귀하의 언어로 번역을 제공하므로 %{Display language} 설정이 활성화되었습니다.\n\n참고: 모든 메시지가 번역되지는 않습니다. 귀하의 기여를 기다리고 있습니다!\n참고 2: 이슈를 생성하는 경우 **Default로 되돌린 후** 스크린샷, 메시지, 로그를 가져와 주세요. 이는 설정 대화 상자에서 할 수 있습니다.\n간편하게 사용하실 수 있었으면 좋겠습니다!"; - readonly ru: "Self-hosted LiveSync имеет переводы для вашего языка, поэтому была включена настройка языка Display language.\n\nПримечание: Не все сообщения переведены. Мы ждём ваших предложений!\nПримечание 2: При создании Issue, пожалуйста, вернитесь к lang-def, затем сделайте скриншоты, сообщения и логи. Это можно сделать в настройках.\nНадеемся, вам будет удобно использовать!"; - readonly zh: "Self-hosted LiveSync已提供您语言的翻译,因此启用了%{Display language}\n\n注意:并非所有消息都已翻译。我们期待您的贡献!\n注意 2:若您创建问题报告, **请切换回Default** ,然后截取屏幕截图、消息和日志,此操作可在设置对话框中完成\n愿您使用顺心!"; - readonly "zh-tw": "Self-hosted LiveSync 已提供你目前語言的翻譯,因此已啟用顯示語言設定。\n\n注意:並非所有訊息都已完成翻譯,歡迎協助補充!\n注意 2:如果你要回報問題,請先切回預設語言,再附上截圖、訊息與日誌。\n\n希望你使用愉快!"; - }; - readonly "dialog.yourLanguageAvailable.btnRevertToDefault": { - readonly def: "Keep Default"; - readonly fr: "Conserver Par défaut"; - readonly he: "השאר %{lang-def}"; - readonly ja: "Keep Default"; - readonly ko: "Default 유지"; - readonly ru: "Оставить lang-def"; - readonly zh: "保持Default"; - readonly "zh-tw": "恢復為預設語言"; - }; - readonly "dialog.yourLanguageAvailable.Title": { - readonly def: " Translation is available!"; - readonly fr: " Une traduction est disponible !"; - readonly he: " תרגום זמין!"; - readonly ja: "翻訳が利用可能です!"; - readonly ko: " 번역을 사용할 수 있습니다!"; - readonly ru: "Доступен перевод!"; - readonly zh: " 翻译可用!"; - readonly "zh-tw": "已提供你的語言翻譯!"; - }; - readonly "Disables all synchronization and restart.": { - readonly def: "Disables all synchronization and restart."; - readonly es: "Desactiva toda la sincronización y reinicia la aplicación."; - readonly ja: "すべての同期を無効にして再起動します。"; - readonly ko: "모든 동기화를 비활성화하고 재시작합니다."; - readonly ru: "Отключает всю синхронизацию и перезапускает приложение."; - readonly zh: "禁用所有同步并重新启动。"; - readonly "zh-tw": "停用所有同步並重新啟動。"; - }; - readonly "Disables logging, only shows notifications. Please disable if you report an issue.": { - readonly def: "Disables logging, only shows notifications. Please disable if you report an issue."; - readonly es: "Desactiva registros, solo muestra notificaciones. Desactívelo si reporta un problema."; - readonly fr: "Désactive la journalisation, n'affiche que les notifications. Veuillez désactiver si vous signalez un problème."; - readonly he: "מכבה רישום יומן, מציג התראות בלבד. אנא כבה אם אתה מדווח על בעיה."; - readonly ja: "ログを無効にし、通知のみを表示します。Issueを報告する場合は無効にしてください。"; - readonly ko: "로깅을 비활성화하고 알림만 표시합니다. 문제를 신고하는 경우 비활성화해 주세요."; - readonly ru: "Отключает логирование, показывает только уведомления. Пожалуйста, отключите при сообщении о проблеме."; - readonly zh: "禁用日志记录,仅显示通知。如果您报告问题,请禁用此选项"; - readonly "zh-tw": "停用日誌記錄,只顯示通知。如果你要回報問題,請關閉此選項。"; - }; - readonly "Display Language": { - readonly def: "Display Language"; - readonly es: "Idioma de visualización"; - readonly fr: "Langue d'affichage"; - readonly he: "שפת תצוגה"; - readonly ja: "インターフェースの表示言語"; - readonly ko: "표시 언어"; - readonly ru: "Язык интерфейса"; - readonly zh: "显示语言"; - readonly "zh-tw": "顯示語言"; - }; - readonly "Display name": { - readonly def: "Display name"; - readonly es: "Nombre para mostrar"; - readonly ja: "表示名"; - readonly ko: "표시 이름"; - readonly ru: "Отображаемое имя"; - readonly zh: "显示名称"; - readonly "zh-tw": "顯示名稱"; - }; - readonly "Do not check configuration mismatch before replication": { - readonly def: "Do not check configuration mismatch before replication"; - readonly es: "No verificar incompatibilidades antes de replicar"; - readonly fr: "Ne pas vérifier les incohérences de configuration avant la réplication"; - readonly he: "אל תבדוק אי-התאמה בתצורה לפני שכפול"; - readonly ja: "サーバーから同期する前に設定の不一致を確認しない"; - readonly ko: "복제 전 구성 불일치 확인 안 함"; - readonly ru: "Не проверять несовпадение конфигурации перед репликацией"; - readonly zh: "在复制前不检查配置不匹配"; - readonly "zh-tw": "複寫前不檢查設定是否不一致"; - }; - readonly "Do not keep metadata of deleted files.": { - readonly def: "Do not keep metadata of deleted files."; - readonly es: "No conservar metadatos de archivos borrados"; - readonly fr: "Ne pas conserver les métadonnées des fichiers supprimés."; - readonly he: "אל תשמור מטה-נתונים של קבצים שנמחקו."; - readonly ja: "削除済みファイルのメタデータを保持しない"; - readonly ko: "삭제된 파일의 메타데이터를 보관하지 않습니다."; - readonly ru: "Не хранить метаданные удалённых файлов."; - readonly zh: "不保留已删除文件的元数据 "; - readonly "zh-tw": "不保留已刪除檔案的中繼資料。"; - }; - readonly "Do not split chunks in the background": { - readonly def: "Do not split chunks in the background"; - readonly es: "No dividir chunks en segundo plano"; - readonly fr: "Ne pas fragmenter en arrière-plan"; - readonly he: "אל תפצל נתחים ברקע"; - readonly ja: "バックグラウンドでチャンクを分割しない"; - readonly ko: "백그라운드에서 청크 분할 안 함"; - readonly ru: "Не разделять чанки в фоновом режиме"; - readonly zh: "不在后台分割 chunks"; - readonly "zh-tw": "不在背景分割 chunks"; - }; - readonly "Do not use internal API": { - readonly def: "Do not use internal API"; - readonly es: "No usar API interna"; - readonly fr: "Ne pas utiliser l'API interne"; - readonly he: "אל תשתמש ב-API פנימי"; - readonly ja: "内部APIを使用しない"; - readonly ko: "내부 API 사용 안 함"; - readonly ru: "Не использовать внутренний API"; - readonly zh: "不使用内部 API"; - readonly "zh-tw": "不使用內部 API"; - }; - readonly "Doctor.Button.DismissThisVersion": { - readonly def: "No, and do not ask again until the next release"; - readonly fr: "Non, et ne plus demander jusqu'à la prochaine version"; - readonly he: "לא, ואל תשאל שוב עד לגרסה הבאה"; - readonly ja: "いいえ、次のリリースまで再度確認しない"; - readonly ko: "아니요, 다음 릴리스까지 다시 묻지 않음"; - readonly ru: "Нет, и не спрашивать до следующего выпуска"; - readonly zh: "拒绝,并且直到下个版本前不再询问"; - }; - readonly "Doctor.Button.Fix": { - readonly def: "Fix it"; - readonly fr: "Corriger"; - readonly he: "תקן"; - readonly ja: "修正する"; - readonly ko: "수정"; - readonly ru: "Исправить"; - readonly zh: "修复"; - }; - readonly "Doctor.Button.FixButNoRebuild": { - readonly def: "Fix it but no rebuild"; - readonly fr: "Corriger mais sans reconstruction"; - readonly he: "תקן ללא בנייה מחדש"; - readonly ja: "修正するが再構築はしない"; - readonly ko: "수정하지만 재구축하지 않음"; - readonly ru: "Исправить без перестроения"; - readonly zh: "修复但不重建"; - }; - readonly "Doctor.Button.No": { - readonly def: "No"; - readonly fr: "Non"; - readonly he: "לא"; - readonly ja: "いいえ"; - readonly ko: "아니요"; - readonly ru: "Нет"; - readonly zh: "拒绝"; - }; - readonly "Doctor.Button.Skip": { - readonly def: "Leave it as is"; - readonly fr: "Laisser tel quel"; - readonly he: "השאר כפי שהוא"; - readonly ja: "そのままにする"; - readonly ko: "그대로 두기"; - readonly ru: "Оставить как есть"; - readonly zh: "保持不变"; - }; - readonly "Doctor.Button.Yes": { - readonly def: "Yes"; - readonly fr: "Oui"; - readonly he: "כן"; - readonly ja: "はい"; - readonly ko: "예"; - readonly ru: "Да"; - readonly zh: "确定"; - }; - readonly "Doctor.Dialogue.Main": { - readonly def: "Hi! Config Doctor has been activated because of ${activateReason}!\nAnd, unfortunately some configurations were detected as potential problems.\nPlease be assured. Let's solve them one by one.\n\nTo let you know ahead of time, we will ask you about the following items.\n\n${issues}\n\nShall we get started?"; - readonly fr: "Bonjour ! Config Doctor a été activé en raison de ${activateReason} !\nEt, malheureusement, certaines configurations ont été détectées comme des problèmes potentiels.\nPas d'inquiétude. Résolvons-les un par un.\n\nPour information, nous allons vous interroger sur les éléments suivants.\n\n${issues}\n\nVoulez-vous commencer ?"; - readonly he: "שלום! רופא התצורה הופעל בגלל ${activateReason}!\nולמרבה הצער, זוהו תצורות שעשויות להיות בעייתיות.\nהיה רגוע/ה. בואו נפתור אותן אחת אחת.\n\nלידיעתך מראש, נשאל אותך על הפריטים הבאים.\n\n${issues}\n\nהאם להתחיל?"; - readonly ja: "こんにちは!${activateReason}のため、設定診断ツールが起動しました!\n残念ながら、いくつかの設定が潜在的な問題として検出されました。\nご安心ください。一つずつ解決していきましょう。\n\n事前にお知らせしますと、以下の項目についてお尋ねします。\n\n${issues}\n\n始めていいですか?"; - readonly ko: "안녕하세요! ${activateReason} 로 인해 구성 진단 마법사가 활성화되었습니다!\n그리고 일부 구성이 잠재적인 문제로 감지되었습니다.\n안심하세요. 하나씩 해결해 봅시다.\n\n대상 항목은 다음과 같습니다.\n\n${issues}\n\n시작하시겠습니까?"; - readonly ru: "Привет! Диагностика настроек активирована из-за activateReason!\nК сожалению, некоторые настройки были обнаружены как потенциальные проблемы.\nНе волнуйтесь. Давайте решим их по очереди.\n\nСообщаем вам заранее, мы спросим о следующих пунктах.\n\nissues\n\nНачнём?"; - readonly zh: "您好!配置医生已根据您的要求启动(感谢您)!!遗憾的是,检测到部分配置存在潜在问题。请放心,我们将逐一解决这些问题。\n\n提前告知您,我们将就以下事项进行确认:\n\n为数据块计算修订版本(此前行为)\n增强块大小\n\n我们开始处理吗?"; - }; - readonly "Doctor.Dialogue.MainFix": { - readonly def: "\n## ${name}\n\n| Current | Ideal |\n|:---:|:---:|\n| ${current} | ${ideal} |\n\n**Recommendation Level:** ${level}\n\n### Why this has been detected?\n\n${reason}\n\n${note}\n\nFix this to the ideal value?"; - readonly fr: "\n## ${name}\n\n| Actuel | Idéal |\n|:---:|:---:|\n| ${current} | ${ideal} |\n\n**Niveau de recommandation :** ${level}\n\n### Pourquoi ceci a-t-il été détecté ?\n\n${reason}\n\n${note}\n\nCorriger à la valeur idéale ?"; - readonly he: "\n## ${name}\n\n| נוכחי | אידאלי |\n|:---:|:---:|\n| ${current} | ${ideal} |\n\n**רמת המלצה:** ${level}\n\n### מדוע זה זוהה?\n\n${reason}\n\n${note}\n\nלתקן לערך האידאלי?"; - readonly ja: "\n## ${name}\n\n| 現在の値 | 理想値 |\n|:---:|:---:|\n| ${current} | ${ideal} |\n\n**推奨レベル:** ${level}\n\n### 診断理由?\n\n${reason}\n\n${note}\n\nこれを理想値に修正しますか?"; - readonly ko: "**구성 이름:** `${name}`\n**현재 값:** `${current}`, **이상적인 값:** `${ideal}`\n**권장 수준:** ${level}\n**왜 이것이 감지되었나요?**\n${reason}\n\n\n${note}\n\n이상적인 값으로 수정하시겠습니까?"; - readonly ru: "name\n\n| Текущее | Идеальное |\n|:---:|:---:|\n| current | ideal |\n\n**Уровень рекомендации:** level\n\n### Почему это было обнаружено?\n\nreason\n\nnote\n\nИсправить на идеальное значение?"; - readonly zh: "\n## ${name}\n\n| Current | Ideal |\n|:---:|:---:|\n| ${current} | ${ideal} |\n\n**Recommendation Level:** ${level}\n\n### Why this has been detected?\n\n${reason}\n\n${note}\n\nFix this to the ideal value?"; - }; - readonly "Doctor.Dialogue.Title": { - readonly def: "Self-hosted LiveSync Config Doctor"; - readonly fr: "Docteur Config Self-hosted LiveSync"; - readonly he: "Self-hosted LiveSync Config Doctor"; - readonly ja: "Self-hosted LiveSync 設定診断ツール"; - readonly ko: "Self-hosted LiveSync 구성 진단 마법사"; - readonly ru: "Диагностика Self-hosted LiveSync"; - readonly zh: "Self-hosted LiveSync 配置诊断"; - }; - readonly "Doctor.Dialogue.TitleAlmostDone": { - readonly def: "Almost done!"; - readonly fr: "Presque terminé !"; - readonly he: "כמעט סיימנו!"; - readonly ja: "あと少しです!"; - readonly ko: "거의 완료되었습니다!"; - readonly ru: "Почти готово!"; - readonly zh: "全部完成!"; - }; - readonly "Doctor.Dialogue.TitleFix": { - readonly def: "Fix issue ${current}/${total}"; - readonly fr: "Corriger le problème ${current}/${total}"; - readonly he: "תקן בעיה ${current}/${total}"; - readonly ja: "問題の修正 ${current}/${total}"; - readonly ko: "문제 해결 ${current}/${total}"; - readonly ru: "Исправление проблемы current/total"; - readonly zh: "修复问题 ${current}/${total}"; - }; - readonly "Doctor.Level.Must": { - readonly def: "Must"; - readonly fr: "Obligatoire"; - readonly he: "חובה"; - readonly ja: "必須"; - readonly ko: "필수"; - readonly ru: "Обязательно"; - readonly zh: "必须"; - }; - readonly "Doctor.Level.Necessary": { - readonly def: "Necessary"; - readonly fr: "Nécessaire"; - readonly he: "נדרש"; - readonly ja: "必要"; - readonly ko: "필수"; - readonly ru: "Необходимо"; - readonly zh: "必要"; - }; - readonly "Doctor.Level.Optional": { - readonly def: "Optional"; - readonly fr: "Optionnel"; - readonly he: "אופציונלי"; - readonly ja: "任意"; - readonly ko: "선택사항"; - readonly ru: "Опционально"; - readonly zh: "可选"; - }; - readonly "Doctor.Level.Recommended": { - readonly def: "Recommended"; - readonly fr: "Recommandé"; - readonly he: "מומלץ"; - readonly ja: "推奨"; - readonly ko: "권장"; - readonly ru: "Рекомендуется"; - readonly zh: "推荐"; - }; - readonly "Doctor.Message.NoIssues": { - readonly def: "No issues detected!"; - readonly fr: "Aucun problème détecté !"; - readonly he: "לא זוהו בעיות!"; - readonly ja: "問題は検出されませんでした!"; - readonly ko: "문제가 감지되지 않았습니다!"; - readonly ru: "Проблем не обнаружено!"; - readonly zh: "未发现问题!"; - }; - readonly "Doctor.Message.RebuildLocalRequired": { - readonly def: "Attention! A local database rebuild is required to apply this!"; - readonly fr: "Attention ! Une reconstruction de la base locale est requise pour appliquer ceci !"; - readonly he: "שים לב! נדרשת בנייה מחדש של מסד הנתונים המקומי כדי להחיל זאת!"; - readonly ja: "注意!これを適用するにはローカルデータベースの再構築が必要です!"; - readonly ko: "주의! 이를 적용하려면 로컬 데이터베이스 재구축이 필요합니다!"; - readonly ru: "Внимание! Для применения требуется перестроение локальной базы данных!"; - readonly zh: "注意!需要重建本地数据库以应用此项!"; - }; - readonly "Doctor.Message.RebuildRequired": { - readonly def: "Attention! A rebuild is required to apply this!"; - readonly fr: "Attention ! Une reconstruction est requise pour appliquer ceci !"; - readonly he: "שים לב! נדרשת בנייה מחדש כדי להחיל זאת!"; - readonly ja: "注意!これを適用するには再構築が必要です!"; - readonly ko: "주의! 이를 적용하려면 재구축이 필요합니다!"; - readonly ru: "Внимание! Для применения требуется перестроение!"; - readonly zh: "注意!需要重建才能应用此项!"; - }; - readonly "Doctor.Message.SomeSkipped": { - readonly def: "We left some issues as is. Shall I ask you again on next startup?"; - readonly fr: "Nous avons laissé certains problèmes en l'état. Dois-je vous demander à nouveau au prochain démarrage ?"; - readonly he: "השארנו כמה בעיות כפי שהן. האם לשאול שוב בהפעלה הבאה?"; - readonly ja: "いくつかの問題をそのままにしました。次回起動時に再度確認しますか?"; - readonly ko: "일부 문제를 그대로 두었습니다. 다음 시작 시 다시 질문할까요?"; - readonly ru: "Некоторые проблемы оставлены как есть. Спросить снова при следующем запуске?"; - readonly zh: "我们将某些问题留给了以后处理。是否要在下次启动时再次询问您?"; - }; - readonly "Doctor.RULES.E2EE_V02500.REASON": { - readonly def: "The End-to-End Encryption has got now more robust and faster. Also because, the previous E2EE was found to be compromised in a re-conducted code review. It should be applied as soon as possible. Really apologises for your inconvenience. And, this setting is not forward compatible. All synchronised devices must be updated to v0.25.0 or higher. Rebuilds are not required and will be converted from the new transfer to the new format, However, it is recommended to rebuild whenever possible."; - readonly fr: "Le chiffrement de bout en bout est désormais plus robuste et plus rapide. Aussi parce que le précédent E2EE s'est révélé compromis lors d'une nouvelle revue de code. Il doit être appliqué dès que possible. Nous nous excusons sincèrement pour la gêne occasionnée. Ce paramètre n'est pas compatible avec les versions antérieures. Tous les appareils synchronisés doivent être mis à jour en v0.25.0 ou supérieur. Les reconstructions ne sont pas requises et seront converties du nouveau transfert vers le nouveau format. Il est toutefois recommandé de reconstruire dans la mesure du possible."; - readonly he: "ההצפנה מקצה לקצה עודכנה לגרסה חזקה ומהירה יותר. בנוסף, בסקירת קוד שנערכה מחדש הוסבר שההצפנה הקודמת נפרצת. יש להחיל זאת בהקדם האפשרי. מתנצלים על אי הנוחות. שים לב שהגדרה זו אינה תואמת לאחור. כל המכשירים המסונכרנים חייבים להיות מעודכנים לגרסה 0.25.0 ומעלה. אין צורך בבנייה מחדש והנתונים יומרו בהדרגה לפורמט החדש, אך מומלץ לבנות מחדש בהזדמנות הראשונה."; - readonly ja: "エンドツーエンド暗号化がより堅牢で高速になりました。また、以前のE2EEは再コードレビューにより脆弱性が発見されました。できるだけ早く適用することをお勧めします。ご不便をおかけして申し訳ありません。また、この設定は下位互換性がありません。すべての同期デバイスをv0.25.0以降にアップデートする必要があります。再構築は必須ではなく、新しい転送から新しいフォーマットに変換されますが、可能な限り再構築をお勧めします。"; - readonly ru: "Сквозное шифрование стало более надёжным и быстрым. Предыдущее E2EE было скомпрометировано. Следует применить как можно скорее."; - readonly zh: "The End-to-End Encryption has got now more robust and faster. Also because, the previous E2EE was found to be compromised in a re-conducted code review. It should be applied as soon as possible. Really apologises for your inconvenience. And, this setting is not forward compatible. All synchronised devices must be updated to v0.25.0 or higher. Rebuilds are not required and will be converted from the new transfer to the new format, However, it is recommended to rebuild whenever possible."; - }; - readonly "Document History": { - readonly def: "Document History"; - readonly "zh-tw": "文件歷程"; - }; - readonly Duplicate: { - readonly def: "Duplicate"; - readonly es: "Duplicar"; - readonly ja: "複製"; - readonly ko: "복제"; - readonly ru: "Дублировать"; - readonly zh: "复制"; - readonly "zh-tw": "複製"; - }; - readonly "Duplicate remote": { - readonly def: "Duplicate remote"; - readonly es: "Duplicar remoto"; - readonly ja: "リモート設定を複製"; - readonly ko: "원격 구성 복제"; - readonly ru: "Дублировать удалённую конфигурацию"; - readonly zh: "复制远端配置"; - readonly "zh-tw": "複製遠端設定"; - }; - readonly "E2EE Configuration": { - readonly def: "E2EE Configuration"; - readonly es: "Configuración de E2EE"; - readonly ja: "E2EE 設定"; - readonly ko: "E2EE 구성"; - readonly ru: "Конфигурация сквозного шифрования"; - readonly zh: "E2EE 配置"; - readonly "zh-tw": "E2EE 設定"; - }; - readonly "Edge case addressing (Behaviour)": { - readonly def: "Edge case addressing (Behaviour)"; - readonly es: "Tratamiento de casos límite (comportamiento)"; - readonly ja: "特殊なケースへの対応(動作)"; - readonly ko: "특수 상황 처리 (동작)"; - readonly ru: "Обработка особых случаев (поведение)"; - readonly zh: "边缘情况处理(行为)"; - readonly "zh-tw": "邊緣情況處理(行為)"; - }; - readonly "Edge case addressing (Database)": { - readonly def: "Edge case addressing (Database)"; - readonly es: "Tratamiento de casos límite (base de datos)"; - readonly ja: "特殊なケースへの対応(データベース)"; - readonly ko: "특수 상황 처리 (데이터베이스)"; - readonly ru: "Обработка особых случаев (база данных)"; - readonly zh: "边缘情况处理(数据库)"; - readonly "zh-tw": "邊緣情況處理(資料庫)"; - }; - readonly "Edge case addressing (Processing)": { - readonly def: "Edge case addressing (Processing)"; - readonly es: "Tratamiento de casos límite (procesamiento)"; - readonly ja: "特殊なケースへの対応(処理)"; - readonly ko: "특수 상황 처리 (처리)"; - readonly ru: "Обработка особых случаев (обработка)"; - readonly zh: "边缘情况处理(处理)"; - readonly "zh-tw": "邊緣情況處理(處理)"; - }; - readonly "Emergency restart": { - readonly def: "Emergency restart"; - readonly es: "Reinicio de emergencia"; - readonly ja: "緊急再起動"; - readonly ko: "긴급 재시작"; - readonly ru: "Аварийный перезапуск"; - readonly zh: "紧急重启"; - readonly "zh-tw": "緊急重新啟動"; - }; - readonly "Enable advanced features": { - readonly def: "Enable advanced features"; - readonly es: "Habilitar características avanzadas"; - readonly fr: "Activer les fonctionnalités avancées"; - readonly he: "הפעל תכונות מתקדמות"; - readonly ja: "高度な機能を有効にする"; - readonly ko: "고급 기능 활성화"; - readonly ru: "Включить расширенные функции"; - readonly zh: "启用高级功能"; - readonly "zh-tw": "啟用進階功能"; - }; - readonly "Enable customization sync": { - readonly def: "Enable customization sync"; - readonly es: "Habilitar sincronización de personalización"; - readonly fr: "Activer la synchronisation de personnalisation"; - readonly he: "הפעל סנכרון התאמה אישית"; - readonly ja: "カスタマイズ同期を有効"; - readonly ko: "사용자 설정 동기화 활성화"; - readonly ru: "Включить синхронизацию настроек"; - readonly zh: "启用自定义同步"; - readonly "zh-tw": "啟用自訂同步"; - }; - readonly "Enable Developers' Debug Tools.": { - readonly def: "Enable Developers' Debug Tools."; - readonly es: "Habilitar herramientas de depuración"; - readonly fr: "Activer les outils de débogage pour développeurs."; - readonly he: "הפעל כלי ניפוי באגים למפתחים."; - readonly ja: "開発者用デバッグツールを有効にする"; - readonly ko: "개발자 디버그 도구 활성화"; - readonly ru: "Включить инструменты разработчика."; - readonly zh: "启用开发者调试工具 "; - readonly "zh-tw": "啟用開發者除錯工具。"; - }; - readonly "Enable edge case treatment features": { - readonly def: "Enable edge case treatment features"; - readonly es: "Habilitar manejo de casos límite"; - readonly fr: "Activer les fonctionnalités pour cas particuliers"; - readonly he: "הפעל תכונות לטיפול במקרי קצה"; - readonly ja: "エッジケース対応機能を有効にする"; - readonly ko: "특수 사례 처리 기능 활성화"; - readonly ru: "Включить функции обработки граничных случаев"; - readonly zh: "启用边缘情况处理功能"; - readonly "zh-tw": "啟用邊緣情況處理功能"; - }; - readonly "Enable poweruser features": { - readonly def: "Enable poweruser features"; - readonly es: "Habilitar funciones para usuarios avanzados"; - readonly fr: "Activer les fonctionnalités pour utilisateurs avancés"; - readonly he: "הפעל תכונות למשתמש מתקדם"; - readonly ja: "エキスパート機能を有効にする"; - readonly ko: "파워 유저 기능 활성화"; - readonly ru: "Включить функции для опытных пользователей"; - readonly zh: "启用高级用户功能"; - readonly "zh-tw": "啟用進階使用者功能"; - }; - readonly "Enable this if your Object Storage doesn't support CORS": { - readonly def: "Enable this if your Object Storage doesn't support CORS"; - readonly es: "Habilitar si su almacenamiento no soporta CORS"; - readonly fr: "Activer ceci si votre stockage objet ne prend pas en charge CORS"; - readonly he: "הפעל אם אחסון האובייקטים שלך לא תומך ב-CORS"; - readonly ja: "オブジェクトストレージがCORSをサポートしていない場合は有効にしてください"; - readonly ko: "객체 스토리지가 CORS를 지원하지 않는 경우 활성화하세요"; - readonly ru: "Включите, если ваше объектное хранилище не поддерживает CORS"; - readonly zh: "如果您的对象存储不支持 CORS,请启用此功能 "; - readonly "zh-tw": "如果你的物件儲存不支援 CORS,請啟用此選項"; - }; - readonly "Enable this option to automatically apply the most recent change to documents even when it conflicts": { - readonly def: "Enable this option to automatically apply the most recent change to documents even when it conflicts"; - readonly es: "Aplicar cambios recientes automáticamente aunque generen conflictos"; - readonly fr: "Activer cette option pour appliquer automatiquement la modification la plus récente aux documents même en cas de conflit"; - readonly he: "הפעל אפשרות זו כדי להחיל אוטומטית את השינוי האחרון במסמכים גם כשיש קונפליקט"; - readonly ja: "このオプションを有効にすると、競合があっても最新の変更を自動的にドキュメントに適用します"; - readonly ko: "이 옵션을 활성화하면 충돌이 있어도 문서에 가장 최근 변경 사항을 자동으로 적용합니다"; - readonly ru: "Включите эту опцию для автоматического применения последних изменений к документам даже при конфликте"; - readonly zh: "启用此选项可在文档冲突时自动应用最新的更改"; - readonly "zh-tw": "啟用此選項後,即使發生衝突也會自動套用文件的最新變更"; - }; - readonly "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": { - readonly def: "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended."; - readonly es: "Cifrar contenido en la base de datos remota. Se recomienda habilitar si usa la sincronización del plugin."; - readonly fr: "Chiffrer le contenu sur la base de données distante. Si vous utilisez la fonction de synchronisation du plugin, l'activation est recommandée."; - readonly he: "הצפן תוכן במסד הנתונים המרוחק. אם אתה משתמש בתכונת הסנכרון של התוסף, מומלץ להפעיל זאת."; - readonly ja: "リモートデータベースの暗号化(オンにすることを推奨)"; - readonly ko: "원격 데이터베이스의 내용을 암호화합니다. 플러그인의 동기화 기능을 사용하는 경우 활성화를 권장합니다."; - readonly ru: "Шифровать содержимое на удалённой базе данных. Рекомендуется включить при использовании функции синхронизации плагина."; - readonly zh: "加密远程数据库中的内容。如果您使用插件的同步功能,则建议启用此功能 "; - readonly "zh-tw": "加密遠端資料庫中的內容。如果你使用外掛的同步功能,建議啟用此選項。"; - }; - readonly "Encrypting sensitive configuration items": { - readonly def: "Encrypting sensitive configuration items"; - readonly es: "Cifrando elementos sensibles"; - readonly fr: "Chiffrement des éléments de configuration sensibles"; - readonly he: "הצפן פריטי תצורה רגישים"; - readonly ja: "機密性の高い設定項目の暗号化"; - readonly ko: "민감한 구성 항목 암호화"; - readonly ru: "Шифрование конфиденциальных настроек"; - readonly zh: "加密敏感配置项"; - readonly "zh-tw": "加密敏感設定項目"; - }; - readonly "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": { - readonly def: "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files."; - readonly es: "Frase de cifrado. Si la cambia, sobrescriba la base del servidor con los nuevos archivos cifrados."; - readonly fr: "Phrase secrète de chiffrement. Si modifiée, vous devriez écraser la base de données du serveur avec les nouveaux fichiers (chiffrés)."; - readonly he: "ביטוי סיסמה להצפנה. אם שונה, יש לדרוס את מסד הנתונים של השרת בקבצים החדשים (המוצפנים)."; - readonly ja: "暗号化パスフレーズ。変更した場合、新しい(暗号化された)ファイルでサーバーのデータベースを上書きする必要があります。"; - readonly ko: "패스프레이즈는 암호화에 사용되는 긴 암호 문구입니다. 변경한 경우, 암호화된 새 파일로 서버의 데이터베이스를 덮어써야 합니다."; - readonly ru: "Парольная фраза шифрования. При изменении нужно перезаписать базу данных сервера новыми (зашифрованными) файлами."; - readonly zh: "加密密码。如果更改,您应该用新的(加密的)文件覆盖服务器的数据库 "; - readonly "zh-tw": "加密密語。如果變更了它,你應以新的(已加密)檔案覆寫伺服器上的資料庫。"; - }; - readonly "End-to-End Encryption": { - readonly def: "End-to-End Encryption"; - readonly es: "Cifrado de extremo a extremo"; - readonly fr: "Chiffrement de bout en bout"; - readonly he: "הצפנה מקצה לקצה"; - readonly ja: "E2E暗号化"; - readonly ko: "종단간 암호화"; - readonly ru: "Сквозное шифрование"; - readonly zh: "端到端加密"; - readonly "zh-tw": "端對端加密"; - }; - readonly "Endpoint URL": { - readonly def: "Endpoint URL"; - readonly es: "URL del endpoint"; - readonly fr: "URL du point de terminaison"; - readonly he: "כתובת נקודת קצה (Endpoint URL)"; - readonly ja: "エンドポイントURL"; - readonly ko: "엔드포인트 URL"; - readonly ru: "URL конечной точки"; - readonly zh: "终端URL"; - readonly "zh-tw": "端點 URL"; - }; - readonly "Enhance chunk size": { - readonly def: "Enhance chunk size"; - readonly es: "Mejorar tamaño de chunks"; - readonly fr: "Améliorer la taille des fragments"; - readonly he: "הגדל גודל נתח"; - readonly ja: "チャンクサイズを最適化する"; - readonly ko: "청크 크기 향상"; - readonly ru: "Улучшить размер чанка"; - readonly zh: "增大块大小"; - readonly "zh-tw": "擴大 chunk 大小"; - }; - readonly "Enter Server Information": { - readonly def: "Enter Server Information"; - readonly es: "Introducir información del servidor"; - readonly ja: "サーバー情報の入力"; - readonly ko: "서버 정보 입력"; - readonly ru: "Ввести данные сервера"; - readonly zh: "输入服务器信息"; - readonly "zh-tw": "輸入伺服器資訊"; - }; - readonly "Enter the server information manually": { - readonly def: "Enter the server information manually"; - readonly es: "Introducir manualmente la información del servidor"; - readonly ja: "サーバー情報を手動で入力する"; - readonly ko: "서버 정보를 수동으로 입력"; - readonly ru: "Ввести данные сервера вручную"; - readonly zh: "手动输入服务器信息"; - readonly "zh-tw": "手動輸入伺服器資訊"; - }; - readonly Export: { - readonly def: "Export"; - readonly es: "Exportar"; - readonly ja: "エクスポート"; - readonly ko: "내보내기"; - readonly ru: "Экспорт"; - readonly zh: "导出"; - readonly "zh-tw": "匯出"; - }; - readonly "Failed to connect to remote for compaction.": { - readonly def: "Failed to connect to remote for compaction."; - readonly ja: "リモートデータベースに接続できず、コンパクションを実行できませんでした。"; - readonly ko: "압축을 위해 원격 데이터베이스에 연결하지 못했습니다."; - readonly ru: "Не удалось подключиться к удалённой базе данных для компакции."; - readonly zh: "无法连接到远程数据库以执行压缩。"; - readonly "zh-tw": "無法連線到遠端資料庫以執行壓縮。"; - }; - readonly "Failed to connect to remote for compaction. ${reason}": { - readonly def: "Failed to connect to remote for compaction. ${reason}"; - readonly ja: "リモートデータベースに接続できず、コンパクションを実行できませんでした。${reason}"; - readonly ko: "압축을 위해 원격 데이터베이스에 연결하지 못했습니다. ${reason}"; - readonly ru: "Не удалось подключиться к удалённой базе данных для компакции. ${reason}"; - readonly zh: "无法连接到远程数据库以执行压缩。${reason}"; - readonly "zh-tw": "無法連線到遠端資料庫以執行壓縮。${reason}"; - }; - readonly "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": { - readonly def: "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled."; - readonly ja: "Garbage Collection 前にワンショットレプリケーションを開始できませんでした。Garbage Collection はキャンセルされました。"; - readonly ko: "Garbage Collection 전에 일회성 복제를 시작하지 못했습니다. Garbage Collection을 취소합니다."; - readonly ru: "Не удалось запустить одноразовую репликацию перед Garbage Collection. Garbage Collection отменена."; - readonly zh: "无法在垃圾回收前启动一次性复制。垃圾回收已取消。"; - readonly "zh-tw": "無法在垃圾回收前啟動一次性複寫。垃圾回收已取消。"; - }; - readonly "Failed to start replication after Garbage Collection.": { - readonly def: "Failed to start replication after Garbage Collection."; - readonly ja: "Garbage Collection 後にレプリケーションを開始できませんでした。"; - readonly ko: "Garbage Collection 후 복제를 시작하지 못했습니다."; - readonly ru: "Не удалось запустить репликацию после Garbage Collection."; - readonly zh: "垃圾回收后无法启动复制。"; - readonly "zh-tw": "垃圾回收後無法啟動複寫。"; - }; - readonly Fetch: { - readonly def: "Fetch"; - readonly es: "Obtener"; - readonly ja: "取得"; - readonly ko: "가져오기"; - readonly ru: "Получить"; - readonly zh: "获取"; - readonly "zh-tw": "抓取"; - }; - readonly "Fetch chunks on demand": { - readonly def: "Fetch chunks on demand"; - readonly es: "Obtener chunks bajo demanda"; - readonly fr: "Récupérer les fragments à la demande"; - readonly he: "משוך נתחים לפי דרישה"; - readonly ja: "ユーザーのタイミングでチャンクの更新を確認する"; - readonly ko: "필요 시 청크 원격 가져오기"; - readonly ru: "Загружать чанки по требованию"; - readonly zh: "按需获取块"; - readonly "zh-tw": "按需抓取 chunks"; - }; - readonly "Fetch database with previous behaviour": { - readonly def: "Fetch database with previous behaviour"; - readonly es: "Obtener BD con comportamiento anterior"; - readonly fr: "Récupérer la base de données avec le comportement précédent"; - readonly he: "משוך מסד נתונים עם התנהגות קודמת"; - readonly ja: "以前の動作でデータベースを取得"; - readonly ko: "이전 동작으로 데이터베이스 가져오기"; - readonly ru: "Загрузить базу данных с предыдущим поведением"; - readonly zh: "使用以前的行为获取数据库"; - readonly "zh-tw": "以前一種行為抓取資料庫"; - }; - readonly "Fetch remote settings": { - readonly def: "Fetch remote settings"; - readonly es: "Obtener ajustes remotos"; - readonly ja: "リモート設定を取得"; - readonly ko: "원격 설정 가져오기"; - readonly ru: "Получить настройки с удалённого хранилища"; - readonly zh: "获取远端设置"; - readonly "zh-tw": "抓取遠端設定"; - }; - readonly "File to resolve conflict": { - readonly def: "File to resolve conflict"; - readonly es: "Archivo para resolver el conflicto"; - readonly ja: "競合を解決するファイル"; - readonly ko: "충돌을 해결할 파일"; - readonly ru: "Файл для разрешения конфликта"; - readonly zh: "选择要解决冲突的文件"; - readonly "zh-tw": "要解決衝突的檔案"; - }; - readonly "File to view History": { - readonly def: "File to view History"; - readonly "zh-tw": "要檢視歷程的檔案"; - }; - readonly Filename: { - readonly def: "Filename"; - readonly es: "Nombre de archivo"; - readonly fr: "Nom de fichier"; - readonly he: "שם קובץ"; - readonly ja: "ファイル名"; - readonly ko: "파일명"; - readonly ru: "Имя файла"; - readonly zh: "文件名"; - readonly "zh-tw": "檔名"; - }; - readonly "First, please select the option that best describes your current situation.": { - readonly def: "First, please select the option that best describes your current situation."; - readonly es: "Primero, seleccione la opción que describa mejor su situación actual。"; - readonly ja: "まず、現在の状況に最も近い項目を選択してください。"; - readonly ko: "먼저 현재 상황에 가장 잘 맞는 항목을 선택해 주세요。"; - readonly ru: "Сначала выберите вариант, который лучше всего описывает вашу текущую ситуацию。"; - readonly zh: "首先,请选择最符合你当前情况的选项。"; - readonly "zh-tw": "首先,請選擇最符合你目前情況的選項。"; - }; - readonly "Flag and restart": { - readonly def: "Flag and restart"; - readonly es: "Marcar y reiniciar"; - readonly ja: "フラグを立てて再起動"; - readonly ko: "표시 후 재시작"; - readonly ru: "Пометить и перезапустить"; - readonly zh: "标记后重启"; - readonly "zh-tw": "標記後重新啟動"; - }; - readonly "Forces the file to be synced when opened.": { - readonly def: "Forces the file to be synced when opened."; - readonly es: "Forzar sincronización al abrir archivo"; - readonly fr: "Force la synchronisation du fichier à son ouverture."; - readonly he: "מכריח סנכרון הקובץ בעת פתיחתו."; - readonly ja: "ファイルを開いたときに強制的に同期します。"; - readonly ko: "파일을 열 때 강제로 동기화합니다."; - readonly ru: "Принудительно синхронизировать файл при открытии."; - readonly zh: "打开文件时强制同步该文件 "; - readonly "zh-tw": "開啟檔案時強制同步該檔案。"; - }; - readonly "Fresh Start Wipe": { - readonly def: "Fresh Start Wipe"; - readonly es: "Borrado para reinicio completo"; - readonly ja: "初期化ワイプ"; - readonly ko: "새로 시작 지우기"; - readonly ru: "Полный сброс для чистого старта"; - readonly zh: "全新开始清除"; - readonly "zh-tw": "全新開始清除"; - }; - readonly "Garbage Collection cancelled by user.": { - readonly def: "Garbage Collection cancelled by user."; - readonly ja: "ユーザーによって Garbage Collection がキャンセルされました。"; - readonly ko: "사용자가 Garbage Collection을 취소했습니다."; - readonly ru: "Пользователь отменил Garbage Collection."; - readonly zh: "用户已取消垃圾回收。"; - readonly "zh-tw": "使用者已取消垃圾回收。"; - }; - readonly "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": { - readonly def: "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds."; - readonly ja: "Garbage Collection が完了しました。削除したチャンク: ${deletedChunks} / ${totalChunks}。所要時間: ${seconds} 秒。"; - readonly ko: "Garbage Collection이 완료되었습니다. 삭제된 청크: ${deletedChunks} / ${totalChunks}. 소요 시간: ${seconds}초."; - readonly ru: "Garbage Collection завершена. Удалено чанков: ${deletedChunks} / ${totalChunks}. Затраченное время: ${seconds} сек."; - readonly zh: "垃圾回收完成。已删除 chunks:${deletedChunks} / ${totalChunks}。耗时:${seconds} 秒。"; - readonly "zh-tw": "垃圾回收完成。已刪除 chunks:${deletedChunks} / ${totalChunks}。耗時:${seconds} 秒。"; - }; - readonly "Garbage Collection Confirmation": { - readonly def: "Garbage Collection Confirmation"; - readonly ja: "Garbage Collection の確認"; - readonly ko: "Garbage Collection 확인"; - readonly ru: "Подтверждение Garbage Collection"; - readonly zh: "垃圾回收确认"; - readonly "zh-tw": "垃圾回收確認"; - }; - readonly "Garbage Collection V3 (Beta)": { - readonly def: "Garbage Collection V3 (Beta)"; - readonly es: "Recolección de basura V3 (Beta)"; - readonly ja: "ガーベジコレクション V3 (Beta)"; - readonly ko: "가비지 컬렉션 V3 (Beta)"; - readonly ru: "Сборка мусора V3 (Beta)"; - readonly zh: "垃圾回收 V3(Beta)"; - readonly "zh-tw": "垃圾回收 V3(Beta)"; - }; - readonly "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": { - readonly def: "Garbage Collection: Found ${unusedChunks} unused chunks to delete."; - readonly ja: "Garbage Collection: 削除対象の未使用チャンクが ${unusedChunks} 件見つかりました。"; - readonly ko: "Garbage Collection: 삭제할 미사용 청크 ${unusedChunks}개를 찾았습니다."; - readonly ru: "Garbage Collection: найдено ${unusedChunks} неиспользуемых чанков для удаления."; - readonly zh: "垃圾回收:发现 ${unusedChunks} 个未使用的 chunks 待删除。"; - readonly "zh-tw": "垃圾回收:找到 ${unusedChunks} 個未使用的 chunks 可刪除。"; - }; - readonly "Garbage Collection: Scanned ${scanned} / ~${docCount}": { - readonly def: "Garbage Collection: Scanned ${scanned} / ~${docCount}"; - readonly ja: "Garbage Collection: ${scanned} / ~${docCount} をスキャン済み"; - readonly ko: "Garbage Collection: ${scanned} / ~${docCount} 스캔됨"; - readonly ru: "Garbage Collection: просканировано ${scanned} / ~${docCount}"; - readonly zh: "垃圾回收:已扫描 ${scanned} / ~${docCount}"; - readonly "zh-tw": "垃圾回收:已掃描 ${scanned} / ~${docCount}"; - }; - readonly "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": { - readonly def: "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}"; - readonly ja: "Garbage Collection: スキャン完了。総チャンク数: ${totalChunks}、使用中チャンク数: ${usedChunks}"; - readonly ko: "Garbage Collection: 스캔 완료. 전체 청크 수: ${totalChunks}, 사용 중인 청크 수: ${usedChunks}"; - readonly ru: "Garbage Collection: сканирование завершено. Всего чанков: ${totalChunks}, используемых чанков: ${usedChunks}"; - readonly zh: "垃圾回收:扫描完成。总 chunks:${totalChunks},已使用 chunks:${usedChunks}"; - readonly "zh-tw": "垃圾回收:掃描完成。總 chunks:${totalChunks},已使用 chunks:${usedChunks}"; - }; - readonly "Handle files as Case-Sensitive": { - readonly def: "Handle files as Case-Sensitive"; - readonly es: "Manejar archivos como sensibles a mayúsculas"; - readonly fr: "Gérer les fichiers en respectant la casse"; - readonly he: "טפל בקבצים כתלויי רישיות"; - readonly ja: "ファイルの大文字・小文字を区別する"; - readonly ko: "파일을 대소문자 구분으로 처리"; - readonly ru: "Обрабатывать файлы с учётом регистра"; - readonly zh: "将文件视为区分大小写"; - readonly "zh-tw": "將檔案視為區分大小寫"; - }; - readonly "Hidden Files": { - readonly def: "Hidden Files"; - readonly es: "Archivos ocultos"; - readonly ja: "隠しファイル"; - readonly ko: "숨김 파일"; - readonly ru: "Скрытые файлы"; - readonly zh: "隐藏文件"; - readonly "zh-tw": "隱藏檔案"; - }; - readonly "Hide completely": { - readonly def: "Hide completely"; - readonly zh: "完全隐藏"; - readonly "zh-tw": "完全隱藏"; - }; - readonly "Highlight diff": { - readonly def: "Highlight diff"; - readonly "zh-tw": "醒目顯示差異"; - }; - readonly "How to display network errors when the sync server is unreachable.": { - readonly def: "How to display network errors when the sync server is unreachable."; - readonly es: "Cómo mostrar los errores de red cuando el servidor de sincronización no está disponible."; - readonly ja: "同期サーバーに到達できない場合のネットワークエラーの表示方法を設定します。"; - readonly ko: "동기화 서버에 연결할 수 없을 때 네트워크 오류를 어떻게 표시할지 설정합니다."; - readonly ru: "Определяет, как отображать сетевые ошибки, если сервер синхронизации недоступен."; - readonly zh: "当同步服务器不可达时,如何显示网络错误。"; - readonly "zh-tw": "當同步伺服器無法連線時,如何顯示網路錯誤。"; - }; - readonly "How would you like to configure the connection to your server?": { - readonly def: "How would you like to configure the connection to your server?"; - readonly es: "¿Cómo desea configurar la conexión con su servidor?"; - readonly ja: "サーバー接続をどのように設定しますか?"; - readonly ko: "서버 연결을 어떻게 구성하시겠습니까?"; - readonly ru: "Как вы хотите настроить подключение к серверу?"; - readonly zh: "你希望如何配置与服务器的连接?"; - readonly "zh-tw": "你希望如何設定與伺服器的連線?"; - }; - readonly "I am adding a device to an existing synchronisation setup": { - readonly def: "I am adding a device to an existing synchronisation setup"; - readonly es: "Estoy agregando un dispositivo a una configuración de sincronización existente"; - readonly ja: "既存の同期構成に端末を追加します"; - readonly ko: "기존 동기화 구성에 장치를 추가합니다"; - readonly ru: "Я добавляю устройство к существующей настройке синхронизации"; - readonly zh: "我要将设备加入现有同步配置"; - readonly "zh-tw": "我要將裝置加入既有同步設定"; - }; - readonly "I am setting this up for the first time": { - readonly def: "I am setting this up for the first time"; - readonly es: "Estoy configurando esto por primera vez"; - readonly ja: "はじめて設定します"; - readonly ko: "처음으로 설정합니다"; - readonly ru: "Я настраиваю это впервые"; - readonly zh: "我是第一次进行设置"; - readonly "zh-tw": "我是第一次進行設定"; - }; - readonly "I know my server details, let me enter them": { - readonly def: "I know my server details, let me enter them"; - readonly es: "Conozco los datos de mi servidor; permítame introducirlos"; - readonly ja: "サーバー情報を把握しているので、自分で入力します"; - readonly ko: "서버 정보를 알고 있으니 직접 입력하겠습니다"; - readonly ru: "Я знаю параметры сервера, позвольте ввести их вручную"; - readonly zh: "我知道服务器详情,让我手动输入"; - readonly "zh-tw": "我知道伺服器資訊,讓我手動輸入"; - }; - readonly "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": { - readonly def: "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour)."; - readonly es: "Si se desactiva, chunks se dividen en hilo UI (comportamiento anterior)"; - readonly fr: "Si désactivé, les fragments seront découpés sur le thread UI (comportement précédent)."; - readonly he: "אם מכובה, נתחים יפוצלו בשרשור ממשק המשתמש (התנהגות קודמת)."; - readonly ja: "無効(トグル)にすると、チャンクはUIスレッドで分割されます(以前の動作)。"; - readonly ko: "비활성화(토글)되면 청크는 UI 스레드에서 분할됩니다 (이전 동작)."; - readonly ru: "Если отключено, чанки будут разделяться в основном потоке."; - readonly zh: "如果禁用(切换),chunks 将在 UI 线程上分割(以前的行为)"; - readonly "zh-tw": "若停用(關閉)此選項,chunks 會在 UI 執行緒上分割(舊有行為)。"; - }; - readonly "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": { - readonly def: "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions."; - readonly es: "Habilita sincronización eficiente por archivo. Requiere migración y actualizar todos dispositivos a v0.23.18. Pierde compatibilidad con versiones antiguas"; - readonly fr: "Si activée, la synchronisation de personnalisation efficace par fichier sera utilisée. Une petite migration est nécessaire lors de l'activation. Tous les appareils doivent être à jour en v0.23.18. Une fois cette option activée, la compatibilité avec les anciennes versions est perdue."; - readonly he: "אם מופעל, ייעשה שימוש בסנכרון התאמה אישית יעיל לפי קובץ. נדרשת הגירה קטנה בעת ההפעלה. כל המכשירים צריכים להיות מעודכנים לגרסה 0.23.18. לאחר ההפעלה, התאימות לגרסאות ישנות תיפגע."; - readonly ja: "有効にすると、ファイルごとの効率的なカスタマイズ同期が使用されます。有効化時に小規模な移行が必要です。また、すべてのデバイスをv0.23.18にアップデートする必要があります。一度有効にすると、古いバージョンとの互換性がなくなります。"; - readonly ko: "활성화하면 파일별 효율적인 사용자 설정 동기화가 사용됩니다. 이를 활성화할 때 소규모 데이터 구조 전환이 필요합니다. 모든 기기를 v0.23.18로 업데이트해야 합니다. 이를 활성화하면 이전 버전과의 호환성이 사라집니다."; - readonly ru: "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions."; - readonly zh: "如果启用,将使用基于文件的、高效的自定义同步。启用此功能需要进行一次小的迁移。所有设备都应更新到 v0.23.18。一旦启用此功能,我们将失去与旧版本的兼容性"; - readonly "zh-tw": "啟用後會使用以每個檔案為單位的高效率自訂同步。啟用時需要進行一次小型遷移,且所有裝置都應升級到 v0.23.18。啟用後將不再相容舊版本。"; - }; - readonly "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": { - readonly def: "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker."; - readonly es: "Divide chunks en máximo 100 ítems. Menos eficiente en deduplicación"; - readonly fr: "Si activée, les fragments seront découpés en 100 éléments au maximum. Cependant, la déduplication est légèrement moins efficace."; - readonly he: "אם מופעל, נתחים יפוצלו לא ליותר מ-100 פריטים. עם זאת, ביטול כפילויות יהיה חלש מעט יותר."; - readonly ja: "有効にすると、チャンクは最大100項目に分割されます。ただし、重複除去の精度は落ちます。"; - readonly ko: "활성화하면 청크는 최대 100개 항목으로 분할됩니다. 하지만 중복 제거 기능이 약간 약해집니다."; - readonly ru: "Если включено, чанки будут разделены не более чем на 100 элементов."; - readonly zh: "如果启用,数据块将被分割成不超过 100 项。但是,去重效果会稍弱"; - readonly "zh-tw": "啟用後,chunks 最多會分成 100 個項目,但去重效果會稍微變弱。"; - }; - readonly "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": { - readonly def: "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised."; - readonly es: "Chunks nuevos se mantienen temporalmente en el documento hasta estabilizarse"; - readonly fr: "Si activée, les fragments nouvellement créés sont temporairement conservés dans le document et promus en fragments indépendants une fois stabilisés."; - readonly he: "אם מופעל, נתחים שנוצרו לאחרונה נשמרים זמנית בתוך המסמך, ומוסמכים לנתחים עצמאיים לאחר יציבות."; - readonly ja: "有効にすると、新しく作成されたチャンクはドキュメント内に一時的に保持され、安定したら独立したチャンクになります。"; - readonly ko: "활성화하면 새로 생성된 변경 기록(청크)은 문서 안에 임시로 보관되며, 일정 조건을 만족하면 자동으로 문서 밖으로 분리되어 저장됩니다."; - readonly ru: "Если включено, вновь созданные чанки временно хранятся в документе."; - readonly zh: "如果启用,新创建的数据块将暂时保留在文档中,并在稳定后成为独立数据块"; - readonly "zh-tw": "啟用後,新建立的 chunks 會暫時保留在文件中,待穩定後再獨立出去。"; - }; - readonly "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": { - readonly def: "If enabled, the ⛔ icon will be shown inside the status instead of the file warnings banner. No details will be shown."; - readonly es: "Si se activa, se mostrará el icono ⛔ en el estado en lugar del banner de advertencia de archivos. No se mostrarán detalles."; - readonly fr: "Si activée, l'icône ⛔ s'affichera dans le statut à la place de la bannière d'avertissements de fichiers. Aucun détail ne sera affiché."; - readonly he: "אם מופעל, אייקון ⛔ יוצג בסטטוס במקום פס האזהרות. לא יוצגו פרטים."; - readonly ja: "有効にすると、ファイル警告バナーの代わりにステータス内へ ⛔ アイコンのみを表示します。詳細は表示されません。"; - readonly ko: "활성화하면 파일 경고 배너 대신 상태 영역에 ⛔ 아이콘만 표시됩니다. 자세한 내용은 표시되지 않습니다."; - readonly ru: "Если включено, значок будет показан внутри статуса."; - readonly zh: "如果启用,状态栏内将显示 ⛔ 图标,而非文件警告横幅,不会显示任何详细信息。"; - readonly "zh-tw": "啟用後,將在狀態列中顯示 ⛔ 圖示,而不是檔案警告橫幅,且不會顯示詳細資訊。"; - }; - readonly "If enabled, the file under 1kb will be processed in the UI thread.": { - readonly def: "If enabled, the file under 1kb will be processed in the UI thread."; - readonly es: "Archivos <1kb se procesan en hilo UI"; - readonly fr: "Si activée, les fichiers de moins de 1 Ko seront traités sur le thread UI."; - readonly he: "אם מופעל, קבצים קטנים מ-1KB יעובדו בשרשור ממשק המשתמש."; - readonly ja: "有効にすると、1kb未満のファイルはUIスレッドで処理されます。"; - readonly ko: "활성화하면 1kb 미만의 파일은 UI 스레드에서 처리됩니다."; - readonly ru: "Если включено, файлы меньше 1КБ будут обрабатываться в основном потоке."; - readonly zh: "如果启用,小于 1kb 的文件将在 UI 线程中处理"; - readonly "zh-tw": "啟用後,小於 1KB 的檔案會在 UI 執行緒中處理。"; - }; - readonly "If enabled, the notification of hidden files change will be suppressed.": { - readonly def: "If enabled, the notification of hidden files change will be suppressed."; - readonly es: "Si se habilita, se suprimirá la notificación de cambios en archivos ocultos."; - readonly fr: "Si activée, les notifications de modifications des fichiers cachés seront supprimées."; - readonly he: "אם מופעל, התראות על שינוי בקבצים נסתרים יודחקו."; - readonly ja: "有効にすると、隠しファイルの変更通知が抑制されます。"; - readonly ko: "활성화하면 숨겨진 파일 변경 알림이 억제됩니다."; - readonly ru: "Если включено, уведомление об изменении скрытых файлов будет подавлено."; - readonly zh: "如果启用,将不再通知隐藏文件被更改"; - readonly "zh-tw": "啟用後,將不再通知隱藏檔案變更。"; - }; - readonly "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": { - readonly def: "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)"; - readonly es: "Si se habilita, todos los chunks se almacenan con la revisión hecha desde su contenido. (comportamiento anterior)"; - readonly fr: "Si activée, tous les fragments seront stockés avec la révision issue de leur contenu. (Comportement précédent)"; - readonly he: "אם מופעל, כל הנתחים יישמרו עם גרסה המבוססת על תוכנם. (התנהגות קודמת)"; - readonly ja: "有効にすると、すべてのチャンクはコンテンツから作成されたリビジョンと共に保存されます(以前の動作)。"; - readonly ko: "이 옵션이 활성화되면 모든 청크는 콘텐츠에서 생성된 리비전과 함께 저장됩니다. (이전 동작)"; - readonly ru: "Если включено, все чанки будут храниться с ревизией из содержимого."; - readonly zh: "如果启用,所有 chunks 将使用根据其内容生成的修订版本存储(以前的行为)"; - readonly "zh-tw": "啟用後,所有 chunks 都會以其內容產生的修訂版本儲存(舊有行為)。"; - }; - readonly "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": { - readonly def: "If this enabled, All files are handled as case-Sensitive (Previous behaviour)."; - readonly es: "Si se habilita, todos los archivos se manejan como sensibles a mayúsculas (comportamiento anterior)"; - readonly fr: "Si activée, tous les fichiers sont gérés en respectant la casse (comportement précédent)."; - readonly he: "אם מופעל, כל הקבצים מטופלים כתלויי רישיות (התנהגות קודמת)."; - readonly ja: "有効にすると、すべてのファイルは大文字小文字を区別して処理されます(以前の動作)。"; - readonly ko: "이 옵션이 활성화되면 모든 파일이 대소문자를 구분하여 처리됩니다 (이전 동작)."; - readonly ru: "Если включено, все файлы обрабатываются с учётом регистра."; - readonly zh: "如果启用,所有文件都将视为区分大小写(以前的行为)"; - readonly "zh-tw": "啟用後,所有檔案都會以區分大小寫方式處理(舊有行為)。"; - }; - readonly "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": { - readonly def: "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature."; - readonly es: "Divide chunks en segmentos semánticos. No todos los sistemas lo soportan"; - readonly fr: "Si activée, les fragments seront découpés en segments sémantiquement signifiants. Toutes les plateformes ne prennent pas en charge cette fonctionnalité."; - readonly he: "אם מופעל, נתחים יפוצלו לחלקים עם משמעות סמנטית. לא כל הפלטפורמות תומכות בתכונה זו."; - readonly ja: "有効にすると、チャンクは意味的に有意なセグメントに分割されます。すべてのプラットフォームがこの機能をサポートしているわけではありません。"; - readonly ko: "이 옵션을 활성화하면 청크가 문단이나 의미 단위로 나뉘어 저장됩니다. 단, 이 기능은 일부 플랫폼에서는 지원되지 않을 수 있습니다."; - readonly ru: "Если включено, чанки будут разделены на семантически значимые сегменты."; - readonly zh: "如果启用此功能,数据块将被分割成具有语义意义的段落。并非所有平台都支持此功能"; - readonly "zh-tw": "啟用後,chunks 會依語意切分成有意義的區段,並非所有平台都支援此功能。"; - }; - readonly "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": { - readonly def: "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files."; - readonly es: "Saltar cambios en archivos locales que coincidan con ignore files. Cambios remotos usan ignore files locales"; - readonly fr: "Si défini, les modifications des fichiers locaux correspondant aux fichiers d'exclusion seront ignorées. Les changements distants sont déterminés à l'aide des fichiers d'exclusion locaux."; - readonly he: "אם מוגדר, שינויים בקבצים מקומיים התואמים לקבצי ההתעלמות יידלגו. שינויים מרוחקים נקבעים לפי קבצי ההתעלמות המקומיים."; - readonly ja: "これを設定すると、除外ファイルに一致するローカルファイルの変更はスキップされます。リモートの変更はローカルの無視ファイルを使用して判定されます。"; - readonly ko: "이 옵션을 활성화하면, 제외 규칙 파일에 일치하는 로컬 파일의 변경 사항은 건너뜁니다. 원격 변경 여부 또한 로컬의 제외 규칙 파일에 따라 판단됩니다."; - readonly ru: "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files."; - readonly zh: "如果设置了此项,与忽略文件匹配的本地文件的更改将被跳过。远程更改使用本地忽略文件确定"; - }; - readonly "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": { - readonly def: "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage."; - readonly es: "Mantiene conexión 60s. Si no hay cambios, reinicia socket. Útil con proxies limitantes"; - readonly fr: "Si cette option est activée, PouchDB maintiendra la connexion ouverte pendant 60 secondes, et si aucun changement n'arrive durant cette période, fermera et rouvrira la socket au lieu de la garder ouverte indéfiniment. Utile lorsqu'un proxy limite la durée des requêtes, mais peut augmenter l'utilisation des ressources."; - readonly he: "אם אפשרות זו מופעלת, PouchDB ישמור את החיבור פתוח ל-60 שניות, ואם אין שינוי בפרק זמן זה, יסגור ויפתח מחדש את השקע, במקום להחזיק אותו פתוח ללא הגבלה. שימושי כשפרוקסי מגביל משך בקשות, אך עשוי להגביר שימוש במשאבים."; - readonly ja: "このオプションを有効にすると、PouchDBは接続を60秒間保持し、その間に通信がない場合、一度接続を閉じて再接続します。接続を無期限に保持する代わりにこの動作を行います。プロキシ(Cloudflareなど)がリクエストの持続時間を制限している場合に有用ですが、リソース使用量が増加する可能性があります。"; - readonly ko: "이 옵션이 활성화되면 PouchDB는 연결을 더이상 무한히 열어두지 않고 60초 동안 유지합니다. 그 시간 내에 변경 사항이 없으면 소켓을 닫고 다시 엽니다. 프록시가 요청 지속 시간을 제한할 때 유용하지만 리소스 사용량이 증가할 수 있습니다."; - readonly ru: "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage."; - readonly zh: "如果启用此选项,PouchDB 将保持连接打开 60 秒,如果在此时间内没有更改到达,则关闭并重新打开套接字,而不是无限期保持打开。当代理限制请求持续时间时有用,但可能会增加资源使用ß"; - }; - readonly "If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.": { - readonly def: "If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value."; - readonly "zh-tw": "如果你在使用 IBM Cloudant 時遇到負載大小上限,請調低批次大小與批次上限。"; - }; - readonly "Ignore and Proceed": { - readonly def: "Ignore and Proceed"; - readonly ja: "無視して続行"; - readonly ko: "무시하고 계속"; - readonly ru: "Игнорировать и продолжить"; - readonly zh: "忽略并继续"; - readonly "zh-tw": "忽略並繼續"; - }; - readonly "Ignore files": { - readonly def: "Ignore files"; - readonly es: "Archivos a ignorar"; - readonly fr: "Fichiers d'exclusion"; - readonly he: "קבצי התעלמות"; - readonly ja: "除外ファイル"; - readonly ko: "제외 규칙 파일"; - readonly ru: "Файлы для игнорирования"; - readonly zh: "忽略文件"; - }; - readonly "Ignore patterns": { - readonly def: "Ignore patterns"; - readonly es: "Patrones de exclusión"; - readonly ja: "除外パターン"; - readonly ko: "무시 패턴"; - readonly ru: "Шаблоны исключения"; - readonly zh: "忽略模式"; - readonly "zh-tw": "忽略模式"; - }; - readonly "Import connection": { - readonly def: "Import connection"; - readonly es: "Importar conexión"; - readonly ja: "接続をインポート"; - readonly ko: "연결 가져오기"; - readonly ru: "Импортировать подключение"; - readonly zh: "导入连接"; - readonly "zh-tw": "匯入連線"; - }; - readonly "Incubate Chunks in Document": { - readonly def: "Incubate Chunks in Document"; - readonly es: "Incubar chunks en documento"; - readonly fr: "Incuber les fragments dans le document"; - readonly he: "בשל נתחים בתוך המסמך"; - readonly ja: "ドキュメント内でチャンクを一時保管する"; - readonly ko: "문서 내 변경 기록 임시 보관"; - readonly ru: "Инкубировать чанки в документе"; - readonly zh: "在文档中孵化块"; - }; - readonly "Initialise all journal history, On the next sync, every item will be received and sent.": { - readonly def: "Initialise all journal history, On the next sync, every item will be received and sent."; - readonly es: "Restablece todo el historial del diario. En la próxima sincronización se recibirán y enviarán todos los elementos."; - readonly ja: "すべてのジャーナル履歴を初期化します。次回の同期時に、すべての項目が再受信・再送信されます。"; - readonly ko: "모든 저널 기록을 초기화합니다. 다음 동기화 때 모든 항목을 다시 받고 다시 보냅니다."; - readonly ru: "Инициализирует всю историю журнала. При следующей синхронизации каждый элемент будет заново получен и отправлен."; - readonly zh: "初始化所有日志历史。下次同步时,所有项目都会重新接收并发送。"; - readonly "zh-tw": "初始化所有日誌歷史。下次同步時,每個項目都會重新接收與傳送。"; - }; - readonly "Initialise journal received history. On the next sync, every item except this device sent will be downloaded again.": { - readonly def: "Initialise journal received history. On the next sync, every item except this device sent will be downloaded again."; - readonly "zh-tw": "初始化接收日誌歷程。下次同步時,除了此裝置已送出的項目外,其他項目都會再次下載。"; - }; - readonly "Initialise journal sent history. On the next sync, every item except this device received will be sent again.": { - readonly def: "Initialise journal sent history. On the next sync, every item except this device received will be sent again."; - readonly "zh-tw": "初始化傳送日誌歷程。下次同步時,除了此裝置已接收的項目外,其他項目都會再次傳送。"; - }; - readonly "Interval (sec)": { - readonly def: "Interval (sec)"; - readonly es: "Intervalo (segundos)"; - readonly fr: "Intervalle (sec)"; - readonly he: "מרווח (שניות)"; - readonly ja: "秒"; - readonly ko: "간격 (초)"; - readonly ru: "Интервал (сек)"; - readonly zh: "间隔(秒)"; - }; - readonly "K.exp": { - readonly def: "Experimental"; - readonly fr: "Expérimental"; - readonly he: "ניסיוני"; - readonly ja: "試験機能"; - readonly ko: "실험 기능"; - readonly ru: "Экспериментальная"; - readonly zh: "实验性"; - }; - readonly "K.long_p2p_sync": { - readonly def: "Peer-to-Peer Sync"; - readonly fr: "Synchronisation pair-à-pair"; - readonly he: "%{title_p2p_sync}"; - readonly ja: "Peer-to-Peer Sync (試験機能)"; - readonly ko: "피어 투 피어(P2P) 동기화 (실험 기능)"; - readonly ru: "title_p2p_sync"; - readonly zh: "Peer-to-Peer同步 (实验性)"; - }; - readonly "K.P2P": { - readonly def: "Peer-to-Peer"; - readonly fr: "Pair-à-Pair"; - readonly he: "%{Peer}-ל-%{Peer}"; - readonly ja: "Peer-to-Peer"; - readonly ko: "피어-to-피어"; - readonly ru: "Peer-к-Peer"; - readonly zh: "Peer-to-Peer"; - }; - readonly "K.Peer": { - readonly def: "Peer"; - readonly fr: "Pair"; - readonly he: "עמית"; - readonly ja: "Peer"; - readonly ko: "피어"; - readonly ru: "Устройство"; - readonly zh: "Peer"; - }; - readonly "K.ScanCustomization": { - readonly def: "Scan customization"; - readonly fr: "Analyser la personnalisation"; - readonly he: "סרוק התאמה אישית"; - readonly ja: "Scan customization"; - readonly ko: "사용자 설정 검색"; - readonly ru: "Scan customization"; - readonly zh: "扫描自定义"; - }; - readonly "K.short_p2p_sync": { - readonly def: "P2P Sync"; - readonly fr: "Sync P2P"; - readonly he: "סנכרון P2P"; - readonly ja: "P2P Sync (試験機能)"; - readonly ko: "P2P 동기화 (실험 기능)"; - readonly ru: "P2P Синхр."; - readonly zh: "P2P同步(实验性)"; - }; - readonly "K.title_p2p_sync": { - readonly def: "Peer-to-Peer Sync"; - readonly fr: "Synchronisation pair-à-pair"; - readonly he: "סנכרון עמית-לעמית"; - readonly ja: "Peer-to-Peer Sync"; - readonly ko: "피어 투 피어(P2P) 동기화"; - readonly ru: "Синхронизация между устройствами"; - readonly zh: "Peer-to-Peer同步"; - }; - readonly "Keep empty folder": { - readonly def: "Keep empty folder"; - readonly es: "Mantener carpetas vacías"; - readonly fr: "Conserver les dossiers vides"; - readonly he: "שמור תיקייה ריקה"; - readonly ja: "空フォルダの維持"; - readonly ko: "빈 폴더 유지"; - readonly ru: "Сохранять пустые папки"; - readonly zh: "保留空文件夹"; - }; - readonly lang_def: { - readonly def: "Default"; - readonly fr: "Par défaut"; - readonly he: "ברירת מחדל"; - readonly ja: "Default"; - readonly ko: "Default"; - readonly ru: "По умолчанию"; - readonly zh: "Default"; - }; - readonly "lang-de": { - readonly def: "Deutsche"; - readonly es: "Alemán"; - readonly fr: "Deutsche"; - readonly he: "Deutsche"; - readonly ja: "Deutsche"; - readonly ko: "Deutsche"; - readonly ru: "Deutsch"; - readonly zh: "Deutsche"; - }; - readonly "lang-def": { - readonly def: "Default"; - readonly fr: "Par défaut"; - readonly he: "%{lang_def}"; - readonly ja: "Default"; - readonly ko: "Default"; - readonly ru: "lang_def"; - readonly zh: "Default"; - }; - readonly "lang-es": { - readonly def: "Español"; - readonly es: "Español"; - readonly fr: "Español"; - readonly he: "Español"; - readonly ja: "Español"; - readonly ko: "Español"; - readonly ru: "Español"; - readonly zh: "Español"; - }; - readonly "lang-fr": { - readonly def: "Français"; - readonly es: "Français"; - readonly fr: "Français"; - readonly he: "Français"; - readonly ja: "Français"; - readonly ko: "Français"; - readonly ru: "Français"; - readonly zh: "Français"; - }; - readonly "lang-he": { - readonly def: "Hebrew"; - readonly he: "עברית"; - }; - readonly "lang-ja": { - readonly def: "日本語"; - readonly es: "Japonés"; - readonly fr: "日本語"; - readonly he: "日本語"; - readonly ja: "日本語"; - readonly ko: "日本語"; - readonly ru: "日本語"; - readonly zh: "日本語"; - }; - readonly "lang-ko": { - readonly def: "한국어"; - readonly fr: "한국어"; - readonly he: "한국어"; - readonly ja: "한국어"; - readonly ko: "한국어"; - readonly ru: "한국어"; - readonly zh: "한국어"; - }; - readonly "lang-ru": { - readonly def: "Русский"; - readonly es: "Ruso"; - readonly fr: "Русский"; - readonly he: "Русский"; - readonly ja: "Русский"; - readonly ko: "Русский"; - readonly ru: "Русский"; - readonly zh: "Русский"; - }; - readonly "lang-zh": { - readonly def: "简体中文"; - readonly es: "Chino simplificado"; - readonly fr: "简体中文"; - readonly he: "简体中文"; - readonly ja: "简体中文"; - readonly ko: "简体中文"; - readonly ru: "简体中文"; - readonly zh: "简体中文"; - }; - readonly "lang-zh-tw": { - readonly def: "繁體中文"; - readonly es: "Chino tradicional"; - readonly fr: "繁體中文"; - readonly he: "繁體中文"; - readonly ja: "繁體中文"; - readonly ko: "繁體中文"; - readonly ru: "繁體中文"; - readonly zh: "繁體中文"; - }; - readonly Later: { - readonly def: "Later"; - readonly es: "Más tarde"; - readonly ja: "後で"; - readonly ko: "나중에"; - readonly ru: "Позже"; - readonly zh: "稍后"; - readonly "zh-tw": "稍後"; - }; - readonly "Limit: {datetime} ({timestamp})": { - readonly def: "Limit: {datetime} ({timestamp})"; - readonly es: "Límite: {datetime} ({timestamp})"; - readonly ja: "制限: {datetime} ({timestamp})"; - readonly ko: "제한: {datetime} ({timestamp})"; - readonly ru: "Лимит: {datetime} ({timestamp})"; - readonly zh: "限制:{datetime}({timestamp})"; - readonly "zh-tw": "限制:{datetime}({timestamp})"; - }; - readonly "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": { - readonly def: "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured."; - readonly es: "LiveSync no puede manejar múltiples bóvedas con mismo nombre sin prefijo. Se configura automáticamente"; - readonly fr: "LiveSync ne peut pas gérer plusieurs coffres portant le même nom sans préfixe distinct. Ceci devrait être configuré automatiquement."; - readonly he: "LiveSync אינו יכול לטפל במספר כספות עם אותו שם ללא קידומת שונה. הדבר אמור להיות מוגדר אוטומטית."; - readonly ja: "LiveSyncは、接頭辞(プレフィックス)のない同名の保管庫(Vault)を扱うことができません。これは自動的に設定されます。"; - readonly ko: "LiveSync는 서로 다른 접두사 없이 동일한 이름을 가진 여러 볼트를 처리할 수 없습니다. 이는 자동으로 구성되어야 합니다."; - readonly ru: "LiveSync не может обработать несколько хранилищ с одинаковым именем без разных префиксов."; - readonly zh: "LiveSync 无法处理具有相同名称但没有不同前缀的多个库。这应该自动配置"; - }; - readonly "liveSyncReplicator.beforeLiveSync": { - readonly def: "Before LiveSync, start OneShot once..."; - readonly es: "Antes de LiveSync, inicia OneShot..."; - readonly fr: "Avant LiveSync, lancement d'un OneShot initial..."; - readonly he: "לפני LiveSync, התחל OneShot פעם אחת..."; - readonly ja: "LiveSyncの前に、まずOneShotを開始します..."; - readonly ko: "LiveSync 전에 OneShot을 먼저 시작합니다..."; - readonly ru: "Перед LiveSync запускаем OneShot..."; - readonly zh: "在LiveSync前,先启动OneShot一次..."; - }; - readonly "liveSyncReplicator.cantReplicateLowerValue": { - readonly def: "We can't replicate more lower value."; - readonly es: "No podemos replicar un valor más bajo."; - readonly fr: "Impossible de répliquer une valeur inférieure."; - readonly he: "לא ניתן לשכפל ערך נמוך יותר."; - readonly ja: "これ以上低い値ではレプリケーション(複製)できません。"; - readonly ko: "더 낮은 값으로 복제할 수 없습니다."; - readonly ru: "Нельзя реплицировать с меньшим значением."; - readonly zh: "我们无法复制更小的值"; - }; - readonly "liveSyncReplicator.checkingLastSyncPoint": { - readonly def: "Looking for the point last synchronized point."; - readonly es: "Buscando el último punto sincronizado."; - readonly fr: "Recherche du dernier point de synchronisation."; - readonly he: "מחפש נקודת הסנכרון האחרונה."; - readonly ja: "最後に同期したポイントを探しています。"; - readonly ko: "마지막으로 동기화된 지점을 찾고 있습니다."; - readonly ru: "Поиск последней точки синхронизации."; - readonly zh: "查找上次同步点"; - }; - readonly "liveSyncReplicator.couldNotConnectTo": { - readonly def: "Could not connect to ${uri} : ${name}\n(${db})"; - readonly es: "No se pudo conectar a ${uri} : ${name} \n(${db})"; - readonly fr: "Connexion impossible à ${uri} : ${name}\n(${db})"; - readonly he: "לא ניתן להתחבר אל ${uri} : ${name}\n(${db})"; - readonly ja: "${uri} : ${name}に接続できませんでした\n(${db})"; - readonly ko: "${uri}에 연결할 수 없습니다: ${name} \n(${db})"; - readonly ru: "Не удалось подключиться к uri : name\n(db)"; - readonly zh: "无法连接到 ${uri} : ${name}\n(${db})"; - }; - readonly "liveSyncReplicator.couldNotConnectToRemoteDb": { - readonly def: "Could not connect to remote database: ${d}"; - readonly es: "No se pudo conectar a base de datos remota: ${d}"; - readonly fr: "Connexion à la base distante impossible : ${d}"; - readonly he: "לא ניתן להתחבר למסד הנתונים המרוחק: ${d}"; - readonly ja: "リモートデータベースに接続できませんでした: ${d}"; - readonly ko: "원격 데이터베이스에 연결할 수 없습니다: ${d}"; - readonly ru: "Не удалось подключиться к удалённой базе данных: d"; - readonly zh: "无法连接到远程数据库:${d}"; - }; - readonly "liveSyncReplicator.couldNotConnectToServer": { - readonly def: "The connection to the remote has been prevented, or failed."; - readonly es: "No se pudo conectar al servidor."; - readonly fr: "Connexion au serveur impossible."; - readonly he: "לא ניתן להתחבר לשרת."; - readonly ja: "サーバーに接続できませんでした。"; - readonly ko: "서버에 연결할 수 없습니다."; - readonly ru: "Не удалось подключиться к серверу."; - readonly zh: "无法连接到服务器"; - }; - readonly "liveSyncReplicator.couldNotConnectToURI": { - readonly def: "Could not connect to ${uri}:${dbRet}"; - readonly es: "No se pudo conectar a ${uri}:${dbRet}"; - readonly fr: "Connexion impossible à ${uri}:${dbRet}"; - readonly he: "לא ניתן להתחבר אל ${uri}:${dbRet}"; - readonly ja: "${uri}に接続できませんでした: ${dbRet}"; - readonly ko: "${uri}에 연결할 수 없습니다: ${dbRet}"; - readonly ru: "Не удалось подключиться к uri:dbRet"; - readonly zh: "无法连接到 ${uri}:${dbRet}"; - }; - readonly "liveSyncReplicator.couldNotMarkResolveRemoteDb": { - readonly def: "Could not mark resolve remote database."; - readonly es: "No se pudo marcar como resuelta la base de datos remota."; - readonly fr: "Impossible de marquer la résolution de la base distante."; - readonly he: "לא ניתן לסמן פתרון למסד הנתונים המרוחק."; - readonly ja: "リモートデータベースを解決済みとしてマークできませんでした。"; - readonly ko: "원격 데이터베이스를 해결됨으로 표시할 수 없습니다."; - readonly ru: "Не удалось отметить удалённую базу данных как разрешённую."; - readonly zh: "无法标记并解决远程数据库"; - }; - readonly "liveSyncReplicator.liveSyncBegin": { - readonly def: "LiveSync begin..."; - readonly es: "Inicio de LiveSync..."; - readonly fr: "Démarrage de LiveSync..."; - readonly he: "LiveSync מתחיל..."; - readonly ja: "LiveSyncを開始..."; - readonly ko: "LiveSync 시작..."; - readonly ru: "Начало LiveSync..."; - readonly zh: "LiveSync 开始..."; - }; - readonly "liveSyncReplicator.lockRemoteDb": { - readonly def: "Lock remote database to prevent data corruption"; - readonly es: "Bloquear base de datos remota para prevenir corrupción de datos"; - readonly fr: "Verrouillage de la base distante pour éviter la corruption des données"; - readonly he: "נועל מסד נתונים מרוחק למניעת פגיעה בנתונים"; - readonly ja: "データ破損を防ぐためリモートデータベースをロック"; - readonly ko: "데이터 손상을 방지하기 위해 원격 데이터베이스를 잠급니다"; - readonly ru: "Блокировка удалённой базы данных для предотвращения повреждения данных"; - readonly zh: "锁定远程数据库以防止数据损坏"; - }; - readonly "liveSyncReplicator.markDeviceResolved": { - readonly def: "Mark this device as 'resolved'."; - readonly es: "Marcar este dispositivo como 'resuelto'."; - readonly fr: "Marquer cet appareil comme « résolu »."; - readonly he: "סמן מכשיר זה כ'נפתר'."; - readonly ja: "このデバイスを『解決済み』としてマーク。"; - readonly ko: "이 기기를 '해결됨'으로 표시합니다."; - readonly ru: "Отметить это устройство как «разрешённое»."; - readonly zh: "将此设备标记为“已解决”"; - }; - readonly "liveSyncReplicator.mismatchedTweakDetected": { - readonly def: "Some mismatches have been detected in the configuration between devices. Running a manual replication will attempt to resolve this issue."; - }; - readonly "liveSyncReplicator.oneShotSyncBegin": { - readonly def: "OneShot Sync begin... (${syncMode})"; - readonly es: "Inicio de sincronización OneShot... (${syncMode})"; - readonly fr: "Démarrage de la synchronisation OneShot... (${syncMode})"; - readonly he: "סנכרון OneShot מתחיל... (${syncMode})"; - readonly ja: "OneShot同期を開始... (${syncMode})"; - readonly ko: "OneShot 동기화 시작... (${syncMode})"; - readonly ru: "Начало OneShot синхронизации... (syncMode)"; - readonly zh: "OneShot同步开始...(${syncMode})"; - }; - readonly "liveSyncReplicator.remoteDbCorrupted": { - readonly def: "Remote database is newer or corrupted, make sure to latest version of self-hosted-livesync installed"; - readonly es: "La base de datos remota es más nueva o está dañada, asegúrese de tener la última versión de self-hosted-livesync instalada"; - readonly fr: "La base distante est plus récente ou corrompue, assurez-vous d'avoir installé la dernière version de self-hosted-livesync"; - readonly he: "מסד הנתונים המרוחק חדש יותר או פגום, ודא שגרסת self-hosted-livesync המותקנת היא העדכנית ביותר"; - readonly ja: "リモートデータベースが新しいか破損しています。self-hosted-livesyncの最新バージョンがインストールされていることを確認してください"; - readonly ko: "원격 데이터베이스가 더 최신이거나 손상되었습니다. 최신 버전의 self-hosted-livesync가 설치되어 있는지 확인하세요"; - readonly ru: "Удалённая база данных новее или повреждена, убедитесь, что установлена последняя версия self-hosted-livesync"; - readonly zh: "远程数据库较新或已损坏,请确保已安装最新版本的self-hosted-livesync"; - }; - readonly "liveSyncReplicator.remoteDbCreatedOrConnected": { - readonly def: "Remote Database Created or Connected"; - readonly es: "Base de datos remota creada o conectada"; - readonly fr: "Base distante créée ou connectée"; - readonly he: "מסד הנתונים המרוחק נוצר או חובר"; - readonly ja: "リモートデータベースが作成または接続されました"; - readonly ko: "원격 데이터베이스가 생성되거나 연결되었습니다"; - readonly ru: "Удалённая база данных создана или подключена"; - readonly zh: "远程数据库已创建或连接"; - }; - readonly "liveSyncReplicator.remoteDbDestroyed": { - readonly def: "Remote Database Destroyed"; - readonly es: "Base de datos remota destruida"; - readonly fr: "Base distante détruite"; - readonly he: "מסד הנתונים המרוחק נהרס"; - readonly ja: "リモートデータベースが削除されました"; - readonly ko: "원격 데이터베이스가 삭제되었습니다"; - readonly ru: "Удалённая база данных уничтожена"; - readonly zh: "远程数据库已销毁"; - }; - readonly "liveSyncReplicator.remoteDbDestroyError": { - readonly def: "Something happened on Remote Database Destroy:"; - readonly es: "Algo ocurrió al destruir base de datos remota:"; - readonly fr: "Un problème est survenu lors de la destruction de la base distante :"; - readonly he: "אירעה שגיאה בהריסת מסד הנתונים המרוחק:"; - readonly ja: "リモートデータベースの削除中に問題が発生しました:"; - readonly ko: "원격 데이터베이스 삭제 중 오류가 발생했습니다:"; - readonly ru: "Произошла ошибка при уничтожении удалённой базы данных:"; - readonly zh: "远程数据库销毁时发生错误:"; - }; - readonly "liveSyncReplicator.remoteDbMarkedResolved": { - readonly def: "Remote database has been marked resolved."; - readonly es: "Base de datos remota marcada como resuelta."; - readonly fr: "La base distante a été marquée comme résolue."; - readonly he: "מסד הנתונים המרוחק סומן כנפתר."; - readonly ja: "リモートデータベースが解決済みとしてマークされました。"; - readonly ko: "원격 데이터베이스가 해결됨으로 표시되었습니다."; - readonly ru: "Удалённая база данных отмечена как разрешённая."; - readonly zh: "远程数据库已标记为已解决"; - }; - readonly "liveSyncReplicator.replicationClosed": { - readonly def: "Replication closed"; - readonly es: "Replicación cerrada"; - readonly fr: "Réplication fermée"; - readonly he: "השכפול נסגר"; - readonly ja: "レプリケーション(複製)が終了しました"; - readonly ko: "복제가 종료되었습니다"; - readonly ru: "Репликация закрыта"; - readonly zh: "同步已关闭"; - }; - readonly "liveSyncReplicator.replicationInProgress": { - readonly def: "Replication is already in progress"; - readonly es: "Replicación en curso"; - readonly fr: "Une réplication est déjà en cours"; - readonly he: "שכפול כבר מתבצע"; - readonly ja: "レプリケーション(複製)は既に進行中です"; - readonly ko: "복제가 이미 진행 중입니다"; - readonly ru: "Репликация уже выполняется"; - readonly zh: "同步正在进行中"; - }; - readonly "liveSyncReplicator.retryLowerBatchSize": { - readonly def: "Retry with lower batch size:${batch_size}/${batches_limit}"; - readonly es: "Reintentar con tamaño de lote más bajo:${batch_size}/${batches_limit}"; - readonly fr: "Nouvelle tentative avec une taille de lot réduite :${batch_size}/${batches_limit}"; - readonly he: "מנסה שוב עם גודל אצווה קטן יותר:${batch_size}/${batches_limit}"; - readonly ja: "より小さいバッチサイズで再試行: ${batch_size}/${batches_limit}"; - readonly ko: "더 낮은 일괄 크기로 재시도: ${batch_size}/${batches_limit}"; - readonly ru: "Повтор с меньшим размером пакета: batch_size/batches_limit"; - readonly zh: "使用更小的批量大小重试:${batch_size}/${batches_limit}"; - }; - readonly "liveSyncReplicator.unlockRemoteDb": { - readonly def: "Unlock remote database to prevent data corruption"; - readonly es: "Desbloquear base de datos remota para prevenir corrupción de datos"; - readonly fr: "Déverrouillage de la base distante pour éviter la corruption des données"; - readonly he: "מבטל נעילת מסד הנתונים המרוחק למניעת פגיעה בנתונים"; - readonly ja: "データ破損を防ぐためリモートデータベースをアンロック"; - readonly ko: "데이터 손상을 방지하기 위해 원격 데이터베이스를 잠금 해제합니다"; - readonly ru: "Разблокировка удалённой базы данных для предотвращения повреждения данных"; - readonly zh: "解锁远程数据库以防止数据损坏"; - }; - readonly "liveSyncSetting.errorNoSuchSettingItem": { - readonly def: "No such setting item: ${key}"; - readonly es: "No existe el ajuste: ${key}"; - readonly fr: "Élément de paramètre inexistant : ${key}"; - readonly he: "פריט הגדרה לא קיים: ${key}"; - readonly ja: "その設定項目は存在しません: ${key}"; - readonly ko: "해당 설정 항목이 없습니다: ${key}"; - readonly ru: "Такого параметра настройки не существует: key"; - readonly zh: "没有此设置项:${key}"; - }; - readonly "liveSyncSetting.originalValue": { - readonly def: "Original: ${value}"; - readonly es: "Original: ${value}"; - readonly fr: "Original : ${value}"; - readonly he: "ערך מקורי: ${value}"; - readonly ja: "元の値: ${value}"; - readonly ko: "원본: ${value}"; - readonly ru: "Оригинал: value"; - readonly zh: "原始值:${value}"; - }; - readonly "liveSyncSetting.valueShouldBeInRange": { - readonly def: "The value should ${min} < value < ${max}"; - readonly es: "El valor debe estar entre ${min} y ${max}"; - readonly fr: "La valeur doit être ${min} < valeur < ${max}"; - readonly he: "הערך צריך להיות ${min} < ערך < ${max}"; - readonly ja: "値は ${min} < 値 < ${max} の範囲である必要があります"; - readonly ko: "값은 ${min} < 값 < ${max} 범위에 있어야 합니다"; - readonly ru: "Значение должно быть min < значение < max"; - readonly zh: "值应该在 ${min} < value < ${max} 之间"; - }; - readonly "liveSyncSettings.btnApply": { - readonly def: "Apply"; - readonly es: "Aplicar"; - readonly fr: "Appliquer"; - readonly he: "החל"; - readonly ja: "適用"; - readonly ko: "적용"; - readonly ru: "Применить"; - readonly zh: "应用"; - }; - readonly "Local Database Tweak": { - readonly def: "Local Database Tweak"; - readonly es: "Ajustes de la base de datos local"; - readonly ja: "ローカルデータベースの調整"; - readonly ko: "로컬 데이터베이스 조정"; - readonly ru: "Настройки локальной базы данных"; - }; - readonly Lock: { - readonly def: "Lock"; - readonly es: "Bloquear"; - readonly ja: "ロック"; - readonly ko: "잠금"; - readonly ru: "Заблокировать"; - readonly zh: "锁定"; - readonly "zh-tw": "鎖定"; - }; - readonly "Lock Server": { - readonly def: "Lock Server"; - readonly es: "Bloquear servidor"; - readonly ja: "サーバーをロック"; - readonly ko: "서버 잠금"; - readonly ru: "Заблокировать сервер"; - readonly zh: "锁定服务器"; - readonly "zh-tw": "鎖定伺服器"; - }; - readonly "Lock the remote server to prevent synchronization with other devices.": { - readonly def: "Lock the remote server to prevent synchronization with other devices."; - readonly es: "Bloquea el servidor remoto para impedir la sincronización con otros dispositivos."; - readonly ja: "他のデバイスとの同期を防ぐため、リモートサーバーをロックします。"; - readonly ko: "다른 기기와의 동기화를 방지하기 위해 원격 서버를 잠급니다."; - readonly ru: "Блокирует удалённый сервер, чтобы запретить синхронизацию с другими устройствами."; - readonly zh: "锁定远端服务器,以阻止其他设备继续同步。"; - readonly "zh-tw": "鎖定遠端伺服器,以防止其他裝置進行同步。"; - }; - readonly "logPane.autoScroll": { - readonly def: "Auto scroll"; - readonly es: "Autodesplazamiento"; - readonly fr: "Défilement automatique"; - readonly he: "גלילה אוטומטית"; - readonly ja: "自動スクロール"; - readonly ko: "자동 스크롤"; - readonly ru: "Автопрокрутка"; - readonly zh: "自动滚动"; - }; - readonly "logPane.logWindowOpened": { - readonly def: "Log window opened"; - readonly es: "Ventana de registro abierta"; - readonly fr: "Fenêtre des journaux ouverte"; - readonly he: "חלון יומן נפתח"; - readonly ja: "ログウィンドウが開かれました"; - readonly ko: "로그 창이 열렸습니다"; - readonly ru: "Окно лога открыто"; - readonly zh: "日志窗口已打开"; - }; - readonly "logPane.pause": { - readonly def: "Pause"; - readonly es: "Pausar"; - readonly fr: "Pause"; - readonly he: "השהה"; - readonly ja: "一時停止"; - readonly ko: "일시 중단"; - readonly ru: "Пауза"; - readonly zh: "暂停"; - }; - readonly "logPane.title": { - readonly def: "Self-hosted LiveSync Log"; - readonly es: "Registro de Self-hosted LiveSync"; - readonly fr: "Journaux Self-hosted LiveSync"; - readonly he: "יומן Self-hosted LiveSync"; - readonly ja: "Self-hosted LiveSync ログ"; - readonly ko: "Self-hosted LiveSync 로그"; - readonly ru: "Лог Self-hosted LiveSync"; - readonly zh: "Self-hosted LiveSync 日志"; - }; - readonly "logPane.wrap": { - readonly def: "Wrap"; - readonly es: "Ajustar"; - readonly fr: "Retour à la ligne"; - readonly he: "גלישת שורות"; - readonly ja: "折り返し"; - readonly ko: "줄 바꿈"; - readonly ru: "Перенос"; - readonly zh: "自动换行"; - }; - readonly "Maximum delay for batch database updating": { - readonly def: "Maximum delay for batch database updating"; - readonly es: "Retraso máximo para actualización por lotes"; - readonly fr: "Délai maximum pour la mise à jour groupée de la base"; - readonly he: "עיכוב מקסימלי לעדכון אצווה של מסד נתונים"; - readonly ja: "バッチデータベース更新の最大遅延"; - readonly ko: "일괄 데이터베이스 업데이트 최대 지연"; - readonly ru: "Максимальная задержка пакетного обновления базы данных"; - readonly zh: "批量数据库更新的最大延迟"; - }; - readonly "Maximum file size": { - readonly def: "Maximum file size"; - readonly es: "Tamaño máximo de archivo"; - readonly fr: "Taille maximale de fichier"; - readonly he: "גודל קובץ מקסימלי"; - readonly ja: "最大ファイル容量"; - readonly ko: "최대 파일 크기"; - readonly ru: "Максимальный размер файла"; - readonly zh: "最大文件大小"; - }; - readonly "Maximum Incubating Chunk Size": { - readonly def: "Maximum Incubating Chunk Size"; - readonly es: "Tamaño máximo de chunks incubados"; - readonly fr: "Taille maximale des fragments en incubation"; - readonly he: "גודל מקסימלי לנתח בבישול"; - readonly ja: "保持するチャンクの最大サイズ"; - readonly ko: "임시 보관 변경 기록의 최대 크기"; - readonly ru: "Максимальный размер инкубируемого чанка"; - readonly zh: "最大孵化块大小"; - }; - readonly "Maximum Incubating Chunks": { - readonly def: "Maximum Incubating Chunks"; - readonly es: "Máximo de chunks incubados"; - readonly fr: "Nombre maximum de fragments en incubation"; - readonly he: "מספר מקסימלי של נתחים בבישול"; - readonly ja: "一時保管する最大チャンク数"; - readonly ko: "임시 보관 중인 변경 기록 최대 수"; - readonly ru: "Максимальное количество инкубируемых чанков"; - readonly zh: "最大孵化块数"; - }; - readonly "Maximum Incubation Period": { - readonly def: "Maximum Incubation Period"; - readonly es: "Periodo máximo de incubación"; - readonly fr: "Période maximale d'incubation"; - readonly he: "תקופת בישול מקסימלית"; - readonly ja: "最大保持期限"; - readonly ko: "변경 기록 임시 보관 최대 시간"; - readonly ru: "Максимальный период инкубации"; - readonly zh: "最大孵化期"; - }; - readonly "MB (0 to disable).": { - readonly def: "MB (0 to disable)."; - readonly es: "MB (0 para desactivar)"; - readonly fr: "Mo (0 pour désactiver)."; - readonly he: "MB (0 לביטול)."; - readonly ja: "MB (0で無効化)。"; - readonly ko: "MB (0으로 설정하면 비활성화)."; - readonly ru: "МБ (0 для отключения)."; - readonly zh: "MB(0为禁用)"; - }; - readonly "Memory cache": { - readonly def: "Memory cache"; - readonly es: "Caché en memoria"; - readonly ja: "メモリキャッシュ"; - readonly ko: "메모리 캐시"; - readonly ru: "Кэш в памяти"; - }; - readonly "Memory cache size (by total characters)": { - readonly def: "Memory cache size (by total characters)"; - readonly es: "Tamaño caché memoria (por caracteres)"; - readonly fr: "Taille du cache mémoire (par nombre total de caractères)"; - readonly he: "גודל מטמון זיכרון (לפי סה\"כ תווים)"; - readonly ja: "全体でキャッシュする文字数"; - readonly ko: "메모리 캐시 크기 (총 문자 수)"; - readonly ru: "Размер кэша памяти (по общему количеству символов)"; - readonly zh: "内存缓存大小(按总字符数)"; - }; - readonly "Memory cache size (by total items)": { - readonly def: "Memory cache size (by total items)"; - readonly es: "Tamaño caché memoria (por ítems)"; - readonly fr: "Taille du cache mémoire (par nombre total d'éléments)"; - readonly he: "גודל מטמון זיכרון (לפי סה\"כ פריטים)"; - readonly ja: "全体のキャッシュサイズ"; - readonly ko: "메모리 캐시 크기 (총 항목 수)"; - readonly ru: "Размер кэша памяти (по общему количеству элементов)"; - readonly zh: "内存缓存大小(按总项目数)"; - }; - readonly Merge: { - readonly def: "Merge"; - readonly es: "Fusionar"; - readonly ja: "マージ"; - readonly ko: "병합"; - readonly ru: "Объединить"; - readonly zh: "合并"; - }; - readonly "Minimum delay for batch database updating": { - readonly def: "Minimum delay for batch database updating"; - readonly es: "Retraso mínimo para actualización por lotes"; - readonly fr: "Délai minimum pour la mise à jour groupée de la base"; - readonly he: "עיכוב מינימלי לעדכון אצווה של מסד נתונים"; - readonly ja: "バッチデータベース更新の最小遅延"; - readonly ko: "일괄 데이터베이스 업데이트 최소 지연"; - readonly ru: "Минимальная задержка пакетного обновления базы данных"; - readonly zh: "批量数据库更新的最小延迟"; - }; - readonly "Minimum interval for syncing": { - readonly def: "Minimum interval for syncing"; - readonly fr: "Intervalle minimum pour la synchronisation"; - readonly he: "מרווח מינימלי לסנכרון"; - readonly ja: "同期間隔の最小値"; - readonly ko: "동기화 최소 간격"; - readonly ru: "Минимальный интервал синхронизации"; - readonly zh: "同步最小间隔"; - readonly "zh-tw": "同步最小間隔"; - }; - readonly "moduleCheckRemoteSize.logCheckingStorageSizes": { - readonly def: "Checking storage sizes"; - readonly es: "Comprobando tamaños de almacenamiento"; - readonly fr: "Vérification des tailles de stockage"; - readonly he: "בודק גדלי אחסון"; - readonly ja: "ストレージサイズを確認中"; - readonly ko: "스토리지 크기 확인 중"; - readonly ru: "Проверка размеров хранилища"; - readonly zh: "正在检查存储大小"; - }; - readonly "moduleCheckRemoteSize.logCurrentStorageSize": { - readonly def: "Remote storage size: ${measuredSize}"; - readonly es: "Tamaño del almacenamiento remoto: ${measuredSize}"; - readonly fr: "Taille du stockage distant : ${measuredSize}"; - readonly he: "גודל אחסון מרוחק: ${measuredSize}"; - readonly ja: "リモートストレージサイズ: ${measuredSize}"; - readonly ko: "원격 스토리지 크기: ${measuredSize}"; - readonly ru: "Размер удалённого хранилища: measuredSize"; - readonly zh: "远程存储大小:${measuredSize}"; - }; - readonly "moduleCheckRemoteSize.logExceededWarning": { - readonly def: "Remote storage size: ${measuredSize} exceeded ${notifySize}"; - readonly es: "Tamaño del almacenamiento remoto: ${measuredSize} superó ${notifySize}"; - readonly fr: "Taille du stockage distant : ${measuredSize} a dépassé ${notifySize}"; - readonly he: "גודל אחסון מרוחק: ${measuredSize} עלה על ${notifySize}"; - readonly ja: "リモートストレージサイズ: ${measuredSize} が ${notifySize} を超過しました"; - readonly ko: "원격 스토리지 크기: ${measuredSize}가 ${notifySize}를 초과했습니다"; - readonly ru: "Размер удалённого хранилища: measuredSize превысил notifySize"; - readonly zh: "远程存储大小:${measuredSize} 超过 ${notifySize}"; - }; - readonly "moduleCheckRemoteSize.logThresholdEnlarged": { - readonly def: "Threshold has been enlarged to ${size}MB"; - readonly es: "El umbral se ha ampliado a ${size}MB"; - readonly fr: "Le seuil a été augmenté à ${size} Mo"; - readonly he: "הסף הורחב ל-${size}MB"; - readonly ja: "しきい値が ${size}MB に設定されました"; - readonly ko: "임계값이 ${size}MB로 증가되었습니다"; - readonly ru: "Порог увеличен до sizeМБ"; - readonly zh: "阈值已扩大到 ${size}MB"; - }; - readonly "moduleCheckRemoteSize.msgConfirmRebuild": { - readonly def: "This may take a bit of a long time. Do you really want to rebuild everything now?"; - readonly es: "Esto puede llevar un poco de tiempo. ¿Realmente quieres reconstruir todo ahora?"; - readonly fr: "Cela peut prendre un certain temps. Voulez-vous vraiment tout reconstruire maintenant ?"; - readonly he: "פעולה זו עשויה לקחת זמן מה. האם אתה בטוח שברצונך לבנות מחדש עכשיו?"; - readonly ja: "これは少し時間がかかる場合があります。本当に今すべてを再構築しますか?"; - readonly ko: "시간이 꽤 오래 걸릴 수 있습니다. 정말 지금 모든 것을 재구축하시겠습니까?"; - readonly ru: "Это может занять некоторое время. Вы действительно хотите перестроить всё сейчас?"; - readonly zh: "这可能需要一些时间。您真的想现在重建所有内容吗?"; - }; - readonly "moduleCheckRemoteSize.msgDatabaseGrowing": { - readonly def: "**Your database is getting larger!** But do not worry, we can address it now. The time before running out of space on the remote storage.\n\n| Measured size | Configured size |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> If you have been using it for many years, there may be unreferenced chunks - that is, garbage - accumulating in the database. Therefore, we recommend rebuilding everything. It will probably become much smaller.\n>\n> If the volume of your vault is simply increasing, it is better to rebuild everything after organizing the files. Self-hosted LiveSync does not delete the actual data even if you delete it to speed up the process. It is roughly [documented](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md).\n>\n> If you don't mind the increase, you can increase the notification limit by 100MB. This is the case if you are running it on your own server. However, it is better to rebuild everything from time to time.\n>\n\n> [!WARNING]\n> If you perform rebuild everything, make sure all devices are synchronised. The plug-in will merge as much as possible, though.\n"; - readonly es: "**¡Tu base de datos está creciendo!** Pero no te preocupes, podemos abordarlo ahora. El tiempo antes de quedarse sin espacio en el almacenamiento remoto.\n\n| Tamaño medido | Tamaño configurado |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> Si lo has estado utilizando durante muchos años, puede haber fragmentos no referenciados - es decir, basura - acumulándose en la base de datos. Por lo tanto, recomendamos reconstruir todo. Probablemente se volverá mucho más pequeño.\n>\n> Si el volumen de tu bóveda simplemente está aumentando, es mejor reconstruir todo después de organizar los archivos. Self-hosted LiveSync no elimina los datos reales incluso si los eliminas para acelerar el proceso. Está aproximadamente [documentado](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md).\n>\n> Si no te importa el aumento, puedes aumentar el límite de notificación en 100 MB. Este es el caso si lo estás ejecutando en tu propio servidor. Sin embargo, es mejor reconstruir todo de vez en cuando.\n>\n\n> [!WARNING]\n> Si realizas la reconstrucción completa, asegúrate de que todos los dispositivos estén sincronizados. El complemento fusionará tanto como sea posible, sin embargo.\n"; - readonly fr: "**Votre base de données grossit !** Pas d'inquiétude, nous pouvons y remédier dès maintenant, avant de manquer d'espace sur le stockage distant.\n\n| Taille mesurée | Taille configurée |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> Si vous l'utilisez depuis de nombreuses années, il peut y avoir des fragments non référencés — des déchets, en somme — accumulés dans la base. Nous recommandons donc de tout reconstruire. Cela réduira probablement beaucoup la taille.\n>\n> Si le volume de votre coffre augmente simplement, il est préférable de tout reconstruire après avoir organisé les fichiers. Self-hosted LiveSync ne supprime pas réellement les données même si vous les effacez, afin d'accélérer le processus. Ceci est documenté grossièrement [ici](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md).\n>\n> Si cela ne vous dérange pas, vous pouvez augmenter la limite de notification de 100 Mo. C'est le cas si vous l'exécutez sur votre propre serveur. Il reste toutefois préférable de tout reconstruire de temps en temps.\n>\n\n> [!WARNING]\n> Si vous tout reconstruisez, assurez-vous que tous les appareils sont synchronisés. Le plug-in fusionnera autant que possible cependant.\n"; - readonly he: "**מסד הנתונים שלך הולך וגדל!** אל תדאג, אנחנו יכולים לטפל בזה עכשיו. הזמן שנשאר עד לאזול המקום באחסון המרוחק.\n\n| גודל נמדד | גודל מוגדר |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> אם אתה משתמש בפלאגין כבר שנים רבות, ייתכן שנצברו נתחים לא מקושרים — כלומר, זבל — במסד הנתונים. לכן, אנו ממליצים לבנות הכל מחדש. ככל הנראה מסד הנתונים יהיה קטן בהרבה לאחר מכן.\n>\n> אם נפח הכספת שלך פשוט גדל, עדיף לבנות מחדש לאחר ארגון הקבצים. Self-hosted LiveSync אינו מוחק נתונים בפועל גם כאשר אתה מוחק קבצים כדי להאיץ את התהליך. הדבר [מתועד בפירוט](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md).\n>\n> אם אינך מוטרד מהגידול, ניתן להגדיל את סף ההתראה ב-100MB. הדבר מתאים אם השרת הוא שלך. עם זאת, מומלץ לבנות מחדש מעת לעת.\n>\n\n> [!WARNING]\n> אם תבנה מחדש, ודא שכל המכשירים מסונכרנים. הפלאגין ינסה למזג כמה שניתן.\n"; - readonly ja: "**データベースが大きくなっています!** でも心配しないでください。リモートストレージの容量が不足する前に対応できます。\n\n| 測定サイズ | 設定サイズ |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> 長年使用している場合、参照されていないチャンク(つまりゴミ)がデータベースに蓄積している可能性があります。そのため、すべてを再構築することをお勧めします。おそらくかなり小さくなるでしょう。\n>\n> 単純に保管庫の容量が増えている場合は、事前にファイルを整理してからすべてを再構築するのが良いでしょう。Self-hosted LiveSyncは処理速度を上げるため、削除しても実際のデータを削除しません。これはおおまかに[documentation](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md)に記載されています。\n>\n> 増加を気にしない場合は、通知制限を100MB単位で増やすことができます。これは自分のサーバーで実行している場合に適しています。ただし、定期的にすべてを再構築する方が良いでしょう。\n>\n\n> [!WARNING]\n> すべてを再構築する場合は、すべてのデバイスが同期されていることを確認してください。もちろん、プラグインは可能な限り解決しようと努力はしますけど...\n"; - readonly ko: "**데이터베이스 용량이 점점 커지고 있습니다!** 하지만 걱정하지 마세요. 아직 원격 스토리지 공간이 완전히 부족해진 건 아닙니다.\n\n| 측정된 크기 | 설정된 한도 |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> 오랜 기간 사용했다면 참조되지 않는 청크, 즉 '쓰레기 데이터'가 쌓였을 수 있습니다. 이 경우 전체 재구성을 권장합니다. 용량이 훨씬 줄어들 수 있습니다.\n> \n> 단순히 볼트 자체 용량이 커지고 있는 것이라면, 먼저 파일을 정리한 후 전체를 재구성하는 것이 좋습니다. Self-hosted LiveSync는 처리 속도를 위해 삭제해도 실제 데이터를 바로 지우지 않습니다. 이 내용은 [기술 문서](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md)에 간략히 정리되어 있습니다.\n> \n> 용량 증가가 괜찮다면 알림 임계치를 100MB 단위로 높일 수 있습니다. 직접 서버를 운영하는 경우에 적합한 방법입니다. 다만, 가끔은 전체 재구성을 해주는 것이 바람직합니다.\n\n> [!WARNING]\n> 전체 재구성을 실행할 경우, 모든 기기가 반드시 동기화되어 있어야 합니다. 플러그인이 최대한 병합하려고 시도하긴 하지만 완전하지 않을 수 있습니다."; - readonly ru: "Ваша база данных увеличивается! Но не волнуйтесь, мы можем решить это сейчас."; - readonly zh: "**您的数据库正在变大!** 但别担心,我们现在可以解决它。在远程存储空间用完之前还有时间。\n\n| 测量大小 | 配置大小 |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> 如果您已经使用了很多年,数据库中可能会积累未引用的 chunks——也就是垃圾。因此,我们建议重建所有内容。它可能会变得小得多。\n> \n> 如果您的库容量只是在增加,最好在整理文件后重建所有内容。即使您为了加速过程删除了文件,Self-hosted LiveSync 也不会删除实际数据。这大致[有文档记录](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md)。\n> \n> 如果您不介意增加,可以将通知限制增加 100MB。如果您在自己的服务器上运行,就是这种情况。但是,最好还是不时地重建所有内容。\n> \n\n> [!WARNING]\n> 如果您执行重建所有内容,请确保所有设备都已同步。尽管如此,插件会尽可能地合并\n"; - }; - readonly "moduleCheckRemoteSize.msgSetDBCapacity": { - readonly def: "We can set a maximum database capacity warning, **to take action before running out of space on the remote storage**.\nDo you want to enable this?\n\n> [!MORE]-\n> - 0: Do not warn about storage size.\n> This is recommended if you have enough space on the remote storage especially you have self-hosted. And you can check the storage size and rebuild manually.\n> - 800: Warn if the remote storage size exceeds 800MB.\n> This is recommended if you are using fly.io with 1GB limit or IBM Cloudant.\n> - 2000: Warn if the remote storage size exceeds 2GB.\n\nIf we have reached the limit, we will be asked to enlarge the limit step by step.\n"; - readonly es: "Podemos configurar una advertencia de capacidad máxima de base de datos, **para tomar medidas antes de quedarse sin espacio en el almacenamiento remoto**.\n¿Quieres habilitar esto?\n\n> [!MORE]-\n> - 0: No advertir sobre el tamaño del almacenamiento.\n> Esto es recomendado si tienes suficiente espacio en el almacenamiento remoto, especialmente si lo tienes autoalojado. Y puedes comprobar el tamaño del almacenamiento y reconstruir manualmente.\n> - 800: Advertir si el tamaño del almacenamiento remoto supera los 800 MB.\n> Esto es recomendado si estás usando fly.io con un límite de 1 GB o IBM Cloudant.\n> - 2000: Advertir si el tamaño del almacenamiento remoto supera los 2 GB.\n\nSi hemos alcanzado el límite, se nos pedirá que aumentemos el límite paso a paso.\n"; - readonly fr: "Nous pouvons définir un avertissement de capacité maximale de la base de données, **afin d'agir avant de manquer d'espace sur le stockage distant**.\nVoulez-vous activer ceci ?\n\n> [!MORE]-\n> - 0 : Ne pas avertir sur la taille de stockage.\n> Recommandé si vous avez suffisamment d'espace sur le stockage distant, surtout en auto-hébergement. Vous pouvez vérifier la taille et reconstruire manuellement.\n> - 800 : Avertir si la taille du stockage distant dépasse 800 Mo.\n> Recommandé si vous utilisez fly.io avec une limite de 1 Go ou IBM Cloudant.\n> - 2000 : Avertir si la taille du stockage distant dépasse 2 Go.\n\nSi la limite est atteinte, il nous sera proposé de l'augmenter étape par étape.\n"; - readonly he: "ניתן להגדיר אזהרת קיבולת מקסימלית של מסד הנתונים, **כדי לנקוט פעולה לפני שנגמר המקום באחסון המרוחד**.\nהאם להפעיל זאת?\n\n> [!MORE]-\n> - 0: אל תזהיר על גודל האחסון.\n> מומלץ אם יש לך מספיק מקום באחסון המרוחד, בעיקר אם השרת הוא שלך. ניתן לבדוק את גודל האחסון ולבנות מחדש ידנית.\n> - 800: הזהר אם גודל האחסון המרוחד עולה על 800MB.\n> מומלץ אם אתה משתמש ב-fly.io עם מגבלת 1GB או ב-IBM Cloudant.\n> - 2000: הזהר אם גודל האחסון המרוחד עולה על 2GB.\n\nאם הגענו למגבלה, תתבקש להרחיב את הסף בהדרגה.\n"; - readonly ja: "リモートストレージの容量が不足する前に対策を講じるため、**最大データベース容量の警告**を設定できます。\nこれを有効にしますか?\n\n> [!MORE]-\n> - 0: ストレージサイズについて警告しない。\n> 自宅サーバーなど、リモートストレージに十分な容量がある場合に推奨されます。ストレージサイズを確認し、手動で再構築できます。\n> - 800: リモートストレージサイズが800MBを超えたら警告。\n> 1GB制限のfly.ioやIBM Cloudantを使用している場合に推奨されます。\n> - 2000: リモートストレージサイズが2GBを超えたら警告。\n\n制限に達した場合、段階的に制限を増やすよう求められます。\n"; - readonly ko: "**원격 스토리지 공간이 부족해지기 전에 미리 조치할 수 있도록** 데이터베이스 용량 경고를 설정할 수 있습니다.\n이 기능을 활성화하시겠습니까?\n\n> [!MORE]-\n> - 0: 스토리지 용량에 대한 경고 없음\n> 자체 서버를 사용하는 등 여유 공간이 충분한 경우에 권장됩니다. 스토리지 용량을 직접 확인하고 수동으로 재구성할 수 있습니다.\n> - 800: 원격 스토리지 용량이 800MB를 초과하면 경고\n> 1GB 제한이 있는 fly.io나 IBM Cloudant 사용 시 권장됩니다.\n> - 2000: 원격 스토리지 용량이 2GB를 초과하면 경고\n\n설정한 용량 한도에 도달하면, 단계적으로 경고 한도를 늘릴지 여부를 묻게 됩니다.\n"; - readonly ru: "Можно установить предупреждение о максимальной ёмкости базы данных."; - readonly zh: "我们可以设置一个最大数据库容量警告,**以便在远程存储空间耗尽前采取行动**。\n您想启用这个功能吗?\n\n> [!MORE]-\n> - 0: 不警告存储大小。\n> 如果您在远程存储(尤其是自托管)上有足够的空间,则推荐此选项。您可以手动检查存储大小并重建。\n> - 800: 如果远程存储大小超过 800MB 则发出警告。\n> 如果您使用的是 fly.io(1GB 限制) 或 IBM Cloudant,则推荐此选项。\n> - 2000: 如果远程存储大小超过 2GB 则发出警告。\n\n如果达到限制,系统会要求我们逐步增大限制\n"; - }; - readonly "moduleCheckRemoteSize.option2GB": { - readonly def: "2GB (Standard)"; - readonly es: "2GB (Estándar)"; - readonly fr: "2 Go (Standard)"; - readonly he: "2GB (סטנדרטי)"; - readonly ja: "2GB (標準)"; - readonly ko: "2GB (표준)"; - readonly ru: "2ГБ (Стандарт)"; - readonly zh: "2GB (标准)"; - }; - readonly "moduleCheckRemoteSize.option800MB": { - readonly def: "800MB (Cloudant, fly.io)"; - readonly es: "800MB (Cloudant, fly.io)"; - readonly fr: "800 Mo (Cloudant, fly.io)"; - readonly he: "800MB (Cloudant, fly.io)"; - readonly ja: "800MB (Cloudant, fly.io)"; - readonly ko: "800MB (Cloudant, fly.io)"; - readonly ru: "800МБ (Cloudant, fly.io)"; - readonly zh: "800MB (Cloudant, fly.io)"; - }; - readonly "moduleCheckRemoteSize.optionAskMeLater": { - readonly def: "Ask me later"; - readonly es: "Pregúntame más tarde"; - readonly fr: "Me demander plus tard"; - readonly he: "שאל מאוחר יותר"; - readonly ja: "後で確認する"; - readonly ko: "나중에 물어보기"; - readonly ru: "Спросить позже"; - readonly zh: "稍后问我"; - }; - readonly "moduleCheckRemoteSize.optionDismiss": { - readonly def: "Dismiss"; - readonly es: "Descartar"; - readonly fr: "Ignorer"; - readonly he: "דחה"; - readonly ja: "無視"; - readonly ko: "무시"; - readonly ru: "Отклонить"; - readonly zh: "忽略"; - }; - readonly "moduleCheckRemoteSize.optionIncreaseLimit": { - readonly def: "increase to ${newMax}MB"; - readonly es: "aumentar a ${newMax}MB"; - readonly fr: "augmenter à ${newMax} Mo"; - readonly he: "הגדל ל-${newMax}MB"; - readonly ja: "${newMax}MBに設定"; - readonly ko: "${newMax}MB로 증가"; - readonly ru: "увеличить до newMaxМБ"; - readonly zh: "增加到 ${newMax}MB"; - }; - readonly "moduleCheckRemoteSize.optionNoWarn": { - readonly def: "No, never warn please"; - readonly es: "No, nunca advertir por favor"; - readonly fr: "Non, ne jamais avertir"; - readonly he: "לא, אל תזהיר בכלל"; - readonly ja: "いいえ、警告しないでください"; - readonly ko: "아니요, 경고하지 마세요"; - readonly ru: "Нет, не уведомлять"; - readonly zh: "不,请永远不要警告"; - }; - readonly "moduleCheckRemoteSize.optionRebuildAll": { - readonly def: "Rebuild Everything Now"; - readonly es: "Reconstruir todo ahora"; - readonly fr: "Tout reconstruire maintenant"; - readonly he: "בנה הכל מחדש עכשיו"; - readonly ja: "今すべてを再構築"; - readonly ko: "지금 모든 것 재구축"; - readonly ru: "Перестроить всё сейчас"; - readonly zh: "立即重建所有内容"; - }; - readonly "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": { - readonly def: "Remote storage size exceeded the limit"; - readonly es: "El tamaño del almacenamiento remoto superó el límite"; - readonly fr: "Taille du stockage distant au-delà de la limite"; - readonly he: "גודל האחסון המרוחד חרג מהמגבלה"; - readonly ja: "リモートストレージサイズが制限を超過しました"; - readonly ko: "원격 스토리지 크기가 제한을 초과했습니다"; - readonly ru: "Размер удалённого хранилища превысил лимит"; - readonly zh: "远程存储大小超出限制"; - }; - readonly "moduleCheckRemoteSize.titleDatabaseSizeNotify": { - readonly def: "Setting up database size notification"; - readonly es: "Configuración de notificación de tamaño de base de datos"; - readonly fr: "Configuration de la notification de taille de base"; - readonly he: "הגדרת התראה על גודל מסד נתונים"; - readonly ja: "データベースサイズ通知の設定"; - readonly ko: "데이터베이스 크기 알림 설정"; - readonly ru: "Настройка уведомления о размере базы данных"; - readonly zh: "设置数据库大小通知"; - }; - readonly "moduleInputUIObsidian.defaultTitleConfirmation": { - readonly def: "Confirmation"; - readonly es: "Confirmación"; - readonly fr: "Confirmation"; - readonly he: "אישור"; - readonly ja: "確認"; - readonly ko: "확인"; - readonly ru: "Подтверждение"; - readonly zh: "确认"; - }; - readonly "moduleInputUIObsidian.defaultTitleSelect": { - readonly def: "Select"; - readonly es: "Seleccionar"; - readonly fr: "Sélection"; - readonly he: "בחר"; - readonly ja: "選択"; - readonly ko: "선택"; - readonly ru: "Выбор"; - readonly zh: "选择"; - }; - readonly "moduleInputUIObsidian.optionNo": { - readonly def: "No"; - readonly es: "No"; - readonly fr: "Non"; - readonly he: "לא"; - readonly ja: "いいえ"; - readonly ko: "아니요"; - readonly ru: "Нет"; - readonly zh: "否"; - }; - readonly "moduleInputUIObsidian.optionYes": { - readonly def: "Yes"; - readonly es: "Sí"; - readonly fr: "Oui"; - readonly he: "כן"; - readonly ja: "はい"; - readonly ko: "예"; - readonly ru: "Да"; - readonly zh: "是"; - }; - readonly "moduleLiveSyncMain.logAdditionalSafetyScan": { - readonly def: "Additional safety scan..."; - readonly es: "Escanéo de seguridad adicional..."; - readonly fr: "Analyse de sécurité supplémentaire..."; - readonly he: "סריקת בטיחות נוספת..."; - readonly ja: "追加の安全スキャン中..."; - readonly ko: "추가 안전 검사 중..."; - readonly ru: "Дополнительная проверка безопасности..."; - readonly zh: "额外的安全扫描..."; - }; - readonly "moduleLiveSyncMain.logLoadingPlugin": { - readonly def: "Loading plugin..."; - readonly es: "Cargando complemento..."; - readonly fr: "Chargement du plugin..."; - readonly he: "טוען תוסף..."; - readonly ja: "プラグインをロード中..."; - readonly ko: "플러그인 로딩 중..."; - readonly ru: "Загрузка плагина..."; - readonly zh: "正在加载插件..."; - }; - readonly "moduleLiveSyncMain.logPluginInitCancelled": { - readonly def: "Plugin initialisation was cancelled by a module"; - readonly es: "La inicialización del complemento fue cancelada por un módulo"; - readonly fr: "L'initialisation du plugin a été annulée par un module"; - readonly he: "אתחול התוסף בוטל על ידי מודול"; - readonly ja: "プラグインの初期化がモジュールによってキャンセルされました"; - readonly ko: "모듈에 의해 플러그인 초기화가 취소되었습니다"; - readonly ru: "Инициализация плагина отменена модулем"; - readonly zh: "插件初始化被某个模块取消"; - }; - readonly "moduleLiveSyncMain.logPluginVersion": { - readonly def: "Self-hosted LiveSync v${manifestVersion} ${packageVersion}"; - readonly es: "Self-hosted LiveSync v${manifestVersion} ${packageVersion}"; - readonly fr: "Self-hosted LiveSync v${manifestVersion} ${packageVersion}"; - readonly he: "Self-hosted LiveSync גרסה ${manifestVersion} ${packageVersion}"; - readonly ja: "Self-hosted LiveSync v${manifestVersion} ${packageVersion}"; - readonly ko: "Self-hosted LiveSync v${manifestVersion} ${packageVersion}"; - readonly ru: "Self-hosted LiveSync vmanifestVersion packageVersion"; - readonly zh: "Self-hosted LiveSync v${manifestVersion} ${packageVersion}"; - }; - readonly "moduleLiveSyncMain.logReadChangelog": { - readonly def: "LiveSync has updated, please read the changelog!"; - readonly es: "LiveSync se ha actualizado, ¡por favor lee el registro de cambios!"; - readonly fr: "LiveSync a été mis à jour, veuillez lire le journal des modifications !"; - readonly he: "LiveSync עודכן, אנא קרא את יומן השינויים!"; - readonly ja: "LiveSyncが更新されました。変更履歴をお読みください!"; - readonly ko: "LiveSync가 업데이트되었습니다. 변경사항을 읽어보세요!"; - readonly ru: "LiveSync обновлён, пожалуйста, прочитайте список изменений!"; - readonly zh: "LiveSync 已更新,请阅读更新日志!"; - }; - readonly "moduleLiveSyncMain.logSafetyScanCompleted": { - readonly def: "Additional safety scan completed"; - readonly es: "Escanéo de seguridad adicional completado"; - readonly fr: "Analyse de sécurité supplémentaire terminée"; - readonly he: "סריקת הבטיחות הנוספת הושלמה"; - readonly ja: "追加の安全スキャンが完了しました"; - readonly ko: "추가 안전 검사가 완료되었습니다"; - readonly ru: "Дополнительная проверка безопасности завершена"; - readonly zh: "额外的安全扫描完成"; - }; - readonly "moduleLiveSyncMain.logSafetyScanFailed": { - readonly def: "Additional safety scan has failed on a module"; - readonly es: "El escaneo de seguridad adicional ha fallado en un módulo"; - readonly fr: "L'analyse de sécurité supplémentaire a échoué sur un module"; - readonly he: "סריקת הבטיחות הנוספת נכשלה במודול"; - readonly ja: "モジュールで追加の安全スキャンが失敗しました"; - readonly ko: "모듈에서 추가 안전 검사가 실패했습니다"; - readonly ru: "Дополнительная проверка безопасности не удалась в модуле"; - readonly zh: "额外的安全扫描在某个模块上失败"; - }; - readonly "moduleLiveSyncMain.logUnloadingPlugin": { - readonly def: "Unloading plugin..."; - readonly es: "Descargando complemento..."; - readonly fr: "Déchargement du plugin..."; - readonly he: "מסיר תוסף..."; - readonly ja: "プラグインをアンロード中..."; - readonly ko: "플러그인 언로딩 중..."; - readonly ru: "Выгрузка плагина..."; - readonly zh: "正在卸载插件..."; - }; - readonly "moduleLiveSyncMain.logVersionUpdate": { - readonly def: "LiveSync has been updated, In case of breaking updates, all automatic synchronization has been temporarily disabled. Ensure that all devices are up to date before enabling."; - readonly es: "LiveSync se ha actualizado, en caso de actualizaciones que rompan, toda la sincronización automática se ha desactivado temporalmente. Asegúrate de que todos los dispositivos estén actualizados antes de habilitar."; - readonly fr: "LiveSync a été mis à jour. En cas de mises à jour non rétrocompatibles, toute synchronisation automatique a été temporairement désactivée. Assurez-vous que tous les appareils sont à jour avant d'activer."; - readonly he: "LiveSync עודכן. במקרה של עדכונים משמעותיים, כל הסנכרון האוטומטי הושבת זמנית. ודא שכל המכשירים מעודכנים לפני ההפעלה."; - readonly ja: "LiveSyncが更新されました。互換性のない更新の場合、すべての自動同期が一時的に無効化されています。有効にする前に、すべてのデバイスが最新の状態であることを確認してください。"; - readonly ko: "LiveSync가 업데이트되었습니다. 호환성 문제가 있는 업데이트의 경우 모든 자동 동기화가 일시적으로 비활성화되었습니다. 활성화하기 전에 모든 기기가 최신 상태인지 확인하세요."; - readonly ru: "LiveSync обновлён. В случае критических изменений автоматическая синхронизация временно отключена. Убедитесь, что все устройства обновлены перед включением."; - readonly zh: "LiveSync 已更新,如果存在破坏性更新,所有自动同步已暂时禁用。请确保所有设备都更新到最新版本后再启用"; - }; - readonly "moduleLiveSyncMain.msgScramEnabled": { - readonly def: "Self-hosted LiveSync has been configured to ignore some events. Is this correct?\n\n| Type | Status | Note |\n|:---:|:---:|---|\n| Storage Events | ${fileWatchingStatus} | Every modification will be ignored |\n| Database Events | ${parseReplicationStatus} | Every synchronised change will be postponed |\n\nDo you want to resume them and restart Obsidian?\n\n> [!DETAILS]-\n> These flags are set by the plug-in while rebuilding, or fetching. If the process ends abnormally, it may be kept unintended.\n> If you are not sure, you can try to rerun these processes. Make sure to back your vault up.\n"; - readonly es: "Self-hosted LiveSync se ha configurado para ignorar algunos eventos. ¿Es esto correcto?\n\n| Tipo | Estado | Nota |\n|:---:|:---:|---|\n| Eventos de almacenamiento | ${fileWatchingStatus} | Se ignorará cada modificación |\n| Eventos de base de datos | ${parseReplicationStatus} | Cada cambio sincronizado se pospondrá |\n\n¿Quieres reanudarlos y reiniciar Obsidian?\n\n> [!DETAILS]-\n> Estas banderas son establecidas por el complemento mientras se reconstruye o se obtiene. Si el proceso termina de forma anormal, puede mantenerse sin querer.\n> Si no estás seguro, puedes intentar volver a ejecutar estos procesos. Asegúrate de hacer una copia de seguridad de tu bóveda.\n"; - readonly fr: "Self-hosted LiveSync a été configuré pour ignorer certains événements. Est-ce correct ?\n\n| Type | Statut | Note |\n|:---:|:---:|---|\n| Événements de stockage | ${fileWatchingStatus} | Toute modification sera ignorée |\n| Événements de base | ${parseReplicationStatus} | Tout changement synchronisé sera reporté |\n\nVoulez-vous les reprendre et redémarrer Obsidian ?\n\n> [!DETAILS]-\n> Ces indicateurs sont définis par le plug-in lors d'une reconstruction ou d'une récupération. Si le processus se termine anormalement, ils peuvent rester activés involontairement.\n> Si vous n'êtes pas certain, vous pouvez relancer ces processus. Veillez à sauvegarder votre coffre.\n"; - readonly he: "Self-hosted LiveSync הוגדר להתעלם מאירועים מסוימים. האם זה נכון?\n\n| סוג | סטטוס | הערה |\n|:---:|:---:|---|\n| אירועי אחסון | ${fileWatchingStatus} | כל שינוי יתעלם |\n| אירועי מסד נתונים | ${parseReplicationStatus} | כל שינוי מסונכרן יידחה |\n\nהאם לחדש אותם ולהפעיל מחדש את Obsidian?\n\n> [!DETAILS]-\n> דגלים אלה מוגדרים על ידי הפלאגין במהלך בנייה מחדש או משיכה. אם התהליך הסתיים בצורה לא תקינה, ייתכן שהם נשארו כלא מכוון.\n> אם אינך בטוח, ניתן לנסות להריץ מחדש את התהליכים. ודא שיש לך גיבוי של הכספת.\n"; - readonly ja: "Self-hosted LiveSyncは一部のイベントを無視するように設定されています。これは正しいですか?\n\n| タイプ | ステータス | メモ |\n|:---:|:---:|---|\n| ストレージイベント | ${fileWatchingStatus} | すべての変更が無視されます |\n| データベースイベント | ${parseReplicationStatus} | すべての同期された変更が延期されます |\n\nこれらを再開してObsidianを再起動しますか?\n\n> [!DETAILS]-\n> これらのフラグは、プラグインが再構築またはフェッチ中に設定されます。プロセスが異常終了した場合、意図せず保持されることがあります。\n> 不明な場合は、これらのプロセスを再実行してみてください。必ず保管庫をバックアップしてください。\n"; - readonly ko: "Self-hosted LiveSync가 일부 이벤트를 무시하도록 설정되어 있습니다. 이 설정이 맞습니까?\n\n| 유형 | 상태 | 설명 |\n|:---:|:---:|---|\n| 스토리지 이벤트 | ${fileWatchingStatus} | 모든 수정 사항이 무시됩니다 |\n| 데이터베이스 이벤트 | ${parseReplicationStatus} | 모든 동기화 변경이 지연됩니다 |\n\n이벤트 감지를 다시 활성화하고 Obsidian을 재시작하시겠습니까?\n\n> [!DETAILS]-\n> 이러한 설정은 플러그인이 재구성 또는 데이터 가져오기 중에 자동으로 설정한 것입니다. 프로세스가 비정상적으로 종료되면 이 상태가 의도치 않게 유지될 수 있습니다.\n> 상태가 확실하지 않다면 이 과정을 다시 실행해 보세요. 재시작 전에 반드시 볼트를 백업해 주세요."; - readonly ru: "Self-hosted LiveSync has been configured to ignore some events. Is this correct?\n\n| Type | Status | Note |\n|:---:|:---:|---|\n| Storage Events | ${fileWatchingStatus} | Every modification will be ignored |\n| Database Events | ${parseReplicationStatus} | Every synchronised change will be postponed |\n\nDo you want to resume them and restart Obsidian?\n\n> [!DETAILS]-\n> These flags are set by the plug-in while rebuilding, or fetching. If the process ends abnormally, it may be kept unintended.\n> If you are not sure, you can try to rerun these processes. Make sure to back your vault up.\n"; - readonly zh: "Self-hosted LiveSync 已被配置为忽略某些事件。这样对吗?\n\n| 类型 | 状态 | 说明 |\n|:---:|:---:|---|\n| 存储事件 | ${fileWatchingStatus} | 所有修改都将被忽略 |\n| 数据库事件 | ${parseReplicationStatus} | 所有同步的更改都将被推迟 |\n\n您想恢复它们并重启 Obsidian 吗?\n\n> [!DETAILS]-\n> 这些标志是在重建或获取时由插件设置的。如果过程异常结束,它们可能会被无意中保留。\n> 如果您不确定,可以尝试重新运行这些过程。请确保备份您的库。\n"; - }; - readonly "moduleLiveSyncMain.optionKeepLiveSyncDisabled": { - readonly def: "Keep LiveSync disabled"; - readonly es: "Mantener LiveSync desactivado"; - readonly fr: "Garder LiveSync désactivé"; - readonly he: "השאר LiveSync מנוטרל"; - readonly ja: "LiveSyncを無効のままにする"; - readonly ko: "LiveSync 비활성화 유지"; - readonly ru: "Оставить LiveSync отключённым"; - readonly zh: "保持 LiveSync 禁用"; - }; - readonly "moduleLiveSyncMain.optionResumeAndRestart": { - readonly def: "Resume and restart Obsidian"; - readonly es: "Reanudar y reiniciar Obsidian"; - readonly fr: "Reprendre et redémarrer Obsidian"; - readonly he: "חדש והפעל מחדש את Obsidian"; - readonly ja: "再開してObsidianを再起動"; - readonly ko: "재개 후 Obsidian 재시작"; - readonly ru: "Продолжить и перезапустить Obsidian"; - readonly zh: "恢复并重启 Obsidian"; - }; - readonly "moduleLiveSyncMain.titleScramEnabled": { - readonly def: "Scram Enabled"; - readonly es: "Scram habilitado"; - readonly fr: "Mode Scram activé"; - readonly he: "מצב בלימה פעיל"; - readonly ja: "緊急停止(Scram)が有効"; - readonly ko: "Scram 활성화됨"; - readonly ru: "Экстренная остановка включена"; - readonly zh: "紧急停止已启用"; - }; - readonly "moduleLocalDatabase.logWaitingForReady": { - readonly def: "Waiting for ready..."; - readonly es: "Esperando a que la base de datos esté lista..."; - readonly fr: "En attente de disponibilité..."; - readonly he: "ממתין לכשירות..."; - readonly ja: "しばらくお待ちください..."; - readonly ko: "준비 대기 중..."; - readonly ru: "Ожидание готовности..."; - readonly zh: "等待就绪..."; - }; - readonly "moduleLog.showLog": { - readonly def: "Show Log"; - readonly es: "Mostrar registro"; - readonly fr: "Afficher le journal"; - readonly he: "הצג יומן"; - readonly ja: "ログを表示"; - readonly ko: "로그 표시"; - readonly ru: "Показать лог"; - readonly zh: "显示日志"; - }; - readonly "moduleMigration.docUri": { - readonly def: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use"; - readonly es: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README_ES.md#how-to-use"; - readonly fr: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use"; - readonly he: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use"; - readonly ja: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use"; - readonly ko: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use"; - readonly ru: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use"; - readonly zh: "https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/zh/README_zh.md#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8"; - }; - readonly "moduleMigration.fix0256.buttons.checkItLater": { - readonly def: "Check it later"; - readonly fr: "Vérifier plus tard"; - readonly he: "בדוק מאוחר יותר"; - readonly ja: "後で確認する"; - readonly ru: "Проверить позже"; - readonly zh: "稍后检查"; - }; - readonly "moduleMigration.fix0256.buttons.DismissForever": { - readonly def: "I have fixed it, and do not ask again"; - readonly fr: "J'ai corrigé, et ne plus demander"; - readonly he: "תיקנתי, ואל תשאל שוב"; - readonly ja: "修正済み、今後確認しない"; - readonly ru: "Исправлено, больше не спрашивать"; - readonly zh: "我已经修复了,不再询问"; - }; - readonly "moduleMigration.fix0256.buttons.fix": { - readonly def: "Fix"; - readonly fr: "Corriger"; - readonly he: "תקן"; - readonly ja: "修正"; - readonly ru: "Исправить"; - readonly zh: "修复"; - }; - readonly "moduleMigration.fix0256.message": { - readonly def: "Due to a recent bug (in v0.25.6), some files may not have been saved correctly in the sync database.\nWe have scanned our files and found some that need to be fixed.\n\n**Files ready to be fixed:**\n\n${files}\n\nThese files have size-matched original file on the storage, and are likely to be recoverable.\nWe can use them to fix the database, please click the \"Fix\" button below to fix them.\n\n${messageUnrecoverable}\n\nIf you want to run it again, you can do so from Hatch.\n"; - readonly fr: "En raison d'un bug récent (en v0.25.6), certains fichiers peuvent ne pas avoir été enregistrés correctement dans la base de synchronisation.\nNous avons analysé vos fichiers et trouvé ceux à corriger.\n\n**Fichiers prêts à être corrigés :**\n\n${files}\n\nCes fichiers ont un original de taille correspondante sur le stockage, et sont probablement récupérables.\nNous pouvons les utiliser pour corriger la base, veuillez cliquer sur le bouton « Corriger » ci-dessous pour les réparer.\n\n${messageUnrecoverable}\n\nSi vous voulez relancer l'opération, vous pouvez le faire depuis Hatch.\n"; - readonly he: "בשל באג אחרון (בגרסה 0.25.6), ייתכן שחלק מהקבצים לא נשמרו כהלכה במסד הנתונים לסנכרון.\nסרקנו את הקבצים ומצאנו כאלה שיש לתקן.\n\n**קבצים מוכנים לתיקון:**\n\n${files}\n\nלקבצים אלה יש קובץ מקורי תואם גודל באחסון, וסביר שניתן לשחזרם.\nניתן להשתמש בהם לתיקון מסד הנתונים. לחץ על כפתור \"תקן\" למטה לתיקון.\n\n${messageUnrecoverable}\n\nאם ברצונך להריץ שוב, ניתן לעשות זאת מ-Hatch.\n"; - readonly ja: "最近のバグ(v0.25.6)により、一部のファイルが同期データベースに正しく保存されていない可能性があります。\nファイルをスキャンし、修正が必要なものが見つかりました。\n\n**修正準備ができたファイル:**\n\n${files}\n\nこれらのファイルはストレージ上の元ファイルとサイズが一致しており、復元可能です。\n「修正」ボタンをクリックしてデータベースを修正できます。\n\n${messageUnrecoverable}\n\n再実行したい場合は、Hatchから実行できます。\n"; - readonly ru: "Из-за недавней ошибки некоторые файлы могут быть неправильно сохранены."; - readonly zh: "由于最近的一个 bug(在 v0.25.6 版本中),某些文件可能未正确保存到同步数据库中\n我们已经扫描了文件,并发现一些需要修复的文件\n\n**准备修复的文件:**\n\n${files}\n\n这些文件在存储中与原文件的大小匹配,可能是可恢复的\n我们可以使用它们修复数据库,请点击下方的“修复”按钮进行修复\n\n${messageUnrecoverable}\n\n\n如果你希望再次执行此操作,可以前往 Hatch 页面进行操作\n"; - }; - readonly "moduleMigration.fix0256.messageUnrecoverable": { - readonly def: "**Files cannot be fixed on this device:**\n\n${filesNotRecoverable}\n\nThese files have inconsistent metadata, and cannot be fixed on this device (mostly we cannot determine which is correct).\nTo restore them, please check your other devices (also by this feature) or restore them manually from a backup.\n"; - readonly fr: "**Fichiers non réparables sur cet appareil :**\n\n${filesNotRecoverable}\n\nCes fichiers ont des métadonnées incohérentes et ne peuvent être corrigés sur cet appareil (le plus souvent, nous ne pouvons déterminer lequel est correct).\nPour les restaurer, vérifiez vos autres appareils (par cette même fonction) ou restaurez-les manuellement depuis une sauvegarde.\n"; - readonly he: "**קבצים שלא ניתן לתקן במכשיר זה:**\n\n${filesNotRecoverable}\n\nלקבצים אלה יש מטה-נתונים לא עקביים, ולא ניתן לתקנם במכשיר זה (לרוב לא ניתן לקבוע מה נכון). לשחזורם, אנא בדוק מכשירים אחרים שלך (גם בתכונה זו) או שחזר ידנית מגיבוי.\n"; - readonly ja: "**このデバイスで修正できないファイル:**\n\n${filesNotRecoverable}\n\nこれらのファイルはメタデータに不整合があり、このデバイスでは修正できません(ほとんどの場合、どちらが正しいか判定できません)。\n復元するには、他のデバイスで確認するか、バックアップから手動で復元してください。\n"; - readonly ru: "Файлы не могут быть исправлены на этом устройстве:"; - readonly zh: "**无法在此设备上修复的文件:**\n\n${filesNotRecoverable}\n\n这些文件的元数据不一致,无法在此设备上修复(大多数情况下我们无法确定哪一个是正确的)\n要恢复它们,请检查你的其他设备(同样使用此功能),或从备份中手动恢复\n"; - }; - readonly "moduleMigration.fix0256.title": { - readonly def: "Broken files has been detected"; - readonly fr: "Fichiers corrompus détectés"; - readonly he: "זוהו קבצים פגומים"; - readonly ja: "破損ファイルが検出されました"; - readonly ru: "Обнаружены повреждённые файлы"; - readonly zh: "检测到损坏的文件"; - }; - readonly "moduleMigration.insecureChunkExist.buttons.fetch": { - readonly def: "I already rebuilt the remote. Fetch from the remote"; - readonly fr: "J'ai déjà reconstruit le distant. Récupérer depuis le distant"; - readonly he: "כבר בניתי מחדש את השרת המרוחק. משוך מהשרת המרוחד"; - readonly ja: "リモートを既に再構築した。リモートからフェッチ"; - readonly ru: "Я уже перестроил удалённую. Загрузить с удалённой"; - readonly zh: "我已经重建了远程数据库,将从远程获取"; - }; - readonly "moduleMigration.insecureChunkExist.buttons.later": { - readonly def: "I will do it later"; - readonly fr: "Je le ferai plus tard"; - readonly he: "אטפל בזה מאוחר יותר"; - readonly ja: "後で行う"; - readonly ru: "Сделаю позже"; - readonly zh: "我稍后再做"; - }; - readonly "moduleMigration.insecureChunkExist.buttons.rebuild": { - readonly def: "Rebuild Everything"; - readonly fr: "Tout reconstruire"; - readonly he: "בנה הכל מחדש"; - readonly ja: "すべてを再構築"; - readonly ru: "Перестроить всё"; - readonly zh: "重建所有内容"; - }; - readonly "moduleMigration.insecureChunkExist.laterMessage": { - readonly def: "We strongly recommend to treat this as soon as possible!"; - readonly fr: "Nous recommandons fortement de traiter ceci dès que possible !"; - readonly he: "אנו ממליצים בחום לטפל בזה בהקדם האפשרי!"; - readonly ja: "できるだけ早く対処することを強くお勧めします!"; - readonly ru: "Мы настоятельно рекомендуем обработать это как можно скорее!"; - readonly zh: "我们强烈建议尽快处理此问题!"; - }; - readonly "moduleMigration.insecureChunkExist.message": { - readonly def: "Some chunks are not securely stored and are not encrypted in databases.\n**Please rebuild the database to fix this issue**.\n\nIf your Remote Database is not configured with SSL, or using less-secure credentials, **you are at risk of exposing sensitive data**.\n\nNote: Please upgrade your Self-hosted LiveSync v0.25.6 or higher on all your devices, and back your vault up surely.\nNote2: Rebuild Everything and Fetch consumes a bit of time and traffic, please do it in off-peak hours and ensure a stable network connection.\n"; - readonly fr: "Certains fragments ne sont pas stockés de façon sécurisée et ne sont pas chiffrés dans les bases.\n**Veuillez reconstruire la base pour corriger ce problème.**\n\nSi votre base distante n'est pas configurée avec SSL, ou utilise des identifiants peu sûrs, **vous risquez d'exposer des données sensibles**.\n\nNote : Veuillez mettre à jour Self-hosted LiveSync en v0.25.6 ou supérieur sur tous vos appareils, et sauvegardez votre coffre avec soin.\nNote 2 : Tout reconstruire et Récupérer consomme un peu de temps et de bande passante, veuillez le faire hors des heures de pointe et avec une connexion réseau stable.\n"; - readonly he: "חלק מהנתחים לא מאוחסנים בצורה מאובטחת ואינם מוצפנים במסד הנתונים.\n**אנא בנה מחדש את מסד הנתונים כדי לתקן בעיה זו**.\n\nאם מסד הנתונים המרוחד אינו מוגדר עם SSL, או משתמש בפרטי גישה פחות מאובטחים, **אתה בסיכון של חשיפת מידע רגיש**.\n\nהערה: אנא שדרג את Self-hosted LiveSync לגרסה 0.25.6 ומעלה על כל מכשיריך, וגבה את הכספת שלך.\nהערה 2: בנייה מחדש ומשיכה דורשות זמן ותעבורת רשת. אנא עשה זאת בשעות שיא נמוך וודא חיבור רשת יציב.\n"; - readonly ja: "一部のチャンクが安全に保存されておらず、データベースで暗号化されていません。\n**この問題を修正するにはデータベースを再構築してください**。\n\nリモートデータベースがSSLで設定されていない、または安全性の低い認証情報を使用している場合、**機密データが漏洩するリスクがあります**。\n\n注意: すべてのデバイスでSelf-hosted LiveSync v0.25.6以降にアップグレードし、必ず保管庫をバックアップしてください。\n注意2: すべてを再構築とフェッチは時間とトラフィックを消費します。オフピーク時間に安定したネットワークで実行してください。\n"; - readonly ru: "Некоторые чанки хранятся небезопасно. Пожалуйста, перестройте базу данных."; - readonly zh: "一些块未安全存储,并且在数据库中未加密\n**请重建数据库以修复此问题**.\n\n如果你的远程数据库未配置 SSL,或者使用了不安全的凭据 **你可能面临暴露敏感数据的风险**.\n\n注意:请在所有设备上将 Self-hosted LiveSync 升级到 v0.25.6 或更高版本,并确保备份你的保险库\n\n注意2:重建所有内容和获取操作会消耗一些时间和流量,请在非高峰时段进行,并确保网络连接稳定\n"; - }; - readonly "moduleMigration.insecureChunkExist.title": { - readonly def: "Insecure chunks found!"; - readonly fr: "Fragments non sécurisés détectés !"; - readonly he: "נמצאו נתחים לא מאובטחים!"; - readonly ja: "安全でないチャンクが見つかりました!"; - readonly ru: "Обнаружены небезопасные чанки!"; - readonly zh: "发现不安全的块!"; - }; - readonly "moduleMigration.logBulkSendCorrupted": { - readonly def: "Send chunks in bulk has been enabled, however, this feature had been corrupted. Sorry for your inconvenience. Automatically disabled."; - readonly es: "El envío de fragmentos en bloque se ha habilitado, sin embargo, esta función se ha corrompido. Disculpe las molestias. Deshabilitado automáticamente."; - readonly fr: "L'envoi groupé de fragments a été activé, mais cette fonctionnalité était corrompue. Désolé pour la gêne. Désactivée automatiquement."; - readonly he: "שליחת נתחים באצווה הופעלה, אך תכונה זו הייתה פגועה. מתנצלים על אי הנוחות. נוטרלה אוטומטית."; - readonly ja: "チャンクの一括送信が有効にされていましたが、この機能に問題がありました。ご不便をおかけして申し訳ありません。自動的に無効化されました。"; - readonly ko: "청크 일괄 전송이 활성화되었지만, 이 기능에 문제가 있었습니다. 불편을 드려 죄송합니다. 자동으로 비활성화되었습니다."; - readonly ru: "Отправка чанков пакетами была включена, но эта функция была повреждена. Приносим извинения. Автоматически отключено."; - readonly zh: "已启用批量发送 chunks,但此功能已损坏。给您带来不便,我们深表歉意。已自动禁用"; - }; - readonly "moduleMigration.logFetchRemoteTweakFailed": { - readonly def: "Failed to fetch remote tweak values"; - readonly es: "Error al obtener los valores de ajuste remoto"; - readonly fr: "Échec de la récupération des valeurs d'ajustement distantes"; - readonly he: "נכשל במשיכת ערכי כיוונון מרוחקים"; - readonly ja: "リモートの調整値の取得に失敗しました"; - readonly ko: "원격 조정 값을 가져오는데 실패했습니다"; - readonly ru: "Не удалось загрузить удалённые настройки"; - readonly zh: "获取远程调整值失败"; - }; - readonly "moduleMigration.logLocalDatabaseNotReady": { - readonly def: "Something went wrong! The local database is not ready"; - readonly es: "¡Algo salió mal! La base de datos local no está lista"; - readonly fr: "Un problème est survenu ! La base locale n'est pas prête"; - readonly he: "משהו השתבש! מסד הנתונים המקומי אינו מוכן"; - readonly ja: "何か問題が発生しました!ローカルデータベースが準備できていません"; - readonly ko: "문제가 발생했습니다! 로컬 데이터베이스가 준비되지 않았습니다"; - readonly ru: "Что-то пошло не так! Локальная база данных не готова"; - readonly zh: "出错了!本地数据库尚未准备好"; - }; - readonly "moduleMigration.logMigratedSameBehaviour": { - readonly def: "Migrated to db:${current} with the same behaviour as before"; - readonly es: "Migrado a db:${current} con el mismo comportamiento que antes"; - readonly fr: "Migration vers db:${current} avec le même comportement qu'auparavant"; - readonly he: "הוגר ל-db:${current} עם אותה התנהגות כמקודם"; - readonly ja: "以前と同じ動作でdb:${current}に移行しました"; - readonly ko: "이전과 같은 방식으로 동작하도록 db:${current}로 데이터 구조 전환이 완료되었습니다"; - readonly ru: "Миграция на db:current с тем же поведением, что и раньше"; - readonly zh: "已迁移到 db:${current},行为与之前相同"; - }; - readonly "moduleMigration.logMigrationFailed": { - readonly def: "Migration failed or cancelled from ${old} to ${current}"; - readonly es: "La migración falló o se canceló de ${old} a ${current}"; - readonly fr: "Migration échouée ou annulée de ${old} vers ${current}"; - readonly he: "הגירה נכשלה או בוטלה מ-${old} ל-${current}"; - readonly ja: "${old}から${current}への移行が失敗またはキャンセルされました"; - readonly ko: "${old}에서 ${current}로의 데이터 구조 전환이 실패했거나 중단되었습니다"; - readonly ru: "Миграция не удалась или отменена с old на current"; - readonly zh: "从 ${old} 到 ${current} 的迁移失败或已取消"; - }; - readonly "moduleMigration.logRedflag2CreationFail": { - readonly def: "Failed to create redflag2"; - readonly es: "Error al crear redflag2"; - readonly fr: "Échec de création de redflag2"; - readonly he: "יצירת redflag2 נכשלה"; - readonly ja: "redflag2の作成に失敗しました"; - readonly ko: "redflag2 생성에 실패했습니다"; - readonly ru: "Не удалось создать redflag2"; - readonly zh: "创建 redflag2 失败"; - }; - readonly "moduleMigration.logRemoteTweakUnavailable": { - readonly def: "Could not get remote tweak values"; - readonly es: "No se pudieron obtener los valores de ajuste remoto"; - readonly fr: "Impossible d'obtenir les valeurs d'ajustement distantes"; - readonly he: "לא ניתן לקבל ערכי כיוונון מרוחקים"; - readonly ja: "リモートの調整値を取得できませんでした"; - readonly ko: "원격 조정 값을 가져올 수 없습니다"; - readonly ru: "Не удалось получить удалённые настройки"; - readonly zh: "无法获取远程调整值"; - }; - readonly "moduleMigration.logSetupCancelled": { - readonly def: "The setup has been cancelled, Self-hosted LiveSync waiting for your setup!"; - readonly es: "La configuración ha sido cancelada, ¡Self-hosted LiveSync está esperando tu configuración!"; - readonly fr: "La configuration a été annulée, Self-hosted LiveSync attend votre configuration !"; - readonly he: "ההגדרה בוטלה, Self-hosted LiveSync ממתין להגדרתך!"; - readonly ja: "セットアップがキャンセルされました。Self-hosted LiveSyncはセットアップを待っています!"; - readonly ko: "설정이 취소되었습니다. Self-hosted LiveSync가 설정을 기다리고 있습니다!"; - readonly ru: "Настройка отменена, Self-hosted LiveSync ожидает вашей настройки!"; - readonly zh: "设置已取消,Self-hosted LiveSync 正在等待您的设置!"; - }; - readonly "moduleMigration.msgFetchRemoteAgain": { - readonly def: "As you may already know, the self-hosted LiveSync has changed its default behaviour and database structure.\n\nAnd thankfully, with your time and efforts, the remote database appears to have already been migrated. Congratulations!\n\nHowever, we need a bit more. The configuration of this device is not compatible with the remote database. We will need to fetch the remote database again. Should we fetch from the remote again now?\n\n___Note: We cannot synchronise until the configuration has been changed and the database has been fetched again.___\n___Note2: The chunks are completely immutable, we can fetch only the metadata and difference.___"; - readonly es: "Como ya sabrás, Self-hosted LiveSync ha cambiado su comportamiento predeterminado y la estructura de la base de datos.\n\nAfortunadamente, con tu tiempo y esfuerzo, la base de datos remota parece haber sido ya migrada. ¡Felicidades!\n\nSin embargo, necesitamos un poco más. La configuración de este dispositivo no es compatible con la base de datos remota. Necesitaremos volver a obtener la base de datos remota. ¿Debemos obtenerla nuevamente ahora?\n\n___Nota: No podemos sincronizar hasta que la configuración haya sido cambiada y la base de datos haya sido obtenida nuevamente.___\n___Nota2: Los fragmentos son completamente inmutables, solo podemos obtener los metadatos y diferencias.___"; - readonly fr: "Comme vous le savez peut-être déjà, Self-hosted LiveSync a modifié son comportement par défaut et la structure de sa base de données.\n\nEt, grâce à votre temps et vos efforts, la base distante semble déjà avoir été migrée. Félicitations !\n\nCependant, il faut encore un peu plus. La configuration de cet appareil n'est pas compatible avec la base distante. Nous devrons récupérer à nouveau la base distante. Devons-nous récupérer depuis le distant maintenant ?\n\n___Note : Nous ne pouvons pas synchroniser tant que la configuration n'a pas été modifiée et que la base n'a pas été récupérée à nouveau.___\n___Note 2 : Les fragments sont complètement immuables, nous ne pouvons récupérer que les métadonnées et les différences.___"; - readonly he: "כפי שייתכן שכבר ידוע לך, Self-hosted LiveSync שינה את התנהגות ברירת המחדל ומבנה מסד הנתונים.\n\nובזכות זמנך ומאמציך, מסד הנתונים המרוחד נראה כבר הוגר. ברכות!\n\nעם זאת, נדרש עוד קצת. תצורת מכשיר זה אינה תואמת למסד הנתונים המרוחד. נצטרך למשוך את מסד הנתונים המרוחד שוב. האם למשוך מהשרת המרוחד עכשיו?\n\n___הערה: לא ניתן לסנכרן עד שהתצורה תשתנה ומסד הנתונים יימשך שוב.___\n___הערה 2: הנתחים הם בלתי-ניתנים לשינוי לחלוטין, ניתן למשוך רק את המטה-נתונים וההפרש.___"; - readonly ja: "ご存知のとおり、self-hosted LiveSyncはデフォルトの動作とデータベース構造を変更しました。\n\nご協力のおかげで、リモートデータベースはすでに移行されているようです。おめでとうございます!\n\nしかし、もう少し必要です。このデバイスの設定はリモートデータベースと互換性がありません。リモートデータベースを再度フェッチする必要があります。今すぐリモートから再フェッチしますか?\n\n___注意: 設定が変更され、データベースが再フェッチされるまで同期できません。___\n___注意2: チャンクは完全に不変なので、メタデータと差分のみフェッチできます。___"; - readonly ko: "이미 알고 계시겠지만, Self-hosted LiveSync의 기본 동작 방식과 데이터베이스 구조가 변경되었습니다.\n\n다행히도 여러분의 노력 덕분에 원격 데이터베이스는 이미 성공적으로 데이터 구조 전환이 완료된 것으로 보입니다. 축하드립니다!\n\n하지만 아직 일부 추가 작업이 필요합니다. 이 기기의 설정이 원격 데이터베이스와 호환되지 않으므로, 원격 데이터를 다시 가져와야 합니다. 지금 원격 데이터베이스를 다시 가져오시겠습니까?\n\n___참고: 설정이 변경되고 데이터베이스를 다시 불러오기 전까지는 동기화가 불가능합니다.___\n___참고2: 청크는 변경이 불가능한 구조이므로, 메타데이터와 차이점만 가져올 수 있습니다.___"; - readonly ru: "Удалённая база данных, похоже, уже была мигрирована. Конфигурация этого устройства несовместима."; - readonly zh: "您可能已经知道,Self-hosted LiveSync 更改了其默认行为和数据库结构。\n\n值得庆幸的是,在您的时间和努力下,远程数据库似乎已经迁移完成。恭喜!\n\n但是,我们还需要一点点操作。此设备的配置与远程数据库不兼容。我们需要再次从远程数据库获取。我们现在应该再次从远程获取吗?\n\n___注意:在更改配置并再次获取数据库之前,我们无法进行同步。___\n___注意2:chunks 是完全不可变的,我们只能获取元数据和差异"; - }; - readonly "moduleMigration.msgInitialSetup": { - readonly def: "Your device has **not been set up yet**. Let me guide you through the setup process.\n\nPlease keep in mind that every dialogue content can be copied to the clipboard. If you need to refer to it later, you can paste it into a note in Obsidian. You can also translate it into your language using a translation tool.\n\nFirst, do you have **Setup URI**?\n\nNote: If you do not know what it is, please refer to the [documentation](${URI_DOC})."; - readonly es: "Tu dispositivo **aún no ha sido configurado**. Permíteme guiarte a través del proceso de configuración.\n\nTen en cuenta que todo el contenido del diálogo se puede copiar al portapapeles. Si necesitas consultarlo más tarde, puedes pegarlo en una nota en Obsidian. También puedes traducirlo a tu idioma utilizando una herramienta de traducción.\n\nPrimero, ¿tienes **URI de configuración**?\n\nNota: Si no sabes qué es, consulta la [documentación](${URI_DOC})."; - readonly fr: "Votre appareil n'a **pas encore été configuré**. Laissez-moi vous guider dans le processus de configuration.\n\nVeuillez noter que chaque contenu de boîte de dialogue peut être copié dans le presse-papiers. Si vous souhaitez vous y référer plus tard, vous pouvez le coller dans une note d'Obsidian. Vous pouvez également le traduire dans votre langue via un outil de traduction.\n\nTout d'abord, disposez-vous d'une **URI de configuration** ?\n\nNote : Si vous ne savez pas ce que c'est, consultez la [documentation](${URI_DOC})."; - readonly he: "המכשיר שלך **טרם הוגדר**. אנחנו כאן לעזור לך בתהליך ההגדרה.\n\nשים לב שניתן להעתיק את תוכן כל דיאלוג ללוח. אם צריך לחזור אליו מאוחר יותר, ניתן להדביק אותו כפתק ב-Obsidian. ניתן גם לתרגם לשפתך בעזרת כלי תרגום.\n\nראשית, האם יש לך **Setup URI**?\n\nהערה: אם אינך יודע מהו, אנא עיין ב[תיעוד](${URI_DOC})."; - readonly ja: "このデバイスは**まだセットアップされていません**。セットアッププロセスをご案内します。\n\nすべてのダイアログの内容はクリップボードにコピーできます。後で参照する必要があれば、Obsidianのノートに貼り付けてください。翻訳ツールを使ってお使いの言語に翻訳することもできます。\n\nまず、**セットアップURI**をお持ちですか?\n\n注意: それが何か分からない場合は、[documentation](${URI_DOC})を参照してください。"; - readonly ko: "이 기기는 **아직 초기 설정이 완료되지 않았습니다**. 지금부터 설정 과정을 안내해 드리겠습니다.\n\n모든 대화 내용은 클립보드에 복사할 수 있습니다. 나중에 참고하려면 Obsidian 노트에 붙여넣거나 번역 도구를 활용해 번역하셔도 됩니다.\n\n먼저, **Setup URI**를 가지고 계신가요?\n\n참고: Setup URI가 무엇인지 잘 모르시겠다면 [문서](${URI_DOC})를 참고해 주세요."; - readonly ru: "Ваше устройство ещё не настроено. У вас есть Setup URI?"; - readonly zh: "您的设备**尚未设置**。让我引导您完成设置过程。\n\n请记住,每个对话框内容都可以复制到剪贴板。如果以后需要参考,可以将其粘贴到 Obsidian 的笔记中。您也可以使用翻译工具将其翻译成您的语言。\n\n首先,您有**设置 URI** 吗?\n\n注意:如果您不知道这是什么,请参阅[文档](${URI_DOC})"; - }; - readonly "moduleMigration.msgRecommendSetupUri": { - readonly def: "We strongly recommend that you generate a set-up URI and use it.\nIf you do not have knowledge about it, please refer to the [documentation](${URI_DOC}) (Sorry again, but it is important).\n\nHow do you want to set it up manually?"; - readonly es: "Te recomendamos encarecidamente que generes una URI de configuración y la utilices.\nSi no tienes conocimientos al respecto, consulta la [documentación](${URI_DOC}) (Lo siento de nuevo, pero es importante).\n\n¿Cómo quieres configurarlo manualmente?"; - readonly fr: "Nous recommandons vivement de générer une URI de configuration et de l'utiliser.\nSi vous ne connaissez pas, veuillez consulter la [documentation](${URI_DOC}) (Désolé encore, mais c'est important).\n\nComment souhaitez-vous effectuer la configuration manuellement ?"; - readonly he: "אנו ממליצים בחום לייצר Setup URI ולהשתמש בו.\nאם אין לך ידע בנושא, אנא עיין ב[תיעוד](${URI_DOC}) (מתנצלים שוב, אך זה חשוב).\n\nכיצד ברצונך להגדיר ידנית?"; - readonly ja: "セットアップURIを生成して使用することを強くお勧めします。\nこれについて知識がない場合は、[documentation](${URI_DOC})を参照してください(重要です)。\n\n手動でセットアップしますか?"; - readonly ko: "Setup URI를 생성해 사용하는 것을 강력히 권장합니다.\nSetup URI가 무엇인지 잘 모르시겠다면 [문서](${URI_DOC})를 참고해 주세요. 중요한 내용이니 꼭 확인하시기 바랍니다.\n\n직접 수동 설정을 진행하시겠습니까?"; - readonly ru: "Мы рекомендуем сгенерировать Setup URI."; - readonly zh: "我们强烈建议您生成一个设置 URI 并使用它。\n如果您对此不了解,请参阅[文档](${URI_DOC})(再次抱歉,但这很重要)。\n\n您想如何手动设置?"; - }; - readonly "moduleMigration.msgSinceV02321": { - readonly def: "Since v0.23.21, the self-hosted LiveSync has changed the default behaviour and database structure. The following changes have been made:\n\n1. **Case sensitivity of filenames**\n The handling of filenames is now case-insensitive. This is a beneficial change for most platforms, other than Linux and iOS, which do not manage filename case sensitivity effectively.\n (On These, a warning will be displayed for files with the same name but different cases).\n\n2. **Revision handling of the chunks**\n Chunks are immutable, which allows their revisions to be fixed. This change will enhance the performance of file saving.\n\n___However, to enable either of these changes, both remote and local databases need to be rebuilt. This process takes a few minutes, and we recommend doing it when you have ample time.___\n\n- If you wish to maintain the previous behaviour, you can skip this process by using `${KEEP}`.\n- If you do not have enough time, please choose `${DISMISS}`. You will be prompted again later.\n- If you have rebuilt the database on another device, please select `${DISMISS}` and try synchronizing again. Since a difference has been detected, you will be prompted again."; - readonly es: "Desde la versión v0.23.21, Self-hosted LiveSync ha cambiado el comportamiento predeterminado y la estructura de la base de datos. Se han realizado los siguientes cambios:\n\n1. **Sensibilidad a mayúsculas de los nombres de archivo**\n El manejo de los nombres de archivo ahora no distingue entre mayúsculas y minúsculas. Este cambio es beneficioso para la mayoría de las plataformas, excepto Linux y iOS, que no gestionan efectivamente la sensibilidad a mayúsculas de los nombres de archivo.\n (En estos, se mostrará una advertencia para archivos con el mismo nombre pero diferentes mayúsculas).\n\n2. **Manejo de revisiones de los fragmentos**\n Los fragmentos son inmutables, lo que permite que sus revisiones sean fijas. Este cambio mejorará el rendimiento al guardar archivos.\n\n___Sin embargo, para habilitar cualquiera de estos cambios, es necesario reconstruir tanto las bases de datos remota como la local. Este proceso toma unos minutos, y recomendamos hacerlo cuando tengas tiempo suficiente.___\n\n- Si deseas mantener el comportamiento anterior, puedes omitir este proceso usando `${KEEP}`.\n- Si no tienes suficiente tiempo, por favor elige `${DISMISS}`. Se te pedirá nuevamente más tarde.\n- Si has reconstruido la base de datos en otro dispositivo, selecciona `${DISMISS}` e intenta sincronizar nuevamente. Dado que se ha detectado una diferencia, se te solicitará nuevamente."; - readonly fr: "Depuis la v0.23.21, Self-hosted LiveSync a modifié son comportement par défaut et la structure de sa base. Les changements suivants ont été effectués :\n\n1. **Sensibilité à la casse des noms de fichiers**\n La gestion des noms de fichiers est désormais insensible à la casse. C'est un changement bénéfique pour la plupart des plateformes, hormis Linux et iOS, qui ne gèrent pas efficacement la casse des noms de fichiers.\n (Sur celles-ci, un avertissement s'affichera pour les fichiers portant le même nom avec une casse différente).\n\n2. **Gestion des révisions des fragments**\n Les fragments sont immuables, ce qui permet de fixer leurs révisions. Ce changement améliore les performances d'enregistrement des fichiers.\n\n___Cependant, pour activer l'un ou l'autre de ces changements, les bases locale et distante doivent être reconstruites. Ce processus prend quelques minutes, et nous recommandons de le faire quand vous avez le temps.___\n\n- Si vous souhaitez conserver le comportement précédent, vous pouvez ignorer ce processus via `${KEEP}`.\n- Si vous n'avez pas le temps, choisissez `${DISMISS}`. Vous serez invité à nouveau plus tard.\n- Si vous avez reconstruit la base sur un autre appareil, sélectionnez `${DISMISS}` et réessayez la synchronisation. Une différence étant détectée, vous serez invité à nouveau."; - readonly he: "מאז גרסה 0.23.21, Self-hosted LiveSync שינה את התנהגות ברירת המחדל ומבנה מסד הנתונים. השינויים הבאים בוצעו:\n\n1. **תלות רישיות בשמות קבצים**\n הטיפול בשמות קבצים הוא כעת ללא תלות רישיות. זהו שינוי מועיל לרוב הפלטפורמות,\n פרט ל-Linux ו-iOS שאינן מנהלות תלות רישיות בקבצים ביעילות.\n (בפלטפורמות אלה, תוצג אזהרה עבור קבצים עם אותו שם אך רישיות שונה).\n\n2. **טיפול בגרסאות של נתחים**\n נתחים הם בלתי-ניתנים לשינוי, מה שמאפשר גרסאות קבועות. שינוי זה ישפר את\n ביצועי שמירת הקבצים.\n\n___עם זאת, כדי להפעיל אחד מהשינויים הללו, יש לבנות מחדש גם את מסד הנתונים המרוחד וגם את המקומי. תהליך זה לוקח כמה דקות, ואנו ממליצים לעשות זאת כשיש לך זמן פנוי.___\n\n- אם ברצונך לשמור את ההתנהגות הקודמת, ניתן לדלג על תהליך זה באמצעות `${KEEP}`.\n- אם אין לך מספיק זמן, אנא בחר `${DISMISS}`. תקבל תזכורת בהמשך.\n- אם בנית מחדש את מסד הנתונים במכשיר אחר, אנא בחר `${DISMISS}` ונסה לסנכרן שוב. מאחר שזוהה הפרש, תקבל תזכורת שוב."; - readonly ja: "v0.23.21以降、self-hosted LiveSyncはデフォルトの動作とデータベース構造を変更しました。以下の変更が行われました:\n\n1. **ファイル名の大文字小文字の区別**\n ファイル名の処理が大文字小文字を区別しなくなりました。これは、ファイル名の大文字小文字を効果的に管理しないLinuxとiOS以外のほとんどのプラットフォームにとって有益な変更です。\n (これらの環境では、同じ名前で大文字小文字が異なるファイルに対して警告が表示されます)。\n\n2. **チャンクのリビジョン処理**\n チャンクは不変であり、リビジョンを固定できます。この変更により、ファイル保存のパフォーマンスが向上します。\n\n___しかし、これらの変更を有効にするには、リモートとローカルの両方のデータベースを再構築する必要があります。このプロセスは数分かかります。時間に余裕があるときに行うことをお勧めします。___\n\n- 以前の動作を維持したい場合は、`${KEEP}`を使用してこのプロセスをスキップできます。\n- 時間がない場合は、`${DISMISS}`を選択してください。後で再度確認されます。\n- 別のデバイスでデータベースを再構築した場合は、`${DISMISS}`を選択して再度同期してみてください。差異が検出されたため、再度確認されます。"; - readonly ko: "v0.23.21부터 Self-hosted LiveSync의 기본 동작 방식과 데이터베이스 구조가 변경되었습니다. 주요 변경사항은 다음과 같습니다:\n\n1. **파일명 대소문자 구분 처리**\n 이제 파일명은 대소문자를 구분하지 않고 처리됩니다. 이는 파일명 구분을 제대로 지원하지 않는 Linux 및 iOS를 제외한 대부분의 플랫폼에서 유리한 변화입니다.\n (Linux나 iOS에서는 대소문자만 다른 파일이 존재할 경우 경고가 표시됩니다)\n\n2. **청크 리비전 관리 방식 개선**\n 청크는 변경 불가능한(immutable) 구조로 고정되며, 이를 통해 리비전 처리가 안정화되고 파일 저장 성능이 향상됩니다.\n\n___단, 위 기능을 활성화하려면 원격 및 로컬 데이터베이스를 모두 재구성해야 합니다. 이 과정은 수 분이 소요되므로 여유가 있을 때 실행하시는 것을 권장합니다.___\n\n- 기존 방식대로 유지하려면 `${KEEP}`을 선택해 이 과정을 건너뛸 수 있습니다.\n- 시간이 부족하다면 `${DISMISS}`를 눌러주시면 나중에 다시 안내드리겠습니다.\n- 이미 다른 기기에서 데이터베이스를 재구성하셨다면 `${DISMISS}`를 선택한 뒤 다시 동기화해 보세요. 차이점이 감지되면 다시 안내드리겠습니다."; - readonly ru: "Начиная с v0.23.21, self-hosted LiveSync изменил поведение и структуру базы данных."; - readonly zh: "自 v0.23.21 起,Self-hosted LiveSync 更改了默认行为和数据库结构。进行了以下更改:\n\n1. **文件名的区分大小写** \n现在处理文件名时不区分大小写。这对于大多数平台来说是一个有益的更改,除了 Linux 和 iOS,它们不能有效地管理文件名的大小写敏感性。\n(在这些平台上,对于名称相同但大小写不同的文件将显示警告)。\n\n2. **chunks 的版本处理** \nchunks 是不可变的,这使得它们的版本可以固定。此更改将提高文件保存的性能。\n\n___然而,要启用这些更改中的任何一个,都需要重建远程和本地数据库。这个过程需要几分钟,我们建议您在有充足时间时进行。___\n\n- 如果您希望保持以前的行为,可以使用 `${KEEP}` 跳过此过程。\n- 如果您没有足够的时间,请选择 `${DISMISS}`。稍后会再次提示您。\n- 如果您已在另一台设备上重建了数据库,请选择 `${DISMISS}` 并尝试再次同步。由于检测到差异,系统会再次提示您"; - }; - readonly "moduleMigration.optionAdjustRemote": { - readonly def: "Adjust to remote"; - readonly es: "Ajustar al remoto"; - readonly fr: "Ajuster au distant"; - readonly he: "התאם לשרת המרוחד"; - readonly ja: "リモートに合わせる"; - readonly ko: "원격에 맞추기"; - readonly ru: "Настроить под удалённую"; - readonly zh: "调整到远程设置"; - }; - readonly "moduleMigration.optionDecideLater": { - readonly def: "Decide it later"; - readonly es: "Decidirlo más tarde"; - readonly fr: "Décider plus tard"; - readonly he: "החלט מאוחר יותר"; - readonly ja: "後で決める"; - readonly ko: "나중에 결정하기"; - readonly ru: "Решить позже"; - readonly zh: "稍后决定"; - }; - readonly "moduleMigration.optionEnableBoth": { - readonly def: "Enable both"; - readonly es: "Habilitar ambos"; - readonly fr: "Activer les deux"; - readonly he: "הפעל את שניהם"; - readonly ja: "両方を有効にする"; - readonly ko: "둘 다 활성화"; - readonly ru: "Включить оба"; - readonly zh: "启用两者"; - }; - readonly "moduleMigration.optionEnableFilenameCaseInsensitive": { - readonly def: "Enable only #1"; - readonly es: "Habilitar solo #1"; - readonly fr: "Activer seulement #1"; - readonly he: "הפעל רק #1"; - readonly ja: "#1のみ有効にする"; - readonly ko: "#1만 활성화"; - readonly ru: "Включить только #1"; - readonly zh: "仅启用 #1"; - }; - readonly "moduleMigration.optionEnableFixedRevisionForChunks": { - readonly def: "Enable only #2"; - readonly es: "Habilitar solo #2"; - readonly fr: "Activer seulement #2"; - readonly he: "הפעל רק #2"; - readonly ja: "#2のみ有効にする"; - readonly ko: "#2만 활성화"; - readonly ru: "Включить только #2"; - readonly zh: "仅启用 #2"; - }; - readonly "moduleMigration.optionHaveSetupUri": { - readonly def: "Yes, I have"; - readonly es: "Sí, tengo"; - readonly fr: "Oui, j'en ai une"; - readonly he: "כן, יש לי"; - readonly ja: "はい、持っています"; - readonly ko: "예, 있습니다"; - readonly ru: "Да, есть"; - readonly zh: "是的,我有"; - }; - readonly "moduleMigration.optionKeepPreviousBehaviour": { - readonly def: "Keep previous behaviour"; - readonly es: "Mantener comportamiento anterior"; - readonly fr: "Conserver le comportement précédent"; - readonly he: "שמור על התנהגות קודמת"; - readonly ja: "以前の動作を維持"; - readonly ko: "이전 동작 유지"; - readonly ru: "Сохранить предыдущее поведение"; - readonly zh: "保持以前的行为"; - }; - readonly "moduleMigration.optionManualSetup": { - readonly def: "Set it up all manually"; - readonly es: "Configurarlo todo manualmente"; - readonly fr: "Tout configurer manuellement"; - readonly he: "הגדר הכל ידנית"; - readonly ja: "すべて手動でセットアップ"; - readonly ko: "모든 것을 수동으로 설정"; - readonly ru: "Настроить всё вручную"; - readonly zh: "全部手动设置"; - }; - readonly "moduleMigration.optionNoAskAgain": { - readonly def: "No, please ask again"; - readonly es: "No, por favor pregúntame de nuevo"; - readonly fr: "Non, demandez à nouveau"; - readonly he: "לא, אנא שאל שוב"; - readonly ja: "いいえ、後で確認する"; - readonly ko: "아니요 (나중에 다시 물어보기)"; - readonly ru: "Нет, спросить снова"; - readonly zh: "不,请稍后再次询问"; - }; - readonly "moduleMigration.optionNoSetupUri": { - readonly def: "No, I do not have"; - readonly fr: "Non, je n'en ai pas"; - readonly he: "לא, אין לי"; - readonly ja: "いいえ、持っていません"; - readonly ko: "아니요, 없습니다"; - readonly ru: "Нет, нет"; - readonly zh: "不,我没有"; - }; - readonly "moduleMigration.optionRemindNextLaunch": { - readonly def: "Remind me at the next launch"; - readonly fr: "Me rappeler au prochain lancement"; - readonly he: "הזכר לי בהפעלה הבאה"; - readonly ja: "次回起動時にリマインド"; - readonly ko: "다음 시작 시 알림"; - readonly ru: "Напомнить при следующем запуске"; - readonly zh: "下次启动时提醒我"; - }; - readonly "moduleMigration.optionSetupViaP2P": { - readonly def: "Use P2P Sync to set up"; - readonly fr: "Utiliser Sync P2P pour configurer"; - readonly he: "השתמש ב-%{short_p2p_sync} להגדרה"; - readonly ja: "P2P Sync (試験機能)を使ってセットアップ"; - readonly ko: "P2P 동기화 (실험 기능)를 사용하여 설정"; - readonly ru: "Использовать short_p2p_sync для настройки"; - readonly zh: "Use P2P同步(实验性) to set up"; - }; - readonly "moduleMigration.optionSetupWizard": { - readonly def: "Take me into the setup wizard"; - readonly fr: "Ouvrir l'assistant de configuration"; - readonly he: "קח אותי לאשף ההגדרה"; - readonly ja: "セットアップウィザードへ"; - readonly ko: "설정 마법사로 안내"; - readonly ru: "Перейти в мастер настройки"; - readonly zh: "带我进入设置向导"; - }; - readonly "moduleMigration.optionYesFetchAgain": { - readonly def: "Yes, fetch again"; - readonly fr: "Oui, récupérer à nouveau"; - readonly he: "כן, משוך שוב"; - readonly ja: "はい、再フェッチする"; - readonly ko: "예 (다시 가져오기)"; - readonly ru: "Да, загрузить снова"; - readonly zh: "是的,再次获取"; - }; - readonly "moduleMigration.titleCaseSensitivity": { - readonly def: "Case Sensitivity"; - readonly fr: "Sensibilité à la casse"; - readonly he: "תלות רישיות"; - readonly ja: "大文字小文字の区別"; - readonly ko: "대소문자 구분"; - readonly ru: "Чувствительность к регистру"; - readonly zh: "大小写敏感性"; - }; - readonly "moduleMigration.titleRecommendSetupUri": { - readonly def: "Recommendation to use Setup URI"; - readonly fr: "Recommandation d'utilisation de l'URI de configuration"; - readonly he: "המלצה לשימוש ב-Setup URI"; - readonly ja: "セットアップURIの使用を推奨"; - readonly ko: "Setup URI 사용 권장"; - readonly ru: "Рекомендация использовать Setup URI"; - readonly zh: "推荐使用设置 URI"; - }; - readonly "moduleMigration.titleWelcome": { - readonly def: "Welcome to Self-hosted LiveSync"; - readonly fr: "Bienvenue dans Self-hosted LiveSync"; - readonly he: "ברוך הבא ל-Self-hosted LiveSync"; - readonly ja: "Self-hosted LiveSyncへようこそ"; - readonly ko: "Self-hosted LiveSync에 오신 것을 환영합니다"; - readonly ru: "Добро пожаловать в Self-hosted LiveSync"; - readonly zh: "欢迎使用 Self-hosted LiveSync"; - }; - readonly "moduleObsidianMenu.replicate": { - readonly def: "Replicate"; - readonly es: "Replicar"; - readonly fr: "Répliquer"; - readonly he: "שכפל"; - readonly ja: "レプリケート"; - readonly ko: "복제"; - readonly ru: "Реплицировать"; - readonly zh: "复制"; - }; - readonly "More actions": { - readonly def: "More actions"; - readonly es: "Más acciones"; - readonly ja: "その他の操作"; - readonly ko: "추가 작업"; - readonly ru: "Другие действия"; - readonly zh: "更多操作"; - readonly "zh-tw": "更多操作"; - }; - readonly "Move remotely deleted files to the trash, instead of deleting.": { - readonly def: "Move remotely deleted files to the trash, instead of deleting."; - readonly es: "Mover archivos borrados remotos a papelera en lugar de eliminarlos"; - readonly fr: "Déplacer les fichiers supprimés à distance vers la corbeille, au lieu de les supprimer."; - readonly he: "העבר קבצים שנמחקו מרחוק לאשפה, במקום למחוק."; - readonly ja: "リモートで削除されたファイルを削除せずにゴミ箱に移動する。"; - readonly ko: "원격에서 삭제된 파일을 삭제하는 대신 휴지통으로 이동합니다."; - readonly ru: "Перемещать удалённые на удалённом сервере файлы в корзину вместо удаления."; - readonly zh: "将远程删除的文件移至回收站,而不是直接删除"; - }; - readonly "Network warning style": { - readonly def: "Network warning style"; - readonly es: "Estilo de advertencia de red"; - readonly ja: "ネットワーク警告の表示方式"; - readonly ko: "네트워크 경고 표시 방식"; - readonly ru: "Стиль сетевого предупреждения"; - readonly zh: "网络警告样式"; - readonly "zh-tw": "網路警告樣式"; - }; - readonly "New Remote": { - readonly def: "New Remote"; - readonly es: "Nuevo remoto"; - readonly ja: "新しいリモート"; - readonly ko: "새 원격"; - readonly ru: "Новое удалённое хранилище"; - readonly zh: "新建远端"; - readonly "zh-tw": "新增遠端"; - }; - readonly "No connected device information found. Cancelling Garbage Collection.": { - readonly def: "No connected device information found. Cancelling Garbage Collection."; - readonly ja: "接続済みデバイスの情報が見つかりませんでした。Garbage Collection をキャンセルします。"; - readonly ko: "연결된 기기 정보를 찾을 수 없습니다. Garbage Collection을 취소합니다."; - readonly ru: "Не найдена информация о подключённых устройствах. Garbage Collection отменяется."; - readonly zh: "未找到已连接设备的信息。正在取消垃圾回收。"; - readonly "zh-tw": "找不到已連線裝置的資訊。正在取消垃圾回收。"; - }; - readonly "No limit configured": { - readonly def: "No limit configured"; - readonly es: "Sin límite configurado"; - readonly ja: "制限は設定されていません"; - readonly ko: "제한이 설정되지 않음"; - readonly ru: "Лимит не задан"; - readonly zh: "未配置限制"; - readonly "zh-tw": "尚未設定限制"; - }; - readonly "No, please take me back": { - readonly def: "No, please take me back"; - readonly es: "No, volver atrás"; - readonly ja: "いいえ、前に戻ります"; - readonly ko: "아니요, 이전으로 돌아가겠습니다"; - readonly ru: "Нет, верните меня назад"; - readonly zh: "不,返回上一步"; - readonly "zh-tw": "不,返回上一步"; - }; - readonly "Node ID": { - readonly def: "Node ID"; - readonly ja: "ノード ID"; - readonly ko: "노드 ID"; - readonly ru: "ID узла"; - readonly zh: "节点 ID"; - readonly "zh-tw": "節點 ID"; - }; - readonly "Node Information Missing": { - readonly def: "Node Information Missing"; - readonly ja: "ノード情報がありません"; - readonly ko: "노드 정보 누락"; - readonly ru: "Отсутствует информация об узле"; - readonly zh: "节点信息缺失"; - readonly "zh-tw": "節點資訊缺失"; - }; - readonly "Non-Synchronising files": { - readonly def: "Non-Synchronising files"; - readonly es: "Archivos no sincronizados"; - readonly ja: "同期しないファイル"; - readonly ko: "동기화하지 않는 파일"; - readonly ru: "Несинхронизируемые файлы"; - readonly zh: "不同步的文件"; - readonly "zh-tw": "不同步的檔案"; - }; - readonly "Normal Files": { - readonly def: "Normal Files"; - readonly es: "Archivos normales"; - readonly ja: "通常ファイル"; - readonly ko: "일반 파일"; - readonly ru: "Обычные файлы"; - readonly zh: "普通文件"; - readonly "zh-tw": "一般檔案"; - }; - readonly 'Not all messages have been translated. And, please revert to "Default" when reporting errors.': { - readonly def: "Not all messages have been translated. And, please revert to \"Default\" when reporting errors."; - readonly es: "No todos los mensajes están traducidos. Por favor, vuelva a \"Predeterminado\" al reportar errores."; - readonly fr: "Tous les messages n'ont pas été traduits. Et veuillez revenir à « Par défaut » lorsque vous signalez des erreurs."; - readonly he: "לא כל ההודעות תורגמו. בנוסף, אנא חזור ל\"ברירת מחדל\" בעת דיווח על שגיאות."; - readonly ja: "すべてのメッセージが翻訳されているわけではありません。また、Issue報告の際にはいったん\"Default\"に戻してください"; - readonly ko: "모든 메시지가 번역되지 않았습니다. 오류 신고 시 \"기본값\"으로 되돌려 주세요."; - readonly ru: "Не все сообщения переведены. И, пожалуйста, вернитесь к «По умолчанию» при сообщении об ошибках."; - readonly zh: "并非所有消息都已翻译。请在报告错误时恢复为\"默认\""; - }; - readonly "Notify all setting files": { - readonly def: "Notify all setting files"; - readonly es: "Notificar todos los archivos de configuración"; - readonly fr: "Notifier tous les fichiers de paramètres"; - readonly he: "הודע על כל קבצי ההגדרות"; - readonly ja: "すべての設定を通知"; - readonly ko: "모든 설정 파일 알림"; - readonly ru: "Уведомлять обо всех файлах настроек"; - readonly zh: "通知所有设置文件"; - }; - readonly "Notify customized": { - readonly def: "Notify customized"; - readonly es: "Notificar personalizaciones"; - readonly fr: "Notifier les personnalisations"; - readonly he: "הודע על התאמות אישיות"; - readonly ja: "カスタマイズが行われたら通知する"; - readonly ko: "사용자 설정 알림"; - readonly ru: "Уведомлять о настройках"; - readonly zh: "通知自定义设置"; - }; - readonly "Notify when other device has newly customized.": { - readonly def: "Notify when other device has newly customized."; - readonly es: "Notificar cuando otro dispositivo personalice"; - readonly fr: "Notifier lorsqu'un autre appareil a une nouvelle personnalisation."; - readonly he: "הודע כאשר מכשיר אחר הוסיף התאמה אישית חדשה."; - readonly ja: "別の端末がカスタマイズを行なったら通知する"; - readonly ko: "다른 기기에서 새로운 사용자 설정이 있을 때 알림을 받습니다."; - readonly ru: "Уведомлять, когда другое устройство изменило настройки."; - readonly zh: "当其他设备有新的自定义设置时通知 "; - }; - readonly "Notify when the estimated remote storage size exceeds on start up": { - readonly def: "Notify when the estimated remote storage size exceeds on start up"; - readonly es: "Notificar cuando el tamaño estimado del almacenamiento remoto exceda al iniciar"; - readonly fr: "Notifier quand la taille estimée du stockage distant est dépassée au démarrage"; - readonly he: "הודע כשגודל האחסון המרוחד המשוער עולה על הסף בעת הפעלה"; - readonly ja: "起動時に予想リモートストレージサイズを超えたら通知"; - readonly ko: "시작 시 예상 원격 스토리지 크기가 초과되면 알림"; - readonly ru: "Уведомлять, когда оценочный размер удалённого хранилища превышает при запуске"; - readonly zh: "启动时当估计的远程存储大小超出时通知"; - }; - readonly "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": { - readonly def: "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time."; - readonly es: "Número de lotes a procesar. Default 40, mínimo 2. Controla documentos en memoria"; - readonly fr: "Nombre de lots à traiter à la fois. Par défaut 40. Minimum 2. Ceci, avec la taille de lot, contrôle le nombre de documents conservés en mémoire à la fois."; - readonly he: "מספר האצוות לעיבוד בכל פעם. ברירת מחדל 40, מינימום 2. יחד עם גודל האצווה קובע כמה מסמכים נשמרים בזיכרון בו-זמנית."; - readonly ja: "1度に処理するバッチの数。デフォルトは40、最小は2。この数値は、どれだけの容量の書類がメモリに保存されるかも定義します。"; - readonly ko: "한 번에 처리할 일괄 처리 수입니다. 기본값은 40입니다. 최소값은 2입니다. 이는 일괄 크기와 함께 메모리에 보관되는 문서 수를 제어합니다."; - readonly ru: "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time."; - readonly zh: "一次处理的批量数量。默认为 40。最小为 2。此设置与批量大小一起控制一次在内存中保留多少文档"; - }; - readonly "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": { - readonly def: "Number of changes to sync at a time. Defaults to 50. Minimum is 2."; - readonly es: "Número de cambios a sincronizar simultáneamente. Default 50, mínimo 2"; - readonly fr: "Nombre de modifications à synchroniser à la fois. Par défaut 50. Minimum 2."; - readonly he: "מספר השינויים לסנכרון בכל פעם. ברירת מחדל 50, מינימום 2."; - readonly ja: "一度に同期する変更の数。デフォルトは50、最小は2。"; - readonly ko: "한 번에 동기화할 변경 사항의 수입니다. 기본값은 50입니다. 최소값은 2입니다."; - readonly ru: "Количество изменений для синхронизации за раз. По умолчанию 50. Минимум 2."; - readonly zh: "一次同步的更改数量。默认为 50。最小为 2。"; - }; - readonly "Obsidian version": { - readonly def: "Obsidian version"; - readonly ja: "Obsidian バージョン"; - readonly ko: "Obsidian 버전"; - readonly ru: "Версия Obsidian"; - readonly zh: "Obsidian 版本"; - readonly "zh-tw": "Obsidian 版本"; - }; - readonly "obsidianLiveSyncSettingTab.btnApply": { - readonly def: "Apply"; - readonly es: "Aplicar"; - readonly fr: "Appliquer"; - readonly he: "החל"; - readonly ja: "適用"; - readonly ko: "적용"; - readonly ru: "Применить"; - readonly zh: "应用"; - readonly "zh-tw": "套用"; - }; - readonly "obsidianLiveSyncSettingTab.btnCheck": { - readonly def: "Check"; - readonly es: "Verificar"; - readonly fr: "Vérifier"; - readonly he: "בדוק"; - readonly ja: "確認"; - readonly ko: "확인"; - readonly ru: "Проверить"; - readonly zh: "检查"; - }; - readonly "obsidianLiveSyncSettingTab.btnCopy": { - readonly def: "Copy"; - readonly es: "Copiar"; - readonly fr: "Copier"; - readonly he: "העתק"; - readonly ja: "コピー"; - readonly ko: "복사"; - readonly ru: "Копировать"; - readonly zh: "复制"; - }; - readonly "obsidianLiveSyncSettingTab.btnDisable": { - readonly def: "Disable"; - readonly es: "Desactivar"; - readonly fr: "Désactiver"; - readonly he: "נטרל"; - readonly ja: "無効化"; - readonly ko: "비활성화"; - readonly ru: "Отключить"; - readonly zh: "禁用"; - readonly "zh-tw": "停用"; - }; - readonly "obsidianLiveSyncSettingTab.btnDiscard": { - readonly def: "Discard"; - readonly es: "Descartar"; - readonly fr: "Abandonner"; - readonly he: "ביטול שינויים"; - readonly ja: "破棄"; - readonly ko: "삭제"; - readonly ru: "Отменить"; - readonly zh: "丢弃"; - }; - readonly "obsidianLiveSyncSettingTab.btnEnable": { - readonly def: "Enable"; - readonly es: "Activar"; - readonly fr: "Activer"; - readonly he: "הפעל"; - readonly ja: "有効化"; - readonly ko: "활성화"; - readonly ru: "Включить"; - readonly zh: "启用"; - }; - readonly "obsidianLiveSyncSettingTab.btnFix": { - readonly def: "Fix"; - readonly es: "Corregir"; - readonly fr: "Corriger"; - readonly he: "תקן"; - readonly ja: "修正"; - readonly ko: "수정"; - readonly ru: "Исправить"; - readonly zh: "修复"; - }; - readonly "obsidianLiveSyncSettingTab.btnGotItAndUpdated": { - readonly def: "I got it and updated."; - readonly es: "Lo entendí y actualicé."; - readonly fr: "J'ai compris et mis à jour."; - readonly he: "הבנתי ועדכנתי."; - readonly ja: "理解しました、更新しました。"; - readonly ko: "알겠습니다. 업데이트했습니다."; - readonly ru: "Понял и обновил."; - readonly zh: "我明白了并且已更新"; - }; - readonly "obsidianLiveSyncSettingTab.btnNext": { - readonly def: "Next"; - readonly es: "Siguiente"; - readonly fr: "Suivant"; - readonly he: "הבא"; - readonly ja: "次へ"; - readonly ko: "다음"; - readonly ru: "Далее"; - readonly zh: "下一步"; - readonly "zh-tw": "下一步"; - }; - readonly "obsidianLiveSyncSettingTab.btnStart": { - readonly def: "Start"; - readonly es: "Iniciar"; - readonly fr: "Démarrer"; - readonly he: "התחל"; - readonly ja: "開始"; - readonly ko: "시작"; - readonly ru: "Старт"; - readonly zh: "开始"; - }; - readonly "obsidianLiveSyncSettingTab.btnTest": { - readonly def: "Test"; - readonly es: "Probar"; - readonly fr: "Tester"; - readonly he: "בדוק"; - readonly ja: "テスト"; - readonly ko: "테스트"; - readonly ru: "Тест"; - readonly zh: "测试"; - }; - readonly "obsidianLiveSyncSettingTab.btnUse": { - readonly def: "Use"; - readonly es: "Usar"; - readonly fr: "Utiliser"; - readonly he: "השתמש"; - readonly ja: "使用"; - readonly ko: "사용"; - readonly ru: "Использовать"; - readonly zh: "使用"; - }; - readonly "obsidianLiveSyncSettingTab.buttonFetch": { - readonly def: "Fetch"; - readonly es: "Obtener"; - readonly fr: "Récupérer"; - readonly he: "משוך"; - readonly ja: "フェッチ"; - readonly ko: "가져오기"; - readonly ru: "Загрузить"; - readonly zh: "获取"; - }; - readonly "obsidianLiveSyncSettingTab.buttonNext": { - readonly def: "Next"; - readonly es: "Siguiente"; - readonly fr: "Suivant"; - readonly he: "הבא"; - readonly ja: "次へ"; - readonly ko: "다음"; - readonly ru: "Далее"; - readonly zh: "下一步"; - readonly "zh-tw": "下一步"; - }; - readonly "obsidianLiveSyncSettingTab.defaultLanguage": { - readonly def: "Default"; - readonly es: "Predeterminado"; - readonly fr: "Par défaut"; - readonly he: "ברירת מחדל"; - readonly ja: "デフォルト"; - readonly ko: "기본값"; - readonly ru: "По умолчанию"; - readonly zh: "默认语言"; - readonly "zh-tw": "預設語言"; - }; - readonly "obsidianLiveSyncSettingTab.descConnectSetupURI": { - readonly def: "This is the recommended method to set up Self-hosted LiveSync with a Setup URI."; - readonly es: "Este es el método recomendado para configurar Self-hosted LiveSync con una URI de configuración."; - readonly fr: "Méthode recommandée pour configurer Self-hosted LiveSync avec une URI de configuration."; - readonly he: "זוהי השיטה המומלצת להגדרת Self-hosted LiveSync עם Setup URI."; - readonly ja: "セットアップURIを使用してSelf-hosted LiveSyncをセットアップする推奨方法です。"; - readonly ko: "이것은 Setup URI로 Self-hosted LiveSync를 설정하는 권장 방법입니다."; - readonly ru: "Это рекомендуемый способ настройки Self-hosted LiveSync с помощью Setup URI."; - readonly zh: "这是使用设置 URI 设置 Self-hosted LiveSync 的推荐方法"; - }; - readonly "obsidianLiveSyncSettingTab.descCopySetupURI": { - readonly def: "Perfect for setting up a new device!"; - readonly es: "¡Perfecto para configurar un nuevo dispositivo!"; - readonly fr: "Parfait pour configurer un nouvel appareil !"; - readonly he: "מושלם להגדרת מכשיר חדש!"; - readonly ja: "新しいデバイスのセットアップにおすすめ!"; - readonly ko: "새 기기 설정에 완벽합니다!"; - readonly ru: "Идеально подходит для настройки нового устройства!"; - readonly zh: "非常适合设置新设备!"; - }; - readonly "obsidianLiveSyncSettingTab.descEnableLiveSync": { - readonly def: "Only enable this after configuring either of the above two options or completing all configuration manually."; - readonly es: "Solo habilita esto después de configurar cualquiera de las dos opciones anteriores o completar toda la configuración manualmente."; - readonly fr: "N'activez ceci qu'après avoir configuré l'une des deux options ci-dessus ou terminé toute la configuration manuellement."; - readonly he: "הפעל רק לאחר הגדרת אחת משתי האפשרויות לעיל, או לאחר השלמת כל ההגדרות ידנית."; - readonly ja: "上記の2つのオプションのいずれかを設定するか、すべての設定を手動で完了した後にのみ有効にしてください。"; - readonly ko: "위의 두 옵션 중 하나를 구성하거나 모든 구성을 수동으로 완료한 후에만 활성화하세요."; - readonly ru: "Включайте это только после настройки одного из двух вариантов выше или после полного ручного завершения всей конфигурации."; - readonly zh: "仅在配置了上述两个选项之一或手动完成所有配置后启用此选项"; - }; - readonly "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": { - readonly def: "Fetch necessary settings from already configured remote server."; - readonly es: "Obtener las configuraciones necesarias del servidor remoto ya configurado."; - readonly fr: "Récupérer les paramètres nécessaires depuis un serveur distant déjà configuré."; - readonly he: "משוך הגדרות נדרשות מהשרת המרוחד שהוגדר כבר."; - readonly ja: "既に設定済みのリモートサーバーから必要な設定を取得します。"; - readonly ko: "이미 구성된 원격 서버에서 필요한 설정을 가져옵니다."; - readonly ru: "Получить необходимые настройки с уже настроенного удалённого сервера."; - readonly zh: "从已配置的远程服务器获取必要的设置"; - }; - readonly "obsidianLiveSyncSettingTab.descManualSetup": { - readonly def: "Not recommended, but useful if you don't have a Setup URI"; - readonly es: "No recomendado, pero útil si no tienes una URI de configuración"; - readonly fr: "Non recommandé, mais utile si vous n'avez pas d'URI de configuration"; - readonly he: "לא מומלץ, אך שימושי אם אין לך Setup URI"; - readonly ja: "推奨しませんが、セットアップURIがない場合に便利です"; - readonly ko: "권장하지 않지만 Setup URI가 없는 경우에 유용합니다"; - readonly ru: "Не рекомендуется, но полезно, если у вас нет Setup URI."; - readonly zh: "不推荐,但如果您没有设置 URI 则很有用"; - }; - readonly "obsidianLiveSyncSettingTab.descTestDatabaseConnection": { - readonly def: "Open database connection. If the remote database is not found and you have permission to create a database, the database will be created."; - readonly es: "Abrir conexión a la base de datos. Si no se encuentra la base de datos remota y tienes permiso para crear una base de datos, se creará la base de datos."; - readonly fr: "Ouvrir la connexion à la base de données. Si la base distante est introuvable et que vous avez l'autorisation de créer une base, elle sera créée."; - readonly he: "פתח חיבור למסד נתונים. אם מסד הנתונים המרוחד לא נמצא ויש לך הרשאה ליצור אחד, הוא ייצור."; - readonly ja: "データベース接続を開きます。リモートデータベースが見つからず、データベースを作成する権限がある場合は、データベースが作成されます。"; - readonly ko: "데이터베이스 연결을 엽니다. 원격 데이터베이스를 찾을 수 없고 데이터베이스 생성 권한이 있는 경우, 데이터베이스가 생성됩니다."; - readonly ru: "Открыть подключение к базе данных. Если удалённая база данных не найдена и у вас есть право на её создание, база будет создана."; - readonly zh: "打开数据库连接。如果未找到远程数据库并且您有创建数据库的权限,则将创建数据库"; - }; - readonly "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": { - readonly def: "Checks and fixes any potential issues with the database config."; - readonly es: "Verifica y soluciona cualquier problema potencial con la configuración de la base de datos."; - readonly fr: "Vérifie et corrige les problèmes potentiels de la configuration de la base."; - readonly he: "בודק ומתקן בעיות אפשריות בתצורת מסד הנתונים."; - readonly ja: "データベース設定の潜在的な問題を確認し、修正します。"; - readonly ko: "데이터베이스 구성의 잠재적 문제를 확인하고 수정합니다."; - readonly ru: "Проверяет и исправляет любые потенциальные проблемы в конфигурации базы данных."; - readonly zh: "检查并修复数据库配置中的任何潜在问题"; - }; - readonly "obsidianLiveSyncSettingTab.errAccessForbidden": { - readonly def: "❗ Access forbidden."; - readonly es: "Acceso prohibido."; - readonly fr: "❗ Accès interdit."; - readonly he: "❗ גישה נדחתה."; - readonly ja: "❗ アクセスが禁止されています。"; - readonly ko: "❗ 액세스가 금지되었습니다."; - readonly ru: "❗ Доступ запрещён."; - readonly zh: "❗ 访问被禁止"; - }; - readonly "obsidianLiveSyncSettingTab.errCannotContinueTest": { - readonly def: "We could not continue the test."; - readonly es: "No se pudo continuar con la prueba."; - readonly fr: "Impossible de poursuivre le test."; - readonly he: "לא ניתן להמשיך בבדיקה."; - readonly ja: "テストを続行できませんでした。"; - readonly ko: "테스트를 계속할 수 없습니다."; - readonly ru: "Мы не можем продолжить тест."; - readonly zh: "我们无法继续测试。"; - }; - readonly "obsidianLiveSyncSettingTab.errCorsCredentials": { - readonly def: "❗ cors.credentials is wrong"; - readonly es: "❗ cors.credentials es incorrecto"; - readonly fr: "❗ cors.credentials est incorrect"; - readonly he: "❗ cors.credentials שגוי"; - readonly ja: "❗ cors.credentialsが不正です"; - readonly ko: "❗ cors.credentials가 잘못되었습니다"; - readonly ru: "❗ cors.credentials неверно"; - readonly zh: "❗ cors.credentials 设置错误"; - }; - readonly "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": { - readonly def: "❗ CORS is not allowing credentials"; - readonly es: "CORS no permite credenciales"; - readonly fr: "❗ CORS n'autorise pas les identifiants"; - readonly he: "❗ CORS אינו מאפשר פרטי גישה"; - readonly ja: "❗ CORSが認証情報を許可していません"; - readonly ko: "❗ CORS에서 자격 증명을 허용하지 않습니다"; - readonly ru: "❗ CORS не разрешает учётные данные"; - readonly zh: "❗ CORS 不允许凭据"; - }; - readonly "obsidianLiveSyncSettingTab.errCorsOrigins": { - readonly def: "❗ cors.origins is wrong"; - readonly es: "❗ cors.origins es incorrecto"; - readonly fr: "❗ cors.origins est incorrect"; - readonly he: "❗ cors.origins שגוי"; - readonly ja: "❗ cors.originsが不正です"; - readonly ko: "❗ cors.origins가 잘못되었습니다"; - readonly ru: "❗ cors.origins неверно"; - readonly zh: "❗ cors.origins 设置错误"; - }; - readonly "obsidianLiveSyncSettingTab.errEnableCors": { - readonly def: "❗ httpd.enable_cors is wrong"; - readonly es: "❗ httpd.enable_cors es incorrecto"; - readonly fr: "❗ httpd.enable_cors est incorrect"; - readonly he: "❗ httpd.enable_cors שגוי"; - readonly ja: "❗ httpd.enable_corsが不正です"; - readonly ko: "❗ httpd.enable_cors가 잘못되었습니다"; - readonly ru: "❗ httpd.enable_cors неверно"; - readonly zh: "❗ httpd.enable_cors 设置错误"; - }; - readonly "obsidianLiveSyncSettingTab.errEnableCorsChttpd": { - readonly def: "❗ chttpd.enable_cors is wrong"; - readonly fr: "❗ chttpd.enable_cors est incorrect"; - readonly he: "❗ chttpd.enable_cors שגוי"; - readonly ja: "❗ chttpd.enable_corsが不正です"; - readonly ru: "❗ chttpd.enable_cors неверно"; - readonly zh: "❗ chttpd.enable_cors 设置错误"; - }; - readonly "obsidianLiveSyncSettingTab.errMaxDocumentSize": { - readonly def: "❗ couchdb.max_document_size is low)"; - readonly es: "❗ couchdb.max_document_size es bajo)"; - readonly fr: "❗ couchdb.max_document_size est trop bas)"; - readonly he: "❗ couchdb.max_document_size נמוך)"; - readonly ja: "❗ couchdb.max_document_sizeが低すぎます"; - readonly ko: "❗ couchdb.max_document_size가 낮습니다)"; - readonly ru: "❗ couchdb.max_document_size низкое"; - readonly zh: "❗ couchdb.max_document_size 设置过低)"; - }; - readonly "obsidianLiveSyncSettingTab.errMaxRequestSize": { - readonly def: "❗ chttpd.max_http_request_size is low)"; - readonly es: "❗ chttpd.max_http_request_size es bajo)"; - readonly fr: "❗ chttpd.max_http_request_size est trop bas)"; - readonly he: "❗ chttpd.max_http_request_size נמוך)"; - readonly ja: "❗ chttpd.max_http_request_sizeが低すぎます"; - readonly ko: "❗ chttpd.max_http_request_size가 낮습니다)"; - readonly ru: "❗ chttpd.max_http_request_size низкое"; - readonly zh: "❗ chttpd.max_http_request_size 设置过低)"; - }; - readonly "obsidianLiveSyncSettingTab.errMissingWwwAuth": { - readonly def: "❗ httpd.WWW-Authenticate is missing"; - readonly es: "❗ httpd.WWW-Authenticate falta"; - readonly fr: "❗ httpd.WWW-Authenticate est manquant"; - readonly he: "❗ httpd.WWW-Authenticate חסר"; - readonly ja: "❗ httpd.WWW-Authenticateが不足しています"; - readonly ko: "❗ httpd.WWW-Authenticate가 누락되었습니다"; - readonly ru: "❗ httpd.WWW-Authenticate отсутствует"; - readonly zh: "❗ 缺少 httpd.WWW-Authenticate 设置"; - }; - readonly "obsidianLiveSyncSettingTab.errRequireValidUser": { - readonly def: "❗ chttpd.require_valid_user is wrong."; - readonly es: "❗ chttpd.require_valid_user es incorrecto."; - readonly fr: "❗ chttpd.require_valid_user est incorrect."; - readonly he: "❗ chttpd.require_valid_user שגוי."; - readonly ja: "❗ chttpd.require_valid_userが不正です。"; - readonly ko: "❗ chttpd.require_valid_user가 잘못되었습니다."; - readonly ru: "❗ chttpd.require_valid_user неверно."; - readonly zh: "❗ chttpd.require_valid_user 设置错误"; - }; - readonly "obsidianLiveSyncSettingTab.errRequireValidUserAuth": { - readonly def: "❗ chttpd_auth.require_valid_user is wrong."; - readonly es: "❗ chttpd_auth.require_valid_user es incorrecto."; - readonly fr: "❗ chttpd_auth.require_valid_user est incorrect."; - readonly he: "❗ chttpd_auth.require_valid_user שגוי."; - readonly ja: "❗ chttpd_auth.require_valid_userが不正です。"; - readonly ko: "❗ chttpd_auth.require_valid_user가 잘못되었습니다."; - readonly ru: "❗ chttpd_auth.require_valid_user неверно."; - readonly zh: "❗ chttpd_auth.require_valid_user 设置错误"; - }; - readonly "obsidianLiveSyncSettingTab.labelDisabled": { - readonly def: "⏹️ : Disabled"; - readonly es: "⏹️ : Desactivado"; - readonly fr: "⏹️ : Désactivé"; - readonly he: "⏹️ : מנוטרל"; - readonly ja: "⏹️ : 無効"; - readonly ko: "⏹️ : 비활성화됨"; - readonly ru: "⏹️ : Отключено"; - readonly zh: "⏹️:已禁用"; - readonly "zh-tw": "⏹️ : 已停用"; - }; - readonly "obsidianLiveSyncSettingTab.labelEnabled": { - readonly def: "🔁 : Enabled"; - readonly es: "🔁 : Activado"; - readonly fr: "🔁 : Activé"; - readonly he: "🔁 : מופעל"; - readonly ja: "🔁 : 有効"; - readonly ko: "🔁 : 활성화됨"; - readonly ru: "🔁 : Включено"; - readonly zh: "🔁:已启用"; - readonly "zh-tw": "🔁 : 已啟用"; - }; - readonly "obsidianLiveSyncSettingTab.levelAdvanced": { - readonly def: " (Advanced)"; - readonly es: " (avanzado)"; - readonly fr: " (Avancé)"; - readonly he: " (מתקדם)"; - readonly ja: " (上級)"; - readonly ko: " (고급)"; - readonly ru: " (Расширенные)"; - readonly zh: "(进阶)"; - }; - readonly "obsidianLiveSyncSettingTab.levelEdgeCase": { - readonly def: " (Edge Case)"; - readonly es: " (excepción)"; - readonly fr: " (Cas particulier)"; - readonly he: " (מקרה קצה)"; - readonly ja: " (エッジケース)"; - readonly ko: " (특수 사례)"; - readonly ru: " (Граничные случаи)"; - readonly zh: "(边缘情况)"; - }; - readonly "obsidianLiveSyncSettingTab.levelPowerUser": { - readonly def: " (Power User)"; - readonly es: " (experto)"; - readonly fr: " (Utilisateur avancé)"; - readonly he: " (משתמש מתקדם)"; - readonly ja: " (エキスパート)"; - readonly ko: " (파워 유저)"; - readonly ru: " (Опытный пользователь)"; - readonly zh: "(高级用户)"; - }; - readonly "obsidianLiveSyncSettingTab.linkOpenInBrowser": { - readonly def: "Open in browser"; - readonly es: "Abrir en el navegador"; - readonly fr: "Ouvrir dans le navigateur"; - readonly he: "פתח בדפדפן"; - readonly ja: "ブラウザで開く"; - readonly ko: "브라우저에서 열기"; - readonly ru: "Открыть в браузере"; - readonly zh: "在浏览器中打开"; - }; - readonly "obsidianLiveSyncSettingTab.linkPageTop": { - readonly def: "Page Top"; - readonly es: "Ir arriba"; - readonly fr: "Haut de la page"; - readonly he: "ראש העמוד"; - readonly ja: "ページトップ"; - readonly ko: "페이지 상단"; - readonly ru: "В начало страницы"; - readonly zh: "页面顶部"; - }; - readonly "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": { - readonly def: "Tips and Troubleshooting"; - readonly es: "Consejos y solución de problemas"; - readonly fr: "Conseils et dépannage"; - readonly he: "טיפים ופתרון בעיות"; - readonly ja: "ヒントとトラブルシューティング"; - readonly ko: "팁 및 문제 해결"; - readonly ru: "Советы и устранение неполадок"; - readonly zh: "提示和故障排除"; - }; - readonly "obsidianLiveSyncSettingTab.linkTroubleshooting": { - readonly def: "/docs/troubleshooting.md"; - readonly es: "/docs/es/troubleshooting.md"; - readonly fr: "/docs/troubleshooting.md"; - readonly he: "/docs/troubleshooting.md"; - readonly ja: "/docs/troubleshooting.md"; - readonly ko: "/docs/troubleshooting.md"; - readonly ru: "/docs/troubleshooting.md"; - readonly zh: "/docs/troubleshooting.md"; - }; - readonly "obsidianLiveSyncSettingTab.logCannotUseCloudant": { - readonly def: "This feature cannot be used with IBM Cloudant."; - readonly es: "Esta función no se puede utilizar con IBM Cloudant."; - readonly fr: "Cette fonctionnalité ne peut pas être utilisée avec IBM Cloudant."; - readonly he: "לא ניתן להשתמש בתכונה זו עם IBM Cloudant."; - readonly ja: "この機能はIBM Cloudantでは使用できません。"; - readonly ko: "이 기능은 IBM Cloudant와 함께 사용할 수 없습니다."; - readonly ru: "Эта функция недоступна для IBM Cloudant."; - readonly zh: "此功能不能与 IBM Cloudant 一起使用 "; - }; - readonly "obsidianLiveSyncSettingTab.logCheckingConfigDone": { - readonly def: "Checking configuration done"; - readonly es: "Verificación de configuración completada"; - readonly fr: "Vérification de la configuration terminée"; - readonly he: "בדיקת התצורה הושלמה"; - readonly ja: "設定の確認が完了しました"; - readonly ko: "구성 확인 완료"; - readonly ru: "Проверка конфигурации завершена"; - readonly zh: "配置检查完成"; - }; - readonly "obsidianLiveSyncSettingTab.logCheckingConfigFailed": { - readonly def: "Checking configuration failed"; - readonly es: "La verificación de configuración falló"; - readonly fr: "Échec de la vérification de la configuration"; - readonly he: "בדיקת התצורה נכשלה"; - readonly ja: "設定の確認に失敗しました"; - readonly ko: "구성 확인 실패"; - readonly ru: "Проверка конфигурации не удалась"; - readonly zh: "配置检查失败"; - }; - readonly "obsidianLiveSyncSettingTab.logCheckingDbConfig": { - readonly def: "Checking database configuration"; - readonly es: "Verificando la configuración de la base de datos"; - readonly fr: "Vérification de la configuration de la base"; - readonly he: "בודק תצורת מסד נתונים"; - readonly ja: "データベース設定を確認中"; - readonly ko: "데이터베이스 구성 확인 중"; - readonly ru: "Проверка конфигурации базы данных"; - readonly zh: "正在检查数据库配置"; - }; - readonly "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": { - readonly def: "ERROR: Failed to check passphrase with the remote server:\n${db}."; - readonly es: "ERROR: Error al comprobar la frase de contraseña con el servidor remoto: \n${db}."; - readonly fr: "ERREUR : Échec de la vérification de la phrase secrète avec le serveur distant :\n${db}."; - readonly he: "שגיאה: בדיקת ביטוי הסיסמה עם השרת המרוחד נכשלה:\n${db}."; - readonly ja: "エラー: リモートサーバーとのパスフレーズ確認に失敗しました:\n${db}。"; - readonly ko: "오류: 원격 서버와 패스프레이즈 확인에 실패했습니다: \n${db}."; - readonly ru: "ОШИБКА: Не удалось проверить пароль с удалённым сервером: db."; - readonly zh: "错误:无法使用远程服务器检查密码:\n${db} "; - }; - readonly "obsidianLiveSyncSettingTab.logConfiguredDisabled": { - readonly def: "Configured synchronization mode: DISABLED"; - readonly es: "Modo de sincronización configurado: DESACTIVADO"; - readonly fr: "Mode de synchronisation configuré : DÉSACTIVÉ"; - readonly he: "מצב סנכרון שהוגדר: מנוטרל"; - readonly ja: "設定された同期モード: 無効"; - readonly ko: "구성된 동기화 모드: 비활성화됨"; - readonly ru: "Настроенный режим синхронизации: ОТКЛЮЧЕН"; - readonly zh: "已配置的同步模式:已禁用"; - readonly "zh-tw": "已設定的同步模式:已停用"; - }; - readonly "obsidianLiveSyncSettingTab.logConfiguredLiveSync": { - readonly def: "Configured synchronization mode: LiveSync"; - readonly es: "Modo de sincronización configurado: Sincronización en Vivo"; - readonly fr: "Mode de synchronisation configuré : LiveSync"; - readonly he: "מצב סנכרון שהוגדר: LiveSync"; - readonly ja: "設定された同期モード: LiveSync"; - readonly ko: "구성된 동기화 모드: LiveSync"; - readonly ru: "Настроенный режим синхронизации: LiveSync"; - readonly zh: "已配置的同步模式:LiveSync"; - readonly "zh-tw": "已設定的同步模式:LiveSync"; - }; - readonly "obsidianLiveSyncSettingTab.logConfiguredPeriodic": { - readonly def: "Configured synchronization mode: Periodic"; - readonly es: "Modo de sincronización configurado: Periódico"; - readonly fr: "Mode de synchronisation configuré : Périodique"; - readonly he: "מצב סנכרון שהוגדר: תקופתי"; - readonly ja: "設定された同期モード: 定期"; - readonly ko: "구성된 동기화 모드: 주기적"; - readonly ru: "Настроенный режим синхронизации: Периодический"; - readonly zh: "已配置的同步模式:定期同步"; - readonly "zh-tw": "已設定的同步模式:定期同步"; - }; - readonly "obsidianLiveSyncSettingTab.logCouchDbConfigFail": { - readonly def: "CouchDB Configuration: ${title} failed"; - readonly es: "Configuración de CouchDB: ${title} falló"; - readonly fr: "Configuration CouchDB : échec de ${title}"; - readonly he: "תצורת CouchDB: ${title} נכשלה"; - readonly ja: "CouchDB設定: ${title} 失敗"; - readonly ko: "CouchDB 구성: ${title} 실패"; - readonly ru: "Конфигурация CouchDB: title не удалась"; - readonly zh: "CouchDB 配置:${title} 失败"; - }; - readonly "obsidianLiveSyncSettingTab.logCouchDbConfigSet": { - readonly def: "CouchDB Configuration: ${title} -> Set ${key} to ${value}"; - readonly es: "Configuración de CouchDB: ${title} -> Establecer ${key} en ${value}"; - readonly fr: "Configuration CouchDB : ${title} -> ${key} défini à ${value}"; - readonly he: "תצורת CouchDB: ${title} -> הגדר ${key} ל-${value}"; - readonly ja: "CouchDB設定: ${title} -> ${key}を${value}に設定"; - readonly ko: "CouchDB 구성: ${title} -> ${key}를 ${value}로 설정"; - readonly ru: "Конфигурация CouchDB: title -> Установить key в value"; - readonly zh: "CouchDB 配置:${title} -> 设置 ${key} 为 ${value}"; - }; - readonly "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": { - readonly def: "CouchDB Configuration: ${title} successfully updated"; - readonly es: "Configuración de CouchDB: ${title} actualizado correctamente"; - readonly fr: "Configuration CouchDB : ${title} mise à jour avec succès"; - readonly he: "תצורת CouchDB: ${title} עודכנה בהצלחה"; - readonly ja: "CouchDB設定: ${title} 正常に更新されました"; - readonly ko: "CouchDB 구성: ${title} 성공적으로 업데이트됨"; - readonly ru: "Конфигурация CouchDB: title успешно обновлена"; - readonly zh: "CouchDB 配置:${title} 成功更新"; - }; - readonly "obsidianLiveSyncSettingTab.logDatabaseConnected": { - readonly def: "Database connected"; - readonly es: "Base de datos conectada"; - readonly fr: "Base de données connectée"; - readonly he: "מסד הנתונים מחובר"; - readonly ja: "データベースに接続しました"; - readonly ko: "데이터베이스 연결됨"; - readonly ru: "База данных подключена"; - readonly zh: "数据库已连接"; - }; - readonly "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": { - readonly def: "You cannot enable encryption without a passphrase"; - readonly es: "No puedes habilitar el cifrado sin una frase de contraseña"; - readonly fr: "Impossible d'activer le chiffrement sans phrase secrète"; - readonly he: "לא ניתן להפעיל הצפנה ללא ביטוי סיסמה"; - readonly ja: "パスフレーズなしでは暗号化を有効にできません"; - readonly ko: "패스프레이즈 없이는 암호화를 활성화할 수 없습니다"; - readonly ru: "Вы не можете включить шифрование без парольной фразы"; - readonly zh: "没有密码无法启用加密"; - }; - readonly "obsidianLiveSyncSettingTab.logEncryptionNoSupport": { - readonly def: "Your device does not support encryption."; - readonly es: "Tu dispositivo no admite el cifrado."; - readonly fr: "Votre appareil ne prend pas en charge le chiffrement."; - readonly he: "המכשיר שלך אינו תומך בהצפנה."; - readonly ja: "お使いのデバイスは暗号化をサポートしていません。"; - readonly ko: "기기가 암호화를 지원하지 않습니다."; - readonly ru: "Ваше устройство не поддерживает шифрование."; - readonly zh: "您的设备不支持加密 "; - }; - readonly "obsidianLiveSyncSettingTab.logErrorOccurred": { - readonly def: "An error occurred!!"; - readonly es: "¡Ocurrió un error!"; - readonly fr: "Une erreur s'est produite !!"; - readonly he: "אירעה שגיאה!!"; - readonly ja: "エラーが発生しました!!"; - readonly ko: "오류가 발생했습니다!"; - readonly ru: "Произошла ошибка!!"; - readonly zh: "发生错误!!"; - }; - readonly "obsidianLiveSyncSettingTab.logEstimatedSize": { - readonly def: "Estimated size: ${size}"; - readonly es: "Tamaño estimado: ${size}"; - readonly fr: "Taille estimée : ${size}"; - readonly he: "גודל משוער: ${size}"; - readonly ja: "推定サイズ: ${size}"; - readonly ko: "예상 크기: ${size}"; - readonly ru: "Примерный размер: size"; - readonly zh: "估计大小:${size}"; - }; - readonly "obsidianLiveSyncSettingTab.logPassphraseInvalid": { - readonly def: "Passphrase is not valid, please fix it."; - readonly es: "La frase de contraseña no es válida, por favor corrígela."; - readonly fr: "La phrase secrète est invalide, veuillez la corriger."; - readonly he: "ביטוי הסיסמה אינו תקין, אנא תקן אותו."; - readonly ja: "パスフレーズが無効です、修正してください。"; - readonly ko: "패스프레이즈가 유효하지 않습니다. 수정해 주세요."; - readonly ru: "Парольная фраза недействительна, пожалуйста, исправьте."; - readonly zh: "密码无效,请修正"; - }; - readonly "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": { - readonly def: "ERROR: Passphrase is not compatible with the remote server! Please check it again!"; - readonly es: "ERROR: ¡La frase de contraseña no es compatible con el servidor remoto! ¡Por favor, revísala de nuevo!"; - readonly fr: "ERREUR : la phrase secrète n'est pas compatible avec le serveur distant ! Veuillez vérifier à nouveau !"; - readonly he: "שגיאה: ביטוי הסיסמה אינו תואם לשרת המרוחד! אנא בדוק שוב!"; - readonly ja: "エラー: パスフレーズがリモートサーバーと適合しません!再度確認してください!"; - readonly ko: "오류: 패스프레이즈가 원격 서버와 호환되지 않습니다! 다시 확인해 주세요!"; - readonly ru: "ОШИБКА: Парольная фраза несовместима с удалённым сервером!"; - readonly zh: "错误:密码与远程服务器不兼容!请再次检查!"; - }; - readonly "obsidianLiveSyncSettingTab.logRebuildNote": { - readonly def: "Syncing has been disabled, fetch and re-enabled if desired."; - readonly es: "La sincronización ha sido desactivada, obtén y vuelve a activar si lo deseas."; - readonly fr: "La synchronisation a été désactivée, récupérez et réactivez si souhaité."; - readonly he: "הסנכרון הושבת, משוך והפעל מחדש אם רצוי."; - readonly ja: "同期が無効になりました。必要に応じてフェッチして再有効化してください。"; - readonly ko: "동기화가 비활성화되었습니다. 원하는 경우 가져오기 후 다시 활성화하세요."; - readonly ru: "Синхронизация отключена, загрузите и включите снова при желании."; - readonly zh: "同步已禁用,如果需要,请获取并重新启用"; - }; - readonly "obsidianLiveSyncSettingTab.logSelectAnyPreset": { - readonly def: "Select any preset."; - readonly es: "Selecciona cualquier preestablecido."; - readonly fr: "Sélectionnez un préréglage."; - readonly he: "בחר קביעה מראש כלשהי."; - readonly ja: "プリセットを選択してください。"; - readonly ko: "프리셋을 선택하세요."; - readonly ru: "Выберите любой пресет."; - readonly zh: "请选择任一预设。"; - readonly "zh-tw": "請選擇任一預設項目。"; - }; - readonly "obsidianLiveSyncSettingTab.logServerConfigurationCheck": { - readonly def: "obsidianLiveSyncSettingTab.logServerConfigurationCheck"; - }; - readonly "obsidianLiveSyncSettingTab.msgAreYouSureProceed": { - readonly def: "Are you sure to proceed?"; - readonly es: "¿Estás seguro de proceder?"; - readonly fr: "Êtes-vous sûr de vouloir continuer ?"; - readonly he: "האם אתה בטוח שברצונך להמשיך?"; - readonly ja: "本当に続行しますか?"; - readonly ko: "정말로 진행하시겠습니까?"; - readonly ru: "Вы уверены, что хотите продолжить?"; - readonly zh: "您确定要继续吗?"; - }; - readonly "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": { - readonly def: "Changes need to be applied!"; - readonly es: "¡Los cambios deben aplicarse!"; - readonly fr: "Des modifications doivent être appliquées !"; - readonly he: "יש להחיל שינויים!"; - readonly ja: "変更を適用する必要があります!"; - readonly ko: "변경사항을 적용해야 합니다!"; - readonly ru: "Изменения нужно применить!"; - readonly zh: "需要应用更改!"; - }; - readonly "obsidianLiveSyncSettingTab.msgConfigCheck": { - readonly def: "--Config check--"; - readonly es: "--Verificación de configuración--"; - readonly fr: "--Vérification de la configuration--"; - readonly he: "--בדיקת תצורה--"; - readonly ja: "--設定確認--"; - readonly ko: "--구성 확인--"; - readonly ru: "--Проверка конфигурации--"; - readonly zh: "--配置检查--"; - }; - readonly "obsidianLiveSyncSettingTab.msgConfigCheckFailed": { - readonly def: "The configuration check has failed. Do you want to continue anyway?"; - readonly es: "La verificación de configuración ha fallado. ¿Quieres continuar de todos modos?"; - readonly fr: "La vérification de la configuration a échoué. Voulez-vous continuer malgré tout ?"; - readonly he: "בדיקת התצורה נכשלה. האם ברצונך להמשיך בכל זאת?"; - readonly ja: "設定確認に失敗しました。それでも続行しますか?"; - readonly ko: "구성 확인에 실패했습니다. 그래도 계속하시겠습니까?"; - readonly ru: "Проверка конфигурации не удалась. Вы всё равно хотите продолжить?"; - readonly zh: "配置检查失败。仍要继续吗?"; - readonly "zh-tw": "設定檢查失敗。仍要繼續嗎?"; - }; - readonly "obsidianLiveSyncSettingTab.msgConnectionCheck": { - readonly def: "--Connection check--"; - readonly es: "--Verificación de conexión--"; - readonly fr: "--Vérification de la connexion--"; - readonly he: "--בדיקת חיבור--"; - readonly ja: "--接続確認--"; - readonly ko: "--연결 확인--"; - readonly ru: "--Проверка подключения--"; - readonly zh: "--连接检查--"; - }; - readonly "obsidianLiveSyncSettingTab.msgConnectionProxyNote": { - readonly def: "If you're having trouble with the Connection-check (even after checking config), please check your reverse proxy configuration."; - readonly es: "Si tienes problemas con la verificación de conexión (incluso después de verificar la configuración), por favor verifica la configuración de tu proxy reverso."; - readonly fr: "Si vous rencontrez des problèmes de vérification de connexion (même après avoir vérifié la configuration), veuillez vérifier votre configuration de reverse proxy."; - readonly he: "אם אתה נתקל בבעיות עם בדיקת החיבור (גם לאחר בדיקת התצורה), אנא בדוק את הגדרות ה-reverse proxy שלך."; - readonly ja: "設定確認後も接続確認に問題がある場合は、リバースプロキシの設定を確認してください。"; - readonly ko: "구성 확인 후에도 연결 확인에 문제가 있는 경우, 리버스 프록시 구성을 확인해 주세요."; - readonly ru: "Если у вас проблемы с проверкой подключения, проверьте конфигурацию обратного прокси."; - readonly zh: "如果您在连接检查时遇到问题(即使检查了配置后),请检查您的反向代理配置"; - }; - readonly "obsidianLiveSyncSettingTab.msgCurrentOrigin": { - readonly def: "Current origin: ${origin}"; - readonly es: "Origen actual: {origin}"; - readonly fr: "Origine actuelle : ${origin}"; - readonly he: "מקור נוכחי: ${origin}"; - readonly ja: "現在のオリジン: ${origin}"; - readonly ko: "현재 원점: {origin}"; - readonly ru: "Текущий origin: origin"; - readonly zh: "当前源: {origin}"; - }; - readonly "obsidianLiveSyncSettingTab.msgDiscardConfirmation": { - readonly def: "Do you really want to discard existing settings and databases?"; - readonly es: "¿Realmente deseas descartar las configuraciones y bases de datos existentes?"; - readonly fr: "Voulez-vous vraiment abandonner les paramètres et bases existants ?"; - readonly he: "האם אתה בטוח שברצונך לבטל הגדרות ומסדי נתונים קיימים?"; - readonly ja: "本当に既存の設定とデータベースを破棄しますか?"; - readonly ko: "정말로 기존 설정과 데이터베이스를 삭제하시겠습니까?"; - readonly ru: "Вы действительно хотите отменить существующие настройки и базы данных?"; - readonly zh: "您真的要丢弃现有的设置和数据库吗?"; - }; - readonly "obsidianLiveSyncSettingTab.msgDone": { - readonly def: "--Done--"; - readonly es: "--Hecho--"; - readonly fr: "--Terminé--"; - readonly he: "--הסתיים--"; - readonly ja: "--完了--"; - readonly ko: "--완료--"; - readonly ru: "--Готово--"; - readonly zh: "--完成--"; - }; - readonly "obsidianLiveSyncSettingTab.msgEnableCors": { - readonly def: "Set httpd.enable_cors"; - readonly es: "Configurar httpd.enable_cors"; - readonly fr: "Définir httpd.enable_cors"; - readonly he: "הגדר httpd.enable_cors"; - readonly ja: "httpd.enable_corsを設定"; - readonly ko: "httpd.enable_cors 설정"; - readonly ru: "Установить httpd.enable_cors"; - readonly zh: "设置 httpd.enable_cors"; - }; - readonly "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": { - readonly def: "Set chttpd.enable_cors"; - readonly fr: "Définir chttpd.enable_cors"; - readonly he: "הגדר chttpd.enable_cors"; - readonly ja: "chttpd.enable_corsを設定"; - readonly ru: "Установить chttpd.enable_cors"; - readonly zh: "设置 chttpd.enable_cors"; - }; - readonly "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": { - readonly def: "We recommend enabling End-To-End Encryption, and Path Obfuscation. Are you sure you want to continue without encryption?"; - readonly es: "Recomendamos habilitar el cifrado de extremo a extremo y la obfuscación de ruta. ¿Estás seguro de querer continuar sin cifrado?"; - readonly fr: "Nous recommandons d'activer le chiffrement de bout en bout et l'obfuscation des chemins. Êtes-vous sûr de vouloir continuer sans chiffrement ?"; - readonly he: "אנו ממליצים להפעיל הצפנה מקצה לקצה ואת ערפול הנתיב. האם אתה בטוח שברצונך להמשיך ללא הצפנה?"; - readonly ja: "エンドツーエンド暗号化とパス難読化を有効にすることをお勧めします。暗号化なしで続行してもよろしいですか?"; - readonly ko: "종단간 암호화와 경로 난독화를 활성화하는 것을 권장합니다. 정말로 암호화 없이 계속하시겠습니까?"; - readonly ru: "Мы рекомендуем включить сквозное шифрование. Вы уверены, что хотите продолжить без шифрования?"; - readonly zh: "建议启用端到端加密和路径混淆。你确定要在未加密的情况下继续吗?"; - readonly "zh-tw": "我們建議啟用端對端加密與路徑混淆。你確定要在未加密的情況下繼續嗎?"; - }; - readonly "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": { - readonly def: "Do you want to fetch the config from the remote server?"; - readonly es: "¿Quieres obtener la configuración del servidor remoto?"; - readonly fr: "Voulez-vous récupérer la configuration depuis le serveur distant ?"; - readonly he: "האם ברצונך למשוך את התצורה מהשרת המרוחד?"; - readonly ja: "リモートサーバーから設定を取得しますか?"; - readonly ko: "원격 서버에서 구성을 가져오시겠습니까?"; - readonly ru: "Вы хотите загрузить конфигурацию с удалённого сервера?"; - readonly zh: "要从远端服务器获取配置吗?"; - readonly "zh-tw": "要從遠端伺服器抓取設定嗎?"; - }; - readonly "obsidianLiveSyncSettingTab.msgGenerateSetupURI": { - readonly def: "All done! Do you want to generate a setup URI to set up other devices?"; - readonly es: "¡Todo listo! ¿Quieres generar un URI de configuración para configurar otros dispositivos?"; - readonly fr: "Tout est prêt ! Voulez-vous générer une URI de configuration pour configurer d'autres appareils ?"; - readonly he: "הכל מוכן! האם ברצונך לייצר Setup URI להגדרת מכשירים אחרים?"; - readonly ja: "完了!他のデバイスをセットアップするためのセットアップURIを生成しますか?"; - readonly ko: "모든 작업이 완료되었습니다! 다른 기기를 설정하기 위해 Setup URI를 생성하시겠습니까?"; - readonly ru: "Всё готово! Вы хотите сгенерировать Setup URI для настройки других устройств?"; - readonly zh: "全部完成!要生成设置 URI 以便配置其他设备吗?"; - readonly "zh-tw": "全部完成!要產生 Setup URI 以便設定其他裝置嗎?"; - }; - readonly "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": { - readonly def: "If the server configuration is not persistent (e.g., running on docker), the values here may change. Once you are able to connect, please update the settings in the server's local.ini."; - readonly es: "Si la configuración del servidor no es persistente (por ejemplo, ejecutándose en docker), los valores aquí pueden cambiar. Una vez que puedas conectarte, por favor actualiza las configuraciones en el local.ini del servidor."; - readonly fr: "Si la configuration du serveur n'est pas persistante (par ex. fonctionnant sur Docker), les valeurs peuvent changer. Une fois la connexion établie, mettez à jour les paramètres dans le local.ini du serveur."; - readonly he: "אם תצורת השרת אינה קבועה (למשל, פועלת ב-docker), הערכים כאן עשויים להשתנות. לאחר שתצליח להתחבר, אנא עדכן את ההגדרות ב-local.ini של השרת."; - readonly ja: "サーバー設定が永続的でない場合(例: Dockerで実行中)、ここの値は変更される可能性があります。接続できるようになったら、サーバーのlocal.iniの設定を更新してください。"; - readonly ko: "서버 설정이 영구적으로 저장되지 않는 환경(예: Docker에서 실행 중)에서는 이곳의 값들이 변경될 수 있습니다. 연결이 가능해지면 서버의 local.ini 파일에서 설정을 수동으로 업데이트해 주세요."; - readonly ru: "Если конфигурация сервера непостоянна, значения здесь могут измениться."; - readonly zh: "如果服务器配置不是持久的(例如,在 docker 上运行),此处的值可能会更改。一旦能够连接,请更新服务器 local.ini 中的设置"; - }; - readonly "obsidianLiveSyncSettingTab.msgInvalidPassphrase": { - readonly def: "Your encryption passphrase might be invalid. Are you sure you want to continue?"; - readonly es: "Tu frase de contraseña de cifrado podría ser inválida. ¿Estás seguro de querer continuar?"; - readonly fr: "Votre phrase secrète de chiffrement peut être invalide. Êtes-vous sûr de vouloir continuer ?"; - readonly he: "ביטוי הסיסמה להצפנה שלך עשוי להיות לא תקין. האם אתה בטוח שברצונך להמשיך?"; - readonly ja: "暗号化パスフレーズが無効かもしれません。続行してもよろしいですか?"; - readonly ko: "암호화 패스프레이즈가 유효하지 않을 수 있습니다. 정말로 계속하시겠습니까?"; - readonly ru: "Ваша парольная фраза шифрования может быть недействительна."; - readonly zh: "你的加密密码短语可能无效。你确定要继续吗?"; - readonly "zh-tw": "你的加密密語可能無效。你確定要繼續嗎?"; - }; - readonly "obsidianLiveSyncSettingTab.msgNewVersionNote": { - readonly def: "Here due to an upgrade notification? Please review the version history. If you're satisfied, click the button. A new update will prompt this again."; - readonly es: "¿Aquí debido a una notificación de actualización? Por favor, revise el historial de versiones. Si está satisfecho, haga clic en el botón. Una nueva actualización volverá a mostrar esto."; - readonly fr: "Arrivé ici suite à une notification de mise à jour ? Consultez l'historique des versions. Si vous êtes satisfait, cliquez sur le bouton. Une nouvelle mise à jour reproposera ceci."; - readonly he: "הגעת כאן בשל הודעת שדרוג? אנא עיין בהיסטוריית הגרסאות. אם אתה מרוצה, לחץ על הכפתור. עדכון חדש יציג זאת שוב."; - readonly ja: "アップグレード通知でここに来ましたか?バージョン履歴を確認してください。納得したらボタンをクリックしてください。新しい更新があると再度確認されます。"; - readonly ko: "업그레이드 알림으로 여기에 오셨나요? 버전 기록을 검토해 주세요. 만족하신다면 버튼을 클릭하세요. 새로운 업데이트 시 다시 안내됩니다."; - readonly ru: "Вы пришли из-за уведомления об обновлении? Просмотрите историю версий."; - readonly zh: "因为升级通知来到这里?请查看版本历史。如果您满意,请点击按钮。新的更新将再次提示此信息"; - }; - readonly "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": { - readonly def: "Configured as non-HTTPS URI. Be warned that this may not work on mobile devices."; - readonly es: "Configurado como URI que no es HTTPS. Ten en cuenta que esto puede no funcionar en dispositivos móviles."; - readonly fr: "Configuré avec une URI non HTTPS. Attention, ceci peut ne pas fonctionner sur les appareils mobiles."; - readonly he: "מוגדר כ-URI שאינו HTTPS. שים לב שהדבר עשוי שלא לפעול על מכשירים ניידים."; - readonly ja: "非HTTPS URIとして設定されています。モバイルデバイスでは動作しない可能性があります。"; - readonly ko: "비 HTTPS URI로 구성되었습니다. 모바일 기기에서는 작동하지 않을 수 있으니 주의하세요."; - readonly ru: "Настроено как не-HTTPS URI. Это может не работать на мобильных устройствах."; - readonly zh: "配置为非 HTTPS URI。请注意,这可能在移动设备上无法工作"; - }; - readonly "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": { - readonly def: "Cannot connect to non-HTTPS URI. Please update your config and try again."; - readonly es: "No se puede conectar a URI que no sean HTTPS. Por favor, actualiza tu configuración y vuelve a intentarlo."; - readonly fr: "Connexion impossible à une URI non HTTPS. Mettez à jour votre configuration et réessayez."; - readonly he: "לא ניתן להתחבר ל-URI שאינו HTTPS. אנא עדכן את התצורה ונסה שוב."; - readonly ja: "非HTTPS URIに接続できません。設定を更新して再試行してください。"; - readonly ko: "비 HTTPS URI에 연결할 수 없습니다. 구성을 업데이트하고 다시 시도해 주세요."; - readonly ru: "Не удаётся подключиться к не-HTTPS URI. Обновите конфигурацию."; - readonly zh: "无法连接到非 HTTPS URI。请更新您的配置并重试"; - }; - readonly "obsidianLiveSyncSettingTab.msgNotice": { - readonly def: "---Notice---"; - readonly es: "---Aviso---"; - readonly fr: "---Avis---"; - readonly he: "---הודעה---"; - readonly ja: "---お知らせ---"; - readonly ko: "---공지사항---"; - readonly ru: "---Уведомление---"; - readonly zh: "---注意---"; - }; - readonly "obsidianLiveSyncSettingTab.msgObjectStorageWarning": { - readonly def: "WARNING: This feature is a Work In Progress, so please keep in mind the following:\n- Append only architecture. A rebuild is required to shrink the storage.\n- A bit fragile.\n- When first syncing, all history will be transferred from the remote. Be mindful of data caps and slow speeds.\n- Only differences are synced live.\n\nIf you run into any issues, or have ideas about this feature, please create a issue on GitHub.\nI appreciate you for your great dedication."; - readonly es: "ADVERTENCIA: Esta característica está en desarrollo, así que por favor ten en cuenta lo siguiente:\n- Arquitectura de solo anexado. Se requiere una reconstrucción para reducir el almacenamiento.\n- Un poco frágil.\n- Al sincronizar por primera vez, todo el historial será transferido desde el remoto. Ten en cuenta los límites de datos y las velocidades lentas.\n- Solo las diferencias se sincronizan en vivo.\n\nSi encuentras algún problema o tienes ideas sobre esta característica, por favor crea un issue en GitHub.\nAprecio mucho tu gran dedicación."; - readonly fr: "AVERTISSEMENT : cette fonctionnalité est en cours de développement, gardez à l'esprit ce qui suit :\n- Architecture en ajout seul. Une reconstruction est nécessaire pour réduire le stockage.\n- Un peu fragile.\n- Lors de la première synchronisation, tout l'historique sera transféré depuis le distant. Attention aux limites de données et aux débits lents.\n- Seules les différences sont synchronisées en direct.\n\nSi vous rencontrez des problèmes ou avez des idées sur cette fonctionnalité, merci d'ouvrir un ticket sur GitHub.\nMerci pour votre grand dévouement."; - readonly he: "אזהרה: תכונה זו בשלב פיתוח, לכן שים לב לנקודות הבאות:\n- ארכיטקטורת הוספה בלבד. נדרשת בנייה מחדש לצמצום האחסון.\n- קצת רגיש.\n- בסנכרון הראשון, כל ההיסטוריה תועבר מהשרת המרוחד. שים לב למגבלות נתונים ומהירות.\n- רק הפרשים מסונכרנים בזמן אמת.\n\nאם נתקלת בבעיות, או שיש לך רעיונות לגבי תכונה זו, אנא פתח Issue ב-GitHub.\nאנחנו מעריכים את ההקדשה הגדולה שלך."; - readonly ja: "警告: この機能は開発中です。以下の点にご注意ください:\n- 追記専用アーキテクチャ。ストレージを縮小するには再構築が必要です。\n- やや不安定です。\n- 初回同期時、すべての履歴がリモートから転送されます。データ制限と速度に注意してください。\n- ライブ同期は差分のみです。\n\n問題があれば、またはこの機能についてアイデアがあれば、GitHubにIssueを作成してください。\nご協力に感謝します。"; - readonly ko: "⚠️ 주의: 이 기능은 아직 개발 중(WIP)입니다. 다음 사항을 유의해 주세요:\n- 추가 전용 구조(append-only)로 동작합니다. 저장 용량을 줄이려면 데이터 재구성이 필요합니다.\n- 기능이 다소 불안정할 수 있습니다.\n- 최초 동기화 시, 전체 히스토리가 원격 서버에서 전송됩니다. 데이터 용량 제한 및 느린 속도에 유의해 주세요.\n- 실시간 동기화는 변경된 부분만 처리됩니다.\n\n문제가 발생했거나 개선 아이디어가 있으시면 GitHub에 이슈를 등록해 주세요.\n기여에 깊이 감사드립니다."; - readonly ru: "ПРЕДУПРЕЖДЕНИЕ: Эта функция в разработке."; - readonly zh: "警告:此功能仍在开发中,请注意以下几点:\n- 仅追加架构。需要重建才能缩小存储空间。\n- 有点脆弱。\n- 首次同步时,所有历史记录将从远程传输。注意数据上限和慢速。\n- 只有差异会实时同步。\n\n如果您遇到任何问题,或对此功能有任何想法,请在 GitHub 上创建 issue。\n感谢您的巨大贡献"; - }; - readonly "obsidianLiveSyncSettingTab.msgOriginCheck": { - readonly def: "Origin check: ${org}"; - readonly es: "Verificación de origen: {org}"; - readonly fr: "Vérification d'origine : ${org}"; - readonly he: "בדיקת מקור: ${org}"; - readonly ja: "オリジン確認: ${org}"; - readonly ko: "원점 확인: {org}"; - readonly ru: "Проверка origin: org"; - readonly zh: "源检查: {org}"; - }; - readonly "obsidianLiveSyncSettingTab.msgRebuildRequired": { - readonly def: "Rebuilding Databases are required to apply the changes.. Please select the method to apply the changes.\n\n
\nLegends\n\n| Symbol | Meaning |\n|: ------ :| ------- |\n| ⇔ | Up to Date |\n| ⇄ | Synchronise to balance |\n| ⇐,⇒ | Transfer to overwrite |\n| ⇠,⇢ | Transfer to overwrite from other side |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\nAt a glance: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nReconstruct both the local and remote databases using existing files from this device.\nThis causes a lockout other devices, and they need to perform fetching.\n## ${OPTION_FETCH}\nAt a glance: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nInitialise the local database and reconstruct it using data fetched from the remote database.\nThis case includes the case which you have rebuilt the remote database.\n## ${OPTION_ONLY_SETTING}\nStore only the settings. **Caution: This may lead to data corruption**; database reconstruction is generally necessary."; - readonly es: "Es necesario reconstruir las bases de datos para aplicar los cambios. Por favor selecciona el método para aplicar los cambios.\n\n
\nLegendas\n\n| Símbolo | Significado |\n|: ------ :| ------- |\n| ⇔ | Actualizado |\n| ⇄ | Sincronizar para equilibrar |\n| ⇐,⇒ | Transferir para sobrescribir |\n| ⇠,⇢ | Transferir para sobrescribir desde otro lado |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\nA simple vista: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nReconstruir tanto la base de datos local como la remota utilizando los archivos existentes de este dispositivo.\nEsto bloquea a otros dispositivos, y necesitan realizar la obtención.\n## ${OPTION_FETCH}\nA simple vista: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nInicializa la base de datos local y la reconstruye utilizando los datos obtenidos de la base de datos remota.\nEste caso incluye el caso en el que has reconstruido la base de datos remota.\n## ${OPTION_ONLY_SETTING}\nAlmacena solo la configuración. **Precaución: esto puede provocar corrupción de datos**; generalmente es necesario reconstruir la base de datos."; - readonly fr: "La reconstruction des bases de données est nécessaire pour appliquer les changements. Veuillez sélectionner la méthode d'application.\n\n
\nLégende\n\n| Symbole | Signification |\n|: ------ :| ------- |\n| ⇔ | À jour |\n| ⇄ | Synchroniser pour équilibrer |\n| ⇐,⇒ | Transférer pour écraser |\n| ⇠,⇢ | Transférer pour écraser depuis l'autre côté |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\nEn bref : 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nReconstruit les bases locale et distante à partir des fichiers existants de cet appareil.\nCeci provoque un verrouillage des autres appareils, qui devront effectuer une récupération.\n## ${OPTION_FETCH}\nEn bref : 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nInitialise la base locale et la reconstruit à partir des données récupérées depuis la base distante.\nCe cas inclut également celui où vous avez reconstruit la base distante.\n## ${OPTION_ONLY_SETTING}\nNe stocker que les paramètres. **Attention : cela peut entraîner une corruption des données** ; une reconstruction de la base est généralement nécessaire."; - readonly he: "נדרשת בנייה מחדש של מסדי הנתונים כדי להחיל את השינויים. אנא בחר את השיטה.\n\n
\nמקרא\n\n| סמל | משמעות |\n|: ------ :| ------- |\n| ⇔ | מעודכן |\n| ⇄ | סנכרן לאיזון |\n| ⇐,⇒ | העבר לדריסה |\n| ⇠,⇢ | העבר לדריסה מהצד השני |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\nבמבט: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nבנה מחדש גם את מסד הנתונים המקומי וגם המרוחד תוך שימוש בקבצים קיימים ממכשיר זה.\nפעולה זו תנעל מכשירים אחרים שיצטרכו לבצע משיכה.\n## ${OPTION_FETCH}\nבמבט: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nאתחל את מסד הנתונים המקומי ובנה אותו מחדש תוך שימוש בנתונים שנמשכו ממסד הנתונים המרוחד.\nכולל את המקרה שבו בנית מחדש את מסד הנתונים המרוחד.\n## ${OPTION_ONLY_SETTING}\nשמור רק את ההגדרות. **זהירות: עלול לגרום לפגיעה בנתונים**; בנייה מחדש של מסד הנתונים נדרשת בדרך כלל."; - readonly ja: "変更を適用するにはデータベースの再構築が必要です。変更を適用する方法を選択してください。\n\n
\n凡例\n\n| 記号 | 意味 |\n|: ------ :| ------- |\n| ⇔ | 最新 |\n| ⇄ | 同期してバランスを取る |\n| ⇐,⇒ | 上書きするため転送 |\n| ⇠,⇢ | 反対側から上書きするため転送 |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\n概要: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nこのデバイスの既存ファイルを使用してローカルとリモートの両方のデータベースを再構築します。\n他のデバイスはロックアウトされ、フェッチが必要です。\n## ${OPTION_FETCH}\n概要: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nローカルデータベースを初期化し、リモートデータベースから取得したデータを使用して再構築します。\nリモートデータベースを再構築した場合も含まれます。\n## ${OPTION_ONLY_SETTING}\n設定のみを保存します。**注意: データ破損につながる可能性があります**。通常、データベースの再構築が必要です。"; - readonly ko: "변경사항을 적용하려면 데이터베이스를 재구축해야 합니다. 아래 중 한 가지 방법을 선택해 주세요.\n\n
\n범례\n\n| 기호 | 의미 |\n|: ------ :| ------- |\n| ⇔ | 최신 상태 |\n| ⇄ | 동기화 균형 유지 |\n| ⇐,⇒ | 덮어쓰기 방식의 전송 |\n| ⇠,⇢ | 상대편에서 가져와 덮어쓰기 |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\n개요: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\n이 기기의 기존 파일을 기반으로 로컬과 원격 데이터베이스를 모두 재구축합니다.\n이 과정에서 다른 기기는 일시적으로 접근이 제한되며, 가져오기 작업을 별도로 수행해야 합니다.\n\n## ${OPTION_FETCH}\n개요: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\n로컬 데이터베이스를 초기화한 후, 원격 데이터베이스에서 데이터를 가져와 재구축합니다.\n이는 원격 측에서 데이터베이스를 먼저 재구축한 경우에도 해당됩니다.\n\n## ${OPTION_ONLY_SETTING}\n설정만 저장합니다. **⚠️ 주의: 이 방법은 데이터 손상을 일으킬 수 있습니다.** 일반적으로는 전체 데이터베이스 재구축이 필요합니다."; - readonly ru: "Требуется перестроение баз данных для применения изменений."; - readonly zh: "需要重建数据库以应用更改。请选择应用更改的方法。\n\n
\n图例\n\n| 符号 | 含义 |\n|: ------ :| ------- |\n| ⇔ | 最新 |\n| ⇄ | 同步以平衡 |\n| ⇐,⇒ | 传输以覆盖 |\n| ⇠,⇢ | 从另一侧传输以覆盖 |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\n概览:📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\n使用此设备的现有文件重建本地和远程数据库。\n这将导致其他设备被锁定,并且它们需要执行获取操作。\n## ${OPTION_FETCH}\n概览:📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\n初始化本地数据库并使用从远程数据库获取的数据重建它。\n这种情况包括您已经重建了远程数据库的情况。\n## ${OPTION_ONLY_SETTING}\n仅存储设置。**注意:这可能导致数据损坏**;通常需要重建数据库"; - }; - readonly "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": { - readonly def: "Please select and apply any preset item to complete the wizard."; - readonly es: "Por favor, selecciona y aplica cualquier elemento preestablecido para completar el asistente."; - readonly fr: "Veuillez sélectionner et appliquer un préréglage pour terminer l'assistant."; - readonly he: "אנא בחר והחל פריט קבוע מראש כלשהו להשלמת האשף."; - readonly ja: "ウィザードを完了するには、プリセット項目を選択して適用してください。"; - readonly ko: "마법사를 완료하려면 프리셋 항목을 선택하고 적용해 주세요."; - readonly ru: "Выберите и примените любой пресет для завершения мастера."; - readonly zh: "请选择并应用任一预设项以完成向导。"; - readonly "zh-tw": "請選擇並套用任一預設項目以完成精靈。"; - }; - readonly "obsidianLiveSyncSettingTab.msgSetCorsCredentials": { - readonly def: "Set cors.credentials"; - readonly es: "Configurar cors.credentials"; - readonly fr: "Définir cors.credentials"; - readonly he: "הגדר cors.credentials"; - readonly ja: "cors.credentialsを設定"; - readonly ko: "cors.credentials 설정"; - readonly ru: "Установить cors.credentials"; - readonly zh: "设置 cors.credentials"; - }; - readonly "obsidianLiveSyncSettingTab.msgSetCorsOrigins": { - readonly def: "Set cors.origins"; - readonly es: "Configurar cors.origins"; - readonly fr: "Définir cors.origins"; - readonly he: "הגדר cors.origins"; - readonly ja: "cors.originsを設定"; - readonly ko: "cors.origins 설정"; - readonly ru: "Установить cors.origins"; - readonly zh: "设置 cors.origins"; - }; - readonly "obsidianLiveSyncSettingTab.msgSetMaxDocSize": { - readonly def: "Set couchdb.max_document_size"; - readonly es: "Configurar couchdb.max_document_size"; - readonly fr: "Définir couchdb.max_document_size"; - readonly he: "הגדר couchdb.max_document_size"; - readonly ja: "couchdb.max_document_sizeを設定"; - readonly ko: "couchdb.max_document_size 설정"; - readonly ru: "Установить couchdb.max_document_size"; - readonly zh: "设置 couchdb.max_document_size"; - }; - readonly "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": { - readonly def: "Set chttpd.max_http_request_size"; - readonly es: "Configurar chttpd.max_http_request_size"; - readonly fr: "Définir chttpd.max_http_request_size"; - readonly he: "הגדר chttpd.max_http_request_size"; - readonly ja: "chttpd.max_http_request_sizeを設定"; - readonly ko: "chttpd.max_http_request_size 설정"; - readonly ru: "Установить chttpd.max_http_request_size"; - readonly zh: "设置 chttpd.max_http_request_size"; - }; - readonly "obsidianLiveSyncSettingTab.msgSetRequireValidUser": { - readonly def: "Set chttpd.require_valid_user = true"; - readonly es: "Configurar chttpd.require_valid_user = true"; - readonly fr: "Définir chttpd.require_valid_user = true"; - readonly he: "הגדר chttpd.require_valid_user = true"; - readonly ja: "chttpd.require_valid_user = trueを設定"; - readonly ko: "chttpd.require_valid_user = true로 설정"; - readonly ru: "Установить chttpd.require_valid_user = true"; - readonly zh: "设置 chttpd.require_valid_user = true"; - }; - readonly "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": { - readonly def: "Set chttpd_auth.require_valid_user = true"; - readonly es: "Configurar chttpd_auth.require_valid_user = true"; - readonly fr: "Définir chttpd_auth.require_valid_user = true"; - readonly he: "הגדר chttpd_auth.require_valid_user = true"; - readonly ja: "chttpd_auth.require_valid_user = trueを設定"; - readonly ko: "chttpd_auth.require_valid_user = true로 설정"; - readonly ru: "Установить chttpd_auth.require_valid_user = true"; - readonly zh: "设置 chttpd_auth.require_valid_user = true"; - }; - readonly "obsidianLiveSyncSettingTab.msgSettingModified": { - readonly def: "The setting \"${setting}\" was modified from another device. Click {HERE} to reload settings. Click elsewhere to ignore changes."; - readonly es: "La configuración \"${setting}\" fue modificada desde otro dispositivo. Haz clic {HERE} para recargar la configuración. Haz clic en otro lugar para ignorar los cambios."; - readonly fr: "Le paramètre « ${setting} » a été modifié depuis un autre appareil. Cliquez sur {HERE} pour recharger les paramètres. Cliquez ailleurs pour ignorer les modifications."; - readonly he: "ההגדרה \"${setting}\" שונתה ממכשיר אחר. לחץ על {HERE} לטעינה מחדש של ההגדרות. לחץ במקום אחר להתעלמות מהשינויים."; - readonly ja: "設定\"${setting}\"が別のデバイスから変更されました。{HERE}をクリックして設定を再読み込みしてください。変更を無視するには他の場所をクリックしてください。"; - readonly ko: "\"${setting}\" 설정이 다른 기기에서 수정되었습니다. 설정을 다시 로드하려면 {HERE}를 클릭하세요. 변경사항을 무시하려면 다른 곳을 클릭하세요."; - readonly ru: "Настройка setting была изменена с другого устройства."; - readonly zh: "设置 \"${setting}\" 已从另一台设备修改。点击 {HERE} 重新加载设置。点击其他地方忽略更改"; - }; - readonly "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": { - readonly def: "These settings are unable to be changed during synchronization. Please disable all syncing in the \"Sync Settings\" to unlock."; - readonly es: "Estas configuraciones no se pueden cambiar durante la sincronización. Por favor, deshabilita toda la sincronización en las \"Configuraciones de Sincronización\" para desbloquear."; - readonly fr: "Ces paramètres ne peuvent pas être modifiés durant la synchronisation. Désactivez toute synchronisation dans « Paramètres de synchronisation » pour déverrouiller."; - readonly he: "הגדרות אלה אינן ניתנות לשינוי במהלך סנכרון. אנא נטרל את כל הסנכרון ב\"הגדרות סנכרון\" כדי לבטל נעילה."; - readonly ja: "これらの設定は同期中に変更できません。ロックを解除するには、\"同期設定\"ですべての同期を無効にしてください。"; - readonly ko: "동기화 중에는 이 설정들을 변경할 수 없습니다. 잠금을 해제하려면 \"동기화 설정\"에서 모든 동기화를 비활성화해 주세요."; - readonly ru: "Эти настройки нельзя изменить во время синхронизации."; - readonly zh: "这些设置在同步期间无法更改。请在“同步设置”中禁用所有同步以解锁"; - }; - readonly "obsidianLiveSyncSettingTab.msgSetWwwAuth": { - readonly def: "Set httpd.WWW-Authenticate"; - readonly es: "Configurar httpd.WWW-Authenticate"; - readonly fr: "Définir httpd.WWW-Authenticate"; - readonly he: "הגדר httpd.WWW-Authenticate"; - readonly ja: "httpd.WWW-Authenticateを設定"; - readonly ko: "httpd.WWW-Authenticate 설정"; - readonly ru: "Установить httpd.WWW-Authenticate"; - readonly zh: "设置 httpd.WWW-Authenticate"; - }; - readonly "obsidianLiveSyncSettingTab.nameApplySettings": { - readonly def: "Apply Settings"; - readonly es: "Aplicar configuraciones"; - readonly fr: "Appliquer les paramètres"; - readonly he: "החל הגדרות"; - readonly ja: "設定を適用"; - readonly ko: "설정 적용"; - readonly ru: "Применить настройки"; - readonly zh: "应用设置"; - }; - readonly "obsidianLiveSyncSettingTab.nameConnectSetupURI": { - readonly def: "Connect with Setup URI"; - readonly es: "Conectar con URI de configuración"; - readonly fr: "Se connecter avec une URI de configuration"; - readonly he: "התחבר עם Setup URI"; - readonly ja: "セットアップURIで接続"; - readonly ko: "Setup URI로 연결"; - readonly ru: "Подключиться через Setup URI"; - readonly zh: "使用设置 URI 连接"; - }; - readonly "obsidianLiveSyncSettingTab.nameCopySetupURI": { - readonly def: "Copy the current settings to a Setup URI"; - readonly es: "Copiar la configuración actual a una URI de configuración"; - readonly fr: "Copier les paramètres actuels vers une URI de configuration"; - readonly he: "העתק הגדרות נוכחיות ל-Setup URI"; - readonly ja: "現在の設定をセットアップURIにコピー"; - readonly ko: "현재 설정을 Setup URI로 복사"; - readonly ru: "Копировать текущие настройки в Setup URI"; - readonly zh: "将当前设置复制为设置 URI"; - }; - readonly "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": { - readonly def: "Disable Hidden files sync"; - readonly es: "Desactivar sincronización de archivos ocultos"; - readonly fr: "Désactiver la synchronisation des fichiers cachés"; - readonly he: "נטרל סנכרון קבצים נסתרים"; - readonly ja: "隠しファイル同期を無効化"; - readonly ko: "숨김 파일 동기화 비활성화"; - readonly ru: "Отключить синхронизацию скрытых файлов"; - readonly zh: "禁用隐藏文件同步"; - readonly "zh-tw": "停用隱藏檔案同步"; - }; - readonly "obsidianLiveSyncSettingTab.nameDiscardSettings": { - readonly def: "Discard existing settings and databases"; - readonly es: "Descartar configuraciones y bases de datos existentes"; - readonly fr: "Abandonner les paramètres et bases existants"; - readonly he: "בטל הגדרות ומסדי נתונים קיימים"; - readonly ja: "既存の設定とデータベースを破棄"; - readonly ko: "기존 설정 및 데이터베이스 삭제"; - readonly ru: "Отменить существующие настройки и базы данных"; - readonly zh: "丢弃现有设置和数据库"; - }; - readonly "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": { - readonly def: "Enable Hidden files sync"; - readonly es: "Activar sincronización de archivos ocultos"; - readonly fr: "Activer la synchronisation des fichiers cachés"; - readonly he: "הפעל סנכרון קבצים נסתרים"; - readonly ja: "隠しファイル同期を有効化"; - readonly ko: "숨김 파일 동기화 활성화"; - readonly ru: "Включить синхронизацию скрытых файлов"; - readonly zh: "启用隐藏文件同步"; - readonly "zh-tw": "啟用隱藏檔案同步"; - }; - readonly "obsidianLiveSyncSettingTab.nameEnableLiveSync": { - readonly def: "Enable LiveSync"; - readonly es: "Activar LiveSync"; - readonly fr: "Activer LiveSync"; - readonly he: "הפעל LiveSync"; - readonly ja: "LiveSyncを有効化"; - readonly ko: "LiveSync 활성화"; - readonly ru: "Включить LiveSync"; - readonly zh: "启用 LiveSync"; - }; - readonly "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": { - readonly def: "Hidden file synchronization"; - readonly es: "Sincronización de archivos ocultos"; - readonly fr: "Synchronisation des fichiers cachés"; - readonly he: "סנכרון קבצים נסתרים"; - readonly ja: "隠しファイル同期"; - readonly ko: "숨김 파일 동기화"; - readonly ru: "Синхронизация скрытых файлов"; - readonly zh: "隐藏文件同步"; - readonly "zh-tw": "隱藏檔案同步"; - }; - readonly "obsidianLiveSyncSettingTab.nameManualSetup": { - readonly def: "Manual Setup"; - readonly es: "Configuración manual"; - readonly fr: "Configuration manuelle"; - readonly he: "הגדרה ידנית"; - readonly ja: "手動セットアップ"; - readonly ko: "수동 설정"; - readonly ru: "Ручная настройка"; - readonly zh: "手动设置"; - }; - readonly "obsidianLiveSyncSettingTab.nameTestConnection": { - readonly def: "Test Connection"; - readonly es: "Probar conexión"; - readonly fr: "Tester la connexion"; - readonly he: "בדוק חיבור"; - readonly ja: "接続テスト"; - readonly ko: "연결 테스트"; - readonly ru: "Тест подключения"; - readonly zh: "测试连接"; - }; - readonly "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": { - readonly def: "Test Database Connection"; - readonly es: "Probar Conexión de Base de Datos"; - readonly fr: "Tester la connexion à la base de données"; - readonly he: "בדוק חיבור למסד נתונים"; - readonly ja: "データベース接続テスト"; - readonly ko: "데이터베이스 연결 테스트"; - readonly ru: "Тест подключения к базе данных"; - readonly zh: "测试数据库连接"; - }; - readonly "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": { - readonly def: "Validate Database Configuration"; - readonly es: "Validar Configuración de la Base de Datos"; - readonly fr: "Valider la configuration de la base de données"; - readonly he: "אמת תצורת מסד נתונים"; - readonly ja: "データベース設定を検証"; - readonly ko: "데이터베이스 구성 검증"; - readonly ru: "Проверить конфигурацию базы данных"; - readonly zh: "验证数据库配置"; - }; - readonly "obsidianLiveSyncSettingTab.okAdminPrivileges": { - readonly def: "✔ You have administrator privileges."; - readonly es: "✔ Tienes privilegios de administrador."; - readonly fr: "✔ Vous disposez des privilèges administrateur."; - readonly he: "✔ יש לך הרשאות מנהל."; - readonly ja: "✔ 管理者権限があります。"; - readonly ko: "✔ 관리자 권한이 있습니다."; - readonly ru: "✔ У вас есть права администратора."; - readonly zh: "✔ 您拥有管理员权限"; - }; - readonly "obsidianLiveSyncSettingTab.okCorsCredentials": { - readonly def: "✔ cors.credentials is ok."; - readonly es: "✔ cors.credentials está correcto."; - readonly fr: "✔ cors.credentials est correct."; - readonly he: "✔ cors.credentials תקין."; - readonly ja: "✔ cors.credentialsは正常です。"; - readonly ko: "✔ cors.credentials가 정상입니다."; - readonly ru: "✔ cors.credentials в порядке."; - readonly zh: "✔ cors.credentials 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": { - readonly def: "CORS credentials OK"; - readonly es: "CORS credenciales OK"; - readonly fr: "Identifiants CORS OK"; - readonly he: "CORS credentials תקין"; - readonly ja: "CORS認証情報OK"; - readonly ko: "CORS 자격 증명 정상"; - readonly ru: "CORS учётные данные в порядке"; - readonly zh: "CORS 凭据正常"; - }; - readonly "obsidianLiveSyncSettingTab.okCorsOriginMatched": { - readonly def: "✔ CORS origin OK"; - readonly es: "✔ Origen de CORS correcto"; - readonly fr: "✔ Origine CORS OK"; - readonly he: "✔ CORS origin תקין"; - readonly ja: "✔ CORSオリジンOK"; - readonly ko: "✔ CORS 원점 정상"; - readonly ru: "✔ CORS origin в порядке"; - readonly zh: "✔ CORS 源正常"; - }; - readonly "obsidianLiveSyncSettingTab.okCorsOrigins": { - readonly def: "✔ cors.origins is ok."; - readonly es: "✔ cors.origins está correcto."; - readonly fr: "✔ cors.origins est correct."; - readonly he: "✔ cors.origins תקין."; - readonly ja: "✔ cors.originsは正常です。"; - readonly ko: "✔ cors.origins가 정상입니다."; - readonly ru: "✔ cors.origins в порядке."; - readonly zh: "✔ cors.origins 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.okEnableCors": { - readonly def: "✔ httpd.enable_cors is ok."; - readonly es: "✔ httpd.enable_cors está correcto."; - readonly fr: "✔ httpd.enable_cors est correct."; - readonly he: "✔ httpd.enable_cors תקין."; - readonly ja: "✔ httpd.enable_corsは正常です。"; - readonly ko: "✔ httpd.enable_cors가 정상입니다."; - readonly ru: "✔ httpd.enable_cors в порядке."; - readonly zh: "✔ httpd.enable_cors 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.okEnableCorsChttpd": { - readonly def: "✔ chttpd.enable_cors is ok."; - readonly fr: "✔ chttpd.enable_cors est correct."; - readonly he: "✔ chttpd.enable_cors תקין."; - readonly ja: "✔ chttpd.enable_corsは正常です。"; - readonly ru: "✔ chttpd.enable_cors в порядке."; - readonly zh: "✔ chttpd.enable_cors is ok."; - }; - readonly "obsidianLiveSyncSettingTab.okMaxDocumentSize": { - readonly def: "✔ couchdb.max_document_size is ok."; - readonly es: "✔ couchdb.max_document_size está correcto."; - readonly fr: "✔ couchdb.max_document_size est correct."; - readonly he: "✔ couchdb.max_document_size תקין."; - readonly ja: "✔ couchdb.max_document_sizeは正常です。"; - readonly ko: "✔ couchdb.max_document_size가 정상입니다."; - readonly ru: "✔ couchdb.max_document_size в порядке."; - readonly zh: "✔ couchdb.max_document_size 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.okMaxRequestSize": { - readonly def: "✔ chttpd.max_http_request_size is ok."; - readonly es: "✔ chttpd.max_http_request_size está correcto."; - readonly fr: "✔ chttpd.max_http_request_size est correct."; - readonly he: "✔ chttpd.max_http_request_size תקין."; - readonly ja: "✔ chttpd.max_http_request_sizeは正常です。"; - readonly ko: "✔ chttpd.max_http_request_size가 정상입니다."; - readonly ru: "✔ chttpd.max_http_request_size в порядке."; - readonly zh: "✔ chttpd.max_http_request_size 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.okRequireValidUser": { - readonly def: "✔ chttpd.require_valid_user is ok."; - readonly es: "✔ chttpd.require_valid_user está correcto."; - readonly fr: "✔ chttpd.require_valid_user est correct."; - readonly he: "✔ chttpd.require_valid_user תקין."; - readonly ja: "✔ chttpd.require_valid_userは正常です。"; - readonly ko: "✔ chttpd.require_valid_user가 정상입니다."; - readonly ru: "✔ chttpd.require_valid_user в порядке."; - readonly zh: "✔ chttpd.require_valid_user 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.okRequireValidUserAuth": { - readonly def: "✔ chttpd_auth.require_valid_user is ok."; - readonly es: "✔ chttpd_auth.require_valid_user está correcto."; - readonly fr: "✔ chttpd_auth.require_valid_user est correct."; - readonly he: "✔ chttpd_auth.require_valid_user תקין."; - readonly ja: "✔ chttpd_auth.require_valid_userは正常です。"; - readonly ko: "✔ chttpd_auth.require_valid_user가 정상입니다."; - readonly ru: "✔ chttpd_auth.require_valid_user в порядке."; - readonly zh: "✔ chttpd_auth.require_valid_user 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.okWwwAuth": { - readonly def: "✔ httpd.WWW-Authenticate is ok."; - readonly es: "✔ httpd.WWW-Authenticate está correcto."; - readonly fr: "✔ httpd.WWW-Authenticate est correct."; - readonly he: "✔ httpd.WWW-Authenticate תקין."; - readonly ja: "✔ httpd.WWW-Authenticateは正常です。"; - readonly ko: "✔ httpd.WWW-Authenticate가 정상입니다."; - readonly ru: "✔ httpd.WWW-Authenticate в порядке."; - readonly zh: "✔ httpd.WWW-Authenticate 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.optionApply": { - readonly def: "Apply"; - readonly es: "Aplicar"; - readonly fr: "Appliquer"; - readonly he: "החל"; - readonly ja: "適用"; - readonly ko: "적용"; - readonly ru: "Применить"; - readonly zh: "应用"; - }; - readonly "obsidianLiveSyncSettingTab.optionCancel": { - readonly def: "Cancel"; - readonly es: "Cancelar"; - readonly fr: "Annuler"; - readonly he: "ביטול"; - readonly ja: "キャンセル"; - readonly ko: "취소"; - readonly ru: "Отмена"; - readonly zh: "取消"; - }; - readonly "obsidianLiveSyncSettingTab.optionCouchDB": { - readonly def: "CouchDB"; - readonly es: "CouchDB"; - readonly fr: "CouchDB"; - readonly he: "CouchDB"; - readonly ja: "CouchDB"; - readonly ko: "CouchDB"; - readonly ru: "CouchDB"; - readonly zh: "CouchDB"; - }; - readonly "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": { - readonly def: "Disable all automatic"; - readonly es: "Desactivar lo automático"; - readonly fr: "Désactiver toute automatisation"; - readonly he: "נטרל את כל האוטומטי"; - readonly ja: "すべての自動を無効化"; - readonly ko: "모든 자동 비활성화"; - readonly ru: "Отключить всё автоматическое"; - readonly zh: "禁用所有自动同步"; - readonly "zh-tw": "停用所有自動同步"; - }; - readonly "obsidianLiveSyncSettingTab.optionFetchFromRemote": { - readonly def: "Fetch from Remote"; - readonly es: "Obtener del remoto"; - readonly fr: "Récupérer depuis le distant"; - readonly he: "משוך מהשרת המרוחד"; - readonly ja: "リモートからフェッチ"; - readonly ko: "원격에서 가져오기"; - readonly ru: "Загрузить с удалённого"; - readonly zh: "从远程获取"; - }; - readonly "obsidianLiveSyncSettingTab.optionHere": { - readonly def: "HERE"; - readonly es: "AQUÍ"; - readonly fr: "ICI"; - readonly he: "כאן"; - readonly ja: "ここ"; - readonly ko: "여기"; - readonly ru: "ЗДЕСЬ"; - readonly zh: "这里"; - }; - readonly "obsidianLiveSyncSettingTab.optionLiveSync": { - readonly def: "LiveSync"; - readonly es: "Sincronización LiveSync"; - readonly fr: "LiveSync"; - readonly he: "LiveSync"; - readonly ja: "LiveSync 同期"; - readonly ko: "LiveSync 동기화"; - readonly ru: "Синхронизация LiveSync"; - readonly zh: "LiveSync 同步"; - readonly "zh-tw": "LiveSync 同步"; - }; - readonly "obsidianLiveSyncSettingTab.optionMinioS3R2": { - readonly def: "Minio,S3,R2"; - readonly es: "Minio,S3,R2"; - readonly fr: "Minio, S3, R2"; - readonly he: "Minio,S3,R2"; - readonly ja: "Minio,S3,R2"; - readonly ko: "Minio,S3,R2"; - readonly ru: "Minio,S3,R2"; - readonly zh: "Minio, S3, R2"; - }; - readonly "obsidianLiveSyncSettingTab.optionOkReadEverything": { - readonly def: "OK, I have read everything."; - readonly es: "OK, he leído todo."; - readonly fr: "OK, j'ai tout lu."; - readonly he: "בסדר, קראתי הכל."; - readonly ja: "OK、すべて読みました。"; - readonly ko: "네, 모든 것을 읽었습니다."; - readonly ru: "ОК, я всё прочитал."; - readonly zh: "好的,我已经阅读了所有内容 "; - }; - readonly "obsidianLiveSyncSettingTab.optionOnEvents": { - readonly def: "On events"; - readonly es: "En eventos"; - readonly fr: "Sur événements"; - readonly he: "על אירועים"; - readonly ja: "イベント時"; - readonly ko: "이벤트 시"; - readonly ru: "По событиям"; - readonly zh: "事件触发时"; - readonly "zh-tw": "事件觸發時"; - }; - readonly "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": { - readonly def: "Periodic and on events"; - readonly es: "Periódico y en eventos"; - readonly fr: "Périodique et sur événements"; - readonly he: "תקופתי ועל אירועים"; - readonly ja: "定期およびイベント時"; - readonly ko: "주기적 및 이벤트 시"; - readonly ru: "Периодически и по событиям"; - readonly zh: "定期与事件触发"; - readonly "zh-tw": "定期與事件觸發"; - }; - readonly "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": { - readonly def: "Periodic w/ batch"; - readonly es: "Periódico con lote"; - readonly fr: "Périodique avec lot"; - readonly he: "תקופתי עם אצווה"; - readonly ja: "バッチ付き定期"; - readonly ko: "주기적 w/ 일괄"; - readonly ru: "Периодически с пакетами"; - readonly zh: "定期(批处理)"; - readonly "zh-tw": "定期(批次)"; - }; - readonly "obsidianLiveSyncSettingTab.optionRebuildBoth": { - readonly def: "Rebuild Both from This Device"; - readonly es: "Reconstructuir ambos desde este dispositivo"; - readonly fr: "Tout reconstruire depuis cet appareil"; - readonly he: "בנה שניהם מחדש ממכשיר זה"; - readonly ja: "このデバイスから両方を再構築"; - readonly ko: "이 기기에서 둘 다 재구축"; - readonly ru: "Перестроить оба с этого устройства"; - readonly zh: "从此设备重建两者"; - }; - readonly "obsidianLiveSyncSettingTab.optionSaveOnlySettings": { - readonly def: "(Danger) Save Only Settings"; - readonly es: "(Peligro) Guardar solo configuración"; - readonly fr: "(Danger) N'enregistrer que les paramètres"; - readonly he: "(סכנה) שמור הגדרות בלבד"; - readonly ja: "(危険) 設定のみ保存"; - readonly ko: "(위험) 설정만 저장"; - readonly ru: "(Опасно) Сохранить только настройки"; - readonly zh: "(危险)仅保存设置"; - }; - readonly "obsidianLiveSyncSettingTab.panelChangeLog": { - readonly def: "Change Log"; - readonly es: "Registro de cambios"; - readonly fr: "Journal des modifications"; - readonly he: "יומן שינויים"; - readonly ja: "変更履歴"; - readonly ko: "변경 로그"; - readonly ru: "История изменений"; - readonly zh: "更新日志"; - }; - readonly "obsidianLiveSyncSettingTab.panelGeneralSettings": { - readonly def: "General Settings"; - readonly es: "Configuraciones Generales"; - readonly fr: "Paramètres généraux"; - readonly he: "הגדרות כלליות"; - readonly ja: "一般設定"; - readonly ko: "일반 설정"; - readonly ru: "Основные настройки"; - readonly zh: "常规设置"; - }; - readonly "obsidianLiveSyncSettingTab.panelPrivacyEncryption": { - readonly def: "Privacy & Encryption"; - readonly es: "Privacidad y Cifrado"; - readonly fr: "Confidentialité et chiffrement"; - readonly he: "פרטיות והצפנה"; - readonly ja: "プライバシーと暗号化"; - readonly ko: "개인정보 보호 및 암호화"; - readonly ru: "Конфиденциальность и шифрование"; - readonly zh: "隐私与加密"; - }; - readonly "obsidianLiveSyncSettingTab.panelRemoteConfiguration": { - readonly def: "Remote Configuration"; - readonly es: "Configuración remota"; - readonly fr: "Configuration distante"; - readonly he: "תצורת שרת מרוחד"; - readonly ja: "リモート設定"; - readonly ko: "원격 구성"; - readonly ru: "Удалённая конфигурация"; - readonly zh: "远程配置"; - }; - readonly "obsidianLiveSyncSettingTab.panelSetup": { - readonly def: "Setup"; - readonly es: "Configuración"; - readonly fr: "Configuration"; - readonly he: "הגדרה"; - readonly ja: "セットアップ"; - readonly ko: "설정"; - readonly ru: "Настройка"; - readonly zh: "设置"; - }; - readonly "obsidianLiveSyncSettingTab.serverVersion": { - readonly def: "Server info: ${info}"; - readonly fr: "Infos serveur : ${info}"; - readonly he: "פרטי שרת: ${info}"; - readonly ja: "サーバー情報: ${info}"; - readonly ru: "Информация о сервере: info"; - readonly zh: "服务器信息: ${info}"; - }; - readonly "obsidianLiveSyncSettingTab.titleActiveRemoteServer": { - readonly def: "Active Remote Server"; - readonly fr: "Serveur distant actif"; - readonly he: "שרת מרוחד פעיל"; - readonly ja: "アクティブなリモートサーバー"; - readonly ru: "Активный удалённый сервер"; - readonly zh: "活动远程服务器"; - }; - readonly "obsidianLiveSyncSettingTab.titleAppearance": { - readonly def: "Appearance"; - readonly es: "Apariencia"; - readonly fr: "Apparence"; - readonly he: "מראה"; - readonly ja: "外観"; - readonly ko: "외관"; - readonly ru: "Внешний вид"; - readonly zh: "外观"; - readonly "zh-tw": "外觀"; - }; - readonly "obsidianLiveSyncSettingTab.titleConflictResolution": { - readonly def: "Conflict resolution"; - readonly es: "Resolución de conflictos"; - readonly fr: "Résolution des conflits"; - readonly he: "פתרון קונפליקטים"; - readonly ja: "競合解決"; - readonly ko: "충돌 해결"; - readonly ru: "Разрешение конфликтов"; - readonly zh: "冲突处理"; - readonly "zh-tw": "衝突處理"; - }; - readonly "obsidianLiveSyncSettingTab.titleCongratulations": { - readonly def: "Congratulations!"; - readonly es: "¡Felicidades!"; - readonly fr: "Félicitations !"; - readonly he: "מזל טוב!"; - readonly ja: "おめでとうございます!"; - readonly ko: "축하합니다!"; - readonly ru: "Поздравляем!"; - readonly zh: "恭喜!"; - readonly "zh-tw": "恭喜!"; - }; - readonly "obsidianLiveSyncSettingTab.titleCouchDB": { - readonly def: "CouchDB"; - readonly es: "Servidor CouchDB"; - readonly fr: "CouchDB"; - readonly he: "CouchDB"; - readonly ja: "CouchDB サーバー"; - readonly ko: "CouchDB 서버"; - readonly ru: "Сервер CouchDB"; - readonly zh: "CouchDB 服务器"; - readonly "zh-tw": "CouchDB 伺服器"; - }; - readonly "obsidianLiveSyncSettingTab.titleDeletionPropagation": { - readonly def: "Deletion Propagation"; - readonly es: "Propagación de eliminación"; - readonly fr: "Propagation des suppressions"; - readonly he: "הפצת מחיקות"; - readonly ja: "削除の伝播"; - readonly ko: "삭제 전파"; - readonly ru: "Распространение удалений"; - readonly zh: "删除传播"; - readonly "zh-tw": "刪除傳播"; - }; - readonly "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": { - readonly def: "Encryption is not enabled"; - readonly es: "El cifrado no está habilitado"; - readonly fr: "Le chiffrement n'est pas activé"; - readonly he: "ההצפנה אינה מופעלת"; - readonly ja: "暗号化が有効になっていません"; - readonly ko: "암호화가 활성화되지 않음"; - readonly ru: "Шифрование не включено"; - readonly zh: "尚未启用加密"; - readonly "zh-tw": "尚未啟用加密"; - }; - readonly "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": { - readonly def: "Encryption Passphrase Invalid"; - readonly es: "La frase de contraseña de cifrado es inválida"; - readonly fr: "Phrase secrète de chiffrement invalide"; - readonly he: "ביטוי סיסמה להצפנה לא תקין"; - readonly ja: "暗号化パスフレーズが無効です"; - readonly ko: "암호화 패스프레이즈 유효하지 않음"; - readonly ru: "Парольная фраза шифрования недействительна"; - readonly zh: "加密密码短语无效"; - readonly "zh-tw": "加密密語無效"; - }; - readonly "obsidianLiveSyncSettingTab.titleExtraFeatures": { - readonly def: "Enable extra and advanced features"; - readonly es: "Habilitar funciones extras y avanzadas"; - readonly fr: "Activer les fonctionnalités supplémentaires et avancées"; - readonly he: "הפעל תכונות נוספות ומתקדמות"; - readonly ja: "追加および上級機能を有効化"; - readonly ko: "추가 및 고급 기능 활성화"; - readonly ru: "Включить дополнительные и расширенные функции"; - readonly zh: "启用额外和高级功能"; - }; - readonly "obsidianLiveSyncSettingTab.titleFetchConfig": { - readonly def: "Fetch Config"; - readonly es: "Obtener configuración"; - readonly fr: "Récupérer la configuration"; - readonly he: "משוך תצורה"; - readonly ja: "設定を取得"; - readonly ko: "구성 가져오기"; - readonly ru: "Загрузить конфигурацию"; - readonly zh: "获取配置"; - readonly "zh-tw": "抓取設定"; - }; - readonly "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": { - readonly def: "Fetch config from remote server"; - readonly es: "Obtener configuración del servidor remoto"; - readonly fr: "Récupérer la configuration depuis le serveur distant"; - readonly he: "משוך תצורה מהשרת המרוחד"; - readonly ja: "リモートサーバーから設定を取得"; - readonly ko: "원격 서버에서 구성 가져오기"; - readonly ru: "Загрузить конфигурацию с удалённого сервера"; - readonly zh: "从远程服务器获取配置"; - }; - readonly "obsidianLiveSyncSettingTab.titleFetchSettings": { - readonly def: "Fetch Settings"; - readonly es: "Obtener configuraciones"; - readonly fr: "Récupérer les paramètres"; - readonly he: "משוך הגדרות"; - readonly ja: "設定の取得"; - readonly ko: "설정 가져오기"; - readonly ru: "Загрузить настройки"; - readonly zh: "获取设置"; - }; - readonly "obsidianLiveSyncSettingTab.titleHiddenFiles": { - readonly def: "Hidden Files"; - readonly es: "Archivos ocultos"; - readonly fr: "Fichiers cachés"; - readonly he: "קבצים נסתרים"; - readonly ja: "隠しファイル"; - readonly ko: "숨김 파일"; - readonly ru: "Скрытые файлы"; - readonly zh: "隐藏文件"; - readonly "zh-tw": "隱藏檔案"; - }; - readonly "obsidianLiveSyncSettingTab.titleLogging": { - readonly def: "Logging"; - readonly es: "Registro"; - readonly fr: "Journalisation"; - readonly he: "רישום יומן"; - readonly ja: "ログ"; - readonly ko: "로깅"; - readonly ru: "Логирование"; - readonly zh: "日志"; - readonly "zh-tw": "記錄"; - }; - readonly "obsidianLiveSyncSettingTab.titleMinioS3R2": { - readonly def: "Minio,S3,R2"; - readonly es: "MinIO, S3, R2"; - readonly fr: "Minio, S3, R2"; - readonly he: "Minio,S3,R2"; - readonly ja: "MinIO、S3、R2"; - readonly ko: "MinIO, S3, R2"; - readonly ru: "MinIO, S3, R2"; - readonly zh: "MinIO、S3、R2"; - readonly "zh-tw": "MinIO、S3、R2"; - }; - readonly "obsidianLiveSyncSettingTab.titleNotification": { - readonly def: "Notification"; - readonly es: "Notificación"; - readonly fr: "Notification"; - readonly he: "התראה"; - readonly ja: "通知"; - readonly ko: "알림"; - readonly ru: "Уведомления"; - readonly zh: "通知"; - readonly "zh-tw": "通知"; - }; - readonly "obsidianLiveSyncSettingTab.titleOnlineTips": { - readonly def: "Online Tips"; - readonly es: "Consejos en línea"; - readonly fr: "Conseils en ligne"; - readonly he: "טיפים אונליין"; - readonly ja: "オンラインヒント"; - readonly ko: "온라인 팁"; - readonly ru: "Онлайн советы"; - readonly zh: "在线提示"; - }; - readonly "obsidianLiveSyncSettingTab.titleQuickSetup": { - readonly def: "Quick Setup"; - readonly es: "Configuración rápida"; - readonly fr: "Configuration rapide"; - readonly he: "הגדרה מהירה"; - readonly ja: "クイックセットアップ"; - readonly ko: "빠른 설정"; - readonly ru: "Быстрая настройка"; - readonly zh: "快速设置"; - }; - readonly "obsidianLiveSyncSettingTab.titleRebuildRequired": { - readonly def: "Rebuild Required"; - readonly es: "Reconstrucción necesaria"; - readonly fr: "Reconstruction requise"; - readonly he: "נדרשת בנייה מחדש"; - readonly ja: "再構築が必要"; - readonly ko: "재구축 필요"; - readonly ru: "Требуется перестроение"; - readonly zh: "需要重建"; - }; - readonly "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": { - readonly def: "Remote Configuration Check Failed"; - readonly es: "La verificación de configuración remota falló"; - readonly fr: "Échec de la vérification de la configuration distante"; - readonly he: "בדיקת תצורת שרת מרוחד נכשלה"; - readonly ja: "リモート設定の確認に失敗"; - readonly ko: "원격 구성 확인 실패"; - readonly ru: "Проверка удалённой конфигурации не удалась"; - readonly zh: "远端配置检查失败"; - readonly "zh-tw": "遠端設定檢查失敗"; - }; - readonly "obsidianLiveSyncSettingTab.titleRemoteServer": { - readonly def: "Remote Server"; - readonly es: "Servidor remoto"; - readonly fr: "Serveur distant"; - readonly he: "שרת מרוחד"; - readonly ja: "リモートサーバー"; - readonly ko: "원격 서버"; - readonly ru: "Удалённый сервер"; - readonly zh: "远端服务器"; - readonly "zh-tw": "遠端伺服器"; - }; - readonly "obsidianLiveSyncSettingTab.titleReset": { - readonly def: "Reset"; - readonly es: "Reiniciar"; - readonly fr: "Réinitialiser"; - readonly he: "אתחול"; - readonly ja: "リセット"; - readonly ko: "리셋"; - readonly ru: "Сброс"; - readonly zh: "重置"; - }; - readonly "obsidianLiveSyncSettingTab.titleSetupOtherDevices": { - readonly def: "To setup other devices"; - readonly es: "Para configurar otros dispositivos"; - readonly fr: "Pour configurer d'autres appareils"; - readonly he: "להגדרת מכשירים אחרים"; - readonly ja: "他のデバイスのセットアップ"; - readonly ko: "다른 기기 설정"; - readonly ru: "Для настройки других устройств"; - readonly zh: "设置其他设备"; - }; - readonly "obsidianLiveSyncSettingTab.titleSynchronizationMethod": { - readonly def: "Synchronization Method"; - readonly es: "Método de sincronización"; - readonly fr: "Méthode de synchronisation"; - readonly he: "שיטת סנכרון"; - readonly ja: "同期方法"; - readonly ko: "동기화 방법"; - readonly ru: "Метод синхронизации"; - readonly zh: "同步方式"; - readonly "zh-tw": "同步方式"; - }; - readonly "obsidianLiveSyncSettingTab.titleSynchronizationPreset": { - readonly def: "Synchronization Preset"; - readonly es: "Preestablecimiento de sincronización"; - readonly fr: "Préréglage de synchronisation"; - readonly he: "קבוע מראש לסנכרון"; - readonly ja: "同期プリセット"; - readonly ko: "동기화 프리셋"; - readonly ru: "Пресет синхронизации"; - readonly zh: "同步预设"; - readonly "zh-tw": "同步預設"; - }; - readonly "obsidianLiveSyncSettingTab.titleSyncSettings": { - readonly def: "Sync Settings"; - readonly es: "Configuraciones de Sincronización"; - readonly fr: "Paramètres de synchronisation"; - readonly he: "הגדרות סנכרון"; - readonly ja: "同期設定"; - readonly ko: "동기화 설정"; - readonly ru: "Настройки синхронизации"; - readonly zh: "同步设置"; - }; - readonly "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": { - readonly def: "Sync Settings via Markdown"; - readonly es: "Configuración de sincronización a través de Markdown"; - readonly fr: "Synchroniser les paramètres via Markdown"; - readonly he: "סנכרון הגדרות דרך Markdown"; - readonly ja: "Markdown経由で設定を同期"; - readonly ko: "마크다운을 통한 동기화 설정"; - readonly ru: "Синхронизация настроек через Markdown"; - readonly zh: "通过 Markdown 同步设置"; - readonly "zh-tw": "透過 Markdown 同步設定"; - }; - readonly "obsidianLiveSyncSettingTab.titleUpdateThinning": { - readonly def: "Update Thinning"; - readonly es: "Actualización de adelgazamiento"; - readonly fr: "Lissage des mises à jour"; - readonly he: "דילול עדכונים"; - readonly ja: "更新の間引き"; - readonly ko: "업데이트 솎아내기"; - readonly ru: "Оптимизация обновлений"; - readonly zh: "更新精简"; - readonly "zh-tw": "更新精簡"; - }; - readonly "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": { - readonly def: "⚠ CORS Origin is unmatched ${from}->${to}"; - readonly es: "⚠ El origen de CORS no coincide: {from}->{to}"; - readonly fr: "⚠ L'origine CORS ne correspond pas ${from}->${to}"; - readonly he: "⚠ CORS Origin אינו תואם ${from}->${to}"; - readonly ja: "⚠ CORS Originが一致しません ${from}->${to}"; - readonly ko: "⚠ CORS 원점이 일치하지 않습니다 {from}->{to}"; - readonly ru: "⚠ CORS Origin не совпадает from->to"; - readonly zh: "⚠ CORS 源不匹配 {from}->{to}"; - }; - readonly "obsidianLiveSyncSettingTab.warnNoAdmin": { - readonly def: "⚠ You do not have administrator privileges."; - readonly es: "⚠ No tienes privilegios de administrador."; - readonly fr: "⚠ Vous n'avez pas les privilèges administrateur."; - readonly he: "⚠ אין לך הרשאות מנהל."; - readonly ja: "⚠ 管理者権限がありません。"; - readonly ko: "⚠ 관리자 권한이 없습니다."; - readonly ru: "⚠ У вас нет прав администратора."; - readonly zh: "⚠ 您没有管理员权限"; - }; - readonly Ok: { - readonly def: "Ok"; - readonly es: "Aceptar"; - readonly ja: "OK"; - readonly ko: "확인"; - readonly ru: "ОК"; - readonly zh: "确定"; - readonly "zh-tw": "確定"; - }; - readonly "Old Algorithm": { - readonly def: "Old Algorithm"; - readonly es: "Algoritmo antiguo"; - readonly ja: "旧アルゴリズム"; - readonly ko: "이전 알고리즘"; - readonly ru: "Старый алгоритм"; - readonly zh: "旧算法"; - readonly "zh-tw": "舊演算法"; - }; - readonly "Older fallback (Slow, W/O WebAssembly)": { - readonly def: "Older fallback (Slow, W/O WebAssembly)"; - readonly es: "Alternativa anterior (lenta, sin WebAssembly)"; - readonly ja: "旧フォールバック (低速、WebAssembly なし)"; - readonly ko: "이전 대체 방식 (느림, WebAssembly 없음)"; - readonly ru: "Старый вариант fallback (медленный, без WebAssembly)"; - readonly zh: "旧版回退(较慢,无 WebAssembly)"; - readonly "zh-tw": "舊版回退(較慢,無 WebAssembly)"; - }; - readonly Open: { - readonly def: "Open"; - readonly es: "Abrir"; - readonly ja: "開く"; - readonly ko: "열기"; - readonly ru: "Открыть"; - readonly zh: "打开"; - }; - readonly "Open the dialog": { - readonly def: "Open the dialog"; - readonly es: "Abrir el diálogo"; - readonly ja: "ダイアログを開く"; - readonly ko: "대화상자 열기"; - readonly ru: "Открыть диалог"; - readonly zh: "打开对话框"; - }; - readonly Overwrite: { - readonly def: "Overwrite"; - readonly es: "Sobrescribir"; - readonly ja: "上書き"; - readonly ko: "덮어쓰기"; - readonly ru: "Перезаписать"; - readonly zh: "覆盖"; - }; - readonly "Overwrite patterns": { - readonly def: "Overwrite patterns"; - readonly es: "Patrones de sobrescritura"; - readonly ja: "上書きパターン"; - readonly ko: "덮어쓰기 패턴"; - readonly ru: "Шаблоны перезаписи"; - readonly zh: "覆盖模式"; - readonly "zh-tw": "覆寫模式"; - }; - readonly "Overwrite remote": { - readonly def: "Overwrite remote"; - readonly es: "Sobrescribir remoto"; - readonly ja: "リモートを上書き"; - readonly ko: "원격 덮어쓰기"; - readonly ru: "Перезаписать удалённое хранилище"; - readonly zh: "覆盖远端"; - readonly "zh-tw": "覆寫遠端"; - }; - readonly "Overwrite remote with local DB and passphrase.": { - readonly def: "Overwrite remote with local DB and passphrase."; - readonly es: "Sobrescribe el remoto con la base de datos local y la frase de contraseña."; - readonly ja: "ローカル DB とパスフレーズでリモートを上書きします。"; - readonly ko: "로컬 DB와 암호문구로 원격을 덮어씁니다."; - readonly ru: "Перезаписать удалённое хранилище локальной БД и парольной фразой."; - readonly zh: "使用本地数据库和密码短语覆盖远端。"; - readonly "zh-tw": "使用本機資料庫與密語覆寫遠端。"; - }; - readonly "Overwrite Server Data with This Device's Files": { - readonly def: "Overwrite Server Data with This Device's Files"; - readonly es: "Sobrescribir los datos del servidor con los archivos de este dispositivo"; - readonly ja: "このデバイスのファイルでサーバーデータを上書き"; - readonly ko: "이 기기의 파일로 서버 데이터를 덮어쓰기"; - readonly ru: "Перезаписать данные сервера файлами с этого устройства"; - readonly zh: "用本设备文件覆盖服务器数据"; - readonly "zh-tw": "以此裝置的檔案覆寫伺服器資料"; - }; - readonly "P2P.AskPassphraseForDecrypt": { - readonly def: "The remote peer shared the configuration. Please input the passphrase to decrypt the configuration."; - readonly fr: "Le pair distant a partagé la configuration. Veuillez saisir la phrase secrète pour déchiffrer la configuration."; - readonly he: "העמית המרוחד שיתף את התצורה. אנא הזן את ביטוי הסיסמה לפענוח התצורה."; - readonly ja: "リモートピアから設定が共有されました。設定を復号するためのパスフレーズを入力してください。"; - readonly ko: "원격 피어가 구성을 공유했습니다. 구성을 복호화하려면 패스프레이즈를 입력해 주세요."; - readonly ru: "Удалённое устройство предоставило конфигурацию. Введите пароль для расшифровки."; - readonly zh: "远程对等方共享了配置,请输入密码短语以解密配置"; - }; - readonly "P2P.AskPassphraseForShare": { - readonly def: "The remote peer requested this device configuration. Please input the passphrase to share the configuration. You can ignore the request by cancelling this dialogue."; - readonly fr: "Le pair distant a demandé la configuration de cet appareil. Veuillez saisir la phrase secrète pour partager la configuration. Vous pouvez ignorer la demande en annulant cette boîte de dialogue."; - readonly he: "העמית המרוחד ביקש את תצורת מכשיר זה. אנא הזן את ביטוי הסיסמה לשיתוף התצורה. ניתן להתעלם מהבקשה על ידי ביטול הדיאלוג."; - readonly ja: "リモートピアからこのデバイスの設定が要求されました。設定を共有するためのパスフレーズを入力してください。このダイアログをキャンセルすることでリクエストを無視できます。"; - readonly ko: "원격 피어가 이 기기의 구성을 요청했습니다. 구성을 공유하려면 패스프레이즈를 입력해 주세요. 이 대화상자를 취소하여 요청을 무시할 수 있습니다."; - readonly ru: "Удалённое устройство запрашивает эту конфигурацию. Введите пароль для передачи."; - readonly zh: "远程对等方请求此设备配置,请输入密码短语以共享配置。你可以通过取消此对话框来忽略此请求"; - }; - readonly "P2P.DisabledButNeed": { - readonly def: "Peer-to-Peer Sync is disabled. Do you really want to enable it?"; - readonly fr: "Synchronisation pair-à-pair est désactivé. Voulez-vous vraiment l'activer ?"; - readonly he: "%{title_p2p_sync} מנוטרל. האם אתה בטוח שברצונך להפעיל?"; - readonly ja: "Peer-to-Peer Syncは無効になっています。本当に有効にしますか?"; - readonly ko: "피어 투 피어(P2P) 동기화가 비활성화되어 있습니다. 정말로 활성화하시겠습니까?"; - readonly ru: "title_p2p_sync отключён. Вы действительно хотите включить?"; - readonly zh: "Peer-to-Peer同步 已禁用。你确定要启用它吗?"; - }; - readonly "P2P.FailedToOpen": { - readonly def: "Failed to open P2P connection to the signalling server."; - readonly fr: "Échec d'ouverture de la connexion P2P vers le serveur de signalisation."; - readonly he: "לא ניתן לפתוח חיבור P2P לשרת האותות."; - readonly ja: "シグナリングサーバーへのP2P接続を開けませんでした。"; - readonly ko: "시그널링 서버에 P2P 연결을 열 수 없습니다."; - readonly ru: "Не удалось открыть P2P подключение к серверу сигнализации."; - readonly zh: "无法打开 P2P 连接到信令服务器"; - }; - readonly "P2P.NoAutoSyncPeers": { - readonly def: "No auto-sync peers found. Please set peers on the Peer-to-Peer Sync pane."; - readonly fr: "Aucun pair de synchronisation automatique trouvé. Veuillez définir des pairs dans le panneau Synchronisation pair-à-pair."; - readonly he: "לא נמצאו עמיתים לסנכרון אוטומטי. אנא הגדר עמיתים בלוח %{long_p2p_sync}."; - readonly ja: "自動同期ピアが見つかりません。Peer-to-Peer Sync (試験機能)ペインでピアを設定してください。"; - readonly ko: "자동 동기화 피어를 찾을 수 없습니다. 피어 투 피어(P2P) 동기화 (실험 기능) 창에서 피어를 설정해 주세요."; - readonly ru: "Автосинхронизируемые устройства не найдены."; - readonly zh: "未找到自动同步的对等方,请在 Peer-to-Peer同步 (实验性) 面板中设置对等方"; - }; - readonly "P2P.NoKnownPeers": { - readonly def: "No peers has been detected, waiting incoming other peers..."; - readonly fr: "Aucun pair détecté, en attente d'autres pairs entrants..."; - readonly he: "לא זוהו עמיתים, ממתין לעמיתים נכנסים..."; - readonly ja: "ピアが検出されていません。他のピアからの接続を待機中..."; - readonly ko: "피어가 감지되지 않았습니다. 다른 피어의 접속을 기다리고 있습니다..."; - readonly ru: "Устройства не обнаружены, ожидаем другие устройства..."; - readonly zh: "未检测到对等方,正在等待其他对等方的连接..."; - }; - readonly "P2P.Note.description": { - readonly def: " This replicator allows us to synchronise our vault with other devices\nusing a peer-to-peer connection. We can use this to synchronise our vault with our other devices without using a cloud service.\nThis replicator is based on Trystero. It also uses a signalling server to establish a connection between devices. The signalling server is used to exchange connection information between devices. It does (or,should) not know or store any of our data.\n\nThe signalling server can be hosted by anyone. This is just a Nostr relay. For the sake of simplicity and checking the behaviour of the replicator, an instance of the signalling server is hosted by vrtmrz. You can use the experimental server provided by vrtmrz, or you can use any other server.\n\nBy the way, even if the signalling server does not store our data, it can see the connection information of some of our devices. Please be aware of this. Also, be cautious when using the server provided by someone else."; - readonly fr: " Ce réplicateur permet de synchroniser notre coffre avec d'autres\nappareils via une connexion pair-à-pair. Nous pouvons l'utiliser pour synchroniser notre coffre avec nos autres appareils sans recourir à un service cloud.\nCe réplicateur est basé sur Trystero. Il utilise également un serveur de signalisation pour établir une connexion entre les appareils. Le serveur de signalisation sert à échanger les informations de connexion entre appareils. Il ne connaît (ou ne devrait connaître) ni ne stocke aucune de nos données.\n\nLe serveur de signalisation peut être hébergé par n'importe qui. Il s'agit simplement d'un relais Nostr. Par souci de simplicité et pour vérifier le comportement du réplicateur, une instance du serveur de signalisation est hébergée par vrtmrz. Vous pouvez utiliser le serveur expérimental fourni par vrtmrz, ou tout autre serveur.\n\nAu passage, même si le serveur de signalisation ne stocke pas nos données, il peut voir les informations de connexion de certains de nos appareils. Soyez-en conscient. Soyez également prudent avec un serveur fourni par quelqu'un d'autre."; - readonly he: " רפליקטור זה מאפשר לסנכרן את הכספת עם מכשירים אחרים באמצעות חיבור עמית-לעמית.\nניתן להשתמש בזה לסנכרון הכספת עם מכשירים אחרים ללא שירות ענן.\nרפליקטור זה מבוסס על Trystero. הוא משתמש גם בשרת אותות לביסוס חיבור בין מכשירים. שרת האותות משמש להחלפת מידע חיבור בין מכשירים. הוא אינו (ולא אמור) לדעת או לאחסן את הנתונים שלנו.\n\nשרת האותות יכול להיות מאוחסן על ידי כל אחד. זהו ממסר Nostr בלבד. לצורך פשטות ובדיקת התנהגות הרפליקטור, vrtmrz מאחסן עותק של שרת האותות. ניתן להשתמש בשרת הניסיוני של vrtmrz, או בכל שרת אחר.\n\nאגב, גם אם שרת האותות אינו מאחסן נתונים, הוא יכול לראות מידע חיבור של חלק ממכשיריך. אנא שים לב לכך. כמו כן, היה זהיר בשימוש בשרת של מישהו אחר."; - readonly ja: "このレプリケーターは、ピアツーピア接続を使用して、Vaultを他のデバイスと同期することができます。クラウドサービスを使用せずに、他のデバイスとVaultを同期することができます。\nこのレプリケーターはTrysteroをベースにしています。デバイス間の接続を確立するためにシグナリングサーバーを使用します。シグナリングサーバーはデバイス間で接続情報を交換するために使用されます。私たちのデータを知ったり保存したりすることはありません(または、そうあるべきではありません)。\n\nシグナリングサーバーは誰でもホストできます。これは単なるNostrリレーです。簡便さとレプリケーターの動作確認のために、vrtmrzがシグナリングサーバーのインスタンスをホストしています。vrtmrzが提供する実験用サーバーを使用することも、他のサーバーを使用することもできます。\n\nなお、シグナリングサーバーが私たちのデータを保存しなくても、一部のデバイスの接続情報を見ることができます。これにご注意ください。また、他の人が提供するサーバーを使用する場合は注意してください。"; - readonly ko: "이 복제기는 피어 투 피어(P2P) 연결을 통해 다른 기기들과 볼트를 동기화할 수 있도록 합니다. 클라우드 서비스를 거치지 않고도 기기간 동기화를 구현할 수 있습니다.\n\n이 복제기는 Trystero를 기반으로 하며, 기기 간 연결을 설정하기 위해 시그널링 서버를 사용합니다. 시그널링 서버는 단순히 연결 정보를 교환하는 용도로만 사용되며, 사용자 데이터를 저장하거나 접근하지 않습니다 (또는 그래야만 합니다).\n\n시그널링 서버는 누구나 운영할 수 있으며, 이는 단순한 Nostr 릴레이입니다. 편의성과 복제기의 작동 확인을 위해 `vrtmrz`가 자체적으로 시그널링 서버 인스턴스를 운영 중입니다. 사용자는 `vrtmrz`가 제공하는 실험용 서버를 사용할 수도 있고, 별도로 자신만의 서버를 설정할 수도 있습니다.\n\n참고로, 시그널링 서버는 사용자 데이터를 저장하지 않더라도 일부 기기의 연결 정보는 볼 수 있습니다. 이 점을 유의해 주세요. 특히 타인이 운영하는 서버를 사용할 경우 주의가 필요합니다."; - readonly ru: "Этот репликатор позволяет синхронизировать хранилище с другими устройствами с использованием однорангового соединения."; - readonly zh: " This replicator allows us to synchronise our vault with other devices\nusing a peer-to-peer connection. We can use this to synchronise our vault with our other devices without using a cloud service.\nThis replicator is based on Trystero. It also uses a signaling server to establish a connection between devices. The signaling server is used to exchange connection information between devices. It does (or,should) not know or store any of our data.\n\nThe signaling server can be hosted by anyone. This is just a Nostr relay. For the sake of simplicity and checking the behaviour of the replicator, an instance of the signaling server is hosted by vrtmrz. You can use the experimental server provided by vrtmrz, or you can use any other server.\n\nBy the way, even if the signaling server does not store our data, it can see the connection information of some of our devices. Please be aware of this. Also, be cautious when using the server provided by someone else."; - }; - readonly "P2P.Note.important_note": { - readonly def: "Peer-to-Peer Replicator."; - readonly fr: "Réplicateur pair-à-pair."; - readonly he: "רפליקטור עמית-לעמית."; - readonly ja: "ピアツーピアレプリケーターの実験的実装"; - readonly ko: "피어 투 피어(P2P) 복제기의 실험적 구현입니다."; - readonly ru: "P2P репликатор."; - readonly zh: "The Experimental Implementation of the Peer-to-Peer Replicator."; - }; - readonly "P2P.Note.important_note_sub": { - readonly def: "This feature is still on the bleeding edge. Please be aware that ensure your data is backed up before using this feature. And, we would be so happy if you could contribute to the development of this feature."; - readonly fr: "Cette fonctionnalité est encore en tout début de développement. Veillez à sauvegarder vos données avant de l'utiliser. Nous serions très heureux si vous contribuiez au développement de cette fonctionnalité."; - readonly he: "תכונה זו עדיין בשלב מתקדם. ודא שהנתונים שלך מגובים לפני השימוש. ונשמח אם תוכל לתרום לפיתוח תכונה זו."; - readonly ja: "この機能はまだ実験段階です。期待通りに動作しない可能性があることにご注意ください。さらに、バグ、セキュリティの問題、その他の問題がある可能性があります。この機能は自己責任でご使用ください。この機能の開発にご協力ください。"; - readonly ko: "이 기능은 아직 실험 단계에 있습니다. 이 기능이 예상대로 작동하지 않을 수 있음을 알아주세요. 또한 버그, 보안 문제 및 기타 문제가 있을 수 있습니다. 이 기능을 사용할 때는 본인의 책임 하에 사용하세요. 이 기능의 개발에 기여해 주세요."; - readonly ru: "Эта функция всё ещё на стадии разработки. Пожалуйста, убедитесь, что ваши данные зарезервированы."; - readonly zh: "This feature is still in the experimental stage. Please be aware that this feature may not work as expected. Furthermore, it may have some bugs, security issues, and other issues. Please use this feature at your own risk. Please contribute to the development of this feature."; - }; - readonly "P2P.Note.Summary": { - readonly def: "What is this feature? (and some important notes, please read once)"; - readonly fr: "Qu'est-ce que cette fonctionnalité ? (et quelques notes importantes, à lire)"; - readonly he: "מהי תכונה זו? (ועוד הערות חשובות, נא לקרוא פעם אחת)"; - readonly ja: "この機能について(重要な注意事項を含む、一度お読みください)"; - readonly ko: "이 기능은 무엇인가요? (설명과 참고사항이 적혀있습니다. 한 번 읽어보세요!)"; - readonly ru: "Что это за функция? (важные замечания)"; - readonly zh: "What is this feature? (and some important notes, please read once)"; - }; - readonly "P2P.NotEnabled": { - readonly def: "Peer-to-Peer Sync is not enabled. We cannot open a new connection."; - readonly fr: "Synchronisation pair-à-pair n'est pas activé. Nous ne pouvons pas ouvrir de nouvelle connexion."; - readonly he: "%{title_p2p_sync} אינו מופעל. לא ניתן לפתוח חיבור חדש."; - readonly ja: "Peer-to-Peer Syncが有効になっていません。新しい接続を開くことができません。"; - readonly ko: "피어 투 피어(P2P) 동기화가 활성화되지 않았습니다. 새로운 연결을 열 수 없습니다."; - readonly ru: "title_p2p_sync не включён. Мы не можем открыть новое подключение."; - readonly zh: "Peer-to-Peer同步 is not enabled. We cannot open a new connection."; - }; - readonly "P2P.P2PReplication": { - readonly def: "Peer-to-Peer Replication"; - readonly fr: "Réplication Pair-à-Pair"; - readonly he: "שכפול %{P2P}"; - readonly ja: "Peer-to-Peerレプリケーション(複製)"; - readonly ko: "피어-to-피어 복제"; - readonly ru: "P2P Репликация"; - readonly zh: "Peer-to-Peer Replication"; - }; - readonly "P2P.PaneTitle": { - readonly def: "Peer-to-Peer Sync"; - readonly fr: "Synchronisation pair-à-pair"; - readonly he: "%{long_p2p_sync}"; - readonly ja: "Peer-to-Peer Sync (試験機能)"; - readonly ko: "피어 투 피어(P2P) 동기화 (실험 기능)"; - readonly ru: "long_p2p_sync"; - readonly zh: "Peer-to-Peer同步 (实验性)"; - }; - readonly "P2P.ReplicatorInstanceMissing": { - readonly def: "P2P Sync replicator is not found, possibly not have been configured or enabled."; - readonly fr: "Le réplicateur Sync P2P est introuvable, peut-être non configuré ou non activé."; - readonly he: "רפליקטור סנכרון P2P לא נמצא, ייתכן שלא הוגדר או הופעל."; - readonly ja: "P2P同期レプリケーターが見つかりません。設定または有効化されていない可能性があります。"; - readonly ko: "P2P 동기화 복제기를 찾을 수 없습니다. 구성되지 않았거나 활성화되지 않았을 수 있습니다."; - readonly ru: "P2P Sync репликатор не найден, возможно, не настроен."; - readonly zh: "P2P Sync replicator is not found, possibly not have been configured or enabled."; - }; - readonly "P2P.SeemsOffline": { - readonly def: "Peer ${name} seems offline, skipped."; - readonly fr: "Le pair ${name} semble hors ligne, ignoré."; - readonly he: "העמית ${name} נראה לא מחובר, מדלג."; - readonly ja: "ピア${name}はオフラインのようです。スキップしました。"; - readonly ko: "피어 ${name}이(가) 오프라인인 것 같습니다. 건너뜁니다."; - readonly ru: "Устройство name офлайн, пропущено."; - readonly zh: "Peer ${name} seems offline, skipped."; - }; - readonly "P2P.SyncAlreadyRunning": { - readonly def: "P2P Sync is already running."; - readonly fr: "La Sync P2P est déjà en cours."; - readonly he: "סנכרון P2P כבר פועל."; - readonly ja: "P2P同期はすでに実行中です。"; - readonly ko: "P2P 동기화가 이미 실행 중입니다."; - readonly ru: "P2P Sync уже запущен."; - readonly zh: "P2P Sync is already running."; - }; - readonly "P2P.SyncCompleted": { - readonly def: "P2P Sync completed."; - readonly fr: "Sync P2P terminée."; - readonly he: "סנכרון P2P הושלם."; - readonly ja: "P2P同期が完了しました。"; - readonly ko: "P2P 동기화가 완료되었습니다."; - readonly ru: "P2P Sync завершён."; - readonly zh: "P2P Sync completed."; - }; - readonly "P2P.SyncStartedWith": { - readonly def: "P2P Sync with ${name} have been started."; - readonly fr: "La Sync P2P avec ${name} a démarré."; - readonly he: "סנכרון P2P עם ${name} התחיל."; - readonly ja: "${name}とのP2P同期を開始しました。"; - readonly ko: "${name}과의 P2P 동기화가 시작되었습니다."; - readonly ru: "P2P Sync с name начат."; - readonly zh: "P2P Sync with ${name} have been started."; - }; - readonly "paneMaintenance.markDeviceResolvedAfterBackup": { - readonly def: "paneMaintenance.markDeviceResolvedAfterBackup"; - readonly es: "Marcar el dispositivo como resuelto después de hacer una copia de seguridad"; - readonly ja: "バックアップ後にこのデバイスを解決済みにする"; - readonly ko: "백업 후 장치를 해결됨으로 표시"; - readonly ru: "Пометить устройство как обработанное после резервного копирования"; - readonly zh: "请在完成备份后将此设备标记为已处理。"; - readonly "zh-tw": "請在完成備份後將此裝置標記為已處理。"; - }; - readonly "paneMaintenance.remoteLockedAndDeviceNotAccepted": { - readonly def: "paneMaintenance.remoteLockedAndDeviceNotAccepted"; - readonly es: "La base de datos remota está bloqueada y este dispositivo aún no ha sido aceptado."; - readonly ja: "リモートデータベースはロックされており、このデバイスはまだ承認されていません。"; - readonly ko: "원격 데이터베이스가 잠겨 있으며 이 장치는 아직 승인되지 않았습니다."; - readonly ru: "Удалённая база данных заблокирована, и это устройство ещё не одобрено."; - readonly zh: "远端数据库已锁定,且此设备尚未被接受。"; - readonly "zh-tw": "遠端資料庫已鎖定,且此裝置尚未被接受。"; - }; - readonly "paneMaintenance.remoteLockedResolvedDevice": { - readonly def: "paneMaintenance.remoteLockedResolvedDevice"; - readonly es: "La base de datos remota está bloqueada, pero este dispositivo ya fue aceptado."; - readonly ja: "リモートデータベースはロックされていますが、このデバイスはすでに承認されています。"; - readonly ko: "원격 데이터베이스가 잠겨 있지만 이 장치는 이미 승인되었습니다."; - readonly ru: "Удалённая база данных заблокирована, но это устройство уже одобрено."; - readonly zh: "远端数据库已锁定,但此设备已被接受。"; - readonly "zh-tw": "遠端資料庫已鎖定,但此裝置已被接受。"; - }; - readonly "paneMaintenance.unlockDatabaseReady": { - readonly def: "paneMaintenance.unlockDatabaseReady"; - readonly es: "Desbloquear la base de datos"; - readonly ja: "データベースのロックを解除"; - readonly ko: "데이터베이스 잠금 해제"; - readonly ru: "Разблокировать базу данных"; - readonly zh: "现在可以解锁数据库。"; - readonly "zh-tw": "現在可以解鎖資料庫。"; - }; - readonly Passphrase: { - readonly def: "Passphrase"; - readonly es: "Frase de contraseña"; - readonly fr: "Phrase secrète"; - readonly he: "ביטוי סיסמה"; - readonly ja: "パスフレーズ"; - readonly ko: "패스프레이즈"; - readonly ru: "Парольная фраза"; - readonly zh: "密码"; - }; - readonly "Passphrase of sensitive configuration items": { - readonly def: "Passphrase of sensitive configuration items"; - readonly es: "Frase para elementos sensibles"; - readonly fr: "Phrase secrète des éléments de configuration sensibles"; - readonly he: "ביטוי סיסמה לפריטי תצורה רגישים"; - readonly ja: "機密性の高い設定項目にパスフレーズを使用"; - readonly ko: "민감한 구성 항목의 패스프레이즈"; - readonly ru: "Парольная фраза для конфиденциальных настроек"; - readonly zh: "敏感配置项的密码"; - }; - readonly password: { - readonly def: "password"; - readonly es: "contraseña"; - readonly fr: "mot de passe"; - readonly he: "סיסמה"; - readonly ja: "パスワード"; - readonly ko: "비밀번호"; - readonly ru: "пароль"; - readonly zh: "密码"; - }; - readonly Password: { - readonly def: "Password"; - readonly es: "Contraseña"; - readonly fr: "Mot de passe"; - readonly he: "סיסמה"; - readonly ja: "パスワード"; - readonly ko: "비밀번호"; - readonly ru: "Пароль"; - readonly zh: "密码"; - }; - readonly "Paste a connection string": { - readonly def: "Paste a connection string"; - readonly es: "Pegar cadena de conexión"; - readonly ja: "接続文字列を貼り付ける"; - readonly ko: "연결 문자열 붙여넣기"; - readonly ru: "Вставить строку подключения"; - readonly zh: "粘贴连接字符串"; - readonly "zh-tw": "貼上連線字串"; - }; - readonly "Paste the Setup URI generated from one of your active devices.": { - readonly def: "Paste the Setup URI generated from one of your active devices."; - readonly es: "Pegue el URI de configuración generado desde uno de sus dispositivos activos。"; - readonly ja: "稼働中の端末で生成した Setup URI を貼り付けてください。"; - readonly ko: "현재 사용 중인 장치 중 하나에서 생성한 설정 URI를 붙여 넣으세요。"; - readonly ru: "Вставьте Setup URI, созданный на одном из ваших активных устройств。"; - readonly zh: "粘贴从一台已在使用的设备上生成的 Setup URI。"; - readonly "zh-tw": "貼上從一台已在使用裝置上產生的 Setup URI。"; - }; - readonly "Path Obfuscation": { - readonly def: "Path Obfuscation"; - readonly es: "Ofuscación de rutas"; - readonly fr: "Obfuscation des chemins"; - readonly he: "ערפול נתיב"; - readonly ja: "パスの難読化"; - readonly ko: "경로 난독화"; - readonly ru: "Обфускация путей"; - readonly zh: "路径混淆"; - }; - readonly "Patterns to match files for overwriting instead of merging": { - readonly def: "Patterns to match files for overwriting instead of merging"; - readonly es: "Patrones para identificar archivos que se sobrescribirán en lugar de fusionarse"; - readonly ja: "マージではなく上書きするファイルを判定するパターン"; - readonly ko: "병합 대신 덮어쓸 파일을 판별하는 패턴"; - readonly ru: "Шаблоны для файлов, которые нужно перезаписывать вместо объединения"; - readonly zh: "用于匹配需覆盖而非合并文件的模式"; - readonly "zh-tw": "用於匹配需覆寫而非合併檔案的模式"; - }; - readonly "Patterns to match files for syncing": { - readonly def: "Patterns to match files for syncing"; - readonly es: "Patrones para identificar archivos que se sincronizarán"; - readonly ja: "同期対象ファイルを判定するパターン"; - readonly ko: "동기화할 파일을 판별하는 패턴"; - readonly ru: "Шаблоны для файлов, которые нужно синхронизировать"; - readonly zh: "用于匹配同步文件的模式"; - readonly "zh-tw": "用於匹配同步檔案的模式"; - }; - readonly "Peer-to-Peer only": { - readonly def: "Peer-to-Peer only"; - readonly es: "Solo Peer-to-Peer"; - readonly ja: "Peer-to-Peer のみ"; - readonly ko: "Peer-to-Peer 전용"; - readonly ru: "Только Peer-to-Peer"; - readonly zh: "仅 Peer-to-Peer"; - readonly "zh-tw": "僅 Peer-to-Peer"; - }; - readonly "Peer-to-Peer Synchronisation": { - readonly def: "Peer-to-Peer Synchronisation"; - readonly es: "Sincronización entre pares"; - readonly ja: "ピアツーピア同期"; - readonly ko: "피어 투 피어 동기화"; - readonly ru: "Одноранговая синхронизация"; - readonly zh: "点对点同步"; - readonly "zh-tw": "點對點同步"; - }; - readonly "Per-file-saved customization sync": { - readonly def: "Per-file-saved customization sync"; - readonly es: "Sincronización de personalización por archivo"; - readonly fr: "Synchronisation de personnalisation enregistrée par fichier"; - readonly he: "סנכרון התאמה אישית שנשמר לפי קובץ"; - readonly ja: "ファイルごとのカスタマイズ同期"; - readonly ko: "파일별 저장 사용자 설정 동기화"; - readonly ru: "Синхронизация настроек для каждого файла"; - readonly zh: "按文件保存的自定义同步"; - }; - readonly Perform: { - readonly def: "Perform"; - readonly es: "Ejecutar"; - readonly ja: "実行"; - readonly ko: "실행"; - readonly ru: "Выполнить"; - readonly zh: "执行"; - readonly "zh-tw": "執行"; - }; - readonly "Perform cleanup": { - readonly def: "Perform cleanup"; - readonly es: "Ejecutar limpieza"; - readonly ja: "クリーンアップを実行"; - readonly ko: "정리 실행"; - readonly ru: "Выполнить очистку"; - readonly zh: "执行清理"; - readonly "zh-tw": "執行清理"; - }; - readonly "Perform Garbage Collection": { - readonly def: "Perform Garbage Collection"; - readonly es: "Ejecutar recolección de basura"; - readonly ja: "ガーベジコレクションを実行"; - readonly ko: "가비지 컬렉션 실행"; - readonly ru: "Выполнить сборку мусора"; - readonly zh: "执行垃圾回收"; - readonly "zh-tw": "執行垃圾回收"; - }; - readonly "Perform Garbage Collection to remove unused chunks and reduce database size.": { - readonly def: "Perform Garbage Collection to remove unused chunks and reduce database size."; - readonly es: "Ejecuta la recolección de basura para eliminar chunks no usados y reducir el tamaño de la base de datos."; - readonly ja: "未使用のチャンクを削除し、データベースサイズを削減するためにガーベジコレクションを実行します。"; - readonly ko: "사용하지 않는 청크를 제거하고 데이터베이스 크기를 줄이기 위해 가비지 컬렉션을 실행합니다."; - readonly ru: "Выполняет сборку мусора, чтобы удалить неиспользуемые чанки и уменьшить размер базы данных."; - readonly zh: "执行垃圾回收以清理未使用的 chunks 并减小数据库体积。"; - readonly "zh-tw": "執行垃圾回收以移除未使用的 chunks 並減少資料庫大小。"; - }; - readonly "Periodic Sync interval": { - readonly def: "Periodic Sync interval"; - readonly es: "Intervalo de sincronización periódica"; - readonly fr: "Intervalle de synchronisation périodique"; - readonly he: "מרווח סנכרון תקופתי"; - readonly ja: "定時同期の感覚"; - readonly ko: "주기적 동기화 간격"; - readonly ru: "Интервал периодической синхронизации"; - readonly zh: "定期同步间隔"; - }; - readonly "Pick a file to resolve conflict": { - readonly def: "Pick a file to resolve conflict"; - readonly es: "Elegir un archivo para resolver el conflicto"; - readonly ja: "競合を解決するファイルを選択"; - readonly ko: "충돌을 해결할 파일 선택"; - readonly ru: "Выбрать файл для разрешения конфликта"; - readonly zh: "选择要解决冲突的文件"; - readonly "zh-tw": "選擇要解決衝突的檔案"; - }; - readonly "Pick a file to show history": { - readonly def: "Pick a file to show history"; - readonly "zh-tw": "選擇要顯示歷程的檔案"; - }; - readonly "Please disable 'Read chunks online' in settings to use Garbage Collection.": { - readonly def: "Please disable 'Read chunks online' in settings to use Garbage Collection."; - readonly ja: "Garbage Collection を使うには、設定で「Read chunks online」を無効にしてください。"; - readonly ko: "Garbage Collection을 사용하려면 설정에서 \"Read chunks online\"을 비활성화해 주세요."; - readonly ru: "Чтобы использовать Garbage Collection, отключите в настройках «Read chunks online»."; - readonly zh: "要使用垃圾回收,请在设置中禁用“Read chunks online”。"; - readonly "zh-tw": "若要使用垃圾回收,請在設定中停用「Read chunks online」。"; - }; - readonly "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": { - readonly def: "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection."; - readonly ja: "Garbage Collection を使うには、設定で「Compute revisions for chunks」を有効にしてください。"; - readonly ko: "Garbage Collection을 사용하려면 설정에서 \"Compute revisions for chunks\"를 활성화해 주세요."; - readonly ru: "Чтобы использовать Garbage Collection, включите в настройках «Compute revisions for chunks»."; - readonly zh: "要使用垃圾回收,请在设置中启用“Compute revisions for chunks”。"; - readonly "zh-tw": "若要使用垃圾回收,請在設定中啟用「Compute revisions for chunks」。"; - }; - readonly "Please select 'Cancel' explicitly to cancel this operation.": { - readonly def: "Please select 'Cancel' explicitly to cancel this operation."; - readonly ja: "この操作を中止するには、明示的に「キャンセル」を選択してください。"; - readonly ko: "이 작업을 취소하려면 반드시 \"취소\"를 명시적으로 선택해 주세요."; - readonly ru: "Чтобы отменить эту операцию, явно выберите «Отмена»."; - readonly zh: "如需取消此操作,请明确选择“取消”。"; - readonly "zh-tw": "若要取消此操作,請明確選擇「取消」。"; - }; - readonly "Please select a method to import the settings from another device.": { - readonly def: "Please select a method to import the settings from another device."; - readonly es: "Seleccione un método para importar la configuración desde otro dispositivo。"; - readonly ja: "別の端末から設定を取り込む方法を選択してください。"; - readonly ko: "다른 장치에서 설정을 가져올 방법을 선택해 주세요。"; - readonly ru: "Выберите способ импорта настроек с другого устройства。"; - readonly zh: "请选择一种从其他设备导入设置的方法。"; - readonly "zh-tw": "請選擇一種從其他裝置匯入設定的方法。"; - }; - readonly "Please select an option to proceed": { - readonly def: "Please select an option to proceed"; - readonly es: "Seleccione una opción para continuar"; - readonly ja: "続行するには項目を選択してください"; - readonly ko: "계속하려면 항목을 선택해 주세요"; - readonly ru: "Чтобы продолжить, выберите вариант"; - readonly zh: "请选择一个选项以继续"; - readonly "zh-tw": "請選擇一個選項以繼續"; - }; - readonly "Please select the type of server to which you are connecting.": { - readonly def: "Please select the type of server to which you are connecting."; - readonly es: "Seleccione el tipo de servidor al que se está conectando。"; - readonly ja: "接続するサーバーの種類を選択してください。"; - readonly ko: "연결할 서버 유형을 선택해 주세요。"; - readonly ru: "Выберите тип сервера, к которому вы подключаетесь。"; - readonly zh: "请选择你要连接的服务器类型。"; - readonly "zh-tw": "請選擇你要連線的伺服器類型。"; - }; - readonly "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": { - readonly def: "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature."; - readonly es: "Define un nombre para identificar este dispositivo. Debe ser único entre tus dispositivos. Mientras no esté configurado, no podremos habilitar esta función."; - readonly ja: "このデバイスを識別するためのデバイス名を設定してください。この名前は各デバイスで一意である必要があります。設定されるまで、この機能は有効にできません。"; - readonly ko: "이 장치를 식별할 장치 이름을 설정해 주세요. 이 이름은 장치 간에 고유해야 합니다. 설정되기 전까지는 이 기능을 활성화할 수 없습니다."; - readonly ru: "Укажите имя устройства для идентификации этого устройства. Имя должно быть уникальным среди ваших устройств. Пока оно не задано, мы не можем включить эту функцию."; - readonly zh: "请设置设备名称以标识此设备。该名称在你的所有设备之间应保持唯一;未配置前无法启用此功能。"; - }; - readonly "Please set this device name": { - readonly def: "Please set this device name"; - readonly es: "Define el nombre de este dispositivo"; - readonly ja: "このデバイス名を設定してください"; - readonly ko: "이 장치 이름을 설정해 주세요"; - readonly ru: "Укажите имя этого устройства"; - readonly zh: "请设置此设备名称"; - readonly "zh-tw": "請設定此裝置名稱"; - }; - readonly "Plug-in version": { - readonly def: "Plug-in version"; - readonly ja: "プラグインバージョン"; - readonly ko: "플러그인 버전"; - readonly ru: "Версия плагина"; - readonly zh: "插件版本"; - readonly "zh-tw": "外掛版本"; - }; - readonly "Prepare the 'report' to create an issue": { - readonly def: "Prepare the 'report' to create an issue"; - readonly fr: "Préparer le « rapport » pour créer un ticket"; - readonly he: "הכן 'דו\"ח' ליצירת Issue"; - readonly ja: "Issue 作成用の「レポート」を準備"; - readonly ko: "이슈 생성을 위한 '보고서' 준비"; - readonly ru: "Подготовить «отчёт» для создания Issue"; - readonly zh: "准备 '报告' 以创建问题单"; - readonly "zh-tw": "準備建立 Issue 用的「報告」"; - }; - readonly Presets: { - readonly def: "Presets"; - readonly es: "Preconfiguraciones"; - readonly fr: "Préréglages"; - readonly he: "קביעות מראש"; - readonly ja: "プリセット"; - readonly ko: "프리셋"; - readonly ru: "Пресеты"; - readonly zh: "预设"; - }; - readonly "Proceed Garbage Collection": { - readonly def: "Proceed Garbage Collection"; - readonly ja: "Garbage Collection を続行"; - readonly ko: "Garbage Collection 계속"; - readonly ru: "Продолжить Garbage Collection"; - readonly zh: "继续执行垃圾回收"; - readonly "zh-tw": "繼續執行垃圾回收"; - }; - readonly "Proceed with Setup URI": { - readonly def: "Proceed with Setup URI"; - readonly es: "Continuar con el URI de configuración"; - readonly ja: "Setup URI で続行"; - readonly ko: "설정 URI로 계속"; - readonly ru: "Продолжить с Setup URI"; - readonly zh: "继续使用 Setup URI"; - readonly "zh-tw": "繼續使用 Setup URI"; - }; - readonly "Proceeding with Garbage Collection, ignoring missing nodes.": { - readonly def: "Proceeding with Garbage Collection, ignoring missing nodes."; - readonly ja: "不足しているノードを無視して Garbage Collection を続行します。"; - readonly ko: "누락된 노드를 무시하고 Garbage Collection을 계속 진행합니다."; - readonly ru: "Продолжаем Garbage Collection, игнорируя отсутствующие узлы."; - readonly zh: "正在继续执行垃圾回收,并忽略缺失节点。"; - readonly "zh-tw": "正在繼續執行垃圾回收,並忽略缺失節點。"; - }; - readonly "Proceeding with Garbage Collection.": { - readonly def: "Proceeding with Garbage Collection."; - readonly ja: "Garbage Collection を実行します。"; - readonly ko: "Garbage Collection을 진행합니다."; - readonly ru: "Запускаем Garbage Collection."; - readonly zh: "正在执行垃圾回收。"; - readonly "zh-tw": "正在執行垃圾回收。"; - }; - readonly "Process small files in the foreground": { - readonly def: "Process small files in the foreground"; - readonly es: "Procesar archivos pequeños en primer plano"; - readonly fr: "Traiter les petits fichiers au premier plan"; - readonly he: "עבד קבצים קטנים בחזית"; - readonly ja: "小さいファイルを最前面で処理"; - readonly ko: "포그라운드에서 작은 파일 처리"; - readonly ru: "Обрабатывать маленькие файлы в основном потоке"; - readonly zh: "在前台处理小文件"; - }; - readonly Progress: { - readonly def: "Progress"; - readonly ja: "進捗"; - readonly ko: "진행 상태"; - readonly ru: "Прогресс"; - readonly zh: "进度"; - readonly "zh-tw": "進度"; - }; - readonly "Property Encryption": { - readonly def: "Property Encryption"; - readonly fr: "Chiffrement des propriétés"; - readonly he: "הצפנת מאפיינים"; - readonly ru: "Шифрование свойств"; - readonly zh: "属性加密"; - }; - readonly "PureJS fallback (Fast, W/O WebAssembly)": { - readonly def: "PureJS fallback (Fast, W/O WebAssembly)"; - readonly es: "Alternativa PureJS (rápida, sin WebAssembly)"; - readonly ja: "PureJS フォールバック (高速、WebAssembly なし)"; - readonly ko: "PureJS 대체 방식 (빠름, WebAssembly 없음)"; - readonly ru: "Вариант PureJS (быстрый, без WebAssembly)"; - readonly zh: "PureJS 回退(快速,无 WebAssembly)"; - readonly "zh-tw": "PureJS 回退(快速,無 WebAssembly)"; - }; - readonly "Purge all download/upload cache.": { - readonly def: "Purge all download/upload cache."; - readonly es: "Purga toda la caché de descarga y carga."; - readonly ja: "ダウンロード/アップロードキャッシュをすべて削除します。"; - readonly ko: "모든 다운로드/업로드 캐시를 제거합니다."; - readonly ru: "Очистить весь кэш загрузки/выгрузки."; - readonly zh: "清除所有下载/上传缓存。"; - readonly "zh-tw": "清除所有下載/上傳快取。"; - }; - readonly "Purge all journal counter": { - readonly def: "Purge all journal counter"; - readonly es: "Purgar todos los contadores del diario"; - readonly ja: "すべてのジャーナルカウンターを削除"; - readonly ko: "모든 저널 카운터 삭제"; - readonly ru: "Очистить все счётчики журнала"; - readonly zh: "清除所有日志计数器"; - readonly "zh-tw": "清除所有日誌計數器"; - }; - readonly "Rebuild local and remote database with local files.": { - readonly def: "Rebuild local and remote database with local files."; - readonly es: "Reconstruye la base de datos local y remota usando los archivos locales."; - readonly ja: "ローカルファイルを使ってローカルとリモートのデータベースを再構築します。"; - readonly ko: "로컬 파일로 로컬 및 원격 데이터베이스를 다시 구축합니다."; - readonly ru: "Перестроить локальную и удалённую базы данных на основе локальных файлов."; - readonly zh: "使用本地文件重建本地和远端数据库。"; - readonly "zh-tw": "以本機檔案重建本機與遠端資料庫。"; - }; - readonly "Rebuilding Operations (Remote Only)": { - readonly def: "Rebuilding Operations (Remote Only)"; - readonly es: "Operaciones de reconstrucción (solo remoto)"; - readonly ja: "再構築操作 (リモートのみ)"; - readonly ko: "재구축 작업 (원격 전용)"; - readonly ru: "Операции перестроения (только удалённое хранилище)"; - readonly zh: "重建操作(仅远端)"; - readonly "zh-tw": "重建作業(僅遠端)"; - }; - readonly "Recovery and Repair": { - readonly def: "Recovery and Repair"; - readonly "zh-tw": "修復與修補"; - }; - readonly "Recreate all": { - readonly def: "Recreate all"; - readonly es: "Recrear todo"; - readonly ja: "すべて再作成"; - readonly ko: "모두 다시 생성"; - readonly ru: "Пересоздать всё"; - readonly zh: "全部重建"; - readonly "zh-tw": "全部重建"; - }; - readonly "Recreate missing chunks for all files": { - readonly def: "Recreate missing chunks for all files"; - readonly es: "Recrear fragmentos faltantes para todos los archivos"; - readonly ja: "すべてのファイルの不足チャンクを再作成"; - readonly ko: "모든 파일의 누락된 청크 다시 생성"; - readonly ru: "Пересоздать отсутствующие чанки для всех файлов"; - readonly zh: "为所有文件重建缺失的 chunks"; - readonly "zh-tw": "為所有檔案重建遺失的 chunks"; - }; - readonly "RedFlag.Fetch.Method.Desc": { - readonly def: "How do you want to fetch?\n- Create a local database once before fetching.\n **Low Traffic**, **High CPU**, **Low Risk**\n Recommended if ...\n - Files possibly inconsistent\n - Files were not so much\n- Create local file chunks before fetching.\n **Low Traffic**, **Moderate CPU**, **Low to Moderate Risk**\n Recommended if ...\n - Files probably consistent\n - You have a lot of files.\n- Fetch everything from the remote.\n **High Traffic**, **Low CPU**, **Low to Moderate Risk**\n\n>[!INFO]- Details\n> ## Create a local database once before fetching.\n> **Low Traffic**, **High CPU**, **Low Risk**\n> This option first creates a local database using existing local files before fetching data from the remote source.\n> If matching files exist both locally and remotely, only the differences between them will be transferred.\n> However, files present in both locations will initially be handled as conflicted files. They will be resolved automatically if they are not actually conflicted, but this process may take time.\n> This is generally the safest method, minimizing data loss risk.\n> ## Create local file chunks before fetching.\n> **Low Traffic**, **Moderate CPU**, **Low to Moderate Risk** (depending operation)\n> This option first creates chunks from local files for the database, then fetches data. Consequently, only chunks missing locally are transferred. However, all metadata is taken from the remote source.\n> Local files are then compared against this metadata at launch. The content considered newer will overwrite the older one (by modified time). This outcome is then synchronised back to the remote database.\n> This is generally safe if local files are genuinely the latest timestamp. However, it can cause problems if a file has a newer timestamp but older content (like the initial `welcome.md`).\n> This uses less CPU and faster than \"Create a local database once before fetching\", but it may lead to data loss if not used carefully.\n> ## Fetch everything from the remote.\n> **High Traffic**, **Low CPU**, **Low to Moderate Risk** (depending operation)\n> All things will be fetched from the remote.\n> Similar to the Create local file chunks before fetching, but all chunks are fetched from the remote source.\n> This is the most traditional way to fetch, typically consuming the most network traffic and time. It also carries a similar risk of overwriting remote files to the 'Create local file chunks before fetching' option.\n> However, it is often considered the most stable method because it is the longest-established and most straightforward approach."; - readonly fr: "Comment voulez-vous récupérer ?\n- Créer une base locale avant de récupérer.\n **Trafic faible**, **CPU élevé**, **Risque faible**\n Recommandé si ...\n - Fichiers possiblement incohérents\n - Fichiers peu nombreux\n- Créer des fragments de fichiers locaux avant de récupérer.\n **Trafic faible**, **CPU modéré**, **Risque faible à modéré**\n Recommandé si ...\n - Fichiers probablement cohérents\n - Vous avez beaucoup de fichiers.\n- Tout récupérer depuis le distant.\n **Trafic élevé**, **CPU faible**, **Risque faible à modéré**\n\n>[!INFO]- Détails\n> ## Créer une base locale avant de récupérer.\n> **Trafic faible**, **CPU élevé**, **Risque faible**\n> Cette option crée d'abord une base locale à partir des fichiers locaux existants avant de récupérer les données depuis la source distante.\n> Si des fichiers correspondants existent à la fois localement et à distance, seules les différences entre eux seront transférées.\n> Toutefois, les fichiers présents aux deux emplacements seront initialement traités comme en conflit. Ils seront résolus automatiquement s'ils ne le sont pas réellement, mais ce processus peut prendre du temps.\n> C'est généralement la méthode la plus sûre, minimisant le risque de perte de données.\n> ## Créer des fragments de fichiers locaux avant de récupérer.\n> **Trafic faible**, **CPU modéré**, **Risque faible à modéré** (selon l'opération)\n> Cette option crée d'abord des fragments à partir des fichiers locaux pour la base, puis récupère les données. Par conséquent, seuls les fragments manquants localement sont transférés. Cependant, toutes les métadonnées sont prises de la source distante.\n> Les fichiers locaux sont ensuite comparés à ces métadonnées au lancement. Le contenu considéré comme plus récent écrasera le plus ancien (selon la date de modification). Le résultat est ensuite synchronisé vers la base distante.\n> C'est généralement sûr si les fichiers locaux ont bien l'horodatage le plus récent. Cela peut toutefois poser problème si un fichier a un horodatage plus récent mais un contenu plus ancien (comme le `welcome.md` initial).\n> Cette méthode utilise moins de CPU et est plus rapide que « Créer une base locale avant de récupérer », mais peut entraîner une perte de données si elle n'est pas utilisée avec précaution.\n> ## Tout récupérer depuis le distant.\n> **Trafic élevé**, **CPU faible**, **Risque faible à modéré** (selon l'opération)\n> Tout sera récupéré depuis le distant.\n> Similaire à Créer des fragments de fichiers locaux avant de récupérer, mais tous les fragments sont récupérés depuis la source distante.\n> C'est la façon la plus traditionnelle de récupérer, consommant généralement le plus de trafic réseau et de temps. Elle comporte également un risque similaire d'écraser les fichiers distants à l'option « Créer des fragments de fichiers locaux avant de récupérer ».\n> Elle est toutefois souvent considérée comme la méthode la plus stable car c'est la plus ancienne et la plus directe."; - readonly he: "כיצד ברצונך למשוך?\n- %{RedFlag.Fetch.Method.FetchSafer}.\n **תעבורה נמוכה**, **מעבד גבוה**, **סיכון נמוך**\n מומלץ אם...\n - קבצים עשויים להיות לא עקביים\n - אין הרבה קבצים\n- %{RedFlag.Fetch.Method.FetchSmoother}.\n **תעבורה נמוכה**, **מעבד בינוני**, **סיכון נמוך עד בינוני**\n מומלץ אם...\n - הקבצים ככל הנראה עקביים\n - יש לך הרבה קבצים.\n- %{RedFlag.Fetch.Method.FetchTraditional}.\n **תעבורה גבוהה**, **מעבד נמוך**, **סיכון נמוך עד בינוני**\n\n>[!INFO]- פרטים\n> ## %{RedFlag.Fetch.Method.FetchSafer}.\n> **תעבורה נמוכה**, **מעבד גבוה**, **סיכון נמוך**\n> אפשרות זו יוצרת תחילה מסד נתונים מקומי תוך שימוש בקבצים מקומיים קיימים לפני משיכת נתונים מהמקור המרוחד.\n> אם קיימים קבצים תואמים גם מקומית וגם מרחוק, רק ההפרשים ביניהם יועברו.\n> עם זאת, קבצים הקיימים בשני המקומות יטופלו תחילה כקבצים מתנגשים. הם ייפתרו אוטומטית אם לא מתנגשים בפועל, אך תהליך זה עשוי לקחת זמן.\n> זוהי בדרך כלל השיטה הבטוחה ביותר, ממזערת סיכון לאובדן נתונים.\n> ## %{RedFlag.Fetch.Method.FetchSmoother}.\n> **תעבורה נמוכה**, **מעבד בינוני**, **סיכון נמוך עד בינוני** (תלוי בפעולה)\n> אפשרות זו יוצרת תחילה נתחים מקבצים מקומיים למסד הנתונים, ואז מושכת נתונים. כתוצאה מכך, רק נתחים חסרים מקומית מועברים. עם זאת, כל המטה-נתונים נלקחים מהמקור המרוחד.\n> קבצים מקומיים נבדקים לאחר מכן מול מטה-נתונים אלה בעת ההפעלה. התוכן שנחשב חדש יותר ידרוס את הישן יותר (לפי זמן שינוי).\n> בדרך כלל בטוח אם הקבצים המקומיים הם אכן חדשים ביותר. עם זאת, עלול לגרום לבעיות אם לקובץ יש חותמת זמן חדשה יותר אך תוכן ישן יותר (כמו `welcome.md` ראשוני).\n> שיטה זו משתמשת בפחות מעבד ומהירה יותר מ-\"%{RedFlag.Fetch.Method.FetchSafer}\", אך עלולה להוביל לאובדן נתונים אם לא משתמשים בה בזהירות.\n> ## %{RedFlag.Fetch.Method.FetchTraditional}.\n> **תעבורה גבוהה**, **מעבד נמוך**, **סיכון נמוך עד בינוני** (תלוי בפעולה)\n> הכל יימשך מהשרת המרוחד.\n> דומה ל-%{RedFlag.Fetch.Method.FetchSmoother}, אך כל הנתחים נמשכים מהמקור המרוחד.\n> זוהי הדרך המסורתית ביותר למשיכה, צורכת בדרך כלל את רוב תעבורת הרשת והזמן.\n> עם זאת, היא נחשבת לעתים קרובות לשיטה היציבה ביותר מכיוון שהיא הוותיקה והישירה ביותר."; - readonly ja: "どのようにフェッチしますか?\n- フェッチ前にローカルデータベースを作成\n **低トラフィック**, **高CPU負荷**, **低リスク**\n 推奨条件...\n - ファイルの整合性に不安がある\n - ファイル数がそれほど多くない\n- フェッチ前にローカルファイルチャンクを作成\n **低トラフィック**, **中程CPU負荷**, **低~中リスク**\n 推奨条件...\n - ファイルがおそらく整合している\n - ファイル数が多い\n- リモートからすべてをフェッチ\n **高トラフィック**, **低CPU負荷**, **低~中リスク**\n\n>[!INFO]- 詳細\n> ## フェッチ前にローカルデータベースを作成\n> **低トラフィック**, **高CPU負荷**, **低リスク**\n> このオプションは、リモートからデータをフェッチする前に、既存のローカルファイルを使用してローカルデータベースを作成します。\n> ローカルとリモートの両方に一致するファイルがある場合、差分のみが転送されます。\n> ただし、両方の場所に存在するファイルは最初は競合ファイルとして処理されます。実際に競合していなければ自動的に解決されますが、この処理には時間がかかる場合があります。\n> これは一般的に最も安全な方法で、データ損失のリスクを最小限に抑えます。\n> ## フェッチ前にローカルファイルチャンクを作成\n> **低トラフィック**, **中程CPU負荷**, **低~中リスク**(操作による)\n> このオプションは、最初にローカルファイルからデータベース用のチャンクを作成し、その後データをフェッチします。そのため、ローカルにないチャンクのみが転送されます。ただし、すべてのメタデータはリモートから取得されます。\n> ローカルファイルは起動時にこのメタデータと比較されます。新しいと判断されたコンテンツ(更新日時による)が古いものを上書きします。この結果はリモートデータベースに同期されます。\n> ローカルファイルが本当に最新のタイムスタンプであれば一般的に安全です。ただし、ファイルのタイムスタンプが新しくてもコンテンツが古い場合(初期の`welcome.md`など)は問題が発生する可能性があります。\n> これは\"フェッチ前にローカルデータベースを作成\"よりCPU使用量が少なく高速ですが、注意しないとデータ損失につながる可能性があります。\n> ## リモートからすべてをフェッチ\n> **高トラフィック**, **低CPU負荷**, **低~中リスク**(操作による)\n> すべてのデータがリモートからフェッチされます。\n> フェッチ前にローカルファイルチャンクを作成と似ていますが、すべてのチャンクがリモートからフェッチされます。\n> これは最も従来のフェッチ方法で、通常最もネットワークトラフィックと時間を消費します。'フェッチ前にローカルファイルチャンクを作成'オプションと同様のリモートファイル上書きのリスクがあります。\n> ただし、最も歴史があり簡単なアプローチであるため、最も安定した方法と見なされることが多いです。"; - readonly ko: "어떻게 가져오시겠습니까?\n- 가져오기 전에 로컬 데이터베이스를 한 번 생성. (권장)\n **낮은 트래픽**, **높은 CPU**, **낮은 위험**\n- 가져오기 전에 로컬 파일 청크 생성.\n **낮은 트래픽**, **보통 CPU**, **낮음에서 보통 위험**\n- 원격에서 모든 것 가져오기.\n **높은 트래픽**, **낮은 CPU**, **낮음에서 보통 위험**\n\n>[!INFO]- 세부 사항\n> ## 가져오기 전에 로컬 데이터베이스를 한 번 생성. (권장)\n> **낮은 트래픽**, **높은 CPU**, **낮은 위험**\n> 이 옵션은 원격 소스에서 데이터를 가져오기 전에 기존 로컬 파일을 사용하여 로컬 데이터베이스를 먼저 생성합니다.\n> 로컬과 원격 모두에 일치하는 파일이 있으면 둘 사이의 차이점만 전송됩니다.\n> 하지만 두 위치 모두에 있는 파일은 초기에 충돌 파일로 처리됩니다. 실제로 충돌하지 않는다면 자동으로 해결되지만 이 과정은 시간이 걸릴 수 있습니다.\n> 이는 일반적으로 가장 안전한 방법으로 데이터 손실 위험을 최소화합니다.\n> ## 가져오기 전에 로컬 파일 청크 생성.\n> **낮은 트래픽**, **보통 CPU**, **낮음에서 보통 위험** (작업에 따라)\n> 이 옵션은 먼저 로컬 파일에서 데이터베이스용 청크를 생성한 다음 데이터를 가져옵니다. 따라서 로컬에 없는 청크만 전송됩니다. 하지만 모든 메타데이터는 원격 소스에서 가져옵니다.\n> 그런 다음 로컬 파일이 시작 시 이 메타데이터와 비교됩니다. 더 새로운 것으로 간주되는 콘텐츠가 오래된 것을 덮어씁니다(수정 시간 기준). 이 결과는 원격 데이터베이스에 다시 동기화됩니다.\n> 로컬 파일이 실제로 최신 타임스탬프라면 일반적으로 안전합니다. 하지만 파일이 더 새로운 타임스탬프를 가지고 있지만 더 오래된 콘텐츠를 가지고 있다면(초기 `welcome.md`처럼) 문제가 발생할 수 있습니다.\n> 이는 \"가져오기 전에 로컬 데이터베이스를 한 번 생성\"보다 CPU를 덜 사용하고 더 빠르지만 주의 깊게 사용하지 않으면 데이터 손실로 이어질 수 있습니다.\n> ## 원격에서 모든 것 가져오기.\n> **높은 트래픽**, **낮은 CPU**, **낮음에서 보통 위험** (작업에 따라)\n> 모든 것이 원격에서 가져와집니다.\n> 가져오기 전에 로컬 파일 청크 생성와 유사하지만 모든 청크가 원격 소스에서 가져와집니다.\n> 이는 가장 전통적인 가져오기 방법으로 일반적으로 가장 많은 네트워크 트래픽과 시간을 소모합니다. 또한 '가져오기 전에 로컬 파일 청크 생성' 옵션과 유사하게 원격 파일을 덮어쓸 위험이 있습니다.\n> 하지만 가장 오래되고 가장 직접적인 접근 방식이기 때문에 종종 가장 안정적인 방법으로 간주됩니다."; - readonly ru: "Как вы хотите загрузить?"; - readonly zh: "How do you want to fetch?\n- Create a local database once before fetching.\n **Low Traffic**, **High CPU**, **Low Risk**\n Recommended if ...\n - Files possibly inconsistent\n - Files were not so much\n- Create local file chunks before fetching.\n **Low Traffic**, **Moderate CPU**, **Low to Moderate Risk**\n Recommended if ...\n - Files probably consistent\n - You have a lot of files.\n- Fetch everything from the remote.\n **High Traffic**, **Low CPU**, **Low to Moderate Risk**\n\n>[!INFO]- Details\n> ## Create a local database once before fetching.\n> **Low Traffic**, **High CPU**, **Low Risk**\n> This option first creates a local database using existing local files before fetching data from the remote source.\n> If matching files exist both locally and remotely, only the differences between them will be transferred.\n> However, files present in both locations will initially be handled as conflicted files. They will be resolved automatically if they are not actually conflicted, but this process may take time.\n> This is generally the safest method, minimizing data loss risk.\n> ## Create local file chunks before fetching.\n> **Low Traffic**, **Moderate CPU**, **Low to Moderate Risk** (depending operation)\n> This option first creates chunks from local files for the database, then fetches data. Consequently, only chunks missing locally are transferred. However, all metadata is taken from the remote source.\n> Local files are then compared against this metadata at launch. The content considered newer will overwrite the older one (by modified time). This outcome is then synchronised back to the remote database.\n> This is generally safe if local files are genuinely the latest timestamp. However, it can cause problems if a file has a newer timestamp but older content (like the initial `welcome.md`).\n> This uses less CPU and faster than \"Create a local database once before fetching\", but it may lead to data loss if not used carefully.\n> ## Fetch everything from the remote.\n> **High Traffic**, **Low CPU**, **Low to Moderate Risk** (depending operation)\n> All things will be fetched from the remote.\n> Similar to the Create local file chunks before fetching, but all chunks are fetched from the remote source.\n> This is the most traditional way to fetch, typically consuming the most network traffic and time. It also carries a similar risk of overwriting remote files to the 'Create local file chunks before fetching' option.\n> However, it is often considered the most stable method because it is the longest-established and most straightforward approach."; - }; - readonly "RedFlag.Fetch.Method.FetchSafer": { - readonly def: "Create a local database once before fetching"; - readonly fr: "Créer une base locale avant de récupérer"; - readonly he: "צור מסד נתונים מקומי לפני המשיכה"; - readonly ja: "フェッチ前にローカルデータベースを作成"; - readonly ko: "가져오기 전에 로컬 데이터베이스를 한 번 생성"; - readonly ru: "Создать локальную базу данных перед загрузкой"; - readonly zh: "Create a local database once before fetching"; - }; - readonly "RedFlag.Fetch.Method.FetchSmoother": { - readonly def: "Create local file chunks before fetching"; - readonly fr: "Créer des fragments de fichiers locaux avant de récupérer"; - readonly he: "צור נתחי קבצים מקומיים לפני המשיכה"; - readonly ja: "フェッチ前にローカルファイルチャンクを作成"; - readonly ko: "가져오기 전에 로컬 파일 청크 생성"; - readonly ru: "Создать локальные чанки перед загрузкой"; - readonly zh: "Create local file chunks before fetching"; - }; - readonly "RedFlag.Fetch.Method.FetchTraditional": { - readonly def: "Fetch everything from the remote"; - readonly fr: "Tout récupérer depuis le distant"; - readonly he: "משוך הכל מהשרת המרוחד"; - readonly ja: "リモートからすべてをフェッチ"; - readonly ko: "원격에서 모든 것 가져오기"; - readonly ru: "Загрузить всё с удалённого"; - readonly zh: "Fetch everything from the remote"; - }; - readonly "RedFlag.Fetch.Method.Title": { - readonly def: "How do you want to fetch?"; - readonly fr: "Comment voulez-vous récupérer ?"; - readonly he: "כיצד ברצונך למשוך?"; - readonly ja: "どのようにフェッチしますか?"; - readonly ko: "어떻게 가져오시겠습니까?"; - readonly ru: "Как вы хотите загрузить?"; - readonly zh: "How do you want to fetch?"; - }; - readonly "RedFlag.FetchRemoteConfig.Buttons.Cancel": { - readonly def: "No, use local settings"; - readonly fr: "Non, utiliser les paramètres locaux"; - readonly he: "לא, השתמש בהגדרות המקומיות"; - readonly ja: "いいえ、ローカル設定を使用"; - readonly ru: "Нет, использовать локальные настройки"; - readonly zh: "No, use local settings"; - }; - readonly "RedFlag.FetchRemoteConfig.Buttons.Fetch": { - readonly def: "Yes, fetch and apply remote settings"; - readonly fr: "Oui, récupérer et appliquer les paramètres distants"; - readonly he: "כן, משוך והחל הגדרות מרוחקות"; - readonly ja: "はい、リモート設定を取得して適用"; - readonly ru: "Да, загрузить и применить удалённые настройки"; - readonly zh: "Yes, fetch and apply remote settings"; - }; - readonly "RedFlag.FetchRemoteConfig.Message": { - readonly def: "Do you want to fetch and apply remotely stored preference settings to the device?"; - readonly fr: "Voulez-vous récupérer et appliquer les préférences stockées à distance sur cet appareil ?"; - readonly he: "האם ברצונך למשוך ולהחיל הגדרות שמורות מרחוק על מכשיר זה?"; - readonly ja: "リモートに保存された設定を取得して、このデバイスに適用しますか?"; - readonly ru: "Вы хотите загрузить и применить удалённые настройки?"; - readonly zh: "Do you want to fetch and apply remotely stored preference settings to the device?"; - }; - readonly "RedFlag.FetchRemoteConfig.Title": { - readonly def: "Fetch Remote Configuration"; - readonly fr: "Récupérer la configuration distante"; - readonly he: "משוך תצורה מרוחקת"; - readonly ja: "リモート設定の取得"; - readonly ru: "Загрузить удалённую конфигурацию"; - readonly zh: "Fetch Remote Configuration"; - }; - readonly "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.": { - readonly def: "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client."; - readonly ja: "最新版以外のすべてのリビジョンを破棄して、使用容量を削減します。実行には、リモートサーバーとローカルクライアントの両方に同程度の空き容量が必要です。"; - readonly ko: "최신 버전이 아닌 모든 리비전을 제거하여 저장 공간을 줄입니다. 이 작업을 수행하려면 원격 서버와 로컬 클라이언트에 동일한 양의 여유 공간이 필요합니다."; - readonly zh: "通过丢弃所有非最新版本来减少存储空间。这需要远程服务器和本地客户端具备相同数量的可用空间。"; - readonly "zh-tw": "透過捨棄所有非最新版本來減少儲存空間占用。執行此操作時,遠端伺服器與本機用戶端都需要具備相同數量的可用空間。"; - }; - readonly "Reducing the frequency with which on-disk changes are reflected into the DB": { - readonly def: "Reducing the frequency with which on-disk changes are reflected into the DB"; - readonly es: "Reducir frecuencia de actualizaciones de disco a BD"; - readonly fr: "Réduire la fréquence à laquelle les modifications sur disque sont reflétées dans la base"; - readonly he: "הפחת את תדירות השתקפות שינויים בדיסק למסד הנתונים"; - readonly ja: "ローカルでの変更がデータベースに反映される頻度を下げる(所定の回数まとめて同期する、逐一反映しない)"; - readonly ko: "디스크 변경 사항이 데이터베이스에 반영되는 빈도를 줄입니다"; - readonly ru: "Уменьшение частоты отражения изменений с диска в БД"; - readonly zh: "降低将磁盘上的更改反映到数据库中的频率"; - }; - readonly Region: { - readonly def: "Region"; - readonly es: "Región"; - readonly fr: "Région"; - readonly he: "אזור"; - readonly ja: "リージョン"; - readonly ko: "지역"; - readonly ru: "Регион"; - readonly zh: "区域"; - }; - readonly Remediation: { - readonly def: "Remediation"; - readonly es: "Remediación"; - readonly ja: "是正"; - readonly ko: "복구 조치"; - readonly ru: "Исправление"; - readonly zh: "修复设置"; - readonly "zh-tw": "修復設定"; - }; - readonly "Remediation Setting Changed": { - readonly def: "Remediation Setting Changed"; - readonly es: "La configuración de remediación cambió"; - readonly ja: "是正設定が変更されました"; - readonly ko: "복구 설정이 변경됨"; - readonly ru: "Настройки исправления изменены"; - readonly zh: "修复设置已更改"; - readonly "zh-tw": "修復設定已變更"; - }; - readonly "Remote Database Tweak (In sunset)": { - readonly def: "Remote Database Tweak (In sunset)"; - readonly es: "Ajustes de base de datos remota (en retirada)"; - readonly ja: "リモートデータベースの調整 (廃止予定)"; - readonly ko: "원격 데이터베이스 조정 (폐기 예정)"; - readonly ru: "Настройки удалённой базы данных (устаревает)"; - readonly zh: "远端数据库调优(逐步淘汰)"; - readonly "zh-tw": "遠端資料庫調校(即將淘汰)"; - }; - readonly "Remote Databases": { - readonly def: "Remote Databases"; - readonly es: "Bases de datos remotas"; - readonly ja: "リモートデータベース"; - readonly ko: "원격 데이터베이스"; - readonly ru: "Удалённые базы данных"; - readonly zh: "远端数据库"; - readonly "zh-tw": "遠端資料庫"; - }; - readonly "Remote name": { - readonly def: "Remote name"; - readonly es: "Nombre del remoto"; - readonly ja: "リモート名"; - readonly ko: "원격 이름"; - readonly ru: "Имя удалённого хранилища"; - readonly zh: "远端名称"; - readonly "zh-tw": "遠端名稱"; - }; - readonly "Remote server type": { - readonly def: "Remote server type"; - readonly es: "Tipo de servidor remoto"; - readonly fr: "Type de serveur distant"; - readonly he: "סוג שרת מרוחד"; - readonly ja: "リモートの種別"; - readonly ko: "원격 서버 유형"; - readonly ru: "Тип удалённого сервера"; - readonly zh: "远程服务器类型"; - }; - readonly "Remote Type": { - readonly def: "Remote Type"; - readonly es: "Tipo de remoto"; - readonly fr: "Type de distant"; - readonly he: "סוג מרוחד"; - readonly ja: "同期方式"; - readonly ko: "원격 유형"; - readonly ru: "Удалённый тип"; - readonly zh: "远程类型"; - }; - readonly Rename: { - readonly def: "Rename"; - readonly es: "Renombrar"; - readonly ja: "名前を変更"; - readonly ko: "이름 바꾸기"; - readonly ru: "Переименовать"; - readonly zh: "重命名"; - readonly "zh-tw": "重新命名"; - }; - readonly "Replicator.Dialogue.Locked.Action.Dismiss": { - readonly def: "Cancel for reconfirmation"; - readonly fr: "Annuler pour reconfirmer"; - readonly he: "ביטול לאישור מחדש"; - readonly ja: "再確認のためキャンセル"; - readonly ko: "재확인을 위해 취소"; - readonly ru: "Отмена для подтверждения"; - readonly zh: "Cancel for reconfirmation"; - }; - readonly "Replicator.Dialogue.Locked.Action.Fetch": { - readonly def: "Reset Synchronisation on This Device"; - readonly fr: "Réinitialiser la synchronisation sur cet appareil"; - readonly he: "אפס סנכרון במכשיר זה"; - readonly ja: "このデバイスの同期をリセット"; - readonly ko: "원격 데이터베이스에서 모든 것을 다시 가져오기"; - readonly ru: "Сбросить синхронизацию на этом устройстве"; - readonly zh: "Reset Synchronisation on This Device"; - }; - readonly "Replicator.Dialogue.Locked.Action.Unlock": { - readonly def: "Unlock the remote database"; - readonly fr: "Déverrouiller la base distante"; - readonly he: "בטל נעילת מסד הנתונים המרוחד"; - readonly ja: "リモートデータベースのロックを解除"; - readonly ko: "원격 데이터베이스 잠금 해제"; - readonly ru: "Разблокировать удалённую базу данных"; - readonly zh: "Unlock the remote database"; - }; - readonly "Replicator.Dialogue.Locked.Message": { - readonly def: "Remote database is locked. This is due to a rebuild on one of the terminals.\nThe device is therefore asked to withhold the connection to avoid database corruption.\n\nThere are three options that we can do:\n\n- Reset Synchronisation on This Device\n The most preferred and reliable way. This will dispose the local database once, and reset all synchronisation information from the remote database again, In most case, we can perform this safely. However, it takes some time and should be done in stable network.\n- Unlock the remote database\n This method can only be used if we are already reliably synchronised by other replication methods. This does not simply mean that we have the same files. If you are not sure, you should avoid it.\n- Cancel for reconfirmation\n This will cancel the operation. And we will asked again on next request.\n"; - readonly fr: "La base distante est verrouillée. Ceci est dû à une reconstruction sur l'un des terminaux.\nL'appareil est donc prié de suspendre la connexion pour éviter la corruption de la base.\n\nTrois options sont possibles :\n\n- Réinitialiser la synchronisation sur cet appareil\n La méthode la plus recommandée et fiable. Elle supprime la base locale puis réinitialise toutes les informations de synchronisation depuis la base distante. Dans la plupart des cas, c'est sûr. Cela prend cependant du temps et devrait se faire sur un réseau stable.\n- Déverrouiller la base distante\n Cette méthode ne peut être utilisée que si nous sommes déjà synchronisés de manière fiable par d'autres méthodes de réplication. Cela ne signifie pas simplement que nous avons les mêmes fichiers. Dans le doute, évitez.\n- Annuler pour reconfirmer\n Ceci annule l'opération. Vous serez à nouveau interrogé à la prochaine requête.\n"; - readonly he: "מסד הנתונים המרוחד נעול. הסיבה היא בנייה מחדש באחד הטרמינלים.\nלכן המכשיר מתבקש להמנע מחיבור כדי למנוע פגיעה במסד הנתונים.\n\nקיימות שלוש אפשרויות:\n\n- %{Replicator.Dialogue.Locked.Action.Fetch}\n הדרך המועדפת והאמינה ביותר. פעולה זו תמחק את מסד הנתונים המקומי פעם,\n ותאפס את כל מידע הסנכרון ממסד הנתונים המרוחד מחדש. ברוב המקרים ניתן\n לעשות זאת בבטחה. עם זאת, דורשת זמן ויש לבצע ברשת יציבה.\n- %{Replicator.Dialogue.Locked.Action.Unlock}\n ניתן להשתמש בשיטה זו רק אם כבר מסונכרנים באופן אמין בשיטות שכפול\n אחרות. פשוט לא מספיק שיש אותם קבצים. אם אינך בטוח, הימנע מכך.\n- %{Replicator.Dialogue.Locked.Action.Dismiss}\n פעולה זו תבטל את הפעולה. תתבקש שוב בבקשה הבאה.\n"; - readonly ja: "リモートデータベースがロックされています。これはいずれかの端末での再構築が原因です。\nデータベースの破損を避けるため、このデバイスは接続を保留するよう求められています。\n\n3つのオプションがあります:\n\n- このデバイスの同期をリセット\n 最も推奨される信頼性の高い方法です。ローカルデータベースを一度破棄し、リモートデータベースからすべての同期情報を再取得します。ほとんどの場合、これは安全に実行できます。ただし、時間がかかり、安定したネットワークで実行する必要があります。\n- リモートデータベースのロックを解除\n この方法は、他のレプリケーション(複製)方法ですでに確実に同期されている場合のみ使用できます。単に同じファイルがあるという意味ではありません。確信がない場合は避けてください。\n- 再確認のためキャンセル\n 操作をキャンセルします。次回のリクエスト時に再度確認されます。\n"; - readonly ko: "원격 데이터베이스가 잠겨 있습니다. 이는 일부 터미널에서 데이터베이스를 재구축했기 때문입니다.\n따라서 현재 기기는 데이터베이스 손상을 방지하기 위해 연결을 일시적으로 보류해야 합니다.\n\n선택할 수 있는 세 가지 방법이 있습니다:\n\n- 원격 데이터베이스에서 모든 것을 다시 가져오기\n 가장 권장되고 신뢰할 수 있는 방법입니다. 로컬 데이터베이스를 초기화한 뒤, 원격 데이터베이스의 전체 데이터를 다시 가져옵니다. 대부분의 경우 안전하게 수행할 수 있으나, 시간이 다소 걸리며 안정적인 네트워크 환경에서 진행해야 합니다.\n- 원격 데이터베이스 잠금 해제\n 이 방법은 다른 동기화 방식으로 이미 완전하고 안정적으로 동기화된 경우에만 사용할 수 있습니다. 단순히 파일이 같다는 의미가 아니므로, 확신이 없다면 사용을 피하는 것이 좋습니다.\n- 재확인을 위해 취소\n 이번 작업을 취소하고, 다음 요청 시 다시 안내받습니다.\n"; - readonly ru: "Удалённая база данных заблокирована. Это связано с перестроением на одном из устройств."; - readonly zh: "Remote database is locked. This is due to a rebuild on one of the terminals.\nThe device is therefore asked to withhold the connection to avoid database corruption.\n\nThere are three options that we can do:\n\n- Reset Synchronisation on This Device\n The most preferred and reliable way. This will dispose the local database once, and fetch all from the remote database again, In most case, we can perform this safely. However, it takes some time and should be done in stable network.\n- Unlock the remote database\n This method can only be used if we are already reliably synchronised by other replication methods. This does not simply mean that we have the same files. If you are not sure, you should avoid it.\n- Cancel for reconfirmation\n This will cancel the operation. And we will asked again on next request.\n"; - }; - readonly "Replicator.Dialogue.Locked.Message.Fetch": { - readonly def: "Fetch all has been scheduled. Plug-in will be restarted to perform it."; - readonly fr: "Tout récupérer a été planifié. Le plug-in sera redémarré pour l'exécuter."; - readonly he: "משיכה מלאה תוזמנה. הפלאגין יופעל מחדש לביצועה."; - readonly ja: "全フェッチがスケジュールされました。プラグインは実行のために再起動されます。"; - readonly ko: "모든 것 가져오기가 예약되었습니다. 이를 수행하기 위해 플러그인이 재시작됩니다."; - readonly ru: "Загрузка всего запланирована. Плагин будет перезапущен."; - readonly zh: "Fetch all has been scheduled. Plug-in will be restarted to perform it."; - }; - readonly "Replicator.Dialogue.Locked.Message.Unlocked": { - readonly def: "The remote database has been unlocked. Please retry the operation."; - readonly fr: "La base distante a été déverrouillée. Veuillez réessayer l'opération."; - readonly he: "מסד הנתונים המרוחד בוטל נעילתו. אנא נסה שוב את הפעולה."; - readonly ja: "リモートデータベースのロックが解除されました。操作を再試行してください。"; - readonly ko: "원격 데이터베이스 잠금이 해제되었습니다. 작업을 다시 시도해 주세요."; - readonly ru: "Удалённая база данных разблокирована. Повторите операцию."; - readonly zh: "The remote database has been unlocked. Please retry the operation."; - }; - readonly "Replicator.Dialogue.Locked.Title": { - readonly def: "Locked"; - readonly fr: "Verrouillée"; - readonly he: "נעול"; - readonly ja: "ロック中"; - readonly ko: "잠김"; - readonly ru: "Заблокировано"; - readonly zh: "Locked"; - }; - readonly "Replicator.Message.Cleaned": { - readonly def: "Database cleaning up is in process. replication has been cancelled"; - readonly fr: "Nettoyage de la base en cours. La réplication a été annulée"; - readonly he: "ניקוי מסד הנתונים בתהליך. השכפול בוטל"; - readonly ja: "データベースのクリーナップ中です。レプリケーション(複製)はキャンセルされました。"; - readonly ko: "데이터베이스 정리가 진행 중입니다. 복제가 취소되었습니다"; - readonly ru: "Очистка базы данных в процессе. Репликация отменена"; - readonly zh: "Database cleaning up is in process. replication has been cancelled"; - }; - readonly "Replicator.Message.InitialiseFatalError": { - readonly def: "No replicator is available, this is the fatal error."; - readonly fr: "Aucun réplicateur disponible, il s'agit d'une erreur fatale."; - readonly he: "אין רפליקטור זמין, זוהי שגיאה קריטית."; - readonly ja: "レプリケーターが利用できません。これは致命的なエラーです。"; - readonly ko: "사용 가능한 복제기가 없습니다. 치명적인 오류입니다."; - readonly ru: "Репликатор недоступен, это фатальная ошибка."; - readonly zh: "No replicator is available, this is the fatal error."; - }; - readonly "Replicator.Message.Pending": { - readonly def: "Some file events are pending. Replication has been cancelled."; - readonly fr: "Des événements de fichier sont en attente. La réplication a été annulée."; - readonly he: "חלק מאירועי הקבצים ממתינים. השכפול בוטל."; - readonly ja: "ファイルイベントが保留中です。レプリケーション(複製)はキャンセルされました。"; - readonly ko: "일부 파일 이벤트가 대기 중입니다. 복제가 취소되었습니다."; - readonly ru: "Некоторые события файлов ожидают. Репликация отменена."; - readonly zh: "Some file events are pending. Replication has been cancelled."; - }; - readonly "Replicator.Message.SomeModuleFailed": { - readonly def: "Replication has been cancelled by some module failure"; - readonly fr: "La réplication a été annulée suite à l'échec d'un module"; - readonly he: "השכפול בוטל בשל כשל במודול"; - readonly ja: "一部のモジュールの失敗によりレプリケーション(複製)がキャンセルされました。"; - readonly ko: "일부 모듈 실패로 복제가 취소되었습니다"; - readonly ru: "Репликация отменена из-за сбоя модуля"; - readonly zh: "Replication has been cancelled by some module failure"; - }; - readonly "Replicator.Message.VersionUpFlash": { - readonly def: "An update has been detected. Please open the Settings dialogue and check the Change Log. Replication has been cancelled."; - readonly fr: "Une mise à jour a été détectée. Veuillez ouvrir la boîte de dialogue des paramètres et consulter le journal des modifications. La réplication a été annulée."; - readonly he: "זוהה עדכון. אנא פתח את דיאלוג ההגדרות ובדוק את יומן השינויים. השכפול בוטל."; - readonly ja: "更新が検出されました。設定ダイアログを開いて変更ログを確認してください。レプリケーション(複製)はキャンセルされました。"; - readonly ko: "설정을 열고 메시지를 확인해 주세요. 복제가 취소되었습니다."; - readonly ru: "Обновление обнаружено. Откройте настройки и проверьте историю изменений."; - readonly zh: "An update has been detected. Please open the Settings dialogue and check the Change Log. Replication has been cancelled."; - }; - readonly "Requires restart of Obsidian": { - readonly def: "Requires restart of Obsidian"; - readonly es: "Requiere reiniciar Obsidian"; - readonly fr: "Nécessite un redémarrage d'Obsidian"; - readonly he: "דורש הפעלה מחדש של Obsidian"; - readonly ja: "Obsidianの再起動が必要です"; - readonly ko: "Obsidian 재시작 필요"; - readonly ru: "Требуется перезапуск Obsidian"; - readonly zh: "需要重启 Obsidian"; - }; - readonly "Requires restart of Obsidian.": { - readonly def: "Requires restart of Obsidian."; - readonly es: "Requiere reiniciar Obsidian"; - readonly fr: "Nécessite un redémarrage d'Obsidian."; - readonly he: "דורש הפעלה מחדש של Obsidian."; - readonly ja: "Obsidianの再起動が必要です。"; - readonly ko: "Obsidian 재시작이 필요합니다."; - readonly ru: "Требуется перезапуск Obsidian."; - readonly zh: "需要重启 Obsidian "; - }; - readonly "Rerun Onboarding Wizard": { - readonly def: "Rerun Onboarding Wizard"; - readonly fr: "Relancer l'assistant d'intégration"; - readonly he: "הרץ שוב את אשף ההכוונה"; - readonly ja: "オンボーディングウィザードを再実行"; - readonly ko: "온보딩 마법사 다시 실행"; - readonly ru: "Перезапустить мастер настройки"; - readonly zh: "重新运行引导向导"; - readonly "zh-tw": "重新執行導覽精靈"; - }; - readonly "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": { - readonly def: "Rerun the onboarding wizard to set up Self-hosted LiveSync again."; - readonly fr: "Relancer l'assistant d'intégration pour reconfigurer Self-hosted LiveSync."; - readonly he: "הרץ שוב את אשף ההכוונה להגדרת Self-hosted LiveSync מחדש."; - readonly ja: "オンボーディングウィザードを再実行して、Self-hosted LiveSync をもう一度設定します。"; - readonly ko: "온보딩 마법사를 다시 실행하여 Self-hosted LiveSync를 다시 설정합니다."; - readonly ru: "Перезапустить мастер настройки для повторной настройки Self-hosted LiveSync."; - readonly zh: "重新运行引导向导以再次设置 Self-hosted LiveSync。"; - readonly "zh-tw": "重新執行導覽精靈以再次設定 Self-hosted LiveSync。"; - }; - readonly "Rerun Wizard": { - readonly def: "Rerun Wizard"; - readonly fr: "Relancer l'assistant"; - readonly he: "הרץ שוב את האשף"; - readonly ja: "ウィザードを再実行"; - readonly ko: "마법사 다시 실행"; - readonly ru: "Перезапустить мастер"; - readonly zh: "重新运行向导"; - readonly "zh-tw": "重新執行精靈"; - }; - readonly Resend: { - readonly def: "Resend"; - readonly es: "Reenviar"; - readonly ja: "再送信"; - readonly ko: "다시 보내기"; - readonly ru: "Повторно отправить"; - readonly zh: "重新发送"; - readonly "zh-tw": "重新傳送"; - }; - readonly "Resend all chunks to the remote.": { - readonly def: "Resend all chunks to the remote."; - readonly es: "Reenvía todos los chunks al remoto."; - readonly ja: "すべてのチャンクをリモートへ再送信します。"; - readonly ko: "모든 청크를 원격으로 다시 보냅니다."; - readonly ru: "Повторно отправить все чанки в удалённое хранилище."; - readonly zh: "将所有 chunks 重新发送到远端。"; - readonly "zh-tw": "將所有 chunks 重新傳送到遠端。"; - }; - readonly Reset: { - readonly def: "Reset"; - readonly es: "Restablecer"; - readonly ja: "リセット"; - readonly ko: "재설정"; - readonly ru: "Сброс"; - readonly zh: "重置"; - readonly "zh-tw": "重設"; - }; - readonly "Reset all": { - readonly def: "Reset all"; - readonly es: "Restablecer todo"; - readonly ja: "すべてリセット"; - readonly ko: "모두 재설정"; - readonly ru: "Сбросить всё"; - readonly zh: "全部重置"; - readonly "zh-tw": "全部重設"; - }; - readonly "Reset all journal counter": { - readonly def: "Reset all journal counter"; - readonly es: "Restablecer todos los contadores del diario"; - readonly ja: "すべてのジャーナルカウンターをリセット"; - readonly ko: "모든 저널 카운터 재설정"; - readonly ru: "Сбросить все счётчики журнала"; - readonly zh: "重置所有日志计数器"; - readonly "zh-tw": "重設所有日誌計數器"; - }; - readonly "Reset journal received history": { - readonly def: "Reset journal received history"; - readonly es: "Restablecer historial de recepción del diario"; - readonly ja: "ジャーナル受信履歴をリセット"; - readonly ko: "저널 수신 기록 재설정"; - readonly ru: "Сбросить историю полученных записей журнала"; - readonly zh: "重置日志接收历史"; - readonly "zh-tw": "重設日誌接收歷史"; - }; - readonly "Reset journal sent history": { - readonly def: "Reset journal sent history"; - readonly es: "Restablecer historial de envío del diario"; - readonly ja: "ジャーナル送信履歴をリセット"; - readonly ko: "저널 송신 기록 재설정"; - readonly ru: "Сбросить историю отправленных записей журнала"; - readonly zh: "重置日志发送历史"; - readonly "zh-tw": "重設日誌傳送歷史"; - }; - readonly "Reset notification threshold and check the remote database usage": { - readonly def: "Reset notification threshold and check the remote database usage"; - readonly fr: "Réinitialiser le seuil de notification et vérifier l'utilisation de la base distante"; - readonly he: "אפס סף התראה ובדוק שימוש במסד הנתונים המרוחד"; - readonly ja: "通知しきい値をリセットしてリモートデータベース使用量を確認"; - readonly ko: "알림 임계값을 초기화하고 원격 데이터베이스 사용량 확인"; - readonly ru: "Сбросить порог уведомления и проверить использование удалённой базы данных"; - readonly zh: "重置通知阈值并检查远程数据库使用情况"; - readonly "zh-tw": "重設通知閾值並檢查遠端資料庫使用情況"; - }; - readonly "Reset received": { - readonly def: "Reset received"; - readonly es: "Restablecer recepción"; - readonly ja: "受信履歴をリセット"; - readonly ko: "수신 기록 재설정"; - readonly ru: "Сбросить полученные"; - readonly zh: "重置接收记录"; - readonly "zh-tw": "重設接收紀錄"; - }; - readonly "Reset sent history": { - readonly def: "Reset sent history"; - readonly es: "Restablecer historial de envío"; - readonly ja: "送信履歴をリセット"; - readonly ko: "송신 기록 재설정"; - readonly ru: "Сбросить историю отправки"; - readonly zh: "重置发送历史"; - readonly "zh-tw": "重設傳送歷史"; - }; - readonly "Reset Synchronisation information": { - readonly def: "Reset Synchronisation information"; - readonly es: "Restablecer información de sincronización"; - readonly ja: "同期情報をリセット"; - readonly ko: "동기화 정보 재설정"; - readonly ru: "Сбросить информацию о синхронизации"; - readonly zh: "重置同步信息"; - readonly "zh-tw": "重設同步資訊"; - }; - readonly "Reset Synchronisation on This Device": { - readonly def: "Reset Synchronisation on This Device"; - readonly es: "Restablecer sincronización en este dispositivo"; - readonly ja: "このデバイスの同期状態をリセット"; - readonly ko: "이 장치의 동기화 상태 재설정"; - readonly ru: "Сбросить синхронизацию на этом устройстве"; - readonly zh: "重置此设备上的同步"; - readonly "zh-tw": "重設此裝置上的同步"; - }; - readonly "Reset the remote storage size threshold and check the remote storage size again.": { - readonly def: "Reset the remote storage size threshold and check the remote storage size again."; - readonly fr: "Réinitialiser le seuil de taille du stockage distant et vérifier à nouveau la taille du stockage distant."; - readonly he: "אפס את סף גודל האחסון המרוחד ובדוק שוב את גודל האחסון המרוחד."; - readonly ja: "リモートストレージ容量のしきい値をリセットし、リモートストレージ容量を再確認します。"; - readonly ko: "원격 저장소 크기 임계값을 초기화하고 원격 저장소 크기를 다시 확인합니다."; - readonly ru: "Сбросить порог размера удалённого хранилища и проверить размер хранилища снова."; - readonly zh: "重置远程存储大小阈值并再次检查远程存储大小。"; - readonly "zh-tw": "重設遠端儲存空間大小閾值,並再次檢查遠端儲存空間大小。"; - }; - readonly "Resolve All": { - readonly def: "Resolve All"; - readonly es: "Resolver todo"; - readonly ja: "すべて解決"; - readonly ko: "모두 해결"; - readonly ru: "Разрешить всё"; - readonly zh: "全部处理"; - readonly "zh-tw": "全部處理"; - }; - readonly "Resolve all conflicted files": { - readonly def: "Resolve all conflicted files"; - readonly es: "Resolver todos los archivos en conflicto"; - readonly ja: "競合しているすべてのファイルを解決"; - readonly ko: "충돌한 모든 파일 해결"; - readonly ru: "Разрешить все конфликтующие файлы"; - readonly zh: "解决所有冲突文件"; - readonly "zh-tw": "解決所有衝突檔案"; - }; - readonly "Resolve All conflicted files by the newer one": { - readonly def: "Resolve All conflicted files by the newer one"; - readonly es: "Resolver todos los archivos en conflicto con la versión más reciente"; - readonly ja: "競合したすべてのファイルを新しい方で解決"; - readonly ko: "충돌한 모든 파일을 최신 버전으로 해결"; - readonly ru: "Разрешить все конфликтующие файлы в пользу более новой версии"; - readonly zh: "将所有冲突文件解析为较新的版本"; - readonly "zh-tw": "將所有衝突檔案統一為較新的版本"; - }; - readonly "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": { - readonly def: "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one."; - readonly es: "Resuelve todos los archivos en conflicto conservando la versión más reciente. Precaución: esto sobrescribirá la versión anterior y no podrá recuperarse."; - readonly ja: "競合しているすべてのファイルを新しい方の内容で解決します。注意:古い方は上書きされ、復元できません。"; - readonly ko: "충돌한 모든 파일을 더 최신 버전으로 해결합니다. 주의: 이전 버전은 덮어써지며 복원할 수 없습니다."; - readonly ru: "Разрешает все конфликтующие файлы в пользу более новой версии. Внимание: старая версия будет перезаписана и её нельзя будет восстановить."; - readonly zh: "将所有冲突文件统一保留较新的版本。注意:这会覆盖较旧的版本,且被覆盖的内容无法恢复。"; - readonly "zh-tw": "將所有衝突檔案統一保留較新的版本。注意:這會覆寫較舊的版本,且被覆寫的內容無法復原。"; - }; - readonly "Restart Now": { - readonly def: "Restart Now"; - readonly es: "Reiniciar ahora"; - readonly ja: "今すぐ再起動"; - readonly ko: "지금 재시작"; - readonly ru: "Перезапустить сейчас"; - readonly zh: "立即重启"; - readonly "zh-tw": "立即重新啟動"; - }; - readonly "Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and display may be inconsistent. Are you sure to restart now?": { - readonly def: "Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and display may be inconsistent. Are you sure to restart now?"; - readonly "zh-tw": "強烈建議重新啟動 Obsidian。在重新啟動之前,部分變更可能尚未生效,顯示也可能不一致。你確定要現在重新啟動嗎?"; - }; - readonly "Restore or reconstruct local database from remote.": { - readonly def: "Restore or reconstruct local database from remote."; - readonly es: "Restaura o reconstruye la base de datos local desde el remoto."; - readonly ja: "リモートからローカルデータベースを復元または再構築します。"; - readonly ko: "원격에서 로컬 데이터베이스를 복원하거나 재구축합니다."; - readonly ru: "Восстановить или перестроить локальную базу данных из удалённой."; - readonly zh: "从远端恢复或重建本地数据库。"; - readonly "zh-tw": "從遠端還原或重建本機資料庫。"; - }; - readonly "Run Doctor": { - readonly def: "Run Doctor"; - readonly fr: "Lancer le Docteur"; - readonly he: "הפעל Doctor"; - readonly ja: "診断を実行"; - readonly ko: "진단 실행"; - readonly ru: "Запустить диагностику"; - readonly zh: "立即诊断"; - readonly "zh-tw": "執行診斷"; - }; - readonly "S3/MinIO/R2 Object Storage": { - readonly def: "S3/MinIO/R2 Object Storage"; - readonly es: "Almacenamiento de objetos S3/MinIO/R2"; - readonly ja: "S3/MinIO/R2 オブジェクトストレージ"; - readonly ko: "S3/MinIO/R2 객체 스토리지"; - readonly ru: "Объектное хранилище S3/MinIO/R2"; - readonly zh: "S3/MinIO/R2 对象存储"; - readonly "zh-tw": "S3/MinIO/R2 物件儲存"; - }; - readonly "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": { - readonly def: "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform."; - readonly es: "Guardar configuración en archivo markdown. Se notificarán nuevos ajustes. Puede definir diferentes archivos por plataforma"; - readonly fr: "Enregistrer les paramètres dans un fichier markdown. Vous serez notifié à l'arrivée de nouveaux paramètres. Vous pouvez définir des fichiers différents selon la plateforme."; - readonly he: "שמור הגדרות לקובץ Markdown. תיודע כשהגדרות חדשות יגיעו. ניתן להגדיר קבצים שונים לפי פלטפורמה."; - readonly ja: "Markdownファイルに設定を保存します。新しい設定が到着すると通知されます。プラットフォームごとに異なるファイルを設定できます。"; - readonly ko: "설정을 마크다운 파일에 저장합니다. 새로운 설정이 도착하면 알림을 받게 됩니다. 플랫폼별로 다른 파일을 설정할 수 있습니다."; - readonly ru: "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform."; - readonly zh: "将设置保存到一个 Markdown 文件中。当新设置到达时,您将收到通知。您可以根据平台设置不同的文件 "; - }; - readonly "Saving will be performed forcefully after this number of seconds.": { - readonly def: "Saving will be performed forcefully after this number of seconds."; - readonly es: "Guardado forzado tras esta cantidad de segundos"; - readonly fr: "L'enregistrement sera forcé au bout de ce nombre de secondes."; - readonly he: "השמירה תתבצע בכפייה לאחר מספר שניות זה."; - readonly ja: "この秒数後に強制的に保存されます。"; - readonly ko: "이 시간(초) 후에 강제로 저장이 수행됩니다."; - readonly ru: "Сохранение будет принудительно выполнено после этого количества секунд."; - readonly zh: "在此秒数后将强制执行保存 "; - }; - readonly "Scan a QR Code (Recommended for mobile)": { - readonly def: "Scan a QR Code (Recommended for mobile)"; - readonly es: "Escanear un código QR (recomendado para móviles)"; - readonly ja: "QR コードをスキャンする(モバイル推奨)"; - readonly ko: "QR 코드 스캔(모바일 권장)"; - readonly ru: "Сканировать QR-код (рекомендуется для мобильных устройств)"; - readonly zh: "扫描二维码(移动端推荐)"; - readonly "zh-tw": "掃描 QR Code(行動裝置推薦)"; - }; - readonly "Scan changes on customization sync": { - readonly def: "Scan changes on customization sync"; - readonly es: "Escanear cambios en sincronización de personalización"; - readonly fr: "Analyser les modifications de synchronisation de personnalisation"; - readonly he: "סרוק שינויים בסנכרון התאמה אישית"; - readonly ja: "カスタマイズされた同期時に、変更をスキャンする"; - readonly ko: "사용자 설정 동기화 시 변경 사항 검색"; - readonly ru: "Сканировать изменения при синхронизации настроек"; - readonly zh: "在自定义同步时扫描更改"; - }; - readonly "Scan customization automatically": { - readonly def: "Scan customization automatically"; - readonly es: "Escanear personalización automáticamente"; - readonly fr: "Analyser automatiquement la personnalisation"; - readonly he: "סרוק התאמה אישית אוטומטית"; - readonly ja: "自動的にカスタマイズをスキャン"; - readonly ko: "사용자 설정 자동 검색"; - readonly ru: "Сканировать настройки автоматически"; - readonly zh: "自动扫描自定义设置"; - }; - readonly "Scan customization before replicating.": { - readonly def: "Scan customization before replicating."; - readonly es: "Escanear personalización antes de replicar"; - readonly fr: "Analyser la personnalisation avant de répliquer."; - readonly he: "סרוק התאמה אישית לפני שכפול."; - readonly ja: "レプリケーション(複製)前に、カスタマイズをスキャン"; - readonly ko: "복제하기 전에 사용자 설정을 검색합니다."; - readonly ru: "Сканировать настройки перед репликацией."; - readonly zh: "在复制前扫描自定义设置 "; - }; - readonly "Scan customization every 1 minute.": { - readonly def: "Scan customization every 1 minute."; - readonly es: "Escanear personalización cada 1 minuto"; - readonly fr: "Analyser la personnalisation toutes les 1 minute."; - readonly he: "סרוק התאמה אישית כל דקה."; - readonly ja: "カスタマイズのスキャンを1分ごとに行う"; - readonly ko: "1분마다 사용자 설정을 검색합니다."; - readonly ru: "Сканировать настройки каждую минуту."; - readonly zh: "每1分钟扫描自定义设置 "; - }; - readonly "Scan customization periodically": { - readonly def: "Scan customization periodically"; - readonly es: "Escanear personalización periódicamente"; - readonly fr: "Analyser la personnalisation périodiquement"; - readonly he: "סרוק התאמה אישית תקופתית"; - readonly ja: "定期的にカスタマイズをスキャン"; - readonly ko: "주기적으로 사용자 설정 검색"; - readonly ru: "Сканировать настройки периодически"; - readonly zh: "定期扫描自定义设置"; - }; - readonly "Scan for Broken files": { - readonly def: "Scan for Broken files"; - readonly ja: "破損ファイルをスキャン"; - readonly ko: "손상된 파일 검사"; - readonly zh: "扫描损坏文件"; - readonly "zh-tw": "掃描損壞檔案"; - }; - readonly "Scan for hidden files before replication": { - readonly def: "Scan for hidden files before replication"; - readonly es: "Escanear archivos ocultos antes de replicar"; - readonly fr: "Analyser les fichiers cachés avant réplication"; - readonly he: "סרוק קבצים נסתרים לפני שכפול"; - readonly ja: "レプリケーション(複製)開始前に、隠しファイルのスキャンを行う"; - readonly ko: "복제 전 숨겨진 파일 검색"; - readonly ru: "Сканировать скрытые файлы перед репликацией"; - readonly zh: "复制前扫描隐藏文件"; - }; - readonly "Scan hidden files periodically": { - readonly def: "Scan hidden files periodically"; - readonly es: "Escanear archivos ocultos periódicamente"; - readonly fr: "Analyser les fichiers cachés périodiquement"; - readonly he: "סרוק קבצים נסתרים תקופתית"; - readonly ja: "定期的に隠しファイルのスキャンを行う"; - readonly ko: "주기적으로 숨겨진 파일 검색"; - readonly ru: "Сканировать скрытые файлы периодически"; - readonly zh: "定期扫描隐藏文件"; - }; - readonly "Scan the QR code displayed on an active device using this device's camera.": { - readonly def: "Scan the QR code displayed on an active device using this device's camera."; - readonly es: "Escanee con la cámara de este dispositivo el código QR mostrado en un dispositivo activo。"; - readonly ja: "稼働中の端末に表示された QR コードを、この端末のカメラで読み取ってください。"; - readonly ko: "이 장치의 카메라로 활성 장치에 표시된 QR 코드를 스캔하세요。"; - readonly ru: "Отсканируйте QR-код, показанный на активном устройстве, с помощью камеры этого устройства。"; - readonly zh: "使用当前设备的摄像头扫描另一台已在使用设备上显示的二维码。"; - readonly "zh-tw": "使用目前裝置的相機掃描另一台已在使用裝置上顯示的 QR Code。"; - }; - readonly "Schedule and Restart": { - readonly def: "Schedule and Restart"; - readonly es: "Programar y reiniciar"; - readonly ja: "予約して再起動"; - readonly ko: "예약 후 재시작"; - readonly ru: "Запланировать и перезапустить"; - readonly zh: "安排并重启"; - readonly "zh-tw": "排程後重新啟動"; - }; - readonly "Scram Switches": { - readonly def: "Scram Switches"; - readonly ja: "緊急対応スイッチ"; - readonly ko: "긴급 전환 스위치"; - readonly zh: "紧急开关"; - readonly "zh-tw": "緊急處置開關"; - }; - readonly "Scram!": { - readonly def: "Scram!"; - readonly es: "Medidas de emergencia"; - readonly ja: "緊急停止"; - readonly ko: "긴급 조치"; - readonly ru: "Экстренные меры"; - readonly zh: "紧急处置"; - readonly "zh-tw": "緊急處置"; - }; - readonly "Seconds, 0 to disable": { - readonly def: "Seconds, 0 to disable"; - readonly es: "Segundos, 0 para desactivar"; - readonly fr: "Secondes, 0 pour désactiver"; - readonly he: "שניות, 0 לביטול"; - readonly ja: "秒数、0で無効"; - readonly ko: "초 단위, 0으로 설정하면 비활성화"; - readonly ru: "Секунд, 0 для отключения"; - readonly zh: "秒,0为禁用"; - }; - readonly "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": { - readonly def: "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving."; - readonly es: "Segundos. Guardado en BD local se retrasará hasta este valor tras dejar de escribir/guardar"; - readonly fr: "Secondes. L'enregistrement dans la base locale sera différé de cette valeur après l'arrêt de la frappe ou de l'enregistrement."; - readonly he: "שניות. השמירה למסד הנתונים המקומי תתעכב בערך זה לאחר הפסקת הקלדה או שמירה."; - readonly ja: "秒。入力や保存を停止してからこの値の間、ローカルデータベースへの保存が遅延されます。"; - readonly ko: "초 단위입니다. 타이핑이나 저장을 중단한 후 이 시간동안 로컬 데이터베이스 저장이 지연됩니다."; - readonly ru: "Секунды. Сохранение в локальную базу данных будет отложено."; - readonly zh: "秒。在我们停止输入或保存后,保存到本地数据库将延迟此值 "; - }; - readonly "Secret Key": { - readonly def: "Secret Key"; - readonly es: "Clave secreta"; - readonly fr: "Clé secrète"; - readonly he: "מפתח סודי"; - readonly ja: "シークレットキー"; - readonly ko: "시크릿 키"; - readonly ru: "Секретный ключ"; - readonly zh: "Secret Key"; - }; - readonly "Select the database adapter to use.": { - readonly def: "Select the database adapter to use."; - readonly es: "Selecciona el adaptador de base de datos que se usará."; - readonly ja: "使用するデータベースアダプターを選択します。"; - readonly ko: "사용할 데이터베이스 어댑터를 선택합니다."; - readonly ru: "Выберите используемый адаптер базы данных."; - readonly zh: "选择要使用的数据库适配器。"; - readonly "zh-tw": "選擇要使用的資料庫適配器。"; - }; - readonly Send: { - readonly def: "Send"; - readonly es: "Enviar"; - readonly ja: "送信"; - readonly ko: "보내기"; - readonly ru: "Отправить"; - readonly zh: "发送"; - readonly "zh-tw": "傳送"; - }; - readonly "Send chunks": { - readonly def: "Send chunks"; - readonly es: "Enviar chunks"; - readonly ja: "チャンクを送信"; - readonly ko: "청크 보내기"; - readonly ru: "Отправить чанки"; - readonly zh: "发送 chunks"; - readonly "zh-tw": "傳送 chunks"; - }; - readonly "Server URI": { - readonly def: "Server URI"; - readonly es: "URI del servidor"; - readonly fr: "URI du serveur"; - readonly he: "כתובת שרת (URI)"; - readonly ja: "URI"; - readonly ko: "서버 URI"; - readonly ru: "URI сервера"; - readonly zh: "服务器 URI"; - }; - readonly "Setting.GenerateKeyPair.Desc": { - readonly def: "We have generated a key pair!\n\nNote: This key pair will never be shown again. Please save it in a safe place. If you have lost it, you need to generate a new key pair.\nNote 2: The public key is in spki format, and the Private key is in pkcs8 format. For the sake of convenience, newlines are converted to `\\n` in public key.\nNote 3: The public key should be configured in the remote database, and the private key should be configured in local devices.\n\n>[!FOR YOUR EYES ONLY]-\n>
\n>\n> ### Public Key\n> ```\n${public_key}\n> ```\n>\n> ### Private Key\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Both for copying]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n"; - readonly es: "Hemos generado un par de claves.\n\nNota: Este par de claves no volverá a mostrarse. Guárdalo en un lugar seguro. Si lo pierdes, tendrás que generar uno nuevo.\nNota 2: La clave pública está en formato spki y la clave privada en formato pkcs8. Para mayor comodidad, los saltos de línea de la clave pública se convierten en `\\n`.\nNota 3: La clave pública debe configurarse en la base de datos remota y la clave privada en los dispositivos locales.\n\n>[!SOLO PARA TUS OJOS]-\n>
\n>\n> ### Clave pública\n> ```\n${public_key}\n> ```\n>\n> ### Clave privada\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Ambas para copiar]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
"; - readonly fr: "Nous avons généré une paire de clés !\n\nNote : cette paire de clés ne sera plus jamais affichée. Veuillez la conserver dans un endroit sûr. Si vous la perdez, vous devrez générer une nouvelle paire.\nNote 2 : la clé publique est au format spki, et la clé privée au format pkcs8. Pour plus de commodité, les retours à la ligne sont convertis en `\\n` dans la clé publique.\nNote 3 : la clé publique doit être configurée dans la base distante, et la clé privée sur les appareils locaux.\n\n>[!POUR VOS YEUX SEULEMENT]-\n>
\n>\n> ### Clé publique\n> ```\n${public_key}\n> ```\n>\n> ### Clé privée\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Les deux pour copier]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n"; - readonly he: "יצרנו זוג מפתחות!\n\nהערה: זוג מפתחות זה לא יוצג שוב. אנא שמור אותו במקום בטוח. אם אבד לך, יהיה צורך לייצר זוג מפתחות חדש.\nהערה 2: המפתח הציבורי הוא בפורמט spki, והמפתח הפרטי הוא בפורמט pkcs8. לנוחות, שורות חדשות ממוירות ל-`\\n` במפתח הציבורי.\nהערה 3: יש להגדיר את המפתח הציבורי במסד הנתונים המרוחד, ואת המפתח הפרטי במכשירים המקומיים.\n\n>[!FOR YOUR EYES ONLY]-\n>
\n>\n> ### מפתח ציבורי\n> ```\n${public_key}\n> ```\n>\n> ### מפתח פרטי\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Both for copying]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n"; - readonly ja: "キーペアを生成しました!\n\n注意: このキーペアは再度表示されません。安全な場所に保存してください。紛失した場合は、新しいキーペアを生成する必要があります。\n注意2: 公開鍵はspki形式、秘密鍵はpkcs8形式です。利便性のため、公開鍵の改行は`\\n`に変換されています。\n注意3: 公開鍵はリモートデータベースに、秘密鍵はローカルデバイスに設定してください。\n\n>[!FOR YOUR EYES ONLY]-\n>
\n>\n> ### 公開鍵\n> ```\n${public_key}\n> ```\n>\n> ### 秘密鍵\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Both for copying]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n"; - readonly ko: "키 페어를 생성했습니다!\n\n참고: 이 키 페어는 다시 표시되지 않습니다. 안전한 곳에 저장해 주세요. 분실하면 새 키 페어를 생성해야 합니다.\n참고 2: 공개 키는 spki 형식이고, 개인 키는 pkcs8 형식입니다. 편의상 공개 키의 줄 바꿈은 `\\n`으로 변환됩니다.\n참고 3: 공개 키는 원격 데이터베이스에서 구성되어야 하고, 개인 키는 로컬 기기에서 구성되어야 합니다.\n\n>[!FOR YOUR EYES ONLY]-\n>
\n>\n> ### 공개 키\n> ```\n${public_key}\n> ```\n>\n> ### 개인 키\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Both for copying]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n\n"; - readonly ru: "Мы сгенерировали пару ключей!"; - readonly zh: "我们已经生成了一组密钥对!\n\n注意:这组密钥对之后将不会再次显示。请务必妥善保管;如果丢失,你需要重新生成新的密钥对。\n注意 2:公钥采用 spki 格式,私钥采用 pkcs8 格式。为方便复制,公钥中的换行会被转换为 `\\n`。\n注意 3:公钥应配置在远端数据库中,私钥应配置在本地设备上。\n\n>[!仅限本人查看]-\n>
\n>\n> ### 公钥\n> ```\n${public_key}\n> ```\n>\n> ### 私钥\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!整段复制]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
"; - readonly "zh-tw": "我們已產生新的金鑰對!\n\n注意:此金鑰對之後將不會再次顯示。請務必妥善保存;若遺失,必須重新產生新的金鑰對。\n注意 2:公鑰採用 spki 格式,私鑰採用 pkcs8 格式。為了方便複製,公鑰中的換行會轉換為 `\\n`。\n注意 3:公鑰應設定在遠端資料庫中,私鑰則應設定在本機裝置上。\n\n>[!僅供本人查看]-\n>
\n>\n> ### 公鑰\n> ```\n${public_key}\n> ```\n>\n> ### 私鑰\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!便於整段複製]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
"; - }; - readonly "Setting.GenerateKeyPair.Title": { - readonly def: "New key pair has been generated!"; - readonly es: "¡Se ha generado un nuevo par de claves!"; - readonly fr: "Une nouvelle paire de clés a été générée !"; - readonly he: "זוג מפתחות חדש נוצר!"; - readonly ja: "新しいキーペアが生成されました!"; - readonly ko: "새 키 페어가 생성되었습니다!"; - readonly ru: "Новая пара ключей сгенерирована!"; - readonly zh: "已生成新的密钥对!"; - readonly "zh-tw": "已產生新的金鑰對!"; - }; - readonly "Setting.TroubleShooting": { - readonly def: "TroubleShooting"; - readonly fr: "Dépannage"; - readonly he: "פתרון בעיות"; - readonly ja: "トラブルシューティング"; - readonly ko: "문제 해결"; - readonly ru: "Устранение неполадок"; - readonly zh: "故障排除"; - }; - readonly "Setting.TroubleShooting.Doctor": { - readonly def: "Setting Doctor"; - readonly fr: "Docteur des paramètres"; - readonly he: "Doctor הגדרות"; - readonly ja: "設定診断ツール"; - readonly ko: "설정 진단 마법사"; - readonly ru: "Диагностика настроек"; - readonly zh: "设置诊断"; - }; - readonly "Setting.TroubleShooting.Doctor.Desc": { - readonly def: "Detects non optimal settings. (Same as during migration)"; - readonly fr: "Détecte les paramètres non optimaux. (Identique à la migration)"; - readonly he: "מזהה הגדרות לא אופטימליות. (זהה לפעולה במהלך הגירה)"; - readonly ja: "最適でない設定を検出します。(マイグレーション時と同じ)"; - readonly ko: "최적화되지 않은 설정을 감지합니다. (데이터 구조 전환 시와 동일)"; - readonly ru: "Обнаруживает неоптимальные настройки."; - readonly zh: "检测系统中不合理的设置。(与迁移期间逻辑相同)"; - }; - readonly "Setting.TroubleShooting.ScanBrokenFiles": { - readonly def: "Scan for broken files"; - readonly fr: "Analyser les fichiers corrompus"; - readonly he: "סרוק קבצים פגומים"; - readonly ja: "破損ファイルのスキャン"; - readonly ko: "손상된 파일 검사"; - readonly ru: "Сканировать повреждённые файлы"; - readonly zh: "扫描损坏或异常的文件"; - }; - readonly "Setting.TroubleShooting.ScanBrokenFiles.Desc": { - readonly def: "Scans for files that are not stored correctly in the database."; - readonly fr: "Analyse les fichiers qui ne sont pas stockés correctement dans la base."; - readonly he: "סורק קבצים שלא נשמרו כהלכה במסד הנתונים."; - readonly ja: "データベースに正しく保存されていないファイルをスキャンします。"; - readonly ko: "데이터베이스에 올바르게 저장되지 않은 파일을 검사합니다."; - readonly ru: "Сканирует файлы, которые неправильно хранятся в базе данных."; - readonly zh: "扫描数据库中未正确存储的文件。"; - }; - readonly "SettingTab.Message.AskRebuild": { - readonly def: "Your changes require fetching from the remote database. Do you want to proceed?"; - readonly fr: "Vos modifications nécessitent une récupération depuis la base distante. Voulez-vous continuer ?"; - readonly he: "השינויים שלך מצריכים משיכה ממסד הנתונים המרוחד. האם להמשיך?"; - readonly ja: "変更にはリモートデータベースからのフェッチが必要です。続行しますか?"; - readonly ko: "변경 사항을 적용하려면 원격 데이터베이스에서 가져와야 합니다. 계속 진행하시겠습니까?"; - readonly ru: "Ваши изменения требуют загрузки из удалённой базы данных. Хотите продолжить?"; - readonly zh: "Your changes require fetching from the remote database. Do you want to proceed?"; - }; - readonly "Setup URI dialog cancelled.": { - readonly def: "Setup URI dialog cancelled."; - readonly ja: "Setup URI ダイアログはキャンセルされました。"; - readonly ko: "Setup URI 대화 상자가 취소되었습니다."; - readonly ru: "Диалог Setup URI был отменён."; - readonly zh: "Setup URI 对话框已取消。"; - readonly "zh-tw": "Setup URI 對話框已取消。"; - }; - readonly "Setup.Apply.Buttons.ApplyAndFetch": { - readonly def: "Apply and Fetch"; - readonly fr: "Appliquer et récupérer"; - readonly he: "החל ומשוך"; - readonly ja: "適用してフェッチ"; - readonly ru: "Применить и загрузить"; - readonly zh: "Apply and Fetch"; - }; - readonly "Setup.Apply.Buttons.ApplyAndMerge": { - readonly def: "Apply and Merge"; - readonly fr: "Appliquer et fusionner"; - readonly he: "החל ומזג"; - readonly ja: "適用してマージ"; - readonly ru: "Применить и объединить"; - readonly zh: "Apply and Merge"; - }; - readonly "Setup.Apply.Buttons.ApplyAndRebuild": { - readonly def: "Apply and Rebuild"; - readonly fr: "Appliquer et reconstruire"; - readonly he: "החל ובנה מחדש"; - readonly ja: "適用して再構築"; - readonly ru: "Применить и перестроить"; - readonly zh: "Apply and Rebuild"; - }; - readonly "Setup.Apply.Buttons.Cancel": { - readonly def: "Discard and Cancel"; - readonly fr: "Abandonner et annuler"; - readonly he: "בטל ובטל"; - readonly ja: "破棄してキャンセル"; - readonly ru: "Отменить и отменить"; - readonly zh: "Discard and Cancel"; - }; - readonly "Setup.Apply.Buttons.OnlyApply": { - readonly def: "Only Apply"; - readonly fr: "Appliquer seulement"; - readonly he: "החל בלבד"; - readonly ja: "適用のみ"; - readonly ru: "Только применить"; - readonly zh: "Only Apply"; - }; - readonly "Setup.Apply.Message": { - readonly def: "The new configuration is ready. Let us proceed to apply it.\nThere are several ways to apply this:\n\n- Apply and Fetch\n Configure this device as a new client. After applying, synchronise from the remote server.\n- Apply and Merge\n Configure on a device that already has the file. It processes the local files and transfers the differences. Conflicts may arise.\n- Apply and Rebuild\n Rebuild the remote using local files. This is typically done if the server becomes corrupted or we wish to start from scratch.\n Other devices will be locked and required to re-fetch.\n- Only Apply\n Apply only. Conflicts may arise if a rebuild is required."; - readonly fr: "La nouvelle configuration est prête. Procédons à son application.\nPlusieurs manières de l'appliquer :\n\n- Appliquer et récupérer\n Configurer cet appareil comme nouveau client. Après application, synchroniser depuis le serveur distant.\n- Appliquer et fusionner\n Configurer sur un appareil qui possède déjà les fichiers. Traite les fichiers locaux et transfère les différences. Des conflits peuvent apparaître.\n- Appliquer et reconstruire\n Reconstruire le distant à partir des fichiers locaux. Typiquement effectué si le serveur est corrompu ou si l'on souhaite repartir de zéro.\n Les autres appareils seront verrouillés et devront refaire une récupération.\n- Appliquer seulement\n Appliquer uniquement. Des conflits peuvent apparaître si une reconstruction est nécessaire."; - readonly he: "התצורה החדשה מוכנה. בואו נמשיך להחיל אותה.\nישנן מספר דרכים להחיל זאת:\n\n- החל ומשוך\n הגדר מכשיר זה כלקוח חדש. לאחר ההחלה, סנכרן מהשרת המרוחד.\n- החל ומזג\n הגדר על מכשיר שכבר יש בו קבצים. מעבד קבצים מקומיים ומעביר הפרשים. עלולים\n לקום קונפליקטים.\n- החל ובנה מחדש\n בנה את השרת המרוחד מחדש תוך שימוש בקבצים מקומיים. נעשה בדרך כלל אם השרת\n מושחת או אם רוצים להתחיל מאפס. מכשירים אחרים יינעלו ויצטרכו למשוך מחדש.\n- החל בלבד\n החל בלבד. עלולים לקום קונפליקטים אם נדרשת בנייה מחדש."; - readonly ja: "新しい設定の準備ができました。適用に進みましょう。\n適用方法はいくつかあります:\n\n- 適用してフェッチ\n このデバイスを新しいクライアントとして設定します。適用後、リモートサーバーから同期します。\n- 適用してマージ\n 既にファイルがあるデバイスで設定します。ローカルファイルを処理し、差分を転送します。競合が発生する場合があります。\n- 適用して再構築\n ローカルファイルを使用してリモートを再構築します。これは通常、サーバーが破損した場合や最初からやり直したい場合に行います。\n 他のデバイスはロックされ、再フェッチが必要になります。\n- 適用のみ\n 適用のみを行います。再構築が必要な場合、競合が発生する可能性があります。"; - readonly ru: "Новая конфигурация готова. Есть несколько способов применить её."; - readonly zh: "The new configuration is ready. Let us proceed to apply it.\nThere are several ways to apply this:\n\n- Apply and Fetch\n Configure this device as a new client. After applying, synchronise from the remote server.\n- Apply and Merge\n Configure on a device that already has the file. It processes the local files and transfers the differences. Conflicts may arise.\n- Apply and Rebuild\n Rebuild the remote using local files. This is typically done if the server becomes corrupted or we wish to start from scratch.\n Other devices will be locked and required to re-fetch.\n- Only Apply\n Apply only. Conflicts may arise if a rebuild is required."; - }; - readonly "Setup.Apply.Title": { - readonly def: "Apply new configuration from the ${method}"; - readonly fr: "Appliquer la nouvelle configuration depuis ${method}"; - readonly he: "החל תצורה חדשה מה-${method}"; - readonly ja: "${method}からの新しい設定を適用"; - readonly ru: "Применить новую конфигурацию из method"; - readonly zh: "Apply new configuration from the ${method}"; - }; - readonly "Setup.Apply.WarningRebuildRecommended": { - readonly def: "NOTE: after adjusting the settings, it has been determined that a rebuild is required; Just Import is not recommended."; - readonly fr: "NOTE : après ajustement des paramètres, il a été déterminé qu'une reconstruction est requise ; un simple import n'est pas recommandé."; - readonly he: "שים לב: לאחר כוונון ההגדרות, נקבע שנדרשת בנייה מחדש; ייבוא בלבד אינו מומלץ."; - readonly ja: "注意: 設定の調整後、再構築が必要と判断されました。インポートのみは推奨されません。"; - readonly ru: "ПРИМЕЧАНИЕ: после настройки изменений определено, что требуется перестроение."; - readonly zh: "NOTE: after adjusting the settings, it has been determined that a rebuild is required; Just Import is not recommended."; - }; - readonly "Setup.Doctor.Buttons.No": { - readonly def: "No, please use the settings in the URI as is"; - readonly fr: "Non, utiliser les paramètres de l'URI tels quels"; - readonly he: "לא, אנא השתמש בהגדרות ה-URI כפי שהן"; - readonly ja: "いいえ、URIの設定をそのまま使用"; - readonly ru: "Нет, использовать настройки из URI как есть"; - readonly zh: "No, please use the settings in the URI as is"; - }; - readonly "Setup.Doctor.Buttons.Yes": { - readonly def: "Yes, please consult the doctor"; - readonly fr: "Oui, consulter le docteur"; - readonly he: "כן, אנא יעץ ל-Doctor"; - readonly ja: "はい、診断ツールに相談する"; - readonly ru: "Да, пожалуйста, запустить диагностику"; - readonly zh: "Yes, please consult the doctor"; - }; - readonly "Setup.Doctor.Message": { - readonly def: "Self-hosted LiveSync has gradually become longer in history and some recommended settings have changed.\n\nNow, setup is a very good time to do this.\n\nDo you want to run Doctor to check if the imported settings are optimal compared to the latest state?"; - readonly fr: "Self-hosted LiveSync s'est progressivement étoffé et certains paramètres recommandés ont évolué.\n\nLa configuration est un bon moment pour le faire.\n\nVoulez-vous lancer le Docteur pour vérifier si les paramètres importés sont optimaux par rapport au dernier état ?"; - readonly he: "Self-hosted LiveSync הפך ארוך יותר בהיסטוריה שלו וחלק מההגדרות המומלצות השתנו.\n\nעכשיו, הגדרה היא זמן מצוין לכך.\n\nהאם ברצונך להפעיל את Doctor כדי לבדוק אם ההגדרות המיובאות אופטימליות בהשוואה למצב הנוכחי?"; - readonly ja: "Self-hosted LiveSyncは徐々に歴史が長くなり、一部の推奨設定が変更されています。\n\nセットアップは、これを行う非常に良い機会です。\n\nインポートされた設定が最新の状態と比較して最適かどうかを確認するために、診断ツールを実行しますか?"; - readonly ru: "Self-hosted LiveSync постепенно набрал историю и некоторые рекомендуемые настройки изменились."; - readonly zh: "Self-hosted LiveSync has gradually become longer in history and some recommended settings have changed.\n\nNow, setup is a very good time to do this.\n\nDo you want to run Doctor to check if the imported settings are optimal compared to the latest state?"; - }; - readonly "Setup.Doctor.Title": { - readonly def: "Do you want to consult the doctor?"; - readonly fr: "Voulez-vous consulter le docteur ?"; - readonly he: "האם ברצונך להתייעץ עם ה-Doctor?"; - readonly ja: "診断ツールに相談しますか?"; - readonly ru: "Хотите запустить диагностику?"; - readonly zh: "Do you want to consult the doctor?"; - }; - readonly "Setup.FetchRemoteConf.Buttons.Fetch": { - readonly def: "Yes, please fetch the configuration"; - readonly fr: "Oui, récupérer la configuration"; - readonly he: "כן, אנא משוך את התצורה"; - readonly ja: "はい、設定を取得"; - readonly ru: "Да, загрузить конфигурацию"; - readonly zh: "Yes, please fetch the configuration"; - }; - readonly "Setup.FetchRemoteConf.Buttons.Skip": { - readonly def: "No, please use the settings in the URI"; - readonly fr: "Non, utiliser les paramètres de l'URI"; - readonly he: "לא, אנא השתמש בהגדרות ב-URI"; - readonly ja: "いいえ、URIの設定を使用"; - readonly ru: "Нет, использовать настройки из URI"; - readonly zh: "No, please use the settings in the URI"; - }; - readonly "Setup.FetchRemoteConf.Message": { - readonly def: "If we have already synchronised once with another device, the remote database stores the suitable configuration values between the synchronised devices. The plug-in would like to retrieve them for robust configuration.\n\nHowever, we have to make sure the one thing. Are we currently in a situation where we can access the network safely and retrieve the settings?\n\nNote: Mostly, you are safe to do this, that your remote database is hosted with a SSL certificate, and your network is not compromised."; - readonly fr: "Si nous avons déjà synchronisé une fois avec un autre appareil, la base distante stocke les valeurs de configuration adaptées entre les appareils synchronisés. Le plug-in souhaiterait les récupérer pour une configuration robuste.\n\nMais il faut s'assurer d'une chose. Sommes-nous actuellement dans une situation où nous pouvons accéder au réseau en toute sécurité et récupérer les paramètres ?\n\nNote : le plus souvent, c'est sûr si votre base distante est hébergée avec un certificat SSL et si votre réseau n'est pas compromis."; - readonly he: "אם סנכרנו כבר פעם עם מכשיר אחר, מסד הנתונים המרוחד מאחסן ערכי תצורה מתאימים בין המכשירים המסונכרנים. הפלאגין ירצה לאחזר אותם לתצורה חזקה יותר.\n\nעם זאת, עלינו לוודא דבר אחד. האם אנחנו כרגע במצב שבו ניתן לגשת לרשת בבטחה ולאחזר את ההגדרות?\nהערה: ברוב המקרים, אתה בטוח לעשות זאת, כל עוד מסד הנתונים המרוחד שלך מאוחסן עם תעודת SSL, ורשתך אינה פגומה."; - readonly ja: "既に他のデバイスと同期したことがある場合、リモートデータベースには同期されたデバイス間の適切な設定値が保存されています。プラグインは堅牢な設定のためにそれらを取得したいと考えています。\n\nただし、1つ確認が必要です。現在、ネットワークに安全にアクセスして設定を取得できる状況ですか?\n\n注意: リモートデータベースがSSL証明書でホストされており、ネットワークが侵害されていなければ、ほとんどの場合安全に実行できます。"; - readonly ru: "Если мы уже синхронизировались с другим устройством, удалённая база данных хранит подходящие значения конфигурации."; - readonly zh: "If we have already synchronised once with another device, the remote database stores the suitable configuration values between the synchronised devices. The plug-in would like to retrieve them for robust configuration.\n\nHowever, we have to make sure the one thing. Are we currently in a situation where we can access the network safely and retrieve the settings?\n\nNote: Mostly, you are safe to do this, that your remote database is hosted with a SSL certificate, and your network is not compromised."; - }; - readonly "Setup.FetchRemoteConf.Title": { - readonly def: "Fetch configuration from remote database?"; - readonly fr: "Récupérer la configuration depuis la base distante ?"; - readonly he: "אחזר תצורה ממסד הנתונים המרוחד?"; - readonly ja: "リモートデータベースから設定を取得しますか?"; - readonly ru: "Загрузить конфигурацию с удалённой базы данных?"; - readonly zh: "Fetch configuration from remote database?"; - }; - readonly "Setup.QRCode": { - readonly def: "We have generated a QR code to transfer the settings. Please scan the QR code with your phone or other device.\nNote: The QR code is not encrypted, so be careful to open this.\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
"; - readonly fr: "Nous avons généré un QR code pour transférer les paramètres. Scannez-le avec votre téléphone ou un autre appareil.\nNote : le QR code n'est pas chiffré, soyez prudent en l'affichant.\n\n>[!POUR VOS YEUX SEULEMENT]-\n>
${qr_image}
"; - readonly he: "יצרנו קוד QR להעברת ההגדרות. אנא סרוק את קוד ה-QR עם הטלפון או מכשיר אחר.\nהערה: קוד ה-QR אינו מוצפן, אז היה זהיר בפתיחתו.\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
"; - readonly ja: "設定を転送するためのQRコードを生成しました。スマートフォンや他のデバイスでQRコードをスキャンしてください。\n注意: QRコードは暗号化されていないため、開く際は注意してください。\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
"; - readonly ko: "설정을 전송하기 위한 QR 코드를 생성했습니다. 휴대폰이나 다른 기기로 QR 코드를 스캔해 주세요.\n참고: QR 코드는 암호화되지 않았으므로 열 때 주의하세요.\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
"; - readonly ru: "Мы сгенерировали QR-код для передачи настроек. Отсканируйте QR-код телефоном."; - readonly zh: "We have generated a QR code to transfer the settings. Please scan the QR code with your phone or other device.\nNote: The QR code is not encrypted, so be careful to open this.\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
"; - }; - readonly "Setup.RemoteE2EE.AdvancedTitle": { - readonly def: "Advanced"; - readonly es: "Avanzado"; - readonly ja: "詳細設定"; - readonly ko: "고급"; - readonly ru: "Дополнительно"; - readonly zh: "高级"; - readonly "zh-tw": "進階"; - }; - readonly "Setup.RemoteE2EE.AlgorithmWarning": { - readonly def: "Changing the encryption algorithm will prevent access to any data previously encrypted with a different algorithm. Ensure that all your devices are configured to use the same algorithm to maintain access to your data."; - readonly es: "Cambiar el algoritmo de cifrado impedirá el acceso a cualquier dato cifrado anteriormente con otro algoritmo. Asegúrate de que todos tus dispositivos estén configurados para usar el mismo algoritmo y así mantener el acceso a tus datos."; - readonly ja: "暗号化アルゴリズムを変更すると、別のアルゴリズムで暗号化された既存データにはアクセスできなくなります。すべての端末で同じアルゴリズムを使うよう設定し、データにアクセスできる状態を維持してください。"; - readonly ko: "암호화 알고리즘을 변경하면 다른 알고리즘으로 암호화된 기존 데이터에 접근할 수 없게 됩니다. 모든 기기에서 동일한 알고리즘을 사용하도록 설정해 데이터 접근성을 유지하세요."; - readonly ru: "Изменение алгоритма шифрования лишит доступа к данным, которые ранее были зашифрованы другим алгоритмом. Убедитесь, что все ваши устройства настроены на использование одного и того же алгоритма, чтобы сохранить доступ к данным."; - readonly zh: "更改加密算法会导致之前使用其他算法加密的数据无法访问。请确保所有设备都配置为使用同一算法,以保持对数据的访问能力。"; - readonly "zh-tw": "變更加密演算法後,先前以其他演算法加密的資料將無法再存取。請確認所有裝置都設定為使用相同演算法,以維持對資料的存取能力。"; - }; - readonly "Setup.RemoteE2EE.ButtonCancel": { - readonly def: "Cancel"; - readonly es: "Cancelar"; - readonly ja: "キャンセル"; - readonly ko: "취소"; - readonly ru: "Отмена"; - readonly zh: "取消"; - readonly "zh-tw": "取消"; - }; - readonly "Setup.RemoteE2EE.ButtonProceed": { - readonly def: "Proceed"; - readonly es: "Continuar"; - readonly ja: "進む"; - readonly ko: "진행"; - readonly ru: "Продолжить"; - readonly zh: "继续"; - readonly "zh-tw": "繼續"; - }; - readonly "Setup.RemoteE2EE.DefaultAlgorithmDesc": { - readonly def: "In most cases, you should stick with the default algorithm (${algorithm}). This setting is only required if you have an existing Vault encrypted in a different format."; - readonly es: "En la mayoría de los casos, debes mantener el algoritmo predeterminado (${algorithm}). Este ajuste solo es necesario si ya tienes un Vault cifrado con un formato diferente."; - readonly ja: "ほとんどの場合は、既定のアルゴリズム(${algorithm})をそのまま使用してください。この設定が必要になるのは、既存の Vault が別の形式で暗号化されている場合のみです。"; - readonly ko: "대부분의 경우 기본 알고리즘(${algorithm})을 그대로 사용하는 것이 좋습니다. 이 설정은 기존 Vault가 다른 형식으로 암호화되어 있는 경우에만 필요합니다."; - readonly ru: "В большинстве случаев следует оставить алгоритм по умолчанию (${algorithm}). Этот параметр нужен только в том случае, если у вас уже есть Vault, зашифрованный в другом формате."; - readonly zh: "在大多数情况下,你应继续使用默认算法(${algorithm})。只有当你已有一个采用不同格式加密的 Vault 时,才需要调整此设置。"; - readonly "zh-tw": "在大多數情況下,建議維持使用預設演算法(${algorithm})。只有當你現有的 Vault 是以不同格式加密時,才需要調整這項設定。"; - }; - readonly "Setup.RemoteE2EE.Guidance": { - readonly def: "Please configure your end-to-end encryption settings."; - readonly es: "Configura tus ajustes de cifrado de extremo a extremo."; - readonly ja: "エンドツーエンド暗号化の設定を行ってください。"; - readonly ko: "엔드투엔드 암호화 설정을 구성해 주세요."; - readonly ru: "Пожалуйста, настройте параметры сквозного шифрования."; - readonly zh: "请配置你的端到端加密设置。"; - readonly "zh-tw": "請設定你的端對端加密選項。"; - }; - readonly "Setup.RemoteE2EE.LabelEncrypt": { - readonly def: "End-to-End Encryption"; - readonly es: "Cifrado de extremo a extremo"; - readonly ja: "エンドツーエンド暗号化"; - readonly ko: "엔드투엔드 암호화"; - readonly ru: "Сквозное шифрование"; - readonly zh: "端到端加密"; - readonly "zh-tw": "端對端加密"; - }; - readonly "Setup.RemoteE2EE.LabelEncryptionAlgorithm": { - readonly def: "Encryption Algorithm"; - readonly es: "Algoritmo de cifrado"; - readonly ja: "暗号化アルゴリズム"; - readonly ko: "암호화 알고리즘"; - readonly ru: "Алгоритм шифрования"; - readonly zh: "加密算法"; - readonly "zh-tw": "加密演算法"; - }; - readonly "Setup.RemoteE2EE.LabelObfuscateProperties": { - readonly def: "Obfuscate Properties"; - readonly es: "Ofuscar propiedades"; - readonly ja: "プロパティを難読化"; - readonly ko: "속성 난독화"; - readonly ru: "Обфусцировать свойства"; - readonly zh: "混淆属性"; - readonly "zh-tw": "混淆屬性"; - }; - readonly "Setup.RemoteE2EE.MultiDestinationWarning": { - readonly def: "This setting must be the same even when connecting to multiple synchronisation destinations."; - readonly es: "Este ajuste debe ser el mismo incluso cuando te conectes a varios destinos de sincronización."; - readonly ja: "複数の同期先へ接続する場合でも、この設定は同一である必要があります。"; - readonly ko: "여러 동기화 대상에 연결하는 경우에도 이 설정은 동일해야 합니다."; - readonly ru: "Этот параметр должен быть одинаковым даже при подключении к нескольким направлениям синхронизации."; - readonly zh: "即使连接到多个同步目标,此设置也必须保持一致。"; - readonly "zh-tw": "即使連線到多個同步目標,這項設定也必須保持一致。"; - }; - readonly "Setup.RemoteE2EE.ObfuscatePropertiesDesc": { - readonly def: "Obfuscating properties (e.g., path of file, size, creation and modification dates) adds an additional layer of security by making it harder to identify the structure and names of your files and folders on the remote server. This helps protect your privacy and makes it more difficult for unauthorized users to infer information about your data."; - readonly es: "Ofuscar propiedades (por ejemplo, la ruta del archivo, el tamaño y las fechas de creación y modificación) añade una capa adicional de seguridad al dificultar la identificación de la estructura y los nombres de tus archivos y carpetas en el servidor remoto. Esto ayuda a proteger tu privacidad y dificulta que usuarios no autorizados deduzcan información sobre tus datos."; - readonly ja: "プロパティ(ファイルパス、サイズ、作成日時、更新日時など)を難読化すると、リモートサーバー上のファイルやフォルダーの構造や名前を特定しにくくできるため、追加の保護層になります。これによりプライバシーが守られ、権限のない第三者がデータに関する情報を推測しにくくなります。"; - readonly ko: "속성(예: 파일 경로, 크기, 생성일 및 수정일)을 난독화하면 원격 서버에서 파일과 폴더의 구조 및 이름을 식별하기 어렵게 만들어 보안을 한층 강화할 수 있습니다. 이는 개인 정보를 보호하고 권한 없는 사용자가 데이터에 관한 정보를 추론하기 어렵게 만듭니다."; - readonly ru: "Обфускация свойств (например, пути к файлу, размера, дат создания и изменения) добавляет дополнительный уровень защиты, затрудняя определение структуры и названий ваших файлов и папок на удалённом сервере. Это помогает защитить вашу конфиденциальность и усложняет для посторонних вывод информации о ваших данных."; - readonly zh: "混淆属性(例如文件路径、大小、创建和修改日期)可以增加一层额外保护,使远程服务器上的文件与文件夹结构及名称更难被识别。这有助于保护你的隐私,也让未授权用户更难推断你的数据相关信息。"; - readonly "zh-tw": "混淆屬性(例如檔案路徑、大小、建立時間與修改時間)可以額外增加一層安全保護,讓遠端伺服器上的檔案與資料夾結構及名稱更難被辨識。這有助於保護你的隱私,也讓未授權使用者更難推測你的資料資訊。"; - }; - readonly "Setup.RemoteE2EE.PassphraseValidationLine1": { - readonly def: "Please be aware that the End-to-End Encryption passphrase is not validated until the synchronisation process actually commences. This is a security measure designed to protect your data."; - readonly es: "Ten en cuenta que la frase de contraseña del cifrado de extremo a extremo no se valida hasta que el proceso de sincronización comienza realmente. Esta es una medida de seguridad diseñada para proteger tus datos."; - readonly ja: "エンドツーエンド暗号化のパスフレーズは、実際に同期処理が開始されるまで検証されない点にご注意ください。これはデータを保護するためのセキュリティ対策です。"; - readonly ko: "엔드투엔드 암호화 패스프레이즈는 실제 동기화가 시작되기 전까지 검증되지 않는다는 점에 유의하세요. 이것은 데이터를 보호하기 위한 보안 조치입니다."; - readonly ru: "Обратите внимание: парольная фраза для сквозного шифрования не проверяется до фактического начала процесса синхронизации. Это сделано в целях безопасности ваших данных."; - readonly zh: "请注意,在同步过程真正开始之前,端到端加密密码短语不会被校验。这是一项用于保护你数据的安全措施。"; - readonly "zh-tw": "請注意,端對端加密的密語要到同步程序實際開始時才會進行驗證。這是為了保護你資料而設計的安全措施。"; - }; - readonly "Setup.RemoteE2EE.PassphraseValidationLine2": { - readonly def: "Therefore, we ask that you exercise extreme caution when configuring server information manually. If an incorrect passphrase is entered, the data on the server will become corrupted. Please understand that this is intended behaviour."; - readonly es: "Por lo tanto, te pedimos que tengas muchísimo cuidado al configurar manualmente la información del servidor. Si introduces una frase de contraseña incorrecta, los datos del servidor se corromperán. Ten en cuenta que este comportamiento es intencionado."; - readonly ja: "そのため、サーバー情報を手動で設定する際は細心の注意を払ってください。誤ったパスフレーズを入力すると、サーバー上のデータが破損します。これは意図された動作ですので、あらかじめご理解ください。"; - readonly ko: "따라서 서버 정보를 수동으로 구성할 때는 각별히 주의해 주세요. 잘못된 패스프레이즈를 입력하면 서버의 데이터가 손상됩니다. 이는 의도된 동작이니 반드시 이해하고 진행해 주세요."; - readonly ru: "Поэтому при ручной настройке информации о сервере требуется предельная осторожность. Если будет введена неверная парольная фраза, данные на сервере будут повреждены. Пожалуйста, учтите, что это ожидаемое поведение."; - readonly zh: "因此,在手动配置服务器信息时请务必格外小心。如果输入了错误的密码短语,服务器上的数据将会损坏。请理解这属于预期行为。"; - readonly "zh-tw": "因此,在手動設定伺服器資訊時請務必格外小心。如果輸入了錯誤的密語,伺服器上的資料將會損毀。請理解這是系統的預期行為。"; - }; - readonly "Setup.RemoteE2EE.PlaceholderPassphrase": { - readonly def: "Enter your passphrase"; - readonly es: "Introduce tu frase de contraseña"; - readonly ja: "パスフレーズを入力してください"; - readonly ko: "패스프레이즈를 입력하세요"; - readonly ru: "Введите парольную фразу"; - readonly zh: "输入你的密码短语"; - readonly "zh-tw": "輸入你的密語"; - }; - readonly "Setup.RemoteE2EE.StronglyRecommendedLine1": { - readonly def: "Enabling end-to-end encryption ensures that your data is encrypted on your device before being sent to the remote server. This means that even if someone gains access to the server, they won't be able to read your data without the passphrase. Make sure to remember your passphrase, as it will be required to decrypt your data on other devices."; - readonly es: "Al habilitar el cifrado de extremo a extremo, tus datos se cifran en tu dispositivo antes de enviarse al servidor remoto. Esto significa que, incluso si alguien obtiene acceso al servidor, no podrá leer tus datos sin la frase de contraseña. Asegúrate de recordarla, ya que también será necesaria para descifrar tus datos en otros dispositivos."; - readonly ja: "エンドツーエンド暗号化を有効にすると、データはリモートサーバーへ送信される前にこの端末上で暗号化されます。つまり、たとえ誰かがサーバーへアクセスできても、パスフレーズがなければデータを読むことはできません。他の端末でデータを復号する際にも必要になるため、パスフレーズは必ず覚えておいてください。"; - readonly ko: "엔드투엔드 암호화를 활성화하면 데이터가 원격 서버로 전송되기 전에 이 기기에서 암호화됩니다. 즉, 누군가 서버에 접근하더라도 패스프레이즈 없이는 데이터를 읽을 수 없습니다. 다른 기기에서 데이터를 복호화할 때도 필요하므로 패스프레이즈를 반드시 기억해 두세요."; - readonly ru: "При включении сквозного шифрования ваши данные шифруются на устройстве до отправки на удалённый сервер. Это означает, что даже если кто-то получит доступ к серверу, он не сможет прочитать ваши данные без парольной фразы. Обязательно запомните парольную фразу, так как она потребуется для расшифровки данных на других устройствах."; - readonly zh: "启用端到端加密后,数据会先在你的设备上加密,再发送到远程服务器。这意味着即使有人获得了服务器访问权限,没有密码短语也无法读取你的数据。请务必记住你的密码短语,因为在其他设备上解密数据时也需要它。"; - readonly "zh-tw": "啟用端對端加密後,資料會先在你的裝置上完成加密,再傳送到遠端伺服器。這表示即使有人取得伺服器存取權,沒有密語也無法讀取你的資料。請務必記住你的密語,因為其他裝置在解密資料時也需要它。"; - }; - readonly "Setup.RemoteE2EE.StronglyRecommendedLine2": { - readonly def: "Also, please note that if you are using Peer-to-Peer synchronization, this configuration will be used when you switch to other methods and connect to a remote server in the future."; - readonly es: "Además, ten en cuenta que si estás usando sincronización Peer-to-Peer, esta configuración se utilizará cuando más adelante cambies a otros métodos y te conectes a un servidor remoto."; - readonly ja: "また、Peer-to-Peer 同期を使用している場合でも、将来ほかの方式へ切り替えてリモートサーバーへ接続するときには、この設定が使われます。"; - readonly ko: "또한 Peer-to-Peer 동기화를 사용 중이더라도, 나중에 다른 방식으로 전환하여 원격 서버에 연결하면 이 설정이 그대로 사용됩니다."; - readonly ru: "Также обратите внимание: если вы используете синхронизацию Peer-to-Peer, эта конфигурация будет использована, когда вы позже переключитесь на другие методы и подключитесь к удалённому серверу."; - readonly zh: "另外请注意,如果你正在使用 Peer-to-Peer 同步,当你以后切换到其他同步方式并连接远程服务器时,也会使用这组配置。"; - readonly "zh-tw": "另外請注意,即使你目前使用的是 Peer-to-Peer 同步,日後若切換到其他方式並連線到遠端伺服器時,也會沿用這組設定。"; - }; - readonly "Setup.RemoteE2EE.StronglyRecommendedTitle": { - readonly def: "Strongly Recommended"; - readonly es: "Muy recomendable"; - readonly ja: "強く推奨"; - readonly ko: "강력 권장"; - readonly ru: "Настоятельно рекомендуется"; - readonly zh: "强烈推荐"; - readonly "zh-tw": "強烈建議"; - }; - readonly "Setup.RemoteE2EE.Title": { - readonly def: "End-to-End Encryption"; - readonly es: "Cifrado de extremo a extremo"; - readonly ja: "エンドツーエンド暗号化"; - readonly ko: "엔드투엔드 암호화"; - readonly ru: "Сквозное шифрование"; - readonly zh: "端到端加密"; - readonly "zh-tw": "端對端加密"; - }; - readonly "Setup.ScanQRCode.ButtonClose": { - readonly def: "Close this dialog"; - readonly es: "Cerrar este diálogo"; - readonly ja: "このダイアログを閉じる"; - readonly ko: "이 대화 상자 닫기"; - readonly ru: "Закрыть это окно"; - readonly zh: "关闭此对话框"; - readonly "zh-tw": "關閉此對話框"; - }; - readonly "Setup.ScanQRCode.Guidance": { - readonly def: "Please follow the steps below to import settings from your existing device."; - readonly es: "Sigue los pasos de abajo para importar los ajustes desde tu dispositivo actual."; - readonly ja: "既存の端末から設定を取り込むには、以下の手順に従ってください。"; - readonly ko: "기존 기기에서 설정을 가져오려면 아래 단계를 따라 주세요."; - readonly ru: "Чтобы импортировать настройки с существующего устройства, выполните следующие шаги."; - readonly zh: "请按照以下步骤从现有设备导入设置。"; - readonly "zh-tw": "請依照以下步驟,從現有裝置匯入設定。"; - }; - readonly "Setup.ScanQRCode.Step1": { - readonly def: "On this device, please keep this Vault open."; - readonly es: "En este dispositivo, mantén este Vault abierto."; - readonly ja: "この端末では、この Vault を開いたままにしてください。"; - readonly ko: "이 기기에서는 이 Vault를 계속 열어 두세요."; - readonly ru: "На этом устройстве оставьте данный Vault открытым."; - readonly zh: "在这台设备上,请保持此 Vault 处于打开状态。"; - readonly "zh-tw": "在這台裝置上,請保持此 Vault 開啟。"; - }; - readonly "Setup.ScanQRCode.Step2": { - readonly def: "On the source device, open Obsidian."; - readonly es: "En el dispositivo de origen, abre Obsidian."; - readonly ja: "元の端末で Obsidian を開きます。"; - readonly ko: "원본 기기에서 Obsidian을 엽니다."; - readonly ru: "На исходном устройстве откройте Obsidian."; - readonly zh: "在源设备上打开 Obsidian。"; - readonly "zh-tw": "在來源裝置上開啟 Obsidian。"; - }; - readonly "Setup.ScanQRCode.Step3": { - readonly def: "On the source device, from the command palette, run the 'Show settings as a QR code' command."; - readonly es: "En el dispositivo de origen, ejecuta desde la paleta de comandos la orden \"Mostrar ajustes como código QR\"."; - readonly ja: "元の端末でコマンドパレットから「設定を QR コードとして表示」を実行します。"; - readonly ko: "원본 기기에서 명령 팔레트를 열고 \"설정을 QR 코드로 표시\" 명령을 실행합니다."; - readonly ru: "На исходном устройстве в палитре команд выполните команду «Показать настройки как QR-код»."; - readonly zh: "在源设备上,从命令面板运行“将设置显示为二维码”命令。"; - readonly "zh-tw": "在來源裝置上,從命令面板執行「將設定顯示為 QR 碼」命令。"; - }; - readonly "Setup.ScanQRCode.Step4": { - readonly def: "On this device, switch to the camera app or use a QR code scanner to scan the displayed QR code."; - readonly es: "En este dispositivo, cambia a la cámara o usa un escáner QR para escanear el código mostrado."; - readonly ja: "この端末でカメラアプリに切り替えるか QR コードスキャナーを使って、表示された QR コードを読み取ってください。"; - readonly ko: "이 기기에서 카메라 앱으로 전환하거나 QR 코드 스캐너를 사용해 표시된 QR 코드를 스캔하세요."; - readonly ru: "На этом устройстве откройте приложение камеры или используйте сканер QR-кодов, чтобы считать показанный QR-код."; - readonly zh: "在这台设备上,切换到相机应用或使用二维码扫描器扫描显示出的二维码。"; - readonly "zh-tw": "在這台裝置上切換到相機 App,或使用 QR 碼掃描器掃描顯示出的 QR 碼。"; - }; - readonly "Setup.ScanQRCode.Title": { - readonly def: "Scan QR Code"; - readonly es: "Escanear código QR"; - readonly ja: "QRコードをスキャン"; - readonly ko: "QR 코드 스캔"; - readonly ru: "Сканировать QR-код"; - readonly zh: "扫描二维码"; - readonly "zh-tw": "掃描 QR 碼"; - }; - readonly "Setup.ShowQRCode": { - readonly def: "Show QR code"; - readonly fr: "Afficher le QR code"; - readonly he: "הצג קוד QR"; - readonly ja: "QRコードを表示"; - readonly ko: "QR 코드 표시"; - readonly ru: "Показать QR код"; - readonly zh: "使用QR码"; - }; - readonly "Setup.ShowQRCode.Desc": { - readonly def: "Show QR code to transfer the settings."; - readonly fr: "Afficher le QR code pour transférer les paramètres."; - readonly he: "הצג קוד QR להעברת ההגדרות."; - readonly ja: "設定を転送するためのQRコードを表示します。"; - readonly ko: "설정을 전송하기 위한 QR 코드를 표시합니다."; - readonly ru: "Показать QR код для передачи настроек."; - readonly zh: "使用QR码来传递配置"; - }; - readonly "Setup.UseSetupURI.ButtonCancel": { - readonly def: "Cancel"; - readonly es: "Cancelar"; - readonly ja: "キャンセル"; - readonly ko: "취소"; - readonly ru: "Отмена"; - readonly zh: "取消"; - readonly "zh-tw": "取消"; - }; - readonly "Setup.UseSetupURI.ButtonProceed": { - readonly def: "Test Settings and Continue"; - readonly es: "Probar ajustes y continuar"; - readonly ja: "設定をテストして続行"; - readonly ko: "설정 테스트 후 계속"; - readonly ru: "Проверить настройки и продолжить"; - readonly zh: "测试设置并继续"; - readonly "zh-tw": "測試設定並繼續"; - }; - readonly "Setup.UseSetupURI.ErrorFailedToParse": { - readonly def: "Failed to parse the Setup URI. Please check the URI and passphrase."; - readonly es: "No se pudo procesar la URI de configuración. Revisa la URI y la frase de contraseña."; - readonly ja: "Setup URI を解析できませんでした。URI とパスフレーズを確認してください。"; - readonly ko: "Setup URI를 해석하지 못했습니다. URI와 패스프레이즈를 확인해 주세요."; - readonly ru: "Не удалось обработать Setup URI. Проверьте URI и парольную фразу."; - readonly zh: "无法解析 Setup URI,请检查 URI 和密码短语。"; - readonly "zh-tw": "無法解析 Setup URI,請檢查 URI 與密語。"; - }; - readonly "Setup.UseSetupURI.ErrorPassphraseRequired": { - readonly def: "Please enter the vault passphrase."; - readonly es: "Introduce la frase de contraseña del Vault."; - readonly ja: "Vault のパスフレーズを入力してください。"; - readonly ko: "Vault 패스프레이즈를 입력해 주세요."; - readonly ru: "Пожалуйста, введите парольную фразу Vault."; - readonly zh: "请输入 Vault 密码短语。"; - readonly "zh-tw": "請輸入 Vault 的密語。"; - }; - readonly "Setup.UseSetupURI.GuidanceLine1": { - readonly def: "Please enter the Setup URI that was generated during server installation or on another device, along with the vault passphrase."; - readonly es: "Introduce la URI de configuración que se generó durante la instalación del servidor o en otro dispositivo, junto con la frase de contraseña del Vault."; - readonly ja: "サーバーのセットアップ時または別の端末で生成された Setup URI と、Vault のパスフレーズを入力してください。"; - readonly ko: "서버 설치 중 또는 다른 기기에서 생성된 Setup URI와 Vault 패스프레이즈를 입력해 주세요."; - readonly ru: "Введите Setup URI, созданный во время установки сервера или на другом устройстве, а также парольную фразу Vault."; - readonly zh: "请输入在服务器安装期间或另一台设备上生成的 Setup URI,以及 Vault 密码短语。"; - readonly "zh-tw": "請輸入在伺服器安裝期間或其他裝置上產生的 Setup URI,以及 Vault 的密語。"; - }; - readonly "Setup.UseSetupURI.GuidanceLine2": { - readonly def: "Note that you can generate a new Setup URI by running the \"Copy settings as a new Setup URI\" command in the command palette."; - readonly es: "Ten en cuenta que puedes generar una nueva URI de configuración ejecutando el comando \"Copiar ajustes como nueva URI de configuración\" desde la paleta de comandos."; - readonly ja: "コマンドパレットで「設定を新しい Setup URI としてコピー」を実行すると、新しい Setup URI を生成できます。"; - readonly ko: "명령 팔레트에서 \"설정을 새 Setup URI로 복사\" 명령을 실행하면 새 Setup URI를 생성할 수 있습니다."; - readonly ru: "Новый Setup URI можно создать, выполнив в палитре команд команду «Скопировать настройки как новый Setup URI»."; - readonly zh: "你可以在命令面板中运行“将设置复制为新的 Setup URI”命令来生成新的 Setup URI。"; - readonly "zh-tw": "你可以在命令面板中執行「將設定複製為新的 Setup URI」命令來產生新的 Setup URI。"; - }; - readonly "Setup.UseSetupURI.InvalidInfo": { - readonly def: "The Setup URI is invalid. Please check it and try again."; - readonly es: "La URI de configuración no es válida. Revísala e inténtalo de nuevo."; - readonly ja: "Setup URI が無効です。内容を確認して再試行してください。"; - readonly ko: "Setup URI가 올바르지 않습니다. 확인한 뒤 다시 시도해 주세요."; - readonly ru: "Setup URI некорректен. Проверьте его и попробуйте снова."; - readonly zh: "Setup URI 无效,请检查后重试。"; - readonly "zh-tw": "Setup URI 無效,請檢查後再試一次。"; - }; - readonly "Setup.UseSetupURI.LabelPassphrase": { - readonly def: "Vault passphrase"; - readonly es: "Frase de contraseña del Vault"; - readonly ja: "Vault のパスフレーズ"; - readonly ko: "Vault 패스프레이즈"; - readonly ru: "Парольная фраза Vault"; - readonly zh: "Vault 密码短语"; - readonly "zh-tw": "Vault 密語"; - }; - readonly "Setup.UseSetupURI.LabelSetupURI": { - readonly def: "Setup URI"; - readonly es: "URI de configuración"; - readonly ja: "Setup URI"; - readonly ko: "Setup URI"; - readonly ru: "Setup URI"; - readonly zh: "Setup URI"; - readonly "zh-tw": "Setup URI"; - }; - readonly "Setup.UseSetupURI.PlaceholderPassphrase": { - readonly def: "Enter your vault passphrase"; - readonly es: "Introduce la frase de contraseña del Vault"; - readonly ja: "Vault のパスフレーズを入力してください"; - readonly ko: "Vault 패스프레이즈를 입력하세요"; - readonly ru: "Введите парольную фразу Vault"; - readonly zh: "输入 Vault 密码短语"; - readonly "zh-tw": "輸入 Vault 密語"; - }; - readonly "Setup.UseSetupURI.Title": { - readonly def: "Enter Setup URI"; - readonly es: "Introducir URI de configuración"; - readonly ja: "Setup URI を入力"; - readonly ko: "Setup URI 입력"; - readonly ru: "Ввести Setup URI"; - readonly zh: "输入 Setup URI"; - readonly "zh-tw": "輸入 Setup URI"; - }; - readonly "Setup.UseSetupURI.ValidInfo": { - readonly def: "The Setup URI is valid and ready to use."; - readonly es: "La URI de configuración es válida y está lista para usarse."; - readonly ja: "Setup URI は有効で、使用できます。"; - readonly ko: "Setup URI가 유효하며 사용할 준비가 되었습니다."; - readonly ru: "Setup URI корректен и готов к использованию."; - readonly zh: "Setup URI 有效,可以使用。"; - readonly "zh-tw": "Setup URI 有效,可以使用。"; - }; - readonly "Should we keep folders that don't have any files inside?": { - readonly def: "Should we keep folders that don't have any files inside?"; - readonly es: "¿Mantener carpetas vacías?"; - readonly fr: "Conserver les dossiers ne contenant aucun fichier ?"; - readonly he: "האם לשמור תיקיות שאין בהן קבצים?"; - readonly ja: "中にファイルがないフォルダーを保持しますか?"; - readonly ko: "내부에 파일이 없는 폴더를 유지하시겠습니까?"; - readonly ru: "Сохранять папки без файлов?"; - readonly zh: "我们是否应该保留内部没有任何文件的文件夹?"; - }; - readonly "Should we only check for conflicts when a file is opened?": { - readonly def: "Should we only check for conflicts when a file is opened?"; - readonly es: "¿Solo comprobar conflictos al abrir archivo?"; - readonly fr: "Ne vérifier les conflits qu'à l'ouverture d'un fichier ?"; - readonly he: "האם לבדוק קונפליקטים רק בעת פתיחת קובץ?"; - readonly ja: "ファイルを開いたときのみ競合をチェックしますか?"; - readonly ko: "파일을 열 때만 충돌을 확인하시겠습니까?"; - readonly ru: "Проверять конфликты только при открытии файла?"; - readonly zh: "我们是否应该仅在文件打开时检查冲突?"; - }; - readonly "Should we prompt you about conflicting files when a file is opened?": { - readonly def: "Should we prompt you about conflicting files when a file is opened?"; - readonly es: "¿Notificar sobre conflictos al abrir archivo?"; - readonly fr: "Vous demander au sujet des fichiers en conflit à l'ouverture d'un fichier ?"; - readonly he: "האם להציג בקשה לגבי קבצים מתנגשים בעת פתיחת קובץ?"; - readonly ja: "ファイルを開いたときに競合ファイルについて確認を求めますか?"; - readonly ko: "파일을 열 때 충돌하는 파일에 대해 알림을 표시하시겠습니까?"; - readonly ru: "Спрашивать о конфликтующих файлах при открытии файла?"; - readonly zh: "当文件打开时,是否提示冲突文件?"; - }; - readonly "Should we prompt you for every single merge, even if we can safely merge automatcially?": { - readonly def: "Should we prompt you for every single merge, even if we can safely merge automatcially?"; - readonly es: "¿Preguntar en cada fusión aunque sea automática?"; - readonly fr: "Vous demander pour chaque fusion, même si nous pouvons fusionner automatiquement en toute sécurité ?"; - readonly he: "האם להציג בקשת אישור לכל מיזוג יחיד, גם אם ניתן למזג בבטחה אוטומטית?"; - readonly ja: "自動的に安全にマージできる場合でも、すべてのマージについて確認を求めますか?"; - readonly ko: "안전하게 자동 병합할 수 있는 경우에도 모든 병합에 대해 알림을 받으시겠습니까?"; - readonly ru: "Спрашивать о каждом слиянии, даже если мы можем безопасно слить автоматически?"; - readonly zh: "即使我们可以安全地自动合并,是否也应该为每一次合并提示您?"; - }; - readonly "Show full banner": { - readonly def: "Show full banner"; - readonly es: "Mostrar banner completo"; - readonly ja: "完全なバナーを表示"; - readonly ko: "전체 배너 표시"; - readonly ru: "Показывать полный баннер"; - readonly zh: "显示完整横幅"; - readonly "zh-tw": "顯示完整橫幅"; - }; - readonly "Show history": { - readonly def: "Show history"; - readonly "zh-tw": "顯示歷程"; - }; - readonly "Show icon only": { - readonly def: "Show icon only"; - readonly zh: "仅显示图标"; - readonly "zh-tw": "僅顯示圖示"; - }; - readonly "Show only notifications": { - readonly def: "Show only notifications"; - readonly es: "Mostrar solo notificaciones"; - readonly fr: "N'afficher que les notifications"; - readonly he: "הצג התראות בלבד"; - readonly ja: "通知のみ表示"; - readonly ko: "알림만 표시"; - readonly ru: "Показывать только уведомления"; - readonly zh: "仅显示通知"; - }; - readonly "Show status as icons only": { - readonly def: "Show status as icons only"; - readonly es: "Mostrar estado solo con íconos"; - readonly fr: "N'afficher le statut que sous forme d'icônes"; - readonly he: "הצג סטטוס כאייקונים בלבד"; - readonly ja: "ステータス表示をアイコンのみにする"; - readonly ko: "아이콘으로만 상태 표시"; - readonly ru: "Показывать статус только иконками"; - readonly zh: "仅以图标显示状态"; - }; - readonly "Show status icon instead of file warnings banner": { - readonly def: "Show status icon instead of file warnings banner"; - readonly es: "Mostrar icono de estado en lugar del banner de advertencia de archivos"; - readonly fr: "Afficher l'icône de statut au lieu de la bannière d'avertissements"; - readonly he: "הצג אייקון סטטוס במקום פס אזהרות הקובץ"; - readonly ja: "ファイル警告バナーの代わりにステータスアイコンを表示"; - readonly ko: "파일 경고 배너 대신 상태 아이콘 표시"; - readonly ru: "Показывать иконку статуса вместо предупреждения о файлах"; - readonly zh: "显示状态图标,而非文件警告横幅"; - readonly "zh-tw": "以狀態圖示取代檔案警告橫幅"; - }; - readonly "Show status inside the editor": { - readonly def: "Show status inside the editor"; - readonly es: "Mostrar estado dentro del editor"; - readonly fr: "Afficher le statut dans l'éditeur"; - readonly he: "הצג סטטוס בתוך העורך"; - readonly ja: "ステータスをエディタ内に表示"; - readonly ko: "편집기 내부에 상태 표시"; - readonly ru: "Показывать статус внутри редактора"; - readonly zh: "在编辑器内显示状态"; - }; - readonly "Show status on the status bar": { - readonly def: "Show status on the status bar"; - readonly es: "Mostrar estado en la barra de estado"; - readonly fr: "Afficher le statut dans la barre d'état"; - readonly he: "הצג סטטוס בשורת המצב"; - readonly ja: "ステータスバーに、ステータスを表示"; - readonly ko: "상태 바에 상태 표시"; - readonly ru: "Показывать статус в строке состояния"; - readonly zh: "在状态栏上显示状态"; - }; - readonly "Show verbose log. Please enable if you report an issue.": { - readonly def: "Show verbose log. Please enable if you report an issue."; - readonly es: "Mostrar registro detallado. Actívelo si reporta un problema."; - readonly fr: "Afficher un journal verbeux. À activer si vous signalez un problème."; - readonly he: "הצג יומן מפורט. אנא הפעל אם אתה מדווח על בעיה."; - readonly ja: "エラー以外の詳細ログ項目も表示する。問題が発生した場合は有効にしてください。"; - readonly ko: "자세한 로그를 표시합니다. 문제를 신고하는 경우 활성화해 주세요."; - readonly ru: "Показывать подробный лог. Пожалуйста, включите при сообщении о проблеме."; - readonly zh: "显示详细日志。如果您报告问题,请启用此选项 "; - }; - readonly "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": { - readonly def: "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding."; - readonly ja: "一部のデバイスで進捗値が異なっています(最大: ${maxProgress}、最小: ${minProgress})。\nこれは一部のデバイスで同期が完了していない可能性を示しており、競合の原因になることがあります。続行する前に、すべてのデバイスが同期済みであることを確認することを強くおすすめします。"; - readonly ko: "일부 기기의 진행 값이 다릅니다(최대: ${maxProgress}, 최소: ${minProgress}).\n이는 일부 기기가 동기화를 완료하지 않았음을 의미할 수 있으며, 충돌로 이어질 수 있습니다. 계속 진행하기 전에 모든 기기가 동기화되었는지 반드시 확인하는 것을 강력히 권장합니다."; - readonly ru: "У некоторых устройств различаются значения прогресса (макс.: ${maxProgress}, мин.: ${minProgress}).\nЭто может означать, что некоторые устройства ещё не завершили синхронизацию, что может привести к конфликтам. Настоятельно рекомендуется перед продолжением убедиться, что все устройства синхронизированы."; - readonly zh: "某些设备的进度值不同(最大:${maxProgress},最小:${minProgress})。\n这可能表示某些设备尚未完成同步,从而可能引发冲突。强烈建议在继续之前先确认所有设备都已同步。"; - readonly "zh-tw": "某些裝置的進度值不同(最大:${maxProgress},最小:${minProgress})。\n這可能表示某些裝置尚未完成同步,進而可能導致衝突。強烈建議在繼續之前先確認所有裝置都已同步。"; - }; - readonly "Starts synchronisation when a file is saved.": { - readonly def: "Starts synchronisation when a file is saved."; - readonly es: "Inicia sincronización al guardar un archivo"; - readonly fr: "Démarre la synchronisation à l'enregistrement d'un fichier."; - readonly he: "מתחיל סנכרון כאשר קובץ נשמר."; - readonly ja: "ファイルが保存されたときに同期を開始します。"; - readonly ko: "파일이 저장될 때 동기화를 시작합니다."; - readonly ru: "Запускать синхронизацию при сохранении файла."; - readonly zh: "当文件保存时启动同步 "; - }; - readonly "Stop reflecting database changes to storage files.": { - readonly def: "Stop reflecting database changes to storage files."; - readonly es: "Dejar de reflejar cambios de BD en archivos"; - readonly fr: "Arrêter de répercuter les modifications de la base vers les fichiers de stockage."; - readonly he: "הפסק לשקף שינויי מסד נתונים לקבצי אחסון."; - readonly ja: "データベースの変更をストレージファイルに反映させない"; - readonly ko: "데이터베이스 변경 사항을 스토리지 파일에 반영하는 것을 중단합니다."; - readonly ru: "Остановить отражение изменений базы данных в файлы хранилища."; - readonly zh: "停止将数据库更改反映到存储文件 "; - }; - readonly "Stop watching for file changes.": { - readonly def: "Stop watching for file changes."; - readonly es: "Dejar de monitorear cambios en archivos"; - readonly fr: "Arrêter la surveillance des modifications de fichiers."; - readonly he: "הפסק לעקוב אחר שינויי קבצים."; - readonly ja: "監視の停止"; - readonly ko: "파일 변경 사항 감시를 중단합니다."; - readonly ru: "Остановить отслеживание изменений файлов."; - readonly zh: "停止监视文件更改 "; - }; - readonly "Storage -> Database": { - readonly def: "Storage -> Database"; - readonly "zh-tw": "儲存空間 -> 資料庫"; - }; - readonly "Suppress notification of hidden files change": { - readonly def: "Suppress notification of hidden files change"; - readonly es: "Suprimir notificaciones de cambios en archivos ocultos"; - readonly fr: "Supprimer les notifications de modification des fichiers cachés"; - readonly he: "דחוק התראת שינוי קבצים נסתרים"; - readonly ja: "隠しファイルの変更通知を抑制"; - readonly ko: "숨겨진 파일 변경 알림 억제"; - readonly ru: "Подавлять уведомления об изменении скрытых файлов"; - readonly zh: "暂停隐藏文件更改的通知"; - }; - readonly "Suspend database reflecting": { - readonly def: "Suspend database reflecting"; - readonly es: "Suspender reflejo de base de datos"; - readonly fr: "Suspendre la répercussion dans la base"; - readonly he: "השהה שיקוף מסד נתונים"; - readonly ja: "データベース反映の一時停止"; - readonly ko: "데이터베이스 반영 일시 중단"; - readonly ru: "Приостановить отражение базы данных"; - readonly zh: "暂停数据库反映"; - }; - readonly "Suspend file watching": { - readonly def: "Suspend file watching"; - readonly es: "Suspender monitorización de archivos"; - readonly fr: "Suspendre la surveillance des fichiers"; - readonly he: "השהה מעקב קבצים"; - readonly ja: "監視の一時停止"; - readonly ko: "파일 감시 일시 중단"; - readonly ru: "Приостановить отслеживание файлов"; - readonly zh: "暂停文件监视"; - }; - readonly "Switch to IDB": { - readonly def: "Switch to IDB"; - readonly es: "Cambiar a IDB"; - readonly ja: "IDB に切り替える"; - readonly ko: "IDB로 전환"; - readonly ru: "Переключиться на IDB"; - readonly zh: "切换到 IDB"; - readonly "zh-tw": "切換至 IDB"; - }; - readonly "Switch to IndexedDB": { - readonly def: "Switch to IndexedDB"; - readonly es: "Cambiar a IndexedDB"; - readonly ja: "IndexedDB に切り替える"; - readonly ko: "IndexedDB로 전환"; - readonly ru: "Переключиться на IndexedDB"; - readonly zh: "切换到 IndexedDB"; - readonly "zh-tw": "切換至 IndexedDB"; - }; - readonly "Sync after merging file": { - readonly def: "Sync after merging file"; - readonly es: "Sincronizar tras fusionar archivo"; - readonly fr: "Synchroniser après fusion d'un fichier"; - readonly he: "סנכרן לאחר מיזוג קובץ"; - readonly ja: "ファイルがマージ(統合)された時に同期"; - readonly ko: "파일 병합 후 동기화"; - readonly ru: "Синхронизировать после слияния файла"; - readonly zh: "合并文件后同步"; - }; - readonly "Sync automatically after merging files": { - readonly def: "Sync automatically after merging files"; - readonly es: "Sincronizar automáticamente tras fusionar archivos"; - readonly fr: "Synchroniser automatiquement après fusion des fichiers"; - readonly he: "סנכרן אוטומטית לאחר מיזוג קבצים"; - readonly ja: "ファイルのマージ後に自動的に同期"; - readonly ko: "파일 병합 후 자동으로 동기화"; - readonly ru: "Синхронизировать автоматически после слияния файлов"; - readonly zh: "合并文件后自动同步"; - }; - readonly "Sync Mode": { - readonly def: "Sync Mode"; - readonly es: "Modo de sincronización"; - readonly fr: "Mode de synchronisation"; - readonly he: "מצב סנכרון"; - readonly ja: "同期モード"; - readonly ko: "동기화 모드"; - readonly ru: "Режим синхронизации"; - readonly zh: "同步模式"; - }; - readonly "Sync on Editor Save": { - readonly def: "Sync on Editor Save"; - readonly es: "Sincronizar al guardar en editor"; - readonly fr: "Synchroniser à l'enregistrement dans l'éditeur"; - readonly he: "סנכרן בשמירת עורך"; - readonly ja: "エディタでの保存時に、同期されます"; - readonly ko: "편집기 저장 시 동기화"; - readonly ru: "Синхронизация при сохранении в редакторе"; - readonly zh: "编辑器保存时同步"; - }; - readonly "Sync on File Open": { - readonly def: "Sync on File Open"; - readonly es: "Sincronizar al abrir archivo"; - readonly fr: "Synchroniser à l'ouverture d'un fichier"; - readonly he: "סנכרן בפתיחת קובץ"; - readonly ja: "ファイルを開いた時に同期"; - readonly ko: "파일 열기 시 동기화"; - readonly ru: "Синхронизация при открытии файла"; - readonly zh: "打开文件时同步"; - }; - readonly "Sync on Save": { - readonly def: "Sync on Save"; - readonly es: "Sincronizar al guardar"; - readonly fr: "Synchroniser à l'enregistrement"; - readonly he: "סנכרן בשמירה"; - readonly ja: "保存時に同期"; - readonly ko: "저장 시 동기화"; - readonly ru: "Синхронизация при сохранении"; - readonly zh: "保存时同步"; - }; - readonly "Sync on Startup": { - readonly def: "Sync on Startup"; - readonly es: "Sincronizar al iniciar"; - readonly fr: "Synchroniser au démarrage"; - readonly he: "סנכרן בהפעלה"; - readonly ja: "起動時同期"; - readonly ko: "시작 시 동기화"; - readonly ru: "Синхронизация при запуске"; - readonly zh: "启动时同步"; - }; - readonly "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": { - readonly def: "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage."; - readonly es: "Sincronización mediante archivos de registro. Debe haber configurado un almacenamiento de objetos compatible con S3/MinIO/R2。"; - readonly ja: "ジャーナルファイルを利用する同期方式です。S3/MinIO/R2 互換のオブジェクトストレージを事前に構成しておく必要があります。"; - readonly ko: "저널 파일을 활용하는 동기화 방식입니다. S3/MinIO/R2 호환 객체 스토리지를 미리 구성해야 합니다。"; - readonly ru: "Синхронизация с использованием файлов журнала. Необходимо заранее настроить объектное хранилище, совместимое с S3/MinIO/R2。"; - readonly zh: "通过日志文件进行同步。你需要事先部署好兼容 S3/MinIO/R2 的对象存储服务。"; - readonly "zh-tw": "透過日誌檔進行同步。你需要事先部署好相容 S3/MinIO/R2 的物件儲存服務。"; - }; - readonly "Synchronising files": { - readonly def: "Synchronising files"; - readonly es: "Archivos sincronizados"; - readonly ja: "同期するファイル"; - readonly ko: "동기화할 파일"; - readonly ru: "Синхронизируемые файлы"; - readonly zh: "同步文件"; - readonly "zh-tw": "同步中的檔案"; - }; - readonly Syncing: { - readonly def: "Syncing"; - readonly es: "Sincronización"; - readonly ja: "同期"; - readonly ko: "동기화"; - readonly ru: "Синхронизация"; - readonly zh: "同步中"; - readonly "zh-tw": "同步中"; - }; - readonly "Target patterns": { - readonly def: "Target patterns"; - readonly es: "Patrones objetivo"; - readonly ja: "対象パターン"; - readonly ko: "대상 패턴"; - readonly ru: "Целевые шаблоны"; - readonly zh: "目标模式"; - readonly "zh-tw": "目標模式"; - }; - readonly "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": { - readonly def: "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned."; - readonly es: "Solo pruebas - Resolver conflictos sincronizando copias nuevas (puede sobrescribir modificaciones)"; - readonly fr: "Test uniquement - Résout les conflits de fichiers en synchronisant les copies plus récentes, ce qui peut écraser des fichiers modifiés. Prudence."; - readonly he: "לבדיקה בלבד - פתור קונפליקטי קבצים על ידי סנכרון עותקים חדשים יותר של הקובץ, פעולה זו עלולה לדרוס קבצים שונו. היה מוזהר."; - readonly ja: "テスト用 - ファイルの新しいコピーを同期してファイル競合を解決します。これにより変更されたファイルが上書きされる可能性があります。注意してください。"; - readonly ko: "테스트 전용 - 파일의 새로운 사본을 동기화하여 파일 충돌을 해결하며, 수정된 파일을 덮어쓸 수 있습니다. 주의하세요."; - readonly ru: "Только для тестирования - разрешать конфликты файлов синхронизацией новых копий."; - readonly zh: "仅供测试 - 通过同步文件的较新副本来解决文件冲突,这可能会覆盖修改过的文件。请注意 "; - }; - readonly "The delay for consecutive on-demand fetches": { - readonly def: "The delay for consecutive on-demand fetches"; - readonly es: "Retraso entre obtenciones consecutivas"; - readonly fr: "Le délai entre récupérations consécutives à la demande"; - readonly he: "העיכוב עבור משיכות לפי דרישה עוקבות"; - readonly ja: "連続したオンデマンドフェッチの遅延"; - readonly ko: "연속 청크 요청 간 대기 시간"; - readonly ru: "Задержка для последовательных запросов по требованию"; - readonly zh: "连续按需获取的延迟"; - }; - readonly "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": { - readonly def: "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once."; - readonly ja: "次の承認済みノードにはノード情報がありません:\n- ${missingNodes}\n\nこれは、それらがしばらく接続されていないか、古いバージョンのままになっていることを示しています。\n可能であれば、まずすべてのデバイスを更新することをおすすめします。すでに使用していないデバイスがある場合は、リモートを一度ロックすることで承認済みノードをすべてクリアできます。"; - readonly ko: "다음 승인된 노드에는 노드 정보가 없습니다:\n- ${missingNodes}\n\n이는 해당 노드가 한동안 연결되지 않았거나 이전 버전에 머물러 있음을 의미합니다.\n가능하다면 먼저 모든 기기를 업데이트하는 것이 좋습니다. 더 이상 사용하지 않는 기기가 있다면 원격을 한 번 잠가 승인된 노드를 모두 정리할 수 있습니다."; - readonly ru: "Для следующих принятых узлов отсутствует информация об узле:\n- ${missingNodes}\n\nЭто означает, что они давно не подключались или остались на старой версии.\nПо возможности рекомендуется сначала обновить все устройства. Если у вас есть устройства, которые больше не используются, вы можете очистить список всех принятых узлов, один раз заблокировав удалённую базу."; - readonly zh: "以下已接受节点缺少节点信息:\n- ${missingNodes}\n\n这表示它们已有一段时间未连接,或仍停留在较旧版本。\n如有可能,建议先更新所有设备。如果有已不再使用的设备,可以先锁定一次远程端以清除全部已接受节点。"; - readonly "zh-tw": "以下已接受節點缺少節點資訊:\n- ${missingNodes}\n\n這表示它們已有一段時間未連線,或仍停留在較舊版本。\n如有可能,建議先更新所有裝置。如果有已不再使用的裝置,可以先鎖定一次遠端端以清除全部已接受節點。"; - }; - readonly "The Hash algorithm for chunk IDs": { - readonly def: "The Hash algorithm for chunk IDs"; - readonly es: "Algoritmo hash para IDs de chunks"; - readonly fr: "L'algorithme de hachage pour les identifiants de fragments"; - readonly he: "אלגוריתם Hash עבור מזהי נתחים"; - readonly ja: "チャンクIDのハッシュアルゴリズム"; - readonly ko: "청크 ID용 해시 알고리즘"; - readonly ru: "Хэш-алгоритм для ID чанков"; - readonly zh: "块 ID 的哈希算法(实验性)"; - }; - readonly "The IndexedDB adapter often offers superior performance in certain scenarios, but it has been found to cause memory leaks when used with LiveSync mode. When using LiveSync mode, please use IDB adapter instead.": { - readonly def: "The IndexedDB adapter often offers superior performance in certain scenarios, but it has been found to cause memory leaks when used with LiveSync mode. When using LiveSync mode, please use IDB adapter instead."; - readonly "zh-tw": "IndexedDB 適配器在某些情況下通常能提供較佳效能,但在 LiveSync 模式下已發現可能導致記憶體洩漏。使用 LiveSync 模式時,請改用 IDB 適配器。"; - }; - readonly "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": { - readonly def: "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks."; - readonly es: "Duración máxima para incubar chunks. Excedentes se independizan"; - readonly fr: "La durée maximale pendant laquelle les fragments peuvent être incubés dans le document. Les fragments dépassant cette période seront promus en fragments indépendants."; - readonly he: "משך הזמן המקסימלי שנתחים יכולים להישמר זמנית בתוך המסמך. נתחים שחורגים מתקופה זו יהפכו לנתחים עצמאיים."; - readonly ja: "ドキュメント内でチャンクを保持できる最大期間。この期間を超えたチャンクは独立したチャンクに昇格します。"; - readonly ko: "변경 기록이 문서에 함께 보관될 수 있는 최대 시간입니다. 초과 시 문서에서 분리되어 개별로 저장됩니다."; - readonly ru: "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks."; - readonly zh: "文档中可以孵化的数据块的最大持续时间。超过此时间的数据块将成为独立数据块 "; - }; - readonly "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": { - readonly def: "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks."; - readonly es: "Número máximo de chunks que pueden incubarse en el documento. Excedentes se independizan"; - readonly fr: "Le nombre maximum de fragments pouvant être incubés dans le document. Les fragments dépassant ce nombre seront immédiatement promus en fragments indépendants."; - readonly he: "המספר המקסימלי של נתחים שיכולים להישמר זמנית בתוך המסמך. נתחים שחורגים ממספר זה יהפכו מיד לנתחים עצמאיים."; - readonly ja: "ドキュメント内で保持できるチャンクの最大数。この数を超えたチャンクは即座に独立したチャンクに昇格します。"; - readonly ko: "문서 안에 임시로 보관할 수 있는 변경 기록의 최대 개수입니다. 이 수를 초과하면 즉시 독립된 청크로 분리되어 저장됩니다."; - readonly ru: "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks."; - readonly zh: "文档中可以孵化的数据块的最大数量。超过此数量的数据块将立即成为独立数据块 "; - }; - readonly "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": { - readonly def: "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks."; - readonly es: "Tamaño total máximo de chunks incubados. Excedentes se independizan"; - readonly fr: "La taille totale maximale des fragments pouvant être incubés dans le document. Les fragments dépassant cette taille seront immédiatement promus en fragments indépendants."; - readonly he: "הגודל הכולל המקסימלי של נתחים שיכולים להישמר זמנית בתוך המסמך. נתחים שחורגים מגודל זה יהפכו מיד לנתחים עצמאיים."; - readonly ja: "ドキュメント内で保持できるチャンクの最大合計サイズ。このサイズを超えたチャンクは即座に独立したチャンクに昇格します。"; - readonly ko: "문서 안에 임시로 보관할 수 있는 변경 기록의 전체 크기 제한입니다. 초과 시 자동으로 분리됩니다."; - readonly ru: "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks."; - readonly zh: "文档中可以孵化的数据块的最大总大小。超过此大小的数据块将立即成为独立数据块 "; - }; - readonly "The minimum interval for automatic synchronisation on event.": { - readonly def: "The minimum interval for automatic synchronisation on event."; - readonly fr: "L'intervalle minimum pour la synchronisation automatique sur événement."; - readonly he: "מרווח הזמן המינימלי לסנכרון אוטומטי על אירוע."; - readonly ja: "イベント発生時の自動同期における最小間隔です。"; - readonly ko: "이벤트 발생 시 자동 동기화의 최소 간격입니다."; - readonly ru: "Минимальный интервал автоматической синхронизации по событию."; - readonly zh: "基于事件自动同步的最小间隔。"; - readonly "zh-tw": "事件觸發自動同步的最小間隔。"; - }; - readonly "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": { - readonly def: "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer."; - readonly es: "Esta función permite la sincronización directa entre dispositivos. No requiere servidor, pero ambos dispositivos deben estar en línea al mismo tiempo para que la sincronización se produzca, y algunas funciones pueden ser limitadas. La conexión a Internet solo se necesita para la señalización (detección de pares), no para la transferencia de datos。"; - readonly ja: "この機能では端末同士を直接同期できます。サーバーは不要ですが、同期を行うには両端末が同時にオンラインである必要があり、一部機能は制限されます。インターネット接続はシグナリング(ピア検出)にのみ必要で、データ転送自体には使われません。"; - readonly ko: "이 기능은 장치 간 직접 동기화를 제공합니다. 서버는 필요 없지만 동기화가 이루어지려면 두 장치가 동시에 온라인 상태여야 하며 일부 기능은 제한될 수 있습니다. 인터넷 연결은 시그널링(피어 감지)에만 필요하며 데이터 전송 자체에는 필요하지 않습니다。"; - readonly ru: "Эта функция обеспечивает прямую синхронизацию между устройствами. Сервер не требуется, но для синхронизации оба устройства должны быть одновременно в сети, а некоторые функции могут быть ограничены. Подключение к Интернету нужно только для сигнализации (обнаружения пиров), а не для передачи данных。"; - readonly zh: "此功能可在设备之间直接同步,无需服务器;但同步时两台设备必须同时在线,且部分功能可能受限。互联网连接仅用于信令(发现对端),不用于数据传输。"; - readonly "zh-tw": "此功能可在裝置之間直接同步,無需伺服器;但同步時兩台裝置必須同時在線,且部分功能可能受限。網際網路連線僅用於訊號交換(偵測對端),不用於資料傳輸。"; - }; - readonly "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": { - readonly def: "This is an advanced option for users who do not have a URI or who wish to configure detailed settings."; - readonly es: "Esta es una opción avanzada para usuarios que no disponen de un URI o que desean configurar parámetros detallados。"; - readonly ja: "URI を持っていない場合や、詳細設定を手動で行いたいユーザー向けの上級者オプションです。"; - readonly ko: "URI가 없거나 세부 설정을 직접 구성하려는 사용자를 위한 고급 옵션입니다。"; - readonly ru: "Это расширенный вариант для пользователей, у которых нет URI или которые хотят вручную задать подробные параметры。"; - readonly zh: "这是面向没有 URI 或希望手动配置详细参数的高级选项。"; - readonly "zh-tw": "這是面向沒有 URI 或希望手動設定詳細參數的進階選項。"; - }; - readonly "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": { - readonly def: "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance."; - readonly es: "Este es el método de sincronización más adecuado para el diseño. Todas las funciones están disponibles. Debe tener configurada una instancia de CouchDB。"; - readonly ja: "この設計に最も適した同期方式です。すべての機能が利用できます。CouchDB インスタンスを事前に構成しておく必要があります。"; - readonly ko: "이 설계에 가장 적합한 동기화 방식입니다. 모든 기능을 사용할 수 있습니다. CouchDB 인스턴스를 미리 구성해야 합니다。"; - readonly ru: "Это наиболее подходящий для данной архитектуры способ синхронизации. Доступны все функции. Необходимо заранее развернуть экземпляр CouchDB。"; - readonly zh: "这是最符合当前设计的同步方式,所有功能均可用。你需要事先部署好 CouchDB 实例。"; - readonly "zh-tw": "這是最符合目前設計的同步方式,所有功能皆可使用。你需要事先部署好 CouchDB 實例。"; - }; - readonly "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": { - readonly def: "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again."; - readonly es: "Esta frase no se copia a otros dispositivos. Usará `Default` hasta reconfigurar"; - readonly fr: "Cette phrase secrète ne sera pas copiée vers un autre appareil. Elle sera définie à `Default` jusqu'à ce que vous la configuriez à nouveau."; - readonly he: "ביטוי סיסמה זה לא יועתק למכשיר אחר. הוא יוגדר ל-`Default` עד שתגדיר אותו שוב."; - readonly ja: "このパスフレーズは他のデバイスにコピーされません。再度設定するまで`Default`に設定されます。"; - readonly ko: "이 패스프레이즈는 다른 기기로 복사되지 않습니다. 다시 구성할 때까지 `기본값`으로 설정됩니다."; - readonly ru: "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again."; - readonly zh: "此密码不会复制到另一台设备。在您再次配置之前,它将设置为 `Default` "; - }; - readonly "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": { - readonly def: "This will recreate chunks for all files. If there were missing chunks, this may fix the errors."; - readonly es: "Esto recreará los fragmentos de todos los archivos. Si faltaban fragmentos, esto puede corregir los errores."; - readonly ja: "すべてのファイルについてチャンクを再作成します。欠損しているチャンクがあった場合、エラーが解消される可能性があります。"; - readonly ko: "모든 파일의 청크를 다시 생성합니다. 누락된 청크가 있었다면 이 작업으로 오류가 해결될 수 있습니다."; - readonly ru: "Это пересоздаст чанки для всех файлов. Если какие-то чанки отсутствовали, это может исправить ошибки."; - readonly zh: "这会为所有文件重新生成 chunks。如果之前存在缺失的 chunks,这可能修复相关错误。"; - readonly "zh-tw": "這會為所有檔案重新建立 chunks。若先前有遺失的 chunks,這可能修復相關錯誤。"; - }; - readonly "Transfer Tweak": { - readonly def: "Transfer Tweak"; - readonly es: "Ajustes de transferencia"; - readonly ja: "転送の調整"; - readonly ko: "전송 조정"; - readonly ru: "Настройки передачи"; - }; - readonly "TweakMismatchResolve.Action.DisableAutoAcceptCompatible": { - readonly def: "Disable auto-accept"; - }; - readonly "TweakMismatchResolve.Action.Dismiss": { - readonly def: "Dismiss"; - readonly fr: "Ignorer"; - readonly he: "דחה"; - readonly ja: "無視"; - readonly ko: "무시"; - readonly ru: "Отмена"; - readonly zh: "Dismiss"; - }; - readonly "TweakMismatchResolve.Action.EnableAutoAcceptCompatible": { - readonly def: "Enable auto-accept"; - }; - readonly "TweakMismatchResolve.Action.UseConfigured": { - readonly def: "Use configured settings"; - readonly fr: "Utiliser les paramètres configurés"; - readonly he: "השתמש בהגדרות המוגדרות"; - readonly ja: "設定済みの設定を使用"; - readonly ko: "구성된 설정 사용"; - readonly ru: "Использовать настроенные параметры"; - readonly zh: "Use configured settings"; - }; - readonly "TweakMismatchResolve.Action.UseMine": { - readonly def: "Update remote database settings"; - readonly fr: "Mettre à jour les paramètres de la base distante"; - readonly he: "עדכן הגדרות מסד הנתונים המרוחד"; - readonly ja: "リモートデータベースの設定を更新"; - readonly ko: "원격 데이터베이스 설정 업데이트"; - readonly ru: "Обновить настройки удалённой базы данных"; - readonly zh: "Update remote database settings"; - }; - readonly "TweakMismatchResolve.Action.UseMineAcceptIncompatible": { - readonly def: "Update remote database settings but keep as is"; - readonly fr: "Mettre à jour la base distante mais garder en l'état"; - readonly he: "עדכן הגדרות מסד הנתונים המרוחד אך השאר כפי שהוא"; - readonly ja: "リモートデータベースの設定を更新するがそのまま維持"; - readonly ko: "원격 데이터베이스 설정 업데이트하지만 그대로 유지"; - readonly ru: "Обновить настройки, но оставить как есть"; - readonly zh: "Update remote database settings but keep as is"; - }; - readonly "TweakMismatchResolve.Action.UseMineWithRebuild": { - readonly def: "Update remote database settings and rebuild again"; - readonly fr: "Mettre à jour la base distante et reconstruire"; - readonly he: "עדכן הגדרות מסד הנתונים המרוחד ובנה מחדש"; - readonly ja: "リモートデータベースの設定を更新して再構築"; - readonly ko: "원격 데이터베이스 설정 업데이트하고 다시 재구축"; - readonly ru: "Обновить настройки и перестроить снова"; - readonly zh: "Update remote database settings and rebuild again"; - }; - readonly "TweakMismatchResolve.Action.UseRemote": { - readonly def: "Apply settings to this device"; - readonly fr: "Appliquer les paramètres à cet appareil"; - readonly he: "החל הגדרות על מכשיר זה"; - readonly ja: "このデバイスに設定を適用"; - readonly ko: "이 기기에 설정 적용"; - readonly ru: "Применить настройки к этому устройству"; - readonly zh: "Apply settings to this device"; - }; - readonly "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": { - readonly def: "Apply settings to this device, but and ignore incompatibility"; - readonly fr: "Appliquer à cet appareil, mais ignorer l'incompatibilité"; - readonly he: "החל הגדרות על מכשיר זה, אך התעלם מאי-תאימות"; - readonly ja: "このデバイスに設定を適用し、非互換性を無視"; - readonly ko: "이 기기에 설정 적용하지만 호환성 문제 무시"; - readonly ru: "Применить настройки, но игнорировать несовместимость"; - readonly zh: "Apply settings to this device, but and ignore incompatibility"; - }; - readonly "TweakMismatchResolve.Action.UseRemoteWithRebuild": { - readonly def: "Apply settings to this device, and fetch again"; - readonly fr: "Appliquer à cet appareil et récupérer à nouveau"; - readonly he: "החל הגדרות על מכשיר זה ומשוך שוב"; - readonly ja: "このデバイスに設定を適用し、再フェッチ"; - readonly ko: "이 기기에 설정 적용하고 다시 가져오기"; - readonly ru: "Применить настройки и загрузить снова"; - readonly zh: "Apply settings to this device, and fetch again"; - }; - readonly "TweakMismatchResolve.Message.AutoAcceptCompatibleUndefined": { - readonly def: "\nIt appears that the settings differ for each device. You can now automatically apply compatible changes to these configurations.\nWould you like to enable this `auto-accept` setting?"; - }; - readonly "TweakMismatchResolve.Message.Main": { - readonly def: "\nThe settings in the remote database are as follows. These values are configured by other devices, which are synchronised with this device at least once.\n\nIf you want to use these settings, please select Use configured settings.\nIf you want to keep the settings of this device, please select Dismiss.\n\n${table}\n\n>[!TIP]\n> If you want to synchronise all settings, please use `Sync settings via markdown` after applying minimal configuration with this feature.\n\n${additionalMessage}"; - readonly fr: "\nLes paramètres de la base distante sont les suivants. Ces valeurs sont configurées par d'autres appareils, synchronisés au moins une fois avec celui-ci.\n\nPour utiliser ces paramètres, sélectionnez Utiliser les paramètres configurés.\nPour conserver les paramètres de cet appareil, sélectionnez Ignorer.\n\n${table}\n\n>[!ASTUCE]\n> Pour synchroniser tous les paramètres, utilisez « Synchroniser les paramètres via markdown » après application de la configuration minimale avec cette fonctionnalité.\n\n${additionalMessage}"; - readonly he: "\nההגדרות במסד הנתונים המרוחד הן כדלקמן. ערכים אלה הוגדרו על ידי מכשירים אחרים, אשר סונכרנו עם מכשיר זה לפחות פעם אחת.\n\nאם ברצונך להשתמש בהגדרות אלה, אנא בחר %{TweakMismatchResolve.Action.UseConfigured}.\nאם ברצונך לשמור את הגדרות מכשיר זה, אנא בחר %{TweakMismatchResolve.Action.Dismiss}.\n\n${table}\n\n>[!TIP]\n> אם ברצונך לסנכרן את כל ההגדרות, אנא השתמש ב-`סנכרון הגדרות דרך Markdown` לאחר החלת תצורה מינימלית עם תכונה זו.\n\n${additionalMessage}"; - readonly ja: "\nリモートデータベースの設定は以下の通りです。これらの値は、このデバイスと少なくとも1回同期された他のデバイスによって設定されています。\n\nこれらの設定を使用する場合は、設定済みの設定を使用を選択してください。\nこのデバイスの設定を維持する場合は、無視を選択してください。\n\n${table}\n\n>[!TIP]\n> すべての設定を同期したい場合は、この機能で最小限の設定を適用した後、`Sync settings via markdown`を使用してください。\n\n${additionalMessage}"; - readonly ko: "\n원격 데이터베이스의 설정은 다음과 같습니다. 이 값들은 이 기기와 최소 한 번 동기화된 다른 기기에서 구성된 것입니다.\n\n이 설정을 사용하려면 구성된 설정 사용를 선택해 주세요.\n이 기기의 설정을 유지하려면 무시를 선택해 주세요.\n\n${table}\n\n>[!TIP]\n> 모든 설정을 동기화하려면 이 기능으로 최소 구성을 적용한 후 `마크다운을 통한 설정 동기화`를 사용해 주세요.\n\n${additionalMessage}"; - readonly ru: "Настройки в удалённой базе данных следующие. Эти значения настроены другими устройствами."; - readonly zh: "\nThe settings in the remote database are as follows. These values are configured by other devices, which are synchronised with this device at least once.\n\nIf you want to use these settings, please select Use configured settings.\nIf you want to keep the settings of this device, please select Dismiss.\n\n${table}\n\n>[!TIP]\n> If you want to synchronise all settings, please use `Sync settings via markdown` after applying minimal configuration with this feature.\n\n${additionalMessage}"; - }; - readonly "TweakMismatchResolve.Message.MainTweakResolving": { - readonly def: "Your configuration has not been matched with the one on the remote server.\n\nFollowing configuration should be matched:\n\n${table}\n\nLet us know your decision.\n\n${additionalMessage}"; - readonly fr: "Votre configuration ne correspond pas à celle du serveur distant.\n\nLa configuration suivante devrait correspondre :\n\n${table}\n\nFaites-nous part de votre décision.\n\n${additionalMessage}"; - readonly he: "התצורה שלך אינה תואמת לזו שבשרת המרוחד.\n\nיש להתאים את התצורות הבאות:\n\n${table}\n\nאנא הודע לנו על החלטתך.\n\n${additionalMessage}"; - readonly ja: "設定がリモートサーバーの設定と一致しません。\n\n以下の設定が一致している必要があります:\n\n${table}\n\n判断をお知らせください。\n\n${additionalMessage}"; - readonly ko: "구성이 원격 서버의 것과 일치하지 않습니다.\n\n다음 구성이 일치해야 합니다:\n\n${table}\n\n결정을 알려주세요.\n\n${additionalMessage}"; - readonly ru: "Ваша конфигурация не совпадает с удалённым сервером."; - readonly zh: "Your configuration has not been matched with the one on the remote server.\n\nFollowing configuration should be matched:\n\n${table}\n\nLet us know your decision.\n\n${additionalMessage}"; - }; - readonly "TweakMismatchResolve.Message.mineUpdated": { - readonly def: "The device configuration have been adjusted."; - }; - readonly "TweakMismatchResolve.Message.remoteUpdated": { - readonly def: "The configuration stored remotely has been updated."; - }; - readonly "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": { - readonly def: "\n>[!NOTICE]\n> Some changes are compatible but may consume extra storage and transfer volumes. A rebuild is recommended. However, a rebuild may not be performed at present, but may be implemented in future maintenance.\n> ***Please ensure that you have time and are connected to a stable network to apply!***"; - readonly fr: "\n>[!AVIS]\n> Certains changements sont compatibles mais peuvent consommer du stockage et du trafic supplémentaires. Une reconstruction est recommandée. Cependant, elle peut ne pas être effectuée maintenant, mais pourra l'être lors d'une maintenance future.\n> ***Assurez-vous d'avoir du temps et une connexion stable pour appliquer !***"; - readonly he: "\n>[!NOTICE]\n> חלק מהשינויים תואמים אך עלולים לצרוך אחסון ותעבורה נוספים. מומלצת בנייה מחדש. עם זאת, ייתכן שבנייה מחדש לא תתבצע כעת, אך תיושם בתחזוקה עתידית.\n> ***ודא שיש לך זמן ושאתה מחובר לרשת יציבה לפני ההחלה!***"; - readonly ja: "\n>[!NOTICE]\n> 一部の変更は互換性がありますが、追加のストレージと転送量を消費する可能性があります。再構築をお勧めします。ただし、再構築は現時点では実行されない場合がありますが、将来のメンテナンスで実装される可能性があります。\n> ***適用には時間と安定したネットワーク接続が必要です!***"; - readonly ko: "\n>[!NOTICE]\n> 일부 변경사항은 호환 가능하지만 추가 스토리지 및 전송량을 소모할 수 있습니다. 재구축을 권장합니다. 하지만 현재 재구축을 수행하지 않더라도 향후 유지보수에서 구현될 수 있습니다.\n> ***시간적 여유가 있고 안정적인 네트워크에 연결된 상태에서 적용해 주세요!***"; - readonly ru: "Некоторые изменения совместимы, но могут потребовать дополнительного хранилища. Рекомендуется перестроение."; - readonly zh: "\n>[!NOTICE]\n> Some changes are compatible but may consume extra storage and transfer volumes. A rebuild is recommended. However, a rebuild may not be performed at present, but may be implemented in future maintenance.\n> ***Please ensure that you have time and are connected to a stable network to apply!***"; - }; - readonly "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": { - readonly def: "\n>[!WARNING]\n> Some remote configurations are not compatible with the local database of this device. Rebuilding the local database will be required.\n> ***Please ensure that you have time and are connected to a stable network to apply!***"; - readonly fr: "\n>[!AVERTISSEMENT]\n> Certaines configurations distantes ne sont pas compatibles avec la base locale de cet appareil. Une reconstruction de la base locale sera requise.\n> ***Assurez-vous d'avoir du temps et une connexion stable pour appliquer !***"; - readonly he: "\n>[!WARNING]\n> חלק מהתצורות המרוחקות אינן תואמות למסד הנתונים המקומי של מכשיר זה. נדרשת בנייה מחדש של מסד הנתונים המקומי.\n> ***ודא שיש לך זמן ושאתה מחובר לרשת יציבה לפני ההחלה!***"; - readonly ja: "\n>[!WARNING]\n> 一部のリモート設定はこのデバイスのローカルデータベースと互換性がありません。ローカルデータベースの再構築が必要です。\n> ***適用には時間と安定したネットワーク接続が必要です!***"; - readonly ko: "\n>[!WARNING]\n> 일부 원격 구성이 이 기기의 로컬 데이터베이스와 호환되지 않습니다. 로컬 데이터베이스 재구축이 필요합니다.\n> ***시간적 여유가 있고 안정적인 네트워크에 연결된 상태에서 적용해 주세요!***"; - readonly ru: "Некоторые удалённые конфигурации несовместимы с локальной базой данных. Требуется перестроение."; - readonly zh: "\n>[!WARNING]\n> Some remote configurations are not compatible with the local database of this device. Rebuilding the local database will be required.\n> ***Please ensure that you have time and are connected to a stable network to apply!***"; - }; - readonly "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": { - readonly def: "\n>[!NOTICE]\n> We have detected that some of the values are different to make incompatible the local database with the remote database.\n> Some changes are compatible but may consume extra storage and transfer volumes. A rebuild is recommended. However, a rebuild may not be performed at present, but may be implemented in future maintenance.\n> If you want to rebuild, it takes a few minutes or more. **Make sure it is safe to perform it now.**"; - readonly fr: "\n>[!AVIS]\n> Nous avons détecté que certaines valeurs diffèrent et rendent la base locale incompatible avec la base distante.\n> Certains changements sont compatibles mais peuvent consommer du stockage et du trafic supplémentaires. Une reconstruction est recommandée. Cependant, elle peut ne pas être effectuée maintenant, mais pourra l'être lors d'une maintenance future.\n> Si vous souhaitez reconstruire, cela prend quelques minutes ou plus. **Assurez-vous qu'il est sûr de le faire maintenant.**"; - readonly he: "\n>[!NOTICE]\n> זיהינו שחלק מהערכים שונים, מה שגורם לאי-תאימות בין מסד הנתונים המקומי למרוחד.\n> חלק מהשינויים תואמים אך עלולים לצרוך אחסון ותעבורה נוספים. מומלצת בנייה מחדש. עם זאת, ייתכן שבנייה מחדש לא תתבצע כעת, אך תיושם בתחזוקה עתידית.\n> אם ברצונך לבנות מחדש, הדבר ייקח כמה דקות או יותר. **ודא שבטוח לבצע זאת עכשיו.**"; - readonly ja: "\n>[!NOTICE]\n> ローカルデータベースとリモートデータベースの非互換性を引き起こす値の違いが検出されました。\n> 一部の変更は互換性がありますが、追加のストレージと転送量を消費する可能性があります。再構築をお勧めします。ただし、再構築は現時点では実行されない場合がありますが、将来のメンテナンスで実装される可能性があります。\n> 再構築を行う場合は数分以上かかります。**今実行しても安全か確認してください。**"; - readonly ko: "\n>[!NOTICE]\n> 로컬 데이터베이스와 원격 데이터베이스가 호환되지 않도록 만드는 값들이 다른 것을 감지했습니다.\n> 일부 변경사항은 호환 가능하지만 추가 스토리지 및 전송량을 소모할 수 있습니다. 재구축을 권장합니다. 하지만 현재 재구축을 수행하지 않더라도 향후 유지보수에서 구현될 수 있습니다.\n> 재구축을 원한다면 몇 분 이상 소요됩니다. **지금 수행해도 안전한지 확인해 주세요.**"; - readonly ru: "Обнаружены значения, несовместимые с удалённой базой данных. Рекомендуется перестроение."; - readonly zh: "\n>[!NOTICE]\n> We have detected that some of the values are different to make incompatible the local database with the remote database.\n> Some changes are compatible but may consume extra storage and transfer volumes. A rebuild is recommended. However, a rebuild may not be performed at present, but may be implemented in future maintenance.\n> If you want to rebuild, it takes a few minutes or more. **Make sure it is safe to perform it now.**"; - }; - readonly "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": { - readonly def: "\n>[!WARNING]\n> We have detected that some of the values are different to make incompatible the local database with the remote database.\n> Either local or remote rebuilds are required. Both of them takes a few minutes or more. **Make sure it is safe to perform it now.**"; - readonly fr: "\n>[!AVERTISSEMENT]\n> Nous avons détecté que certaines valeurs diffèrent et rendent la base locale incompatible avec la base distante.\n> Une reconstruction locale ou distante est nécessaire. L'une comme l'autre prend quelques minutes ou plus. **Assurez-vous qu'il est sûr de le faire maintenant.**"; - readonly he: "\n>[!WARNING]\n> זיהינו שחלק מהערכים שונים, מה שגורם לאי-תאימות בין מסד הנתונים המקומי למרוחד.\n> נדרשת בנייה מחדש של המסד המקומי או המרוחד. שניהם ייקחו כמה דקות או יותר. **ודא שבטוח לבצע זאת עכשיו.**"; - readonly ja: "\n>[!WARNING]\n> ローカルデータベースとリモートデータベースの非互換性を引き起こす値の違いが検出されました。\n> ローカルまたはリモートの再構築が必要です。どちらも数分以上かかります。**今実行しても安全か確認してください。**"; - readonly ko: "\n>[!WARNING]\n> 로컬 데이터베이스와 원격 데이터베이스가 호환되지 않도록 만드는 값들이 다른 것을 감지했습니다.\n> 로컬 또는 원격 재구축이 필요합니다. 둘 다 몇 분 이상 소요됩니다. **지금 수행해도 안전한지 확인해 주세요.**"; - readonly ru: "Обнаружены значения, несовместимые с удалённой базой данных. Требуется перестроение."; - readonly zh: "\n>[!WARNING]\n> We have detected that some of the values are different to make incompatible the local database with the remote database.\n> Either local or remote rebuilds are required. Both of them takes a few minutes or more. **Make sure it is safe to perform it now.**"; - }; - readonly "TweakMismatchResolve.Table": { - readonly def: "| Value name | This device | On Remote |\n|: --- |: ---- :|: ---- :|\n${rows}\n\n"; - readonly fr: "| Nom de la valeur | Cet appareil | Sur le distant |\n|: --- |: ---- :|: ---- :|\n${rows}\n\n"; - readonly he: "| שם ערך | מכשיר זה | מרוחד |\n|: --- |: ---- :|: ---- :|\n${rows}\n\n"; - readonly ja: "| 値の名前 | このデバイス | リモート |\n|: --- |: ---- :|: ---- :|\n${rows}\n\n"; - readonly ko: "| 값 이름 | 이 기기 | 원격 |\n|: --- |: ---- :|: ---- :|\n${rows}\n\n"; - readonly ru: "| Имя значения | Это устройство | На удалённом |\n|: --- |: ---- :|: ---- :|"; - readonly zh: "| Value name | This device | On Remote |\n|: --- |: ---- :|: ---- :|\n${rows}\n\n"; - }; - readonly "TweakMismatchResolve.Table.Row": { - readonly def: "| ${name} | ${self} | ${remote} |"; - readonly fr: "| ${name} | ${self} | ${remote} |"; - readonly he: "| ${name} | ${self} | ${remote} |"; - readonly ja: "| ${name} | ${self} | ${remote} |"; - readonly ko: "| ${name} | ${self} | ${remote} |"; - readonly ru: "| name | self | remote |"; - readonly zh: "| ${name} | ${self} | ${remote} |"; - }; - readonly "TweakMismatchResolve.Title": { - readonly def: "Configuration Mismatch Detected"; - readonly fr: "Incohérence de configuration détectée"; - readonly he: "זוהתה אי-התאמה בתצורה"; - readonly ja: "設定の不一致が検出されました"; - readonly ko: "구성 불일치 감지"; - readonly ru: "Обнаружено несоответствие конфигурации"; - readonly zh: "Configuration Mismatch Detected"; - }; - readonly "TweakMismatchResolve.Title.AutoAcceptCompatible": { - readonly def: "Auto-Accept Available"; - }; - readonly "TweakMismatchResolve.Title.TweakResolving": { - readonly def: "Configuration Mismatch Detected"; - readonly fr: "Incohérence de configuration détectée"; - readonly he: "זוהתה אי-התאמה בתצורה"; - readonly ja: "設定の不一致が検出されました"; - readonly ko: "구성 불일치 감지"; - readonly ru: "Обнаружено несоответствие конфигурации"; - readonly zh: "Configuration Mismatch Detected"; - }; - readonly "TweakMismatchResolve.Title.UseRemoteConfig": { - readonly def: "Use Remote Configuration"; - readonly fr: "Utiliser la configuration distante"; - readonly he: "השתמש בתצורה המרוחקת"; - readonly ja: "リモート設定を使用"; - readonly ko: "원격 구성 사용"; - readonly ru: "Использовать удалённую конфигурацию"; - readonly zh: "Use Remote Configuration"; - }; - readonly "Ui.Common.Signal.Caution": { - readonly def: "CAUTION"; - readonly zh: "注意"; - }; - readonly "Ui.Common.Signal.Danger": { - readonly def: "DANGER"; - readonly zh: "危险"; - }; - readonly "Ui.Common.Signal.Notice": { - readonly def: "NOTICE"; - readonly zh: "提示"; - }; - readonly "Ui.Common.Signal.Warning": { - readonly def: "WARNING"; - readonly zh: "警告"; - }; - readonly "Ui.Settings.Advanced.LocalDatabaseTweak": { - readonly def: "Local Database Tweak"; - readonly zh: "本地数据库调整"; - }; - readonly "Ui.Settings.Advanced.MemoryCache": { - readonly def: "Memory Cache"; - readonly zh: "内存缓存"; - }; - readonly "Ui.Settings.Advanced.TransferTweak": { - readonly def: "Transfer Tweak"; - readonly zh: "传输调整"; - }; - readonly "Ui.Settings.Common.Analyse": { - readonly def: "Analyse"; - readonly zh: "分析"; - }; - readonly "Ui.Settings.Common.Back": { - readonly def: "Back"; - readonly zh: "返回"; - }; - readonly "Ui.Settings.Common.Check": { - readonly def: "Check"; - readonly zh: "检查"; - }; - readonly "Ui.Settings.Common.Configure": { - readonly def: "Configure"; - readonly zh: "配置"; - }; - readonly "Ui.Settings.Common.Continue": { - readonly def: "Continue"; - readonly zh: "继续"; - }; - readonly "Ui.Settings.Common.Delete": { - readonly def: "Delete"; - readonly zh: "删除"; - }; - readonly "Ui.Settings.Common.Fetch": { - readonly def: "Fetch"; - readonly zh: "获取"; - }; - readonly "Ui.Settings.Common.Lock": { - readonly def: "Lock"; - readonly zh: "锁定"; - }; - readonly "Ui.Settings.Common.Merge": { - readonly def: "Merge"; - readonly zh: "合并"; - }; - readonly "Ui.Settings.Common.Open": { - readonly def: "Open"; - readonly zh: "打开"; - }; - readonly "Ui.Settings.Common.Overwrite": { - readonly def: "Overwrite"; - readonly zh: "覆盖"; - }; - readonly "Ui.Settings.Common.Perform": { - readonly def: "Perform"; - readonly zh: "执行"; - }; - readonly "Ui.Settings.Common.ResetAll": { - readonly def: "Reset all"; - readonly zh: "全部重置"; - }; - readonly "Ui.Settings.Common.ResolveAll": { - readonly def: "Resolve All"; - readonly zh: "全部解决"; - }; - readonly "Ui.Settings.Common.Scan": { - readonly def: "Scan"; - readonly zh: "扫描"; - }; - readonly "Ui.Settings.Common.Send": { - readonly def: "Send"; - readonly zh: "发送"; - }; - readonly "Ui.Settings.Common.Use": { - readonly def: "Use"; - readonly zh: "使用"; - }; - readonly "Ui.Settings.Common.VerifyAll": { - readonly def: "Verify all"; - readonly zh: "全部校验"; - }; - readonly "Ui.Settings.CustomizationSync.OpenDesc": { - readonly def: "Open the dialog"; - readonly zh: "打开此对话框"; - }; - readonly "Ui.Settings.CustomizationSync.Panel": { - readonly def: "Customization Sync"; - readonly zh: "自定义同步"; - }; - readonly "Ui.Settings.CustomizationSync.WarnChangeDeviceName": { - readonly def: "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name."; - readonly zh: "启用此功能时无法修改设备名称。请先关闭此功能,再修改设备名称。"; - }; - readonly "Ui.Settings.CustomizationSync.WarnSetDeviceName": { - readonly def: "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature."; - readonly zh: "请先设置用于标识此设备的设备名称。该名称应在你的设备之间保持唯一。未设置前无法启用此功能。"; - }; - readonly "Ui.Settings.Hatch.AnalyseDatabaseUsage": { - readonly def: "Analyse database usage"; - readonly zh: "分析数据库使用情况"; - }; - readonly "Ui.Settings.Hatch.AnalyseDatabaseUsageDesc": { - readonly def: "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like."; - readonly zh: "分析数据库使用情况,并生成 TSV 报告供你自行诊断。你可以将生成的报告粘贴到任意电子表格工具中查看。"; - }; - readonly "Ui.Settings.Hatch.BackToNonConfigured": { - readonly def: "Back to non-configured"; - readonly zh: "返回未配置状态"; - }; - readonly "Ui.Settings.Hatch.ConvertNonObfuscated": { - readonly def: "Check and convert non-path-obfuscated files"; - readonly zh: "检查并转换未进行路径混淆的文件"; - }; - readonly "Ui.Settings.Hatch.ConvertNonObfuscatedDesc": { - readonly def: "Check the local database for files that were stored without path obfuscation and convert them when needed."; - readonly zh: "检查本地数据库中未按路径混淆方式存储的文件,并在需要时将其转换为正确格式。"; - }; - readonly "Ui.Settings.Hatch.CopyIssueReport": { - readonly def: "Copy Report to clipboard"; - readonly zh: "复制报告到剪贴板"; - }; - readonly "Ui.Settings.Hatch.DatabaseLabel": { - readonly def: "Database: ${details}"; - readonly zh: "数据库:${details}"; - }; - readonly "Ui.Settings.Hatch.DatabaseToStorage": { - readonly def: "Database -> Storage"; - readonly zh: "数据库 -> 存储"; - }; - readonly "Ui.Settings.Hatch.DeleteCustomizationSyncData": { - readonly def: "Delete all customization sync data"; - readonly zh: "删除所有自定义同步数据"; - }; - readonly "Ui.Settings.Hatch.GeneratedReport": { - readonly def: "Generated report"; - readonly zh: "已生成的报告"; - }; - readonly "Ui.Settings.Hatch.Missing": { - readonly def: "Missing"; - readonly zh: "缺失"; - }; - readonly "Ui.Settings.Hatch.ModifiedSize": { - readonly def: "Modified: ${modified}, Size: ${size}"; - readonly zh: "修改时间:${modified},大小:${size}"; - }; - readonly "Ui.Settings.Hatch.ModifiedSizeActual": { - readonly def: "Modified: ${modified}, Size: ${size} (actual size: ${actualSize})"; - readonly zh: "修改时间:${modified},大小:${size}(实际大小:${actualSize})"; - }; - readonly "Ui.Settings.Hatch.PrepareIssueReport": { - readonly def: "Prepare the 'report' to create an issue"; - readonly zh: "准备用于提交问题的报告"; - }; - readonly "Ui.Settings.Hatch.RecoveryAndRepair": { - readonly def: "Recovery and Repair"; - readonly zh: "恢复与修复"; - }; - readonly "Ui.Settings.Hatch.RecreateAll": { - readonly def: "Recreate all"; - readonly zh: "全部重建"; - }; - readonly "Ui.Settings.Hatch.RecreateMissingChunks": { - readonly def: "Recreate missing chunks for all files"; - readonly zh: "为所有文件重新创建缺失的数据块"; - }; - readonly "Ui.Settings.Hatch.RecreateMissingChunksDesc": { - readonly def: "This will recreate chunks for all files. If there were missing chunks, this may fix the errors."; - readonly zh: "此操作会为所有文件重新创建数据块。如果存在缺失的数据块,可能会修复相关错误。"; - }; - readonly "Ui.Settings.Hatch.ResetPanel": { - readonly def: "Reset"; - readonly zh: "重置"; - }; - readonly "Ui.Settings.Hatch.ResetRemoteUsage": { - readonly def: "Reset notification threshold and check the remote database usage"; - readonly zh: "重置通知阈值并检查远程数据库使用情况"; - }; - readonly "Ui.Settings.Hatch.ResetRemoteUsageDesc": { - readonly def: "Reset the remote storage size threshold and check the remote storage size again."; - readonly zh: "重置远程存储大小阈值,并再次检查远程存储大小。"; - }; - readonly "Ui.Settings.Hatch.ResolveAllConflictedFiles": { - readonly def: "Resolve all conflicted files by the newer one"; - readonly zh: "使用较新的版本解决所有冲突文件"; - }; - readonly "Ui.Settings.Hatch.ResolveAllConflictedFilesDesc": { - readonly def: "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one."; - readonly zh: "使用较新的版本解决所有冲突文件。注意:此操作会覆盖较旧版本,且无法恢复被覆盖的内容。"; - }; - readonly "Ui.Settings.Hatch.RunDoctor": { - readonly def: "Run Doctor"; - readonly zh: "运行诊断"; - }; - readonly "Ui.Settings.Hatch.ScanBrokenFiles": { - readonly def: "Scan for broken files"; - readonly zh: "扫描损坏文件"; - }; - readonly "Ui.Settings.Hatch.ScramSwitches": { - readonly def: "Scram Switches"; - readonly zh: "紧急开关"; - }; - readonly "Ui.Settings.Hatch.ShowHistory": { - readonly def: "Show history"; - readonly zh: "查看历史"; - }; - readonly "Ui.Settings.Hatch.StorageLabel": { - readonly def: "Storage: ${details}"; - readonly zh: "存储:${details}"; - }; - readonly "Ui.Settings.Hatch.StorageToDatabase": { - readonly def: "Storage -> Database"; - readonly zh: "存储 -> 数据库"; - }; - readonly "Ui.Settings.Hatch.VerifyAndRepairAllFiles": { - readonly def: "Verify and repair all files"; - readonly zh: "校验并修复所有文件"; - }; - readonly "Ui.Settings.Hatch.VerifyAndRepairAllFilesDesc": { - readonly def: "Compare the content of files between the local database and storage. If they do not match, you will be asked which one to keep."; - readonly zh: "比较本地数据库与存储中的文件内容。如果内容不一致,系统会询问你保留哪一份。"; - }; - readonly "Ui.Settings.Maintenance.Cleanup": { - readonly def: "Perform cleanup"; - readonly zh: "执行清理"; - }; - readonly "Ui.Settings.Maintenance.CleanupDesc": { - readonly def: "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client."; - readonly zh: "丢弃所有非最新修订版本,以减少存储空间占用。此操作要求远程服务器和本地客户端都具备同等大小的可用空间。"; - }; - readonly "Ui.Settings.Maintenance.DeleteLocalDatabase": { - readonly def: "Delete local database to reset or uninstall Self-hosted LiveSync"; - readonly zh: "删除本地数据库以重置或卸载 Self-hosted LiveSync"; - }; - readonly "Ui.Settings.Maintenance.EmergencyRestart": { - readonly def: "Emergency restart"; - readonly zh: "紧急重启"; - }; - readonly "Ui.Settings.Maintenance.EmergencyRestartDesc": { - readonly def: "Disable all synchronisation and restart."; - readonly zh: "禁用所有同步并重新启动。"; - }; - readonly "Ui.Settings.Maintenance.FreshStartWipe": { - readonly def: "Fresh Start Wipe"; - readonly zh: "全新开始清空"; - }; - readonly "Ui.Settings.Maintenance.FreshStartWipeDesc": { - readonly def: "Delete all data on the remote server."; - readonly zh: "删除远程服务器上的所有数据。"; - }; - readonly "Ui.Settings.Maintenance.GarbageCollection": { - readonly def: "Garbage Collection V3 (Beta)"; - readonly zh: "垃圾回收 V3(测试版)"; - }; - readonly "Ui.Settings.Maintenance.GarbageCollectionAction": { - readonly def: "Perform Garbage Collection"; - readonly zh: "执行垃圾回收"; - }; - readonly "Ui.Settings.Maintenance.GarbageCollectionDesc": { - readonly def: "Perform Garbage Collection to remove unused chunks and reduce database size."; - readonly zh: "执行垃圾回收以移除未使用的数据块并减少数据库大小。"; - }; - readonly "Ui.Settings.Maintenance.LockServer": { - readonly def: "Lock Server"; - readonly zh: "锁定服务器"; - }; - readonly "Ui.Settings.Maintenance.LockServerDesc": { - readonly def: "Lock the remote server to prevent synchronisation with other devices."; - readonly zh: "锁定远程服务器,防止与其他设备继续同步。"; - }; - readonly "Ui.Settings.Maintenance.OverwriteRemote": { - readonly def: "Overwrite remote"; - readonly zh: "覆盖远程端"; - }; - readonly "Ui.Settings.Maintenance.OverwriteRemoteDesc": { - readonly def: "Overwrite remote with local DB and passphrase."; - readonly zh: "使用本地数据库和密码短语覆盖远程端数据。"; - }; - readonly "Ui.Settings.Maintenance.OverwriteServerData": { - readonly def: "Overwrite Server Data with This Device's Files"; - readonly zh: "用此设备的文件覆盖服务器数据"; - }; - readonly "Ui.Settings.Maintenance.OverwriteServerDataDesc": { - readonly def: "Rebuild the local and remote database with files from this device."; - readonly zh: "使用此设备上的文件重建本地和远程数据库。"; - }; - readonly "Ui.Settings.Maintenance.PurgeAllJournalCounter": { - readonly def: "Purge all journal counter"; - readonly zh: "清空全部日志计数器"; - }; - readonly "Ui.Settings.Maintenance.PurgeAllJournalCounterDesc": { - readonly def: "Purge all download and upload caches."; - readonly zh: "清空所有下载与上传缓存。"; - }; - readonly "Ui.Settings.Maintenance.RebuildingOperations": { - readonly def: "Rebuilding Operations (Remote Only)"; - readonly zh: "重建操作(仅远程端)"; - }; - readonly "Ui.Settings.Maintenance.Resend": { - readonly def: "Resend"; - readonly zh: "重新发送"; - }; - readonly "Ui.Settings.Maintenance.ResendDesc": { - readonly def: "Resend all chunks to the remote."; - readonly zh: "将所有数据块重新发送到远程端。"; - }; - readonly "Ui.Settings.Maintenance.Reset": { - readonly def: "Reset"; - readonly zh: "重置"; - }; - readonly "Ui.Settings.Maintenance.ResetAllJournalCounter": { - readonly def: "Reset all journal counter"; - readonly zh: "重置全部日志计数器"; - }; - readonly "Ui.Settings.Maintenance.ResetAllJournalCounterDesc": { - readonly def: "Initialise all journal history. On the next sync, every item will be received and sent again."; - readonly zh: "初始化全部日志历史。下次同步时,所有项目都会重新接收并重新发送。"; - }; - readonly "Ui.Settings.Maintenance.ResetJournalReceived": { - readonly def: "Reset journal received history"; - readonly zh: "重置日志接收历史"; - }; - readonly "Ui.Settings.Maintenance.ResetJournalReceivedDesc": { - readonly def: "Initialise journal received history. On the next sync, every item except those sent by this device will be downloaded again."; - readonly zh: "初始化日志接收历史。下次同步时,除当前设备发送的项目外,其余项目都会重新下载。"; - }; - readonly "Ui.Settings.Maintenance.ResetJournalSent": { - readonly def: "Reset journal sent history"; - readonly zh: "重置日志发送历史"; - }; - readonly "Ui.Settings.Maintenance.ResetJournalSentDesc": { - readonly def: "Initialise journal sent history. On the next sync, every item except those received by this device will be sent again."; - readonly zh: "初始化日志发送历史。下次同步时,除当前设备已接收的项目外,其余项目都会重新发送。"; - }; - readonly "Ui.Settings.Maintenance.ResetLocalSyncInfo": { - readonly def: "Reset Synchronisation information"; - readonly zh: "重置同步信息"; - }; - readonly "Ui.Settings.Maintenance.ResetLocalSyncInfoDesc": { - readonly def: "Restore or reconstruct local database from remote."; - readonly zh: "从远程端恢复或重建本地数据库。"; - }; - readonly "Ui.Settings.Maintenance.ResetReceived": { - readonly def: "Reset received"; - readonly zh: "重置接收记录"; - }; - readonly "Ui.Settings.Maintenance.ResetSentHistory": { - readonly def: "Reset sent history"; - readonly zh: "重置发送记录"; - }; - readonly "Ui.Settings.Maintenance.ResetThisDevice": { - readonly def: "Reset Synchronisation on This Device"; - readonly zh: "重置此设备上的同步状态"; - }; - readonly "Ui.Settings.Maintenance.ScheduleAndRestart": { - readonly def: "Schedule and Restart"; - readonly zh: "计划执行并重启"; - }; - readonly "Ui.Settings.Maintenance.Scram": { - readonly def: "Scram!"; - readonly zh: "紧急处理"; - }; - readonly "Ui.Settings.Maintenance.SendChunks": { - readonly def: "Send chunks"; - readonly zh: "发送数据块"; - }; - readonly "Ui.Settings.Maintenance.Syncing": { - readonly def: "Syncing"; - readonly zh: "同步"; - }; - readonly "Ui.Settings.Maintenance.WarningLockedReadyAction": { - readonly def: "I am ready, unlock the database"; - readonly zh: "我已准备好,立即解锁数据库"; - }; - readonly "Ui.Settings.Maintenance.WarningLockedReadyText": { - readonly def: "To prevent unwanted vault corruption, the remote database has been locked for synchronisation. (This device is marked as 'resolved'.) When all your devices are marked as 'resolved', unlock the database. This warning will continue to appear until replication confirms the device is resolved."; - readonly zh: "为防止意外的数据仓库损坏,远程数据库已被锁定,暂停同步。(此设备已被标记为“已确认”)当你的所有设备都标记为“已确认”后,再解锁数据库。在复制过程确认此设备已完成确认之前,此警告会持续显示。"; - }; - readonly "Ui.Settings.Maintenance.WarningLockedResolveAction": { - readonly def: "I have made a backup, mark this device as resolved"; - readonly zh: "我已完成备份,将此设备标记为“已确认”"; - }; - readonly "Ui.Settings.Maintenance.WarningLockedResolveText": { - readonly def: "The remote database is locked for synchronisation to prevent vault corruption because this device is not marked as 'resolved'. Please back up your vault, reset the local database, and select 'Mark this device as resolved'. This warning will persist until replication confirms the device is resolved."; - readonly zh: "为防止数据仓库损坏,由于此设备尚未标记为“已确认”,远程数据库已被锁定,暂停同步。请先备份你的仓库、重置本地数据库,然后选择“将此设备标记为已确认”。在复制过程确认此设备已完成确认之前,此警告会持续显示。"; - }; - readonly "Ui.Settings.Maintenance.WriteRedFlagAndRestart": { - readonly def: "Flag and restart"; - readonly zh: "标记并重启"; - }; - readonly "Ui.Settings.Patches.CompatibilityConflict": { - readonly def: "Compatibility (Conflict Behaviour)"; - readonly zh: "兼容性(冲突行为)"; - }; - readonly "Ui.Settings.Patches.CompatibilityDatabase": { - readonly def: "Compatibility (Database structure)"; - readonly zh: "兼容性(数据库结构)"; - }; - readonly "Ui.Settings.Patches.CompatibilityInternalApi": { - readonly def: "Compatibility (Internal API Usage)"; - readonly zh: "兼容性(内部 API 使用)"; - }; - readonly "Ui.Settings.Patches.CompatibilityMetadata": { - readonly def: "Compatibility (Metadata)"; - readonly zh: "兼容性(元数据)"; - }; - readonly "Ui.Settings.Patches.CompatibilityRemote": { - readonly def: "Compatibility (Remote Database)"; - readonly zh: "兼容性(远程数据库)"; - }; - readonly "Ui.Settings.Patches.CompatibilityTrouble": { - readonly def: "Compatibility (Trouble addressed)"; - readonly zh: "兼容性(已处理问题)"; - }; - readonly "Ui.Settings.Patches.CurrentAdapter": { - readonly def: "Current adapter: ${adapter}"; - readonly zh: "当前适配器:${adapter}"; - }; - readonly "Ui.Settings.Patches.DatabaseAdapter": { - readonly def: "Database Adapter"; - readonly zh: "数据库适配器"; - }; - readonly "Ui.Settings.Patches.DatabaseAdapterDesc": { - readonly def: "Select the database adapter to use."; - readonly zh: "选择要使用的数据库适配器。"; - }; - readonly "Ui.Settings.Patches.EdgeCaseBehaviour": { - readonly def: "Edge case addressing (Behaviour)"; - readonly zh: "边界情况处理(行为)"; - }; - readonly "Ui.Settings.Patches.EdgeCaseDatabase": { - readonly def: "Edge case addressing (Database)"; - readonly zh: "边界情况处理(数据库)"; - }; - readonly "Ui.Settings.Patches.EdgeCaseProcessing": { - readonly def: "Edge case addressing (Processing)"; - readonly zh: "边界情况处理(处理流程)"; - }; - readonly "Ui.Settings.Patches.IndexedDbWarning": { - readonly def: "The IndexedDB adapter often offers superior performance in certain scenarios, but it has been found to cause memory leaks when used with LiveSync mode. When using LiveSync mode, please use the IDB adapter instead."; - readonly zh: "IndexedDB 适配器在某些场景下通常具有更好的性能,但在 LiveSync 模式下已发现可能导致内存泄漏。使用 LiveSync 模式时,请改用 IDB 适配器。"; - }; - readonly "Ui.Settings.Patches.MigratingToIdb": { - readonly def: "Migrating all data to IDB..."; - readonly zh: "正在将所有数据迁移到 IDB..."; - }; - readonly "Ui.Settings.Patches.MigratingToIndexedDb": { - readonly def: "Migrating all data to IndexedDB..."; - readonly zh: "正在将所有数据迁移到 IndexedDB..."; - }; - readonly "Ui.Settings.Patches.MigrationIdbCompleted": { - readonly def: "Migration to IDB completed. Obsidian will be restarted with the new configuration immediately."; - readonly zh: "已完成迁移到 IDB。Obsidian 将立即使用新配置重新启动。"; - }; - readonly "Ui.Settings.Patches.MigrationIdbCompletedFollowUp": { - readonly def: "Migration to IDB completed. Please switch the adapter and restart Obsidian."; - readonly zh: "已完成迁移到 IDB。请切换适配器并重新启动 Obsidian。"; - }; - readonly "Ui.Settings.Patches.MigrationIndexedDbCompleted": { - readonly def: "Migration to IndexedDB completed. Obsidian will be restarted with the new configuration immediately."; - readonly zh: "已完成迁移到 IndexedDB。Obsidian 将立即使用新配置重新启动。"; - }; - readonly "Ui.Settings.Patches.MigrationIndexedDbCompletedFollowUp": { - readonly def: "Migration to IndexedDB completed. Please switch the adapter and restart Obsidian."; - readonly zh: "已完成迁移到 IndexedDB。请切换适配器并重新启动 Obsidian。"; - }; - readonly "Ui.Settings.Patches.MigrationWarning": { - readonly def: "Changing this setting requires migrating existing data, which may take some time, and restarting Obsidian. Please make sure to back up your data before proceeding."; - readonly zh: "修改此设置需要迁移现有数据(可能需要一些时间)并重新启动 Obsidian。请先备份你的数据后再继续。"; - }; - readonly "Ui.Settings.Patches.OperationToIdb": { - readonly def: "to IDB"; - readonly zh: "迁移到 IDB"; - }; - readonly "Ui.Settings.Patches.OperationToIndexedDb": { - readonly def: "to IndexedDB"; - readonly zh: "迁移到 IndexedDB"; - }; - readonly "Ui.Settings.Patches.Remediation": { - readonly def: "Remediation"; - readonly zh: "修正"; - }; - readonly "Ui.Settings.Patches.RemediationChanged": { - readonly def: "Remediation Setting Changed"; - readonly zh: "修正设置已更改"; - }; - readonly "Ui.Settings.Patches.RemediationNoLimit": { - readonly def: "No limit configured"; - readonly zh: "未设置限制"; - }; - readonly "Ui.Settings.Patches.RemediationRestarting": { - readonly def: "Remediation setting changed. Restarting Obsidian..."; - readonly zh: "修正设置已更改,正在重新启动 Obsidian..."; - }; - readonly "Ui.Settings.Patches.RemediationRestartLater": { - readonly def: "Later"; - readonly zh: "稍后"; - }; - readonly "Ui.Settings.Patches.RemediationRestartMessage": { - readonly def: "Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and the display may be inconsistent. Are you sure you want to restart now?"; - readonly zh: "强烈建议重新启动 Obsidian。在重启之前,部分更改可能不会生效,界面显示也可能不一致。确定要现在重启吗?"; - }; - readonly "Ui.Settings.Patches.RemediationRestartNow": { - readonly def: "Restart Now"; - readonly zh: "立即重启"; - }; - readonly "Ui.Settings.Patches.RemediationSuffixChanged": { - readonly def: "Suffix has been changed. Reopening database..."; - readonly zh: "后缀已更改,正在重新打开数据库..."; - }; - readonly "Ui.Settings.Patches.RemediationWithValue": { - readonly def: "Limit: ${date} (${timestamp})"; - readonly zh: "限制:${date}(${timestamp})"; - }; - readonly "Ui.Settings.Patches.RemoteDatabaseSunset": { - readonly def: "Remote Database Tweak (In sunset)"; - readonly zh: "远程数据库调整(即将弃用)"; - }; - readonly "Ui.Settings.Patches.SwitchToIDB": { - readonly def: "Switch to IDB"; - readonly zh: "切换到 IDB"; - }; - readonly "Ui.Settings.Patches.SwitchToIndexedDb": { - readonly def: "Switch to IndexedDB"; - readonly zh: "切换到 IndexedDB"; - }; - readonly "Ui.Settings.PowerUsers.ConfigurationEncryption": { - readonly def: "Configuration Encryption"; - readonly zh: "配置加密"; - }; - readonly "Ui.Settings.PowerUsers.ConnectionTweak": { - readonly def: "CouchDB Connection Tweak"; - readonly zh: "CouchDB 连接调整"; - }; - readonly "Ui.Settings.PowerUsers.ConnectionTweakDesc": { - readonly def: "If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value."; - readonly zh: "如果你在使用 IBM Cloudant 时遇到负载大小限制,请将 batch size 和 batch limit 调低。"; - }; - readonly "Ui.Settings.PowerUsers.Default": { - readonly def: "Default"; - readonly zh: "默认"; - }; - readonly "Ui.Settings.PowerUsers.Developer": { - readonly def: "Developer"; - readonly zh: "开发者"; - }; - readonly "Ui.Settings.PowerUsers.EncryptSensitiveConfig": { - readonly def: "Encrypt sensitive configuration items"; - readonly zh: "加密敏感配置项"; - }; - readonly "Ui.Settings.PowerUsers.PromptPassphraseEveryLaunch": { - readonly def: "Ask for a passphrase at every launch"; - readonly zh: "每次启动时询问密码短语"; - }; - readonly "Ui.Settings.PowerUsers.UseCustomPassphrase": { - readonly def: "Use a custom passphrase"; - readonly zh: "使用自定义密码短语"; - }; - readonly "Ui.Settings.Remote.Activate": { - readonly def: "Activate"; - readonly zh: "启用"; - }; - readonly "Ui.Settings.Remote.ActiveSuffix": { - readonly def: " (Active)"; - readonly zh: "(当前启用)"; - }; - readonly "Ui.Settings.Remote.AddConnection": { - readonly def: "Add new connection"; - readonly zh: "新增连接"; - }; - readonly "Ui.Settings.Remote.AddRemoteDefaultName": { - readonly def: "New Remote"; - readonly zh: "新远程端"; - }; - readonly "Ui.Settings.Remote.ConfigureAndChangeRemote": { - readonly def: "Configure and change remote"; - readonly zh: "配置并切换远程端"; - }; - readonly "Ui.Settings.Remote.ConfigureE2EE": { - readonly def: "Configure E2EE"; - readonly zh: "配置端到端加密"; - }; - readonly "Ui.Settings.Remote.ConfigureRemote": { - readonly def: "Configure Remote"; - readonly zh: "配置远程端"; - }; - readonly "Ui.Settings.Remote.DeleteRemoteConfirm": { - readonly def: "Delete remote configuration '${name}'?"; - readonly zh: "确定要删除远程配置“${name}”吗?"; - }; - readonly "Ui.Settings.Remote.DeleteRemoteTitle": { - readonly def: "Delete Remote Configuration"; - readonly zh: "删除远程配置"; - }; - readonly "Ui.Settings.Remote.DisplayName": { - readonly def: "Display name"; - readonly zh: "显示名称"; - }; - readonly "Ui.Settings.Remote.DuplicateRemote": { - readonly def: "Duplicate remote"; - readonly zh: "复制远程配置"; - }; - readonly "Ui.Settings.Remote.DuplicateRemoteSuffix": { - readonly def: "${name} (Copy)"; - readonly zh: "${name}(副本)"; - }; - readonly "Ui.Settings.Remote.E2EEConfiguration": { - readonly def: "E2EE Configuration"; - readonly zh: "端到端加密配置"; - }; - readonly "Ui.Settings.Remote.Export": { - readonly def: "Export"; - readonly zh: "导出"; - }; - readonly "Ui.Settings.Remote.FetchRemoteSettings": { - readonly def: "Fetch remote settings"; - readonly zh: "获取远程设置"; - }; - readonly "Ui.Settings.Remote.ImportConnection": { - readonly def: "Import connection"; - readonly zh: "导入连接"; - }; - readonly "Ui.Settings.Remote.ImportConnectionPrompt": { - readonly def: "Paste a connection string"; - readonly zh: "粘贴连接字符串"; - }; - readonly "Ui.Settings.Remote.ImportedCouchDb": { - readonly def: "Imported CouchDB"; - readonly zh: "已导入的 CouchDB"; - }; - readonly "Ui.Settings.Remote.ImportedRemote": { - readonly def: "Remote"; - readonly zh: "远程端"; - }; - readonly "Ui.Settings.Remote.MoreActions": { - readonly def: "More actions"; - readonly zh: "更多操作"; - }; - readonly "Ui.Settings.Remote.PeerToPeerPanel": { - readonly def: "Peer-to-Peer Synchronisation"; - readonly zh: "点对点同步"; - }; - readonly "Ui.Settings.Remote.RemoteConfigurationPrefix": { - readonly def: "Remote configuration"; - readonly zh: "远程配置"; - }; - readonly "Ui.Settings.Remote.RemoteDatabases": { - readonly def: "Remote Databases"; - readonly zh: "远程数据库"; - }; - readonly "Ui.Settings.Remote.RemoteName": { - readonly def: "Remote name"; - readonly zh: "远程名称"; - }; - readonly "Ui.Settings.Remote.RemoteNameCouchDb": { - readonly def: "CouchDB ${host}"; - readonly zh: "CouchDB ${host}"; - }; - readonly "Ui.Settings.Remote.RemoteNameP2P": { - readonly def: "P2P ${room}"; - readonly zh: "P2P ${room}"; - }; - readonly "Ui.Settings.Remote.RemoteNameS3": { - readonly def: "S3 ${bucket}"; - readonly zh: "S3 ${bucket}"; - }; - readonly "Ui.Settings.Remote.Rename": { - readonly def: "Rename"; - readonly zh: "重命名"; - }; - readonly "Ui.Settings.Selector.AddDefaultPatterns": { - readonly def: "Add default patterns"; - readonly zh: "添加默认模式"; - }; - readonly "Ui.Settings.Selector.CrossPlatform": { - readonly def: "Cross-platform"; - readonly zh: "跨平台"; - }; - readonly "Ui.Settings.Selector.Default": { - readonly def: "Default"; - readonly zh: "默认"; - }; - readonly "Ui.Settings.Selector.HiddenFiles": { - readonly def: "Hidden Files"; - readonly zh: "隐藏文件"; - }; - readonly "Ui.Settings.Selector.IgnorePatterns": { - readonly def: "Ignore patterns"; - readonly zh: "忽略模式"; - }; - readonly "Ui.Settings.Selector.NonSynchronisingFiles": { - readonly def: "Non-Synchronising files"; - readonly zh: "不同步文件"; - }; - readonly "Ui.Settings.Selector.NonSynchronisingFilesDesc": { - readonly def: "(RegExp) If this is set, any changes to local and remote files that match this will be skipped."; - readonly zh: "(RegExp)如果设置了该项,则本地和远程中匹配这些规则的文件变更将被跳过。"; - }; - readonly "Ui.Settings.Selector.NormalFiles": { - readonly def: "Normal Files"; - readonly zh: "普通文件"; - }; - readonly "Ui.Settings.Selector.OverwritePatterns": { - readonly def: "Overwrite patterns"; - readonly zh: "覆盖模式"; - }; - readonly "Ui.Settings.Selector.OverwritePatternsDesc": { - readonly def: "Patterns to match files for overwriting instead of merging"; - readonly zh: "匹配后将执行覆盖而非合并的文件模式"; - }; - readonly "Ui.Settings.Selector.SynchronisingFiles": { - readonly def: "Synchronising files"; - readonly zh: "同步文件"; - }; - readonly "Ui.Settings.Selector.SynchronisingFilesDesc": { - readonly def: "(RegExp) Empty to sync all files. Set a regular expression filter to limit synchronised files."; - readonly zh: "(RegExp)留空则同步所有文件。可设置正则表达式以限制需要同步的文件。"; - }; - readonly "Ui.Settings.Selector.TargetPatterns": { - readonly def: "Target patterns"; - readonly zh: "目标模式"; - }; - readonly "Ui.Settings.Selector.TargetPatternsDesc": { - readonly def: "Patterns to match files for syncing"; - readonly zh: "用于匹配需要同步文件的模式"; - }; - readonly "Ui.Settings.Setup.RerunWizardButton": { - readonly def: "Rerun Wizard"; - readonly zh: "重新运行向导"; - }; - readonly "Ui.Settings.Setup.RerunWizardDesc": { - readonly def: "Rerun the onboarding wizard to set up Self-hosted LiveSync again."; - readonly zh: "重新运行引导向导,再次设置 Self-hosted LiveSync。"; - }; - readonly "Ui.Settings.Setup.RerunWizardName": { - readonly def: "Rerun Onboarding Wizard"; - readonly zh: "重新运行引导向导"; - }; - readonly "Ui.Settings.SyncSettings.Fetch": { - readonly def: "Fetch"; - readonly zh: "获取"; - }; - readonly "Ui.Settings.SyncSettings.Merge": { - readonly def: "Merge"; - readonly zh: "合并"; - }; - readonly "Ui.Settings.SyncSettings.Overwrite": { - readonly def: "Overwrite"; - readonly zh: "覆盖"; - }; - readonly "Ui.SetupWizard.Common.Back": { - readonly def: "No, please take me back"; - readonly zh: "不,带我返回"; - }; - readonly "Ui.SetupWizard.Common.Cancel": { - readonly def: "Cancel"; - readonly zh: "取消"; - }; - readonly "Ui.SetupWizard.Common.ProceedSelectOption": { - readonly def: "Please select an option to proceed"; - readonly zh: "请选择一个选项后继续"; - }; - readonly "Ui.SetupWizard.Intro.ExistingOption": { - readonly def: "I am adding a device to an existing synchronisation setup"; - readonly zh: "将此设备加入已有同步配置"; - }; - readonly "Ui.SetupWizard.Intro.ExistingOptionDesc": { - readonly def: "Select this if you are already using synchronisation on another computer or smartphone. Use this option to connect this device to that existing setup."; - readonly zh: "如果你已经在另一台电脑或手机上使用同步,请选择此项。此选项用于将当前设备连接到既有同步配置。"; - }; - readonly "Ui.SetupWizard.Intro.Guidance": { - readonly def: "We will now guide you through a few questions to simplify the synchronisation setup."; - readonly zh: "接下来我们会通过几个问题,帮助你更轻松地完成同步配置。"; - }; - readonly "Ui.SetupWizard.Intro.NewOption": { - readonly def: "I am setting this up for the first time"; - readonly zh: "首次设置同步"; - }; - readonly "Ui.SetupWizard.Intro.NewOptionDesc": { - readonly def: "Select this if you are configuring this device as the first synchronisation device."; - readonly zh: "如果你正把这台设备作为第一台同步设备进行配置,请选择此项。"; - }; - readonly "Ui.SetupWizard.Intro.ProceedExisting": { - readonly def: "Yes, I want to add this device to my existing synchronisation"; - readonly zh: "是的,我要将此设备加入现有同步"; - }; - readonly "Ui.SetupWizard.Intro.ProceedNew": { - readonly def: "Yes, I want to set up a new synchronisation"; - readonly zh: "是的,我要开始新的同步配置"; - }; - readonly "Ui.SetupWizard.Intro.Question": { - readonly def: "First, please select the option that best describes your current situation."; - readonly zh: "首先,请选择最符合你当前情况的选项。"; - }; - readonly "Ui.SetupWizard.Intro.Title": { - readonly def: "Welcome to Self-hosted LiveSync"; - readonly zh: "欢迎使用 Self-hosted LiveSync"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.CompatibleOption": { - readonly def: "The remote is already set up, and the configuration is compatible (or became compatible through this operation)."; - readonly zh: "远程端已配置完成,且当前配置兼容(或已通过本次操作变为兼容)。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.CompatibleOptionDesc": { - readonly def: "Unless you are certain, selecting this option is risky. It assumes the server configuration is compatible with this device. If that is not the case, data loss may occur. Please make sure you understand the consequences."; - readonly zh: "除非你非常确定,否则选择此项存在风险。它假定服务器配置与当前设备兼容。如果事实并非如此,可能会导致数据丢失。请确认你了解后果。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.ExistingOption": { - readonly def: "My remote server is already set up. I want to join this device."; - readonly zh: "远程服务器已经配置完成,我想让此设备加入同步。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.ExistingOptionDesc": { - readonly def: "Selecting this option will make this device join the existing server. You need to fetch the existing synchronisation data from the server to this device."; - readonly zh: "选择此项后,此设备会加入已有服务器。你需要将服务器上的现有同步数据获取到此设备。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.Guidance": { - readonly def: "The connection to the server has been configured successfully. As the next step, the local database, in other words the synchronisation information, must be rebuilt."; - readonly zh: "服务器连接已成功配置。下一步需要重建本地数据库,也就是同步状态信息。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.NewOption": { - readonly def: "I am setting up a new server for the first time / I want to reset my existing server."; - readonly zh: "我是第一次配置新服务器 / 我想重置现有服务器。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.NewOptionDesc": { - readonly def: "Selecting this option will initialise the server using the current data on this device. Any existing data on the server will be completely overwritten."; - readonly zh: "选择此项后,服务器会使用当前设备上的数据进行初始化。服务器上的现有数据将被完全覆盖。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.ProceedApplySettings": { - readonly def: "Apply the settings"; - readonly zh: "应用这些设置"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.ProceedNext": { - readonly def: "Proceed to the next step."; - readonly zh: "继续下一步"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.Question": { - readonly def: "Please select your situation."; - readonly zh: "请选择你的当前情况。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.Title": { - readonly def: "Mostly Complete: Decision Required"; - readonly zh: "即将完成:还需要做出选择"; - }; - readonly "Ui.SetupWizard.OutroNewUser.GuidancePrimary": { - readonly def: "The connection to the server has been configured successfully. As the next step, the synchronisation data on the server will be built from the current data on this device."; - readonly zh: "服务器连接已成功配置。下一步将根据当前设备上的数据,在服务器端建立同步数据。"; - }; - readonly "Ui.SetupWizard.OutroNewUser.GuidanceWarning": { - readonly def: "After restarting, the data on this device will be uploaded to the server as the master copy. Please note that any unintended data currently on the server will be completely overwritten."; - readonly zh: "重启后,当前设备上的数据会作为主副本上传到服务器。请注意,服务器上现有的非预期数据将被完全覆盖。"; - }; - readonly "Ui.SetupWizard.OutroNewUser.Important": { - readonly def: "IMPORTANT"; - readonly zh: "重要"; - }; - readonly "Ui.SetupWizard.OutroNewUser.Proceed": { - readonly def: "Restart and Initialise Server"; - readonly zh: "重启并初始化服务器"; - }; - readonly "Ui.SetupWizard.OutroNewUser.Question": { - readonly def: "Please select the button below to restart and proceed to the final confirmation."; - readonly zh: "请选择下方按钮,重启并进入最终确认步骤。"; - }; - readonly "Ui.SetupWizard.OutroNewUser.Title": { - readonly def: "Setup Complete: Preparing to Initialise Server"; - readonly zh: "设置完成:准备初始化服务器"; - }; - readonly "Ui.SetupWizard.SelectExisting.Guidance": { - readonly def: "You are adding this device to an existing synchronisation setup."; - readonly zh: "你正在将此设备加入已有同步配置。"; - }; - readonly "Ui.SetupWizard.SelectExisting.ManualOption": { - readonly def: "Enter the server information manually"; - readonly zh: "手动输入服务器信息"; - }; - readonly "Ui.SetupWizard.SelectExisting.ManualOptionDesc": { - readonly def: "Configure the same server information as your other devices again manually. This is intended only for advanced users."; - readonly zh: "手动重新配置与你其他设备相同的服务器信息。此方式仅适用于高级用户。"; - }; - readonly "Ui.SetupWizard.SelectExisting.ProceedManual": { - readonly def: "I know my server details, let me enter them"; - readonly zh: "我知道服务器信息,让我手动输入"; - }; - readonly "Ui.SetupWizard.SelectExisting.ProceedQr": { - readonly def: "Scan the QR code displayed on an active device using this device's camera."; - readonly zh: "使用本设备摄像头扫描活动设备上显示的二维码"; - }; - readonly "Ui.SetupWizard.SelectExisting.ProceedSetupUri": { - readonly def: "Proceed with Setup URI"; - readonly zh: "使用 Setup URI 继续"; - }; - readonly "Ui.SetupWizard.SelectExisting.QrOption": { - readonly def: "Scan a QR Code (Recommended for mobile)"; - readonly zh: "扫描二维码(移动端推荐)"; - }; - readonly "Ui.SetupWizard.SelectExisting.QrOptionDesc": { - readonly def: "Scan the QR code displayed on an active device using this device's camera."; - readonly zh: "使用本设备摄像头扫描活动设备上显示的二维码。"; - }; - readonly "Ui.SetupWizard.SelectExisting.Question": { - readonly def: "Please select a method to import the settings from another device."; - readonly zh: "请选择一种从其他设备导入设置的方法。"; - }; - readonly "Ui.SetupWizard.SelectExisting.SetupUriOption": { - readonly def: "Use a Setup URI (Recommended)"; - readonly zh: "使用 Setup URI(推荐)"; - }; - readonly "Ui.SetupWizard.SelectExisting.SetupUriOptionDesc": { - readonly def: "Paste the Setup URI generated from one of your active devices."; - readonly zh: "粘贴从某台已启用设备生成的 Setup URI。"; - }; - readonly "Ui.SetupWizard.SelectExisting.Title": { - readonly def: "Device Setup Method"; - readonly zh: "设备设置方式"; - }; - readonly "Ui.SetupWizard.SelectNew.Guidance": { - readonly def: "We will now proceed with the server configuration."; - readonly zh: "接下来将继续配置服务器连接信息。"; - }; - readonly "Ui.SetupWizard.SelectNew.ManualOption": { - readonly def: "Enter the server information manually"; - readonly zh: "手动输入服务器信息"; - }; - readonly "Ui.SetupWizard.SelectNew.ManualOptionDesc": { - readonly def: "This is an advanced option for users who do not have a Setup URI or who want to configure detailed settings."; - readonly zh: "如果你没有 Setup URI,或希望自行配置更详细的参数,可选择此高级选项。"; - }; - readonly "Ui.SetupWizard.SelectNew.ProceedManual": { - readonly def: "I know my server details, let me enter them"; - readonly zh: "我知道服务器信息,让我手动输入"; - }; - readonly "Ui.SetupWizard.SelectNew.ProceedSetupUri": { - readonly def: "Proceed with Setup URI"; - readonly zh: "使用 Setup URI 继续"; - }; - readonly "Ui.SetupWizard.SelectNew.Question": { - readonly def: "How would you like to configure the connection to your server?"; - readonly zh: "你希望如何配置服务器连接?"; - }; - readonly "Ui.SetupWizard.SelectNew.SetupUriOption": { - readonly def: "Use a Setup URI (Recommended)"; - readonly zh: "使用 Setup URI(推荐)"; - }; - readonly "Ui.SetupWizard.SelectNew.SetupUriOptionDesc": { - readonly def: "A Setup URI is a single string containing your server address and authentication details. If one was generated by your server installation script, it provides a simple and secure configuration method."; - readonly zh: "Setup URI 是一段包含服务器地址和认证信息的文本。如果你的服务器安装脚本已经生成了它,这是最简单且安全的配置方式。"; - }; - readonly "Ui.SetupWizard.SelectNew.Title": { - readonly def: "Connection Method"; - readonly zh: "连接方式"; - }; - readonly "Ui.SetupWizard.SetupRemote.BucketOption": { - readonly def: "S3/MinIO/R2 Object Storage"; - readonly zh: "S3/MinIO/R2 对象存储"; - }; - readonly "Ui.SetupWizard.SetupRemote.BucketOptionDesc": { - readonly def: "Synchronisation using journal files. You must already have an S3/MinIO/R2 compatible object storage service set up."; - readonly zh: "使用日志文件进行同步。你需要先准备好兼容 S3/MinIO/R2 的对象存储服务。"; - }; - readonly "Ui.SetupWizard.SetupRemote.CouchDbOptionDesc": { - readonly def: "This is the most suitable synchronisation method for the current design. All features are available. You must already have a CouchDB instance set up."; - readonly zh: "这是当前设计下最适合的同步方式,所有功能都可用。你需要先准备好 CouchDB 实例。"; - }; - readonly "Ui.SetupWizard.SetupRemote.Guidance": { - readonly def: "Please select the type of server you are connecting to."; - readonly zh: "请选择你要连接的服务器类型。"; - }; - readonly "Ui.SetupWizard.SetupRemote.P2POption": { - readonly def: "Peer-to-Peer only"; - readonly zh: "仅点对点"; - }; - readonly "Ui.SetupWizard.SetupRemote.P2POptionDesc": { - readonly def: "This enables direct synchronisation between devices. No server is required, but both devices must be online at the same time and some features may be limited. Internet connectivity is required only for signalling, not for data transfer."; - readonly zh: "启用设备之间的直接同步。无需服务器,但两台设备必须同时在线,且部分功能可能受限。互联网连接仅用于信令,不用于传输数据。"; - }; - readonly "Ui.SetupWizard.SetupRemote.ProceedBucket": { - readonly def: "Continue to S3/MinIO/R2 setup"; - readonly zh: "继续配置 S3/MinIO/R2"; - }; - readonly "Ui.SetupWizard.SetupRemote.ProceedCouchDb": { - readonly def: "Continue to CouchDB setup"; - readonly zh: "继续配置 CouchDB"; - }; - readonly "Ui.SetupWizard.SetupRemote.ProceedP2P": { - readonly def: "Continue to Peer-to-Peer only setup"; - readonly zh: "继续配置仅点对点模式"; - }; - readonly "Ui.SetupWizard.SetupRemote.Title": { - readonly def: "Enter Server Information"; - readonly zh: "输入服务器信息"; - }; - readonly "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": { - readonly def: "Unique name between all synchronized devices. To edit this setting, please disable customization sync once."; - readonly es: "Nombre único entre dispositivos sincronizados. Para editarlo, desactive sincronización de personalización"; - readonly fr: "Nom unique parmi tous les appareils synchronisés. Pour modifier ce paramètre, désactivez d'abord la synchronisation de personnalisation."; - readonly he: "שם ייחודי בין כל המכשירים המסונכרנים. כדי לערוך הגדרה זו, אנא נטרל את סנכרון ההתאמה האישית פעם אחת."; - readonly ja: "同期するすべての端末間で重複しない(一意の)名前。この設定を変更する場合、カスタマイズ同期を無効にしてください。"; - readonly ko: "모든 동기화된 기기 간 고유 이름입니다. 이 설정을 편집하려면 사용자 설정 동기화를 한 번 비활성화해 주세요."; - readonly ru: "Уникальное имя между всеми синхронизируемыми устройствами."; - readonly zh: "所有同步设备之间的唯一名称。要编辑此设置,请首先禁用自定义同步"; - }; - readonly "Use a custom passphrase": { - readonly def: "Use a custom passphrase"; - readonly es: "Usar una frase de contraseña personalizada"; - readonly ja: "カスタムパスフレーズを使う"; - readonly ko: "사용자 지정 암호문구 사용"; - readonly ru: "Использовать пользовательскую парольную фразу"; - readonly zh: "使用自定义密码短语"; - }; - readonly "Use a Setup URI (Recommended)": { - readonly def: "Use a Setup URI (Recommended)"; - readonly es: "Usar un URI de configuración (recomendado)"; - readonly ja: "Setup URI を使う(推奨)"; - readonly ko: "설정 URI 사용(권장)"; - readonly ru: "Использовать Setup URI (рекомендуется)"; - readonly zh: "使用 Setup URI(推荐)"; - readonly "zh-tw": "使用 Setup URI(推薦)"; - }; - readonly "Use Custom HTTP Handler": { - readonly def: "Use Custom HTTP Handler"; - readonly es: "Usar manejador HTTP personalizado"; - readonly fr: "Utiliser un gestionnaire HTTP personnalisé"; - readonly he: "השתמש ב-HTTP Handler מותאם אישית"; - readonly ja: "カスタムHTTPハンドラーの利用"; - readonly ko: "커스텀 HTTP 핸들러 사용"; - readonly ru: "Использовать пользовательский HTTP обработчик"; - readonly zh: "使用自定义 HTTP 处理程序"; - }; - readonly "Use dynamic iteration count": { - readonly def: "Use dynamic iteration count"; - readonly es: "Usar conteo de iteraciones dinámico"; - readonly fr: "Utiliser un compteur d'itérations dynamique"; - readonly he: "השתמש בספירת איטרציות דינמית"; - readonly ja: "動的な繰り返し回数"; - readonly ko: "동적 반복 횟수 사용"; - readonly ru: "Использовать динамическое количество итераций"; - readonly zh: "使用动态迭代次数"; - }; - readonly "Use Segmented-splitter": { - readonly def: "Use Segmented-splitter"; - readonly es: "Usar divisor segmentado"; - readonly fr: "Utiliser le découpeur segmenté"; - readonly he: "השתמש ב-Segmented-splitter"; - readonly ja: "セグメント分割を使用"; - readonly ko: "의미 기반 분할 사용"; - readonly ru: "Использовать сегментный разделитель"; - readonly zh: "使用分段分割器"; - }; - readonly "Use splitting-limit-capped chunk splitter": { - readonly def: "Use splitting-limit-capped chunk splitter"; - readonly es: "Usar divisor de chunks con límite"; - readonly fr: "Utiliser le découpeur de fragments plafonné"; - readonly he: "השתמש ב-chunk splitter עם מגבלת פיצול"; - readonly ja: "分割制限付きチャンク分割を使用"; - readonly ko: "분할 제한 상한 청크 분할기 사용"; - readonly ru: "Использовать разделитель чанков с ограничением"; - readonly zh: "使用分割限制上限的块分割器"; - }; - readonly "Use the trash bin": { - readonly def: "Use the trash bin"; - readonly es: "Usar papelera"; - readonly fr: "Utiliser la corbeille"; - readonly he: "השתמש בסל האשפה"; - readonly ja: "ゴミ箱を使用"; - readonly ko: "휴지통 사용"; - readonly ru: "Использовать корзину"; - readonly zh: "使用回收站"; - }; - readonly "Use timeouts instead of heartbeats": { - readonly def: "Use timeouts instead of heartbeats"; - readonly es: "Usar timeouts en lugar de latidos"; - readonly fr: "Utiliser des délais d'attente au lieu de battements"; - readonly he: "השתמש בפסק זמן במקום פעימות לב"; - readonly ja: "ハートビートの代わりにタイムアウトを使用"; - readonly ko: "하트비트 대신 타임아웃 사용"; - readonly ru: "Использовать таймауты вместо пульса"; - readonly zh: "使用超时而不是心跳"; - }; - readonly username: { - readonly def: "username"; - readonly es: "nombre de usuario"; - readonly fr: "nom d'utilisateur"; - readonly he: "שם משתמש"; - readonly ja: "ユーザー名"; - readonly ko: "사용자명"; - readonly ru: "имя пользователя"; - readonly zh: "用户名"; - }; - readonly Username: { - readonly def: "Username"; - readonly es: "Usuario"; - readonly fr: "Nom d'utilisateur"; - readonly he: "שם משתמש"; - readonly ja: "ユーザー名"; - readonly ko: "사용자명"; - readonly ru: "Имя пользователя"; - readonly zh: "用户名"; - }; - readonly "Verbose Log": { - readonly def: "Verbose Log"; - readonly es: "Registro detallado"; - readonly fr: "Journal verbeux"; - readonly he: "יומן מפורט"; - readonly ja: "エラー以外のログ項目"; - readonly ko: "자세한 로그"; - readonly ru: "Подробный лог"; - readonly zh: "详细日志"; - }; - readonly "Verify all": { - readonly def: "Verify all"; - readonly es: "Verificar todo"; - readonly ja: "すべて検証"; - readonly ko: "모두 검증"; - readonly ru: "Проверить всё"; - readonly zh: "全部校验"; - readonly "zh-tw": "全部驗證"; - }; - readonly "Verify and repair all files": { - readonly def: "Verify and repair all files"; - readonly es: "Verificar y reparar todos los archivos"; - readonly ja: "すべてのファイルを検証して修復"; - readonly ko: "모든 파일 검증 및 복구"; - readonly ru: "Проверить и восстановить все файлы"; - readonly zh: "校验并修复所有文件"; - readonly "zh-tw": "驗證並修復所有檔案"; - }; - readonly "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": { - readonly def: "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information."; - readonly es: "¡Advertencia! Impacta rendimiento. Los logs no se sincronizan con nombre predeterminado. Contienen información confidencial"; - readonly fr: "Attention ! Ceci aura un impact important sur les performances. De plus, les journaux ne seront pas synchronisés sous le nom par défaut. Soyez prudent avec les journaux ; ils contiennent souvent des informations confidentielles."; - readonly he: "אזהרה! לכך תהיה השפעה רצינית על הביצועים. בנוסף, היומנים לא יסונכרנו תחת השם ברירת המחדל. אנא היה זהיר עם יומנים; הם לרוב מכילים מידע סודי שלך."; - readonly ja: "警告!これはパフォーマンスに重大な影響を与えます。また、ログはデフォルト名では同期されません。ログには機密情報が含まれることが多いため、注意してください。"; - readonly ko: "경고! 이는 성능에 심각한 영향을 미칩니다. 로그는 기본 이름으로 동기화되지 않습니다. 로그에는 종종 기밀 정보가 포함되어 있으므로 주의해 주세요."; - readonly ru: "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information."; - readonly zh: "警告!这将严重影响性能。并且日志不会以默认名称同步。请小心处理日志;它们通常包含您的敏感信息 "; - }; - readonly "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": { - readonly def: "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name."; - readonly es: "No podemos cambiar el nombre del dispositivo mientras esta función esté habilitada. Deshabilita la función para cambiarlo."; - readonly ja: "この機能が有効な間はデバイス名を変更できません。変更するにはこの機能を無効にしてください。"; - readonly ko: "이 기능이 활성화되어 있는 동안에는 장치 이름을 변경할 수 없습니다. 장치 이름을 변경하려면 이 기능을 비활성화하세요."; - readonly ru: "Невозможно изменить имя устройства, пока эта функция включена. Отключите её, чтобы изменить имя устройства."; - readonly zh: "启用此功能时无法更改设备名称。如需修改设备名称,请先禁用此功能。"; - }; - readonly "We will now guide you through a few questions to simplify the synchronisation setup.": { - readonly def: "We will now guide you through a few questions to simplify the synchronisation setup."; - readonly es: "Ahora le guiaremos con unas pocas preguntas para simplificar la configuración de la sincronización。"; - readonly ja: "これからいくつかの質問に沿って、同期設定を簡単に進めます。"; - readonly ko: "동기화 설정을 더 쉽게 진행할 수 있도록 몇 가지 질문으로 안내해 드리겠습니다。"; - readonly ru: "Сейчас мы зададим несколько вопросов, чтобы упростить настройку синхронизации。"; - readonly zh: "接下来我们会通过几个问题,引导你更轻松地完成同步设置。"; - readonly "zh-tw": "接下來我們會透過幾個問題,引導你更輕鬆地完成同步設定。"; - }; - readonly "We will now proceed with the server configuration.": { - readonly def: "We will now proceed with the server configuration."; - readonly es: "Ahora continuaremos con la configuración del servidor。"; - readonly ja: "次にサーバー設定を進めます。"; - readonly ko: "이제 서버 구성을 진행하겠습니다。"; - readonly ru: "Теперь перейдём к настройке сервера。"; - readonly zh: "接下来将继续进行服务器配置。"; - readonly "zh-tw": "接下來將繼續進行伺服器設定。"; - }; - readonly "Welcome to Self-hosted LiveSync": { - readonly def: "Welcome to Self-hosted LiveSync"; - readonly es: "Bienvenido a Self-hosted LiveSync"; - readonly ja: "Self-hosted LiveSync へようこそ"; - readonly ko: "Self-hosted LiveSync에 오신 것을 환영합니다"; - readonly ru: "Добро пожаловать в Self-hosted LiveSync"; - readonly zh: "欢迎使用 Self-hosted LiveSync"; - readonly "zh-tw": "歡迎使用 Self-hosted LiveSync"; - }; - readonly "When you save a file in the editor, start a sync automatically": { - readonly def: "When you save a file in the editor, start a sync automatically"; - readonly es: "Iniciar sincronización automática al guardar en editor"; - readonly fr: "À l'enregistrement d'un fichier dans l'éditeur, démarrer automatiquement une synchronisation"; - readonly he: "כאשר אתה שומר קובץ בעורך, התחל סנכרון אוטומטית"; - readonly ja: "エディタでファイルを保存すると、自動的に同期を開始します"; - readonly ko: "편집기에서 파일을 저장할 때 자동으로 동기화를 시작합니다"; - readonly ru: "Когда вы сохраняете файл в редакторе, автоматически запускать синхронизацию"; - readonly zh: "当您在编辑器中保存文件时,自动开始同步"; - }; - readonly "Write credentials in the file": { - readonly def: "Write credentials in the file"; - readonly es: "Escribir credenciales en archivo"; - readonly fr: "Écrire les identifiants dans le fichier"; - readonly he: "כתוב פרטי גישה בקובץ"; - readonly ja: "認証情報のファイル内保存"; - readonly ko: "파일에 자격 증명 저장"; - readonly ru: "Записывать учётные данные в файл"; - readonly zh: "将凭据写入文件"; - }; - readonly "Write logs into the file": { - readonly def: "Write logs into the file"; - readonly es: "Escribir logs en archivo"; - readonly fr: "Écrire les journaux dans le fichier"; - readonly he: "כתוב יומנים לקובץ"; - readonly ja: "ファイルにログを記録"; - readonly ko: "파일에 로그 기록"; - readonly ru: "Записывать логи в файл"; - readonly zh: "将日志写入文件"; - }; - readonly "xxhash32 (Fast but less collision resistance)": { - readonly def: "xxhash32 (Fast but less collision resistance)"; - readonly es: "xxhash32 (rápido, pero con menor resistencia a colisiones)"; - readonly ja: "xxhash32 (高速ですが衝突耐性は低め)"; - readonly ko: "xxhash32 (빠르지만 충돌 저항성은 낮음)"; - readonly ru: "xxhash32 (быстрый, но с меньшей устойчивостью к коллизиям)"; - readonly zh: "xxhash32(速度快,但抗碰撞能力较弱)"; - readonly "zh-tw": "xxhash32(速度快,但抗碰撞能力較弱)"; - }; - readonly "xxhash64 (Fastest)": { - readonly def: "xxhash64 (Fastest)"; - readonly es: "xxhash64 (el más rápido)"; - readonly ja: "xxhash64 (最速)"; - readonly ko: "xxhash64 (가장 빠름)"; - readonly ru: "xxhash64 (самый быстрый)"; - readonly zh: "xxhash64(最快)"; - readonly "zh-tw": "xxhash64(最快)"; - }; - readonly "Yes, I want to add this device to my existing synchronisation": { - readonly def: "Yes, I want to add this device to my existing synchronisation"; - readonly es: "Sí, quiero añadir este dispositivo a mi sincronización existente"; - readonly ja: "はい、この端末を既存の同期に追加します"; - readonly ko: "예, 이 장치를 기존 동기화에 추가하겠습니다"; - readonly ru: "Да, я хочу добавить это устройство к существующей синхронизации"; - readonly zh: "是的,我要把这台设备加入现有同步"; - readonly "zh-tw": "是的,我要把這台裝置加入既有同步"; - }; - readonly "Yes, I want to set up a new synchronisation": { - readonly def: "Yes, I want to set up a new synchronisation"; - readonly es: "Sí, quiero configurar una nueva sincronización"; - readonly ja: "はい、新しい同期を設定します"; - readonly ko: "예, 새 동기화를 설정하겠습니다"; - readonly ru: "Да, я хочу настроить новую синхронизацию"; - readonly zh: "是的,我要配置新的同步"; - readonly "zh-tw": "是的,我要設定新的同步"; - }; - readonly "You are adding this device to an existing synchronisation setup.": { - readonly def: "You are adding this device to an existing synchronisation setup."; - readonly es: "Está añadiendo este dispositivo a una configuración de sincronización existente。"; - readonly ja: "この端末を既存の同期構成に追加しようとしています。"; - readonly ko: "이 장치를 기존 동기화 구성에 추가하려고 합니다。"; - readonly ru: "Вы добавляете это устройство к существующей настройке синхронизации。"; - readonly zh: "你正在将此设备加入到现有同步配置中。"; - readonly "zh-tw": "你正在將此裝置加入既有同步設定中。"; - }; - readonly "Compute revisions for chunks (Previous behaviour)": { - readonly es: "Calcular revisiones para chunks (comportamiento anterior)"; - }; - readonly "Setup.> [!INFO]- The connected devices have been detected as follows:\n${devices}": { - readonly es: "> [!INFO]- Se detectaron los siguientes dispositivos conectados:\n${devices}"; - }; - readonly "Setup.All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": { - readonly es: "Todos los dispositivos tienen el mismo valor de progreso (${progress}). Parece que tus dispositivos están sincronizados y se puede continuar con la recolección de basura."; - }; - readonly "Setup.Cancel Garbage Collection": { - readonly es: "Cancelar la recolección de basura"; - }; - readonly "Setup.Compaction in progress on remote database...": { - readonly es: "La compactación está en curso en la base de datos remota..."; - }; - readonly "Setup.Compaction on remote database completed successfully.": { - readonly es: "La compactación en la base de datos remota se completó correctamente."; - }; - readonly "Setup.Compaction on remote database failed.": { - readonly es: "La compactación en la base de datos remota falló."; - }; - readonly "Setup.Compaction on remote database timed out.": { - readonly es: "La compactación en la base de datos remota agotó el tiempo de espera."; - }; - readonly "Setup.Device": { - readonly es: "Dispositivo"; - }; - readonly "Setup.Failed to connect to remote for compaction.": { - readonly es: "No se pudo conectar a la base de datos remota para la compactación."; - }; - readonly "Setup.Failed to connect to remote for compaction. ${reason}": { - readonly es: "No se pudo conectar a la base de datos remota para la compactación. ${reason}"; - }; - readonly "Setup.Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": { - readonly es: "No se pudo iniciar la replicación de una sola vez antes de la recolección de basura. La recolección de basura se canceló."; - }; - readonly "Setup.Failed to start replication after Garbage Collection.": { - readonly es: "No se pudo iniciar la replicación después de la recolección de basura."; - }; - readonly "Setup.Garbage Collection cancelled by user.": { - readonly es: "El usuario canceló la recolección de basura."; - }; - readonly "Setup.Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": { - readonly es: "Recolección de basura completada. Chunks eliminados: ${deletedChunks} / ${totalChunks}. Tiempo empleado: ${seconds} segundos."; - }; - readonly "Setup.Garbage Collection Confirmation": { - readonly es: "Confirmación de recolección de basura"; - }; - readonly "Setup.Garbage Collection: Found ${unusedChunks} unused chunks to delete.": { - readonly es: "Recolección de basura: se encontraron ${unusedChunks} chunks no usados para eliminar."; - }; - readonly "Setup.Garbage Collection: Scanned ${scanned} / ~${docCount}": { - readonly es: "Recolección de basura: escaneados ${scanned} / ~${docCount}"; - }; - readonly "Setup.Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": { - readonly es: "Recolección de basura: escaneo completado. Chunks totales: ${totalChunks}, chunks usados: ${usedChunks}"; - }; - readonly "Setup.Ignore and Proceed": { - readonly es: "Ignorar y continuar"; - }; - readonly "Setup.No connected device information found. Cancelling Garbage Collection.": { - readonly es: "No se encontró información de dispositivos conectados. Cancelando la recolección de basura."; - }; - readonly "Setup.Node ID": { - readonly es: "ID del nodo"; - }; - readonly "Setup.Node Information Missing": { - readonly es: "Falta información del nodo"; - }; - readonly "Setup.Obsidian version": { - readonly es: "Versión de Obsidian"; - }; - readonly "Setup.optionNoSetupUri": { - readonly es: "No, no tengo"; - }; - readonly "Setup.optionRemindNextLaunch": { - readonly es: "Recordármelo en el próximo inicio"; - }; - readonly "Setup.optionSetupWizard": { - readonly es: "Llévame al asistente de configuración"; - }; - readonly "Setup.optionYesFetchAgain": { - readonly es: "Sí, obtener nuevamente"; - }; - readonly "Setup.Please disable 'Read chunks online' in settings to use Garbage Collection.": { - readonly es: "Desactiva \"Read chunks online\" en los ajustes para usar la recolección de basura."; - }; - readonly "Setup.Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": { - readonly es: "Activa \"Compute revisions for chunks\" en los ajustes para usar la recolección de basura."; - }; - readonly "Setup.Please select 'Cancel' explicitly to cancel this operation.": { - readonly es: "Selecciona explícitamente \"Cancelar\" para cancelar esta operación."; - }; - readonly "Setup.Plug-in version": { - readonly es: "Versión del complemento"; - }; - readonly "Setup.Proceed Garbage Collection": { - readonly es: "Continuar con la recolección de basura"; - }; - readonly "Setup.Proceeding with Garbage Collection, ignoring missing nodes.": { - readonly es: "Continuando con la recolección de basura e ignorando los nodos faltantes."; - }; - readonly "Setup.Proceeding with Garbage Collection.": { - readonly es: "Continuando con la recolección de basura."; - }; - readonly "Setup.Progress": { - readonly es: "Progreso"; - }; - readonly "Setup.Setup URI dialog cancelled.": { - readonly es: "Se canceló el diálogo de Setup URI."; - }; - readonly "Setup.Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": { - readonly es: "Algunos dispositivos tienen valores de progreso diferentes (máx.: ${maxProgress}, mín.: ${minProgress}).\nEsto puede indicar que algunos dispositivos no han completado la sincronización, lo que podría causar conflictos. Se recomienda encarecidamente confirmar que todos los dispositivos estén sincronizados antes de continuar."; - }; - readonly "Setup.The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": { - readonly es: "Los siguientes nodos aceptados no tienen información del nodo:\n- ${missingNodes}\n\nEsto indica que no se han conectado desde hace algún tiempo o que se han quedado en una versión anterior.\nSi es posible, es preferible actualizar todos los dispositivos. Si tienes dispositivos que ya no se usan, puedes borrar todos los nodos aceptados bloqueando el remoto una vez."; - }; - readonly "Setup.titleCaseSensitivity": { - readonly es: "Sensibilidad a mayúsculas"; - }; - readonly "Setup.titleRecommendSetupUri": { - readonly es: "Recomendación de uso de URI de configuración"; - }; - readonly "Setup.titleWelcome": { - readonly es: "Bienvenido a Self-hosted LiveSync"; - }; - readonly "(Not recommended) If set, credentials will be stored in the file": { - readonly ru: "(Не рекомендуется) Если установлено, учётные данные будут сохранены в файле"; - }; - readonly "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding.": { - readonly ru: "До v0.17.16 мы использовали старый адаптер для локальной базы данных. Теперь предпочтителен новый адаптер. Однако требуется перестроение локальной базы данных."; - }; - readonly descConnectSetupURI: { - readonly ru: "Это рекомендуемый способ настройки Self-hosted LiveSync с помощью Setup URI."; - }; - readonly descCopySetupURI: { - readonly ru: "Идеально для настройки нового устройства!"; - }; - readonly descEnableLiveSync: { - readonly ru: "Включайте это только после настройки одного из двух вариантов выше."; - }; - readonly descFetchConfigFromRemote: { - readonly ru: "Загрузить необходимые настройки с уже настроенного удалённого сервера."; - }; - readonly descManualSetup: { - readonly ru: "Не рекомендуется, но полезно, если у вас нет Setup URI"; - }; - readonly descTestDatabaseConnection: { - readonly ru: "Открыть подключение к базе данных."; - }; - readonly descValidateDatabaseConfig: { - readonly ru: "Проверяет и исправляет потенциальные проблемы с конфигурацией базы данных."; - }; - readonly "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this.": { - readonly ru: "Если включено, будет использоваться эффективная синхронизация настроек для каждого файла."; - }; - readonly "If this is set, changes to local files which are matched by the ignore files will be skipped.": { - readonly ru: "Если установлено, изменения файлов из списка игнорирования будут пропущены."; - }; - readonly "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely.": { - readonly ru: "Если эта опция включена, PouchDB будет держать соединение открытым 60 секунд."; - }; - readonly "Number of batches to process at a time. Defaults to 40. Minimum is 2.": { - readonly ru: "Количество пакетов для обработки за раз. По умолчанию 40. Минимум 2."; - }; - readonly "Save settings to a markdown file.": { - readonly ru: "Сохранить настройки в файл markdown."; - }; - readonly "The maximum duration for which chunks can be incubated within the document.": { - readonly ru: "Максимальная продолжительность инкубации чанков в документе."; - }; - readonly "The maximum number of chunks that can be incubated within the document.": { - readonly ru: "Максимальное количество инкубируемых чанков в документе."; - }; - readonly "The maximum total size of chunks that can be incubated within the document.": { - readonly ru: "Максимальный общий размер инкубируемых чанков в документе."; - }; - readonly "This passphrase will not be copied to another device. It will be set to until you configure it again.": { - readonly ru: "Эта парольная фраза не будет скопирована на другое устройство."; - }; - readonly "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name.": { - readonly ru: "Внимание! Это серьёзно повлияет на производительность."; - }; -}; diff --git a/_types/src/lib/src/common/messages/de.d.ts b/_types/src/lib/src/common/messages/de.d.ts deleted file mode 100644 index d7fe2e52..00000000 --- a/_types/src/lib/src/common/messages/de.d.ts +++ /dev/null @@ -1,299 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly de: { - "(Active)": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - Activate: string; - "Add default patterns": string; - "Add new connection": string; - "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - Back: string; - "Back to non-configured": string; - Cancel: string; - "Cancel Garbage Collection": string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Compaction in progress on remote database...": string; - "Compaction on remote database completed successfully.": string; - "Compaction on remote database failed.": string; - "Compaction on remote database timed out.": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync (Beta3)": string; - "Database Adapter": string; - Default: string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - desktop: string; - Device: string; - "Device name": string; - "Device Setup Method": string; - "Disables all synchronization and restart.": string; - "Display name": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Encrypting sensitive configuration items": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - "Failed to connect to remote for compaction.": string; - "Failed to connect to remote for compaction. ${reason}": string; - "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Failed to start replication after Garbage Collection.": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Fresh Start Wipe": string; - "Garbage Collection cancelled by user.": string; - "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Garbage Collection Confirmation": string; - "Garbage Collection V3 (Beta)": string; - "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Hidden Files": string; - "Hide completely": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "Ignore and Proceed": string; - "Ignore patterns": string; - "Import connection": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "More actions": string; - "Network warning style": string; - "New Remote": string; - "No connected device information found. Cancelling Garbage Collection.": string; - "No limit configured": string; - "No, please take me back": string; - "Node ID": string; - "Node Information Missing": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Obsidian version": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Pick a file to resolve conflict": string; - "Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Please select 'Cancel' explicitly to cancel this operation.": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set this device name": string; - "Plug-in version": string; - "Proceed Garbage Collection": string; - "Proceed with Setup URI": string; - "Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Proceeding with Garbage Collection.": string; - Progress: string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - Rename: string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restore or reconstruct local database from remote.": string; - "S3/MinIO/R2 Object Storage": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram!": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setup URI dialog cancelled.": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Show full banner": string; - "Show icon only": string; - "Show status icon instead of file warnings banner": string; - "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "Use a Setup URI (Recommended)": string; - "Verify all": string; - "Verify and repair all files": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/def.d.ts b/_types/src/lib/src/common/messages/def.d.ts deleted file mode 100644 index 5b94a24d..00000000 --- a/_types/src/lib/src/common/messages/def.d.ts +++ /dev/null @@ -1,1121 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly def: { - "(Active)": string; - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - "Access Key": string; - Activate: string; - "Active Remote Configuration": string; - "Add default patterns": string; - "Add new connection": string; - "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - "Always prompt merge conflicts": string; - Analyse: string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Ask a passphrase at every launch": string; - "Automatically Sync all files when opening Obsidian.": string; - Back: string; - "Back to non-configured": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Cancel: string; - "Cancel Garbage Collection": string; - "Changing this setting requires migrating existing data (a bit time may be taken) and restarting Obsidian. Please make sure to back up your data before proceeding.": string; - Check: string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compaction in progress on remote database...": string; - "Compaction on remote database completed successfully.": string; - "Compaction on remote database failed.": string; - "Compaction on remote database timed out.": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - "Compute revisions for chunks": string; - "Configuration Encryption": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "Copy Report to clipboard": string; - "CouchDB Connection Tweak": string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync": string; - "Customization Sync (Beta3)": string; - "Data Compression": string; - "Database -> Storage": string; - "Database Adapter": string; - "Database Name": string; - "Database suffix": string; - Default: string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete old metadata of deleted files on start-up": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - desktop: string; - Developer: string; - Device: string; - "Device name": string; - "Device Setup Method": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables all synchronization and restart.": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Display name": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Doctor.Button.DismissThisVersion": string; - "Doctor.Button.Fix": string; - "Doctor.Button.FixButNoRebuild": string; - "Doctor.Button.No": string; - "Doctor.Button.Skip": string; - "Doctor.Button.Yes": string; - "Doctor.Dialogue.Main": string; - "Doctor.Dialogue.MainFix": string; - "Doctor.Dialogue.Title": string; - "Doctor.Dialogue.TitleAlmostDone": string; - "Doctor.Dialogue.TitleFix": string; - "Doctor.Level.Must": string; - "Doctor.Level.Necessary": string; - "Doctor.Level.Optional": string; - "Doctor.Level.Recommended": string; - "Doctor.Message.NoIssues": string; - "Doctor.Message.RebuildLocalRequired": string; - "Doctor.Message.RebuildRequired": string; - "Doctor.Message.SomeSkipped": string; - "Doctor.RULES.E2EE_V02500.REASON": string; - "Document History": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - "Failed to connect to remote for compaction.": string; - "Failed to connect to remote for compaction. ${reason}": string; - "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Failed to start replication after Garbage Collection.": string; - Fetch: string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - "File to view History": string; - Filename: string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Forces the file to be synced when opened.": string; - "Fresh Start Wipe": string; - "Garbage Collection cancelled by user.": string; - "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Garbage Collection Confirmation": string; - "Garbage Collection V3 (Beta)": string; - "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Handle files as Case-Sensitive": string; - "Hidden Files": string; - "Hide completely": string; - "Highlight diff": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.": string; - "Ignore and Proceed": string; - "Ignore files": string; - "Ignore patterns": string; - "Import connection": string; - "Incubate Chunks in Document": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - "Initialise journal received history. On the next sync, every item except this device sent will be downloaded again.": string; - "Initialise journal sent history. On the next sync, every item except this device received will be sent again.": string; - "Interval (sec)": string; - "K.exp": string; - "K.long_p2p_sync": string; - "K.P2P": string; - "K.Peer": string; - "K.ScanCustomization": string; - "K.short_p2p_sync": string; - "K.title_p2p_sync": string; - "Keep empty folder": string; - lang_def: string; - "lang-de": string; - "lang-def": string; - "lang-es": string; - "lang-fr": string; - "lang-he": string; - "lang-ja": string; - "lang-ko": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.mismatchedTweakDetected": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - "Local Database Tweak": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - Merge: string; - "Minimum delay for batch database updating": string; - "Minimum interval for syncing": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.fix0256.buttons.checkItLater": string; - "moduleMigration.fix0256.buttons.DismissForever": string; - "moduleMigration.fix0256.buttons.fix": string; - "moduleMigration.fix0256.message": string; - "moduleMigration.fix0256.messageUnrecoverable": string; - "moduleMigration.fix0256.title": string; - "moduleMigration.insecureChunkExist.buttons.fetch": string; - "moduleMigration.insecureChunkExist.buttons.later": string; - "moduleMigration.insecureChunkExist.buttons.rebuild": string; - "moduleMigration.insecureChunkExist.laterMessage": string; - "moduleMigration.insecureChunkExist.message": string; - "moduleMigration.insecureChunkExist.title": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleMigration.optionNoSetupUri": string; - "moduleMigration.optionRemindNextLaunch": string; - "moduleMigration.optionSetupViaP2P": string; - "moduleMigration.optionSetupWizard": string; - "moduleMigration.optionYesFetchAgain": string; - "moduleMigration.titleCaseSensitivity": string; - "moduleMigration.titleRecommendSetupUri": string; - "moduleMigration.titleWelcome": string; - "moduleObsidianMenu.replicate": string; - "More actions": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Network warning style": string; - "New Remote": string; - "No connected device information found. Cancelling Garbage Collection.": string; - "No limit configured": string; - "No, please take me back": string; - "Node ID": string; - "Node Information Missing": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "Obsidian version": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.logServerConfigurationCheck": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.serverVersion": string; - "obsidianLiveSyncSettingTab.titleActiveRemoteServer": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - Open: string; - "Open the dialog": string; - Overwrite: string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "P2P.AskPassphraseForDecrypt": string; - "P2P.AskPassphraseForShare": string; - "P2P.DisabledButNeed": string; - "P2P.FailedToOpen": string; - "P2P.NoAutoSyncPeers": string; - "P2P.NoKnownPeers": string; - "P2P.Note.description": string; - "P2P.Note.important_note": string; - "P2P.Note.important_note_sub": string; - "P2P.Note.Summary": string; - "P2P.NotEnabled": string; - "P2P.P2PReplication": string; - "P2P.PaneTitle": string; - "P2P.ReplicatorInstanceMissing": string; - "P2P.SeemsOffline": string; - "P2P.SyncAlreadyRunning": string; - "P2P.SyncCompleted": string; - "P2P.SyncStartedWith": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Path Obfuscation": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - "Per-file-saved customization sync": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Periodic Sync interval": string; - "Pick a file to resolve conflict": string; - "Pick a file to show history": string; - "Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Please select 'Cancel' explicitly to cancel this operation.": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": string; - "Please set this device name": string; - "Plug-in version": string; - "Prepare the 'report' to create an issue": string; - Presets: string; - "Proceed Garbage Collection": string; - "Proceed with Setup URI": string; - "Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Proceeding with Garbage Collection.": string; - "Process small files in the foreground": string; - Progress: string; - "Property Encryption": string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recovery and Repair": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - "RedFlag.Fetch.Method.Desc": string; - "RedFlag.Fetch.Method.FetchSafer": string; - "RedFlag.Fetch.Method.FetchSmoother": string; - "RedFlag.Fetch.Method.FetchTraditional": string; - "RedFlag.Fetch.Method.Title": string; - "RedFlag.FetchRemoteConfig.Buttons.Cancel": string; - "RedFlag.FetchRemoteConfig.Buttons.Fetch": string; - "RedFlag.FetchRemoteConfig.Message": string; - "RedFlag.FetchRemoteConfig.Title": string; - "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - "Remote server type": string; - "Remote Type": string; - Rename: string; - "Replicator.Dialogue.Locked.Action.Dismiss": string; - "Replicator.Dialogue.Locked.Action.Fetch": string; - "Replicator.Dialogue.Locked.Action.Unlock": string; - "Replicator.Dialogue.Locked.Message": string; - "Replicator.Dialogue.Locked.Message.Fetch": string; - "Replicator.Dialogue.Locked.Message.Unlocked": string; - "Replicator.Dialogue.Locked.Title": string; - "Replicator.Message.Cleaned": string; - "Replicator.Message.InitialiseFatalError": string; - "Replicator.Message.Pending": string; - "Replicator.Message.SomeModuleFailed": string; - "Replicator.Message.VersionUpFlash": string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset notification threshold and check the remote database usage": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and display may be inconsistent. Are you sure to restart now?": string; - "Restore or reconstruct local database from remote.": string; - "Run Doctor": string; - "S3/MinIO/R2 Object Storage": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for Broken files": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram Switches": string; - "Scram!": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setting.TroubleShooting": string; - "Setting.TroubleShooting.Doctor": string; - "Setting.TroubleShooting.Doctor.Desc": string; - "Setting.TroubleShooting.ScanBrokenFiles": string; - "Setting.TroubleShooting.ScanBrokenFiles.Desc": string; - "SettingTab.Message.AskRebuild": string; - "Setup URI dialog cancelled.": string; - "Setup.Apply.Buttons.ApplyAndFetch": string; - "Setup.Apply.Buttons.ApplyAndMerge": string; - "Setup.Apply.Buttons.ApplyAndRebuild": string; - "Setup.Apply.Buttons.Cancel": string; - "Setup.Apply.Buttons.OnlyApply": string; - "Setup.Apply.Message": string; - "Setup.Apply.Title": string; - "Setup.Apply.WarningRebuildRecommended": string; - "Setup.Doctor.Buttons.No": string; - "Setup.Doctor.Buttons.Yes": string; - "Setup.Doctor.Message": string; - "Setup.Doctor.Title": string; - "Setup.FetchRemoteConf.Buttons.Fetch": string; - "Setup.FetchRemoteConf.Buttons.Skip": string; - "Setup.FetchRemoteConf.Message": string; - "Setup.FetchRemoteConf.Title": string; - "Setup.QRCode": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.ShowQRCode": string; - "Setup.ShowQRCode.Desc": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show full banner": string; - "Show history": string; - "Show icon only": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Storage -> Database": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "The Hash algorithm for chunk IDs": string; - "The IndexedDB adapter often offers superior performance in certain scenarios, but it has been found to cause memory leaks when used with LiveSync mode. When using LiveSync mode, please use IDB adapter instead.": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "Transfer Tweak": string; - "TweakMismatchResolve.Action.DisableAutoAcceptCompatible": string; - "TweakMismatchResolve.Action.Dismiss": string; - "TweakMismatchResolve.Action.EnableAutoAcceptCompatible": string; - "TweakMismatchResolve.Action.UseConfigured": string; - "TweakMismatchResolve.Action.UseMine": string; - "TweakMismatchResolve.Action.UseMineAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseMineWithRebuild": string; - "TweakMismatchResolve.Action.UseRemote": string; - "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseRemoteWithRebuild": string; - "TweakMismatchResolve.Message.AutoAcceptCompatibleUndefined": string; - "TweakMismatchResolve.Message.Main": string; - "TweakMismatchResolve.Message.MainTweakResolving": string; - "TweakMismatchResolve.Message.mineUpdated": string; - "TweakMismatchResolve.Message.remoteUpdated": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": string; - "TweakMismatchResolve.Table": string; - "TweakMismatchResolve.Table.Row": string; - "TweakMismatchResolve.Title": string; - "TweakMismatchResolve.Title.AutoAcceptCompatible": string; - "TweakMismatchResolve.Title.TweakResolving": string; - "TweakMismatchResolve.Title.UseRemoteConfig": string; - "Ui.Common.Signal.Caution": string; - "Ui.Common.Signal.Danger": string; - "Ui.Common.Signal.Notice": string; - "Ui.Common.Signal.Warning": string; - "Ui.Settings.Advanced.LocalDatabaseTweak": string; - "Ui.Settings.Advanced.MemoryCache": string; - "Ui.Settings.Advanced.TransferTweak": string; - "Ui.Settings.Common.Analyse": string; - "Ui.Settings.Common.Back": string; - "Ui.Settings.Common.Check": string; - "Ui.Settings.Common.Configure": string; - "Ui.Settings.Common.Continue": string; - "Ui.Settings.Common.Delete": string; - "Ui.Settings.Common.Fetch": string; - "Ui.Settings.Common.Lock": string; - "Ui.Settings.Common.Merge": string; - "Ui.Settings.Common.Open": string; - "Ui.Settings.Common.Overwrite": string; - "Ui.Settings.Common.Perform": string; - "Ui.Settings.Common.ResetAll": string; - "Ui.Settings.Common.ResolveAll": string; - "Ui.Settings.Common.Scan": string; - "Ui.Settings.Common.Send": string; - "Ui.Settings.Common.Use": string; - "Ui.Settings.Common.VerifyAll": string; - "Ui.Settings.CustomizationSync.OpenDesc": string; - "Ui.Settings.CustomizationSync.Panel": string; - "Ui.Settings.CustomizationSync.WarnChangeDeviceName": string; - "Ui.Settings.CustomizationSync.WarnSetDeviceName": string; - "Ui.Settings.Hatch.AnalyseDatabaseUsage": string; - "Ui.Settings.Hatch.AnalyseDatabaseUsageDesc": string; - "Ui.Settings.Hatch.BackToNonConfigured": string; - "Ui.Settings.Hatch.ConvertNonObfuscated": string; - "Ui.Settings.Hatch.ConvertNonObfuscatedDesc": string; - "Ui.Settings.Hatch.CopyIssueReport": string; - "Ui.Settings.Hatch.DatabaseLabel": string; - "Ui.Settings.Hatch.DatabaseToStorage": string; - "Ui.Settings.Hatch.DeleteCustomizationSyncData": string; - "Ui.Settings.Hatch.GeneratedReport": string; - "Ui.Settings.Hatch.Missing": string; - "Ui.Settings.Hatch.ModifiedSize": string; - "Ui.Settings.Hatch.ModifiedSizeActual": string; - "Ui.Settings.Hatch.PrepareIssueReport": string; - "Ui.Settings.Hatch.RecoveryAndRepair": string; - "Ui.Settings.Hatch.RecreateAll": string; - "Ui.Settings.Hatch.RecreateMissingChunks": string; - "Ui.Settings.Hatch.RecreateMissingChunksDesc": string; - "Ui.Settings.Hatch.ResetPanel": string; - "Ui.Settings.Hatch.ResetRemoteUsage": string; - "Ui.Settings.Hatch.ResetRemoteUsageDesc": string; - "Ui.Settings.Hatch.ResolveAllConflictedFiles": string; - "Ui.Settings.Hatch.ResolveAllConflictedFilesDesc": string; - "Ui.Settings.Hatch.RunDoctor": string; - "Ui.Settings.Hatch.ScanBrokenFiles": string; - "Ui.Settings.Hatch.ScramSwitches": string; - "Ui.Settings.Hatch.ShowHistory": string; - "Ui.Settings.Hatch.StorageLabel": string; - "Ui.Settings.Hatch.StorageToDatabase": string; - "Ui.Settings.Hatch.VerifyAndRepairAllFiles": string; - "Ui.Settings.Hatch.VerifyAndRepairAllFilesDesc": string; - "Ui.Settings.Maintenance.Cleanup": string; - "Ui.Settings.Maintenance.CleanupDesc": string; - "Ui.Settings.Maintenance.DeleteLocalDatabase": string; - "Ui.Settings.Maintenance.EmergencyRestart": string; - "Ui.Settings.Maintenance.EmergencyRestartDesc": string; - "Ui.Settings.Maintenance.FreshStartWipe": string; - "Ui.Settings.Maintenance.FreshStartWipeDesc": string; - "Ui.Settings.Maintenance.GarbageCollection": string; - "Ui.Settings.Maintenance.GarbageCollectionAction": string; - "Ui.Settings.Maintenance.GarbageCollectionDesc": string; - "Ui.Settings.Maintenance.LockServer": string; - "Ui.Settings.Maintenance.LockServerDesc": string; - "Ui.Settings.Maintenance.OverwriteRemote": string; - "Ui.Settings.Maintenance.OverwriteRemoteDesc": string; - "Ui.Settings.Maintenance.OverwriteServerData": string; - "Ui.Settings.Maintenance.OverwriteServerDataDesc": string; - "Ui.Settings.Maintenance.PurgeAllJournalCounter": string; - "Ui.Settings.Maintenance.PurgeAllJournalCounterDesc": string; - "Ui.Settings.Maintenance.RebuildingOperations": string; - "Ui.Settings.Maintenance.Resend": string; - "Ui.Settings.Maintenance.ResendDesc": string; - "Ui.Settings.Maintenance.Reset": string; - "Ui.Settings.Maintenance.ResetAllJournalCounter": string; - "Ui.Settings.Maintenance.ResetAllJournalCounterDesc": string; - "Ui.Settings.Maintenance.ResetJournalReceived": string; - "Ui.Settings.Maintenance.ResetJournalReceivedDesc": string; - "Ui.Settings.Maintenance.ResetJournalSent": string; - "Ui.Settings.Maintenance.ResetJournalSentDesc": string; - "Ui.Settings.Maintenance.ResetLocalSyncInfo": string; - "Ui.Settings.Maintenance.ResetLocalSyncInfoDesc": string; - "Ui.Settings.Maintenance.ResetReceived": string; - "Ui.Settings.Maintenance.ResetSentHistory": string; - "Ui.Settings.Maintenance.ResetThisDevice": string; - "Ui.Settings.Maintenance.ScheduleAndRestart": string; - "Ui.Settings.Maintenance.Scram": string; - "Ui.Settings.Maintenance.SendChunks": string; - "Ui.Settings.Maintenance.Syncing": string; - "Ui.Settings.Maintenance.WarningLockedReadyAction": string; - "Ui.Settings.Maintenance.WarningLockedReadyText": string; - "Ui.Settings.Maintenance.WarningLockedResolveAction": string; - "Ui.Settings.Maintenance.WarningLockedResolveText": string; - "Ui.Settings.Maintenance.WriteRedFlagAndRestart": string; - "Ui.Settings.Patches.CompatibilityConflict": string; - "Ui.Settings.Patches.CompatibilityDatabase": string; - "Ui.Settings.Patches.CompatibilityInternalApi": string; - "Ui.Settings.Patches.CompatibilityMetadata": string; - "Ui.Settings.Patches.CompatibilityRemote": string; - "Ui.Settings.Patches.CompatibilityTrouble": string; - "Ui.Settings.Patches.CurrentAdapter": string; - "Ui.Settings.Patches.DatabaseAdapter": string; - "Ui.Settings.Patches.DatabaseAdapterDesc": string; - "Ui.Settings.Patches.EdgeCaseBehaviour": string; - "Ui.Settings.Patches.EdgeCaseDatabase": string; - "Ui.Settings.Patches.EdgeCaseProcessing": string; - "Ui.Settings.Patches.IndexedDbWarning": string; - "Ui.Settings.Patches.MigratingToIdb": string; - "Ui.Settings.Patches.MigratingToIndexedDb": string; - "Ui.Settings.Patches.MigrationIdbCompleted": string; - "Ui.Settings.Patches.MigrationIdbCompletedFollowUp": string; - "Ui.Settings.Patches.MigrationIndexedDbCompleted": string; - "Ui.Settings.Patches.MigrationIndexedDbCompletedFollowUp": string; - "Ui.Settings.Patches.MigrationWarning": string; - "Ui.Settings.Patches.OperationToIdb": string; - "Ui.Settings.Patches.OperationToIndexedDb": string; - "Ui.Settings.Patches.Remediation": string; - "Ui.Settings.Patches.RemediationChanged": string; - "Ui.Settings.Patches.RemediationNoLimit": string; - "Ui.Settings.Patches.RemediationRestarting": string; - "Ui.Settings.Patches.RemediationRestartLater": string; - "Ui.Settings.Patches.RemediationRestartMessage": string; - "Ui.Settings.Patches.RemediationRestartNow": string; - "Ui.Settings.Patches.RemediationSuffixChanged": string; - "Ui.Settings.Patches.RemediationWithValue": string; - "Ui.Settings.Patches.RemoteDatabaseSunset": string; - "Ui.Settings.Patches.SwitchToIDB": string; - "Ui.Settings.Patches.SwitchToIndexedDb": string; - "Ui.Settings.PowerUsers.ConfigurationEncryption": string; - "Ui.Settings.PowerUsers.ConnectionTweak": string; - "Ui.Settings.PowerUsers.ConnectionTweakDesc": string; - "Ui.Settings.PowerUsers.Default": string; - "Ui.Settings.PowerUsers.Developer": string; - "Ui.Settings.PowerUsers.EncryptSensitiveConfig": string; - "Ui.Settings.PowerUsers.PromptPassphraseEveryLaunch": string; - "Ui.Settings.PowerUsers.UseCustomPassphrase": string; - "Ui.Settings.Remote.Activate": string; - "Ui.Settings.Remote.ActiveSuffix": string; - "Ui.Settings.Remote.AddConnection": string; - "Ui.Settings.Remote.AddRemoteDefaultName": string; - "Ui.Settings.Remote.ConfigureAndChangeRemote": string; - "Ui.Settings.Remote.ConfigureE2EE": string; - "Ui.Settings.Remote.ConfigureRemote": string; - "Ui.Settings.Remote.DeleteRemoteConfirm": string; - "Ui.Settings.Remote.DeleteRemoteTitle": string; - "Ui.Settings.Remote.DisplayName": string; - "Ui.Settings.Remote.DuplicateRemote": string; - "Ui.Settings.Remote.DuplicateRemoteSuffix": string; - "Ui.Settings.Remote.E2EEConfiguration": string; - "Ui.Settings.Remote.Export": string; - "Ui.Settings.Remote.FetchRemoteSettings": string; - "Ui.Settings.Remote.ImportConnection": string; - "Ui.Settings.Remote.ImportConnectionPrompt": string; - "Ui.Settings.Remote.ImportedCouchDb": string; - "Ui.Settings.Remote.ImportedRemote": string; - "Ui.Settings.Remote.MoreActions": string; - "Ui.Settings.Remote.PeerToPeerPanel": string; - "Ui.Settings.Remote.RemoteConfigurationPrefix": string; - "Ui.Settings.Remote.RemoteDatabases": string; - "Ui.Settings.Remote.RemoteName": string; - "Ui.Settings.Remote.RemoteNameCouchDb": string; - "Ui.Settings.Remote.RemoteNameP2P": string; - "Ui.Settings.Remote.RemoteNameS3": string; - "Ui.Settings.Remote.Rename": string; - "Ui.Settings.Selector.AddDefaultPatterns": string; - "Ui.Settings.Selector.CrossPlatform": string; - "Ui.Settings.Selector.Default": string; - "Ui.Settings.Selector.HiddenFiles": string; - "Ui.Settings.Selector.IgnorePatterns": string; - "Ui.Settings.Selector.NonSynchronisingFiles": string; - "Ui.Settings.Selector.NonSynchronisingFilesDesc": string; - "Ui.Settings.Selector.NormalFiles": string; - "Ui.Settings.Selector.OverwritePatterns": string; - "Ui.Settings.Selector.OverwritePatternsDesc": string; - "Ui.Settings.Selector.SynchronisingFiles": string; - "Ui.Settings.Selector.SynchronisingFilesDesc": string; - "Ui.Settings.Selector.TargetPatterns": string; - "Ui.Settings.Selector.TargetPatternsDesc": string; - "Ui.Settings.Setup.RerunWizardButton": string; - "Ui.Settings.Setup.RerunWizardDesc": string; - "Ui.Settings.Setup.RerunWizardName": string; - "Ui.Settings.SyncSettings.Fetch": string; - "Ui.Settings.SyncSettings.Merge": string; - "Ui.Settings.SyncSettings.Overwrite": string; - "Ui.SetupWizard.Common.Back": string; - "Ui.SetupWizard.Common.Cancel": string; - "Ui.SetupWizard.Common.ProceedSelectOption": string; - "Ui.SetupWizard.Intro.ExistingOption": string; - "Ui.SetupWizard.Intro.ExistingOptionDesc": string; - "Ui.SetupWizard.Intro.Guidance": string; - "Ui.SetupWizard.Intro.NewOption": string; - "Ui.SetupWizard.Intro.NewOptionDesc": string; - "Ui.SetupWizard.Intro.ProceedExisting": string; - "Ui.SetupWizard.Intro.ProceedNew": string; - "Ui.SetupWizard.Intro.Question": string; - "Ui.SetupWizard.Intro.Title": string; - "Ui.SetupWizard.OutroAskUserMode.CompatibleOption": string; - "Ui.SetupWizard.OutroAskUserMode.CompatibleOptionDesc": string; - "Ui.SetupWizard.OutroAskUserMode.ExistingOption": string; - "Ui.SetupWizard.OutroAskUserMode.ExistingOptionDesc": string; - "Ui.SetupWizard.OutroAskUserMode.Guidance": string; - "Ui.SetupWizard.OutroAskUserMode.NewOption": string; - "Ui.SetupWizard.OutroAskUserMode.NewOptionDesc": string; - "Ui.SetupWizard.OutroAskUserMode.ProceedApplySettings": string; - "Ui.SetupWizard.OutroAskUserMode.ProceedNext": string; - "Ui.SetupWizard.OutroAskUserMode.Question": string; - "Ui.SetupWizard.OutroAskUserMode.Title": string; - "Ui.SetupWizard.OutroNewUser.GuidancePrimary": string; - "Ui.SetupWizard.OutroNewUser.GuidanceWarning": string; - "Ui.SetupWizard.OutroNewUser.Important": string; - "Ui.SetupWizard.OutroNewUser.Proceed": string; - "Ui.SetupWizard.OutroNewUser.Question": string; - "Ui.SetupWizard.OutroNewUser.Title": string; - "Ui.SetupWizard.SelectExisting.Guidance": string; - "Ui.SetupWizard.SelectExisting.ManualOption": string; - "Ui.SetupWizard.SelectExisting.ManualOptionDesc": string; - "Ui.SetupWizard.SelectExisting.ProceedManual": string; - "Ui.SetupWizard.SelectExisting.ProceedQr": string; - "Ui.SetupWizard.SelectExisting.ProceedSetupUri": string; - "Ui.SetupWizard.SelectExisting.QrOption": string; - "Ui.SetupWizard.SelectExisting.QrOptionDesc": string; - "Ui.SetupWizard.SelectExisting.Question": string; - "Ui.SetupWizard.SelectExisting.SetupUriOption": string; - "Ui.SetupWizard.SelectExisting.SetupUriOptionDesc": string; - "Ui.SetupWizard.SelectExisting.Title": string; - "Ui.SetupWizard.SelectNew.Guidance": string; - "Ui.SetupWizard.SelectNew.ManualOption": string; - "Ui.SetupWizard.SelectNew.ManualOptionDesc": string; - "Ui.SetupWizard.SelectNew.ProceedManual": string; - "Ui.SetupWizard.SelectNew.ProceedSetupUri": string; - "Ui.SetupWizard.SelectNew.Question": string; - "Ui.SetupWizard.SelectNew.SetupUriOption": string; - "Ui.SetupWizard.SelectNew.SetupUriOptionDesc": string; - "Ui.SetupWizard.SelectNew.Title": string; - "Ui.SetupWizard.SetupRemote.BucketOption": string; - "Ui.SetupWizard.SetupRemote.BucketOptionDesc": string; - "Ui.SetupWizard.SetupRemote.CouchDbOptionDesc": string; - "Ui.SetupWizard.SetupRemote.Guidance": string; - "Ui.SetupWizard.SetupRemote.P2POption": string; - "Ui.SetupWizard.SetupRemote.P2POptionDesc": string; - "Ui.SetupWizard.SetupRemote.ProceedBucket": string; - "Ui.SetupWizard.SetupRemote.ProceedCouchDb": string; - "Ui.SetupWizard.SetupRemote.ProceedP2P": string; - "Ui.SetupWizard.SetupRemote.Title": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use a custom passphrase": string; - "Use a Setup URI (Recommended)": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Verify all": string; - "Verify and repair all files": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/es.d.ts b/_types/src/lib/src/common/messages/es.d.ts deleted file mode 100644 index e6924555..00000000 --- a/_types/src/lib/src/common/messages/es.d.ts +++ /dev/null @@ -1,691 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly es: { - "(Active)": string; - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - "Access Key": string; - Activate: string; - "Add default patterns": string; - "Add new connection": string; - "Always prompt merge conflicts": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Ask a passphrase at every launch": string; - "Automatically Sync all files when opening Obsidian.": string; - Back: string; - "Back to non-configured": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Cancel: string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - "Compute revisions for chunks (Previous behaviour)": string; - "Configuration Encryption": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "CouchDB Connection Tweak": string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync": string; - "Customization Sync (Beta3)": string; - "Data Compression": string; - "Database Adapter": string; - "Database Name": string; - "Database suffix": string; - Default: string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete old metadata of deleted files on start-up": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - desktop: string; - Developer: string; - "Device name": string; - "Device Setup Method": string; - "Disables all synchronization and restart.": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Display name": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - Fetch: string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - Filename: string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Forces the file to be synced when opened.": string; - "Fresh Start Wipe": string; - "Garbage Collection V3 (Beta)": string; - "Handle files as Case-Sensitive": string; - "Hidden Files": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "Ignore files": string; - "Ignore patterns": string; - "Import connection": string; - "Incubate Chunks in Document": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - "Interval (sec)": string; - "Keep empty folder": string; - "lang-de": string; - "lang-es": string; - "lang-fr": string; - "lang-ja": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - "Local Database Tweak": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - Merge: string; - "Minimum delay for batch database updating": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleObsidianMenu.replicate": string; - "More actions": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Network warning style": string; - "New Remote": string; - "No limit configured": string; - "No, please take me back": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - Open: string; - "Open the dialog": string; - Overwrite: string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Path Obfuscation": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - "Per-file-saved customization sync": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Periodic Sync interval": string; - "Pick a file to resolve conflict": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": string; - "Please set this device name": string; - Presets: string; - "Proceed with Setup URI": string; - "Process small files in the foreground": string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - "Remote server type": string; - "Remote Type": string; - Rename: string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restore or reconstruct local database from remote.": string; - "S3/MinIO/R2 Object Storage": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram!": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setup.> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "Setup.All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - "Setup.Cancel Garbage Collection": string; - "Setup.Compaction in progress on remote database...": string; - "Setup.Compaction on remote database completed successfully.": string; - "Setup.Compaction on remote database failed.": string; - "Setup.Compaction on remote database timed out.": string; - "Setup.Device": string; - "Setup.Failed to connect to remote for compaction.": string; - "Setup.Failed to connect to remote for compaction. ${reason}": string; - "Setup.Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Setup.Failed to start replication after Garbage Collection.": string; - "Setup.Garbage Collection cancelled by user.": string; - "Setup.Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Setup.Garbage Collection Confirmation": string; - "Setup.Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Setup.Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Setup.Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Setup.Ignore and Proceed": string; - "Setup.No connected device information found. Cancelling Garbage Collection.": string; - "Setup.Node ID": string; - "Setup.Node Information Missing": string; - "Setup.Obsidian version": string; - "Setup.optionNoSetupUri": string; - "Setup.optionRemindNextLaunch": string; - "Setup.optionSetupWizard": string; - "Setup.optionYesFetchAgain": string; - "Setup.Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Setup.Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Setup.Please select 'Cancel' explicitly to cancel this operation.": string; - "Setup.Plug-in version": string; - "Setup.Proceed Garbage Collection": string; - "Setup.Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Setup.Proceeding with Garbage Collection.": string; - "Setup.Progress": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.Setup URI dialog cancelled.": string; - "Setup.Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Setup.The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "Setup.titleCaseSensitivity": string; - "Setup.titleRecommendSetupUri": string; - "Setup.titleWelcome": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show full banner": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The Hash algorithm for chunk IDs": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "Transfer Tweak": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use a custom passphrase": string; - "Use a Setup URI (Recommended)": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Verify all": string; - "Verify and repair all files": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/fr.d.ts b/_types/src/lib/src/common/messages/fr.d.ts deleted file mode 100644 index 8eaeed4e..00000000 --- a/_types/src/lib/src/common/messages/fr.d.ts +++ /dev/null @@ -1,584 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly fr: { - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "Access Key": string; - "Active Remote Configuration": string; - "Always prompt merge conflicts": string; - Analyse: string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Automatically Sync all files when opening Obsidian.": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Check: string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compute revisions for chunks": string; - "Copy Report to clipboard": string; - "Data Compression": string; - "Database Name": string; - "Database suffix": string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - "Delete old metadata of deleted files on start-up": string; - "Device name": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Doctor.Button.DismissThisVersion": string; - "Doctor.Button.Fix": string; - "Doctor.Button.FixButNoRebuild": string; - "Doctor.Button.No": string; - "Doctor.Button.Skip": string; - "Doctor.Button.Yes": string; - "Doctor.Dialogue.Main": string; - "Doctor.Dialogue.MainFix": string; - "Doctor.Dialogue.Title": string; - "Doctor.Dialogue.TitleAlmostDone": string; - "Doctor.Dialogue.TitleFix": string; - "Doctor.Level.Must": string; - "Doctor.Level.Necessary": string; - "Doctor.Level.Optional": string; - "Doctor.Level.Recommended": string; - "Doctor.Message.NoIssues": string; - "Doctor.Message.RebuildLocalRequired": string; - "Doctor.Message.RebuildRequired": string; - "Doctor.Message.SomeSkipped": string; - "Doctor.RULES.E2EE_V02500.REASON": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - Filename: string; - "Forces the file to be synced when opened.": string; - "Handle files as Case-Sensitive": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "Ignore files": string; - "Incubate Chunks in Document": string; - "Interval (sec)": string; - "K.exp": string; - "K.long_p2p_sync": string; - "K.P2P": string; - "K.Peer": string; - "K.ScanCustomization": string; - "K.short_p2p_sync": string; - "K.title_p2p_sync": string; - "Keep empty folder": string; - lang_def: string; - "lang-de": string; - "lang-def": string; - "lang-es": string; - "lang-fr": string; - "lang-ja": string; - "lang-ko": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - "Minimum delay for batch database updating": string; - "Minimum interval for syncing": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.fix0256.buttons.checkItLater": string; - "moduleMigration.fix0256.buttons.DismissForever": string; - "moduleMigration.fix0256.buttons.fix": string; - "moduleMigration.fix0256.message": string; - "moduleMigration.fix0256.messageUnrecoverable": string; - "moduleMigration.fix0256.title": string; - "moduleMigration.insecureChunkExist.buttons.fetch": string; - "moduleMigration.insecureChunkExist.buttons.later": string; - "moduleMigration.insecureChunkExist.buttons.rebuild": string; - "moduleMigration.insecureChunkExist.laterMessage": string; - "moduleMigration.insecureChunkExist.message": string; - "moduleMigration.insecureChunkExist.title": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleMigration.optionNoSetupUri": string; - "moduleMigration.optionRemindNextLaunch": string; - "moduleMigration.optionSetupViaP2P": string; - "moduleMigration.optionSetupWizard": string; - "moduleMigration.optionYesFetchAgain": string; - "moduleMigration.titleCaseSensitivity": string; - "moduleMigration.titleRecommendSetupUri": string; - "moduleMigration.titleWelcome": string; - "moduleObsidianMenu.replicate": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.serverVersion": string; - "obsidianLiveSyncSettingTab.titleActiveRemoteServer": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - "P2P.AskPassphraseForDecrypt": string; - "P2P.AskPassphraseForShare": string; - "P2P.DisabledButNeed": string; - "P2P.FailedToOpen": string; - "P2P.NoAutoSyncPeers": string; - "P2P.NoKnownPeers": string; - "P2P.Note.description": string; - "P2P.Note.important_note": string; - "P2P.Note.important_note_sub": string; - "P2P.Note.Summary": string; - "P2P.NotEnabled": string; - "P2P.P2PReplication": string; - "P2P.PaneTitle": string; - "P2P.ReplicatorInstanceMissing": string; - "P2P.SeemsOffline": string; - "P2P.SyncAlreadyRunning": string; - "P2P.SyncCompleted": string; - "P2P.SyncStartedWith": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Path Obfuscation": string; - "Per-file-saved customization sync": string; - "Periodic Sync interval": string; - "Prepare the 'report' to create an issue": string; - Presets: string; - "Process small files in the foreground": string; - "Property Encryption": string; - "RedFlag.Fetch.Method.Desc": string; - "RedFlag.Fetch.Method.FetchSafer": string; - "RedFlag.Fetch.Method.FetchSmoother": string; - "RedFlag.Fetch.Method.FetchTraditional": string; - "RedFlag.Fetch.Method.Title": string; - "RedFlag.FetchRemoteConfig.Buttons.Cancel": string; - "RedFlag.FetchRemoteConfig.Buttons.Fetch": string; - "RedFlag.FetchRemoteConfig.Message": string; - "RedFlag.FetchRemoteConfig.Title": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - "Remote server type": string; - "Remote Type": string; - "Replicator.Dialogue.Locked.Action.Dismiss": string; - "Replicator.Dialogue.Locked.Action.Fetch": string; - "Replicator.Dialogue.Locked.Action.Unlock": string; - "Replicator.Dialogue.Locked.Message": string; - "Replicator.Dialogue.Locked.Message.Fetch": string; - "Replicator.Dialogue.Locked.Message.Unlocked": string; - "Replicator.Dialogue.Locked.Title": string; - "Replicator.Message.Cleaned": string; - "Replicator.Message.InitialiseFatalError": string; - "Replicator.Message.Pending": string; - "Replicator.Message.SomeModuleFailed": string; - "Replicator.Message.VersionUpFlash": string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - "Reset notification threshold and check the remote database usage": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Run Doctor": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setting.TroubleShooting": string; - "Setting.TroubleShooting.Doctor": string; - "Setting.TroubleShooting.Doctor.Desc": string; - "Setting.TroubleShooting.ScanBrokenFiles": string; - "Setting.TroubleShooting.ScanBrokenFiles.Desc": string; - "SettingTab.Message.AskRebuild": string; - "Setup.Apply.Buttons.ApplyAndFetch": string; - "Setup.Apply.Buttons.ApplyAndMerge": string; - "Setup.Apply.Buttons.ApplyAndRebuild": string; - "Setup.Apply.Buttons.Cancel": string; - "Setup.Apply.Buttons.OnlyApply": string; - "Setup.Apply.Message": string; - "Setup.Apply.Title": string; - "Setup.Apply.WarningRebuildRecommended": string; - "Setup.Doctor.Buttons.No": string; - "Setup.Doctor.Buttons.Yes": string; - "Setup.Doctor.Message": string; - "Setup.Doctor.Title": string; - "Setup.FetchRemoteConf.Buttons.Fetch": string; - "Setup.FetchRemoteConf.Buttons.Skip": string; - "Setup.FetchRemoteConf.Message": string; - "Setup.FetchRemoteConf.Title": string; - "Setup.QRCode": string; - "Setup.ShowQRCode": string; - "Setup.ShowQRCode.Desc": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The Hash algorithm for chunk IDs": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "TweakMismatchResolve.Action.Dismiss": string; - "TweakMismatchResolve.Action.UseConfigured": string; - "TweakMismatchResolve.Action.UseMine": string; - "TweakMismatchResolve.Action.UseMineAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseMineWithRebuild": string; - "TweakMismatchResolve.Action.UseRemote": string; - "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseRemoteWithRebuild": string; - "TweakMismatchResolve.Message.Main": string; - "TweakMismatchResolve.Message.MainTweakResolving": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": string; - "TweakMismatchResolve.Table": string; - "TweakMismatchResolve.Table.Row": string; - "TweakMismatchResolve.Title": string; - "TweakMismatchResolve.Title.TweakResolving": string; - "TweakMismatchResolve.Title.UseRemoteConfig": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/he.d.ts b/_types/src/lib/src/common/messages/he.d.ts deleted file mode 100644 index 2930fa5d..00000000 --- a/_types/src/lib/src/common/messages/he.d.ts +++ /dev/null @@ -1,585 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly he: { - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "Access Key": string; - "Active Remote Configuration": string; - "Always prompt merge conflicts": string; - Analyse: string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Automatically Sync all files when opening Obsidian.": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Check: string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compute revisions for chunks": string; - "Copy Report to clipboard": string; - "Data Compression": string; - "Database Name": string; - "Database suffix": string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - "Delete old metadata of deleted files on start-up": string; - "Device name": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Doctor.Button.DismissThisVersion": string; - "Doctor.Button.Fix": string; - "Doctor.Button.FixButNoRebuild": string; - "Doctor.Button.No": string; - "Doctor.Button.Skip": string; - "Doctor.Button.Yes": string; - "Doctor.Dialogue.Main": string; - "Doctor.Dialogue.MainFix": string; - "Doctor.Dialogue.Title": string; - "Doctor.Dialogue.TitleAlmostDone": string; - "Doctor.Dialogue.TitleFix": string; - "Doctor.Level.Must": string; - "Doctor.Level.Necessary": string; - "Doctor.Level.Optional": string; - "Doctor.Level.Recommended": string; - "Doctor.Message.NoIssues": string; - "Doctor.Message.RebuildLocalRequired": string; - "Doctor.Message.RebuildRequired": string; - "Doctor.Message.SomeSkipped": string; - "Doctor.RULES.E2EE_V02500.REASON": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - Filename: string; - "Forces the file to be synced when opened.": string; - "Handle files as Case-Sensitive": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "Ignore files": string; - "Incubate Chunks in Document": string; - "Interval (sec)": string; - "K.exp": string; - "K.long_p2p_sync": string; - "K.P2P": string; - "K.Peer": string; - "K.ScanCustomization": string; - "K.short_p2p_sync": string; - "K.title_p2p_sync": string; - "Keep empty folder": string; - lang_def: string; - "lang-de": string; - "lang-def": string; - "lang-es": string; - "lang-fr": string; - "lang-he": string; - "lang-ja": string; - "lang-ko": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - "Minimum delay for batch database updating": string; - "Minimum interval for syncing": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.fix0256.buttons.checkItLater": string; - "moduleMigration.fix0256.buttons.DismissForever": string; - "moduleMigration.fix0256.buttons.fix": string; - "moduleMigration.fix0256.message": string; - "moduleMigration.fix0256.messageUnrecoverable": string; - "moduleMigration.fix0256.title": string; - "moduleMigration.insecureChunkExist.buttons.fetch": string; - "moduleMigration.insecureChunkExist.buttons.later": string; - "moduleMigration.insecureChunkExist.buttons.rebuild": string; - "moduleMigration.insecureChunkExist.laterMessage": string; - "moduleMigration.insecureChunkExist.message": string; - "moduleMigration.insecureChunkExist.title": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleMigration.optionNoSetupUri": string; - "moduleMigration.optionRemindNextLaunch": string; - "moduleMigration.optionSetupViaP2P": string; - "moduleMigration.optionSetupWizard": string; - "moduleMigration.optionYesFetchAgain": string; - "moduleMigration.titleCaseSensitivity": string; - "moduleMigration.titleRecommendSetupUri": string; - "moduleMigration.titleWelcome": string; - "moduleObsidianMenu.replicate": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.serverVersion": string; - "obsidianLiveSyncSettingTab.titleActiveRemoteServer": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - "P2P.AskPassphraseForDecrypt": string; - "P2P.AskPassphraseForShare": string; - "P2P.DisabledButNeed": string; - "P2P.FailedToOpen": string; - "P2P.NoAutoSyncPeers": string; - "P2P.NoKnownPeers": string; - "P2P.Note.description": string; - "P2P.Note.important_note": string; - "P2P.Note.important_note_sub": string; - "P2P.Note.Summary": string; - "P2P.NotEnabled": string; - "P2P.P2PReplication": string; - "P2P.PaneTitle": string; - "P2P.ReplicatorInstanceMissing": string; - "P2P.SeemsOffline": string; - "P2P.SyncAlreadyRunning": string; - "P2P.SyncCompleted": string; - "P2P.SyncStartedWith": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Path Obfuscation": string; - "Per-file-saved customization sync": string; - "Periodic Sync interval": string; - "Prepare the 'report' to create an issue": string; - Presets: string; - "Process small files in the foreground": string; - "Property Encryption": string; - "RedFlag.Fetch.Method.Desc": string; - "RedFlag.Fetch.Method.FetchSafer": string; - "RedFlag.Fetch.Method.FetchSmoother": string; - "RedFlag.Fetch.Method.FetchTraditional": string; - "RedFlag.Fetch.Method.Title": string; - "RedFlag.FetchRemoteConfig.Buttons.Cancel": string; - "RedFlag.FetchRemoteConfig.Buttons.Fetch": string; - "RedFlag.FetchRemoteConfig.Message": string; - "RedFlag.FetchRemoteConfig.Title": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - "Remote server type": string; - "Remote Type": string; - "Replicator.Dialogue.Locked.Action.Dismiss": string; - "Replicator.Dialogue.Locked.Action.Fetch": string; - "Replicator.Dialogue.Locked.Action.Unlock": string; - "Replicator.Dialogue.Locked.Message": string; - "Replicator.Dialogue.Locked.Message.Fetch": string; - "Replicator.Dialogue.Locked.Message.Unlocked": string; - "Replicator.Dialogue.Locked.Title": string; - "Replicator.Message.Cleaned": string; - "Replicator.Message.InitialiseFatalError": string; - "Replicator.Message.Pending": string; - "Replicator.Message.SomeModuleFailed": string; - "Replicator.Message.VersionUpFlash": string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - "Reset notification threshold and check the remote database usage": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Run Doctor": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setting.TroubleShooting": string; - "Setting.TroubleShooting.Doctor": string; - "Setting.TroubleShooting.Doctor.Desc": string; - "Setting.TroubleShooting.ScanBrokenFiles": string; - "Setting.TroubleShooting.ScanBrokenFiles.Desc": string; - "SettingTab.Message.AskRebuild": string; - "Setup.Apply.Buttons.ApplyAndFetch": string; - "Setup.Apply.Buttons.ApplyAndMerge": string; - "Setup.Apply.Buttons.ApplyAndRebuild": string; - "Setup.Apply.Buttons.Cancel": string; - "Setup.Apply.Buttons.OnlyApply": string; - "Setup.Apply.Message": string; - "Setup.Apply.Title": string; - "Setup.Apply.WarningRebuildRecommended": string; - "Setup.Doctor.Buttons.No": string; - "Setup.Doctor.Buttons.Yes": string; - "Setup.Doctor.Message": string; - "Setup.Doctor.Title": string; - "Setup.FetchRemoteConf.Buttons.Fetch": string; - "Setup.FetchRemoteConf.Buttons.Skip": string; - "Setup.FetchRemoteConf.Message": string; - "Setup.FetchRemoteConf.Title": string; - "Setup.QRCode": string; - "Setup.ShowQRCode": string; - "Setup.ShowQRCode.Desc": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The Hash algorithm for chunk IDs": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "TweakMismatchResolve.Action.Dismiss": string; - "TweakMismatchResolve.Action.UseConfigured": string; - "TweakMismatchResolve.Action.UseMine": string; - "TweakMismatchResolve.Action.UseMineAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseMineWithRebuild": string; - "TweakMismatchResolve.Action.UseRemote": string; - "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseRemoteWithRebuild": string; - "TweakMismatchResolve.Message.Main": string; - "TweakMismatchResolve.Message.MainTweakResolving": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": string; - "TweakMismatchResolve.Table": string; - "TweakMismatchResolve.Table.Row": string; - "TweakMismatchResolve.Title": string; - "TweakMismatchResolve.Title.TweakResolving": string; - "TweakMismatchResolve.Title.UseRemoteConfig": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/ja.d.ts b/_types/src/lib/src/common/messages/ja.d.ts deleted file mode 100644 index bcc2abfd..00000000 --- a/_types/src/lib/src/common/messages/ja.d.ts +++ /dev/null @@ -1,840 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly ja: { - "(Active)": string; - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - "Access Key": string; - Activate: string; - "Add default patterns": string; - "Add new connection": string; - "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - "Always prompt merge conflicts": string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Ask a passphrase at every launch": string; - "Automatically Sync all files when opening Obsidian.": string; - Back: string; - "Back to non-configured": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Cancel: string; - "Cancel Garbage Collection": string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compaction in progress on remote database...": string; - "Compaction on remote database completed successfully.": string; - "Compaction on remote database failed.": string; - "Compaction on remote database timed out.": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - "Compute revisions for chunks": string; - "Configuration Encryption": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "Copy Report to clipboard": string; - "CouchDB Connection Tweak": string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync": string; - "Customization Sync (Beta3)": string; - "Data Compression": string; - "Database Adapter": string; - "Database Name": string; - "Database suffix": string; - Default: string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete old metadata of deleted files on start-up": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - desktop: string; - Developer: string; - Device: string; - "Device name": string; - "Device Setup Method": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables all synchronization and restart.": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Display name": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Doctor.Button.DismissThisVersion": string; - "Doctor.Button.Fix": string; - "Doctor.Button.FixButNoRebuild": string; - "Doctor.Button.No": string; - "Doctor.Button.Skip": string; - "Doctor.Button.Yes": string; - "Doctor.Dialogue.Main": string; - "Doctor.Dialogue.MainFix": string; - "Doctor.Dialogue.Title": string; - "Doctor.Dialogue.TitleAlmostDone": string; - "Doctor.Dialogue.TitleFix": string; - "Doctor.Level.Must": string; - "Doctor.Level.Necessary": string; - "Doctor.Level.Optional": string; - "Doctor.Level.Recommended": string; - "Doctor.Message.NoIssues": string; - "Doctor.Message.RebuildLocalRequired": string; - "Doctor.Message.RebuildRequired": string; - "Doctor.Message.SomeSkipped": string; - "Doctor.RULES.E2EE_V02500.REASON": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - "Failed to connect to remote for compaction.": string; - "Failed to connect to remote for compaction. ${reason}": string; - "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Failed to start replication after Garbage Collection.": string; - Fetch: string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - Filename: string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Forces the file to be synced when opened.": string; - "Fresh Start Wipe": string; - "Garbage Collection cancelled by user.": string; - "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Garbage Collection Confirmation": string; - "Garbage Collection V3 (Beta)": string; - "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Handle files as Case-Sensitive": string; - "Hidden Files": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "Ignore and Proceed": string; - "Ignore files": string; - "Ignore patterns": string; - "Import connection": string; - "Incubate Chunks in Document": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - "Interval (sec)": string; - "K.exp": string; - "K.long_p2p_sync": string; - "K.P2P": string; - "K.Peer": string; - "K.ScanCustomization": string; - "K.short_p2p_sync": string; - "K.title_p2p_sync": string; - "Keep empty folder": string; - lang_def: string; - "lang-de": string; - "lang-def": string; - "lang-es": string; - "lang-fr": string; - "lang-ja": string; - "lang-ko": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - "Local Database Tweak": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - Merge: string; - "Minimum delay for batch database updating": string; - "Minimum interval for syncing": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.fix0256.buttons.checkItLater": string; - "moduleMigration.fix0256.buttons.DismissForever": string; - "moduleMigration.fix0256.buttons.fix": string; - "moduleMigration.fix0256.message": string; - "moduleMigration.fix0256.messageUnrecoverable": string; - "moduleMigration.fix0256.title": string; - "moduleMigration.insecureChunkExist.buttons.fetch": string; - "moduleMigration.insecureChunkExist.buttons.later": string; - "moduleMigration.insecureChunkExist.buttons.rebuild": string; - "moduleMigration.insecureChunkExist.laterMessage": string; - "moduleMigration.insecureChunkExist.message": string; - "moduleMigration.insecureChunkExist.title": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleMigration.optionNoSetupUri": string; - "moduleMigration.optionRemindNextLaunch": string; - "moduleMigration.optionSetupViaP2P": string; - "moduleMigration.optionSetupWizard": string; - "moduleMigration.optionYesFetchAgain": string; - "moduleMigration.titleCaseSensitivity": string; - "moduleMigration.titleRecommendSetupUri": string; - "moduleMigration.titleWelcome": string; - "moduleObsidianMenu.replicate": string; - "More actions": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Network warning style": string; - "New Remote": string; - "No connected device information found. Cancelling Garbage Collection.": string; - "No limit configured": string; - "No, please take me back": string; - "Node ID": string; - "Node Information Missing": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "Obsidian version": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.serverVersion": string; - "obsidianLiveSyncSettingTab.titleActiveRemoteServer": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - Open: string; - "Open the dialog": string; - Overwrite: string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "P2P.AskPassphraseForDecrypt": string; - "P2P.AskPassphraseForShare": string; - "P2P.DisabledButNeed": string; - "P2P.FailedToOpen": string; - "P2P.NoAutoSyncPeers": string; - "P2P.NoKnownPeers": string; - "P2P.Note.description": string; - "P2P.Note.important_note": string; - "P2P.Note.important_note_sub": string; - "P2P.Note.Summary": string; - "P2P.NotEnabled": string; - "P2P.P2PReplication": string; - "P2P.PaneTitle": string; - "P2P.ReplicatorInstanceMissing": string; - "P2P.SeemsOffline": string; - "P2P.SyncAlreadyRunning": string; - "P2P.SyncCompleted": string; - "P2P.SyncStartedWith": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Path Obfuscation": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - "Per-file-saved customization sync": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Periodic Sync interval": string; - "Pick a file to resolve conflict": string; - "Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Please select 'Cancel' explicitly to cancel this operation.": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": string; - "Please set this device name": string; - "Plug-in version": string; - "Prepare the 'report' to create an issue": string; - Presets: string; - "Proceed Garbage Collection": string; - "Proceed with Setup URI": string; - "Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Proceeding with Garbage Collection.": string; - "Process small files in the foreground": string; - Progress: string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - "RedFlag.Fetch.Method.Desc": string; - "RedFlag.Fetch.Method.FetchSafer": string; - "RedFlag.Fetch.Method.FetchSmoother": string; - "RedFlag.Fetch.Method.FetchTraditional": string; - "RedFlag.Fetch.Method.Title": string; - "RedFlag.FetchRemoteConfig.Buttons.Cancel": string; - "RedFlag.FetchRemoteConfig.Buttons.Fetch": string; - "RedFlag.FetchRemoteConfig.Message": string; - "RedFlag.FetchRemoteConfig.Title": string; - "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - "Remote server type": string; - "Remote Type": string; - Rename: string; - "Replicator.Dialogue.Locked.Action.Dismiss": string; - "Replicator.Dialogue.Locked.Action.Fetch": string; - "Replicator.Dialogue.Locked.Action.Unlock": string; - "Replicator.Dialogue.Locked.Message": string; - "Replicator.Dialogue.Locked.Message.Fetch": string; - "Replicator.Dialogue.Locked.Message.Unlocked": string; - "Replicator.Dialogue.Locked.Title": string; - "Replicator.Message.Cleaned": string; - "Replicator.Message.InitialiseFatalError": string; - "Replicator.Message.Pending": string; - "Replicator.Message.SomeModuleFailed": string; - "Replicator.Message.VersionUpFlash": string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset notification threshold and check the remote database usage": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restore or reconstruct local database from remote.": string; - "Run Doctor": string; - "S3/MinIO/R2 Object Storage": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for Broken files": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram Switches": string; - "Scram!": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setting.TroubleShooting": string; - "Setting.TroubleShooting.Doctor": string; - "Setting.TroubleShooting.Doctor.Desc": string; - "Setting.TroubleShooting.ScanBrokenFiles": string; - "Setting.TroubleShooting.ScanBrokenFiles.Desc": string; - "SettingTab.Message.AskRebuild": string; - "Setup URI dialog cancelled.": string; - "Setup.Apply.Buttons.ApplyAndFetch": string; - "Setup.Apply.Buttons.ApplyAndMerge": string; - "Setup.Apply.Buttons.ApplyAndRebuild": string; - "Setup.Apply.Buttons.Cancel": string; - "Setup.Apply.Buttons.OnlyApply": string; - "Setup.Apply.Message": string; - "Setup.Apply.Title": string; - "Setup.Apply.WarningRebuildRecommended": string; - "Setup.Doctor.Buttons.No": string; - "Setup.Doctor.Buttons.Yes": string; - "Setup.Doctor.Message": string; - "Setup.Doctor.Title": string; - "Setup.FetchRemoteConf.Buttons.Fetch": string; - "Setup.FetchRemoteConf.Buttons.Skip": string; - "Setup.FetchRemoteConf.Message": string; - "Setup.FetchRemoteConf.Title": string; - "Setup.QRCode": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.ShowQRCode": string; - "Setup.ShowQRCode.Desc": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show full banner": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "The Hash algorithm for chunk IDs": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "Transfer Tweak": string; - "TweakMismatchResolve.Action.Dismiss": string; - "TweakMismatchResolve.Action.UseConfigured": string; - "TweakMismatchResolve.Action.UseMine": string; - "TweakMismatchResolve.Action.UseMineAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseMineWithRebuild": string; - "TweakMismatchResolve.Action.UseRemote": string; - "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseRemoteWithRebuild": string; - "TweakMismatchResolve.Message.Main": string; - "TweakMismatchResolve.Message.MainTweakResolving": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": string; - "TweakMismatchResolve.Table": string; - "TweakMismatchResolve.Table.Row": string; - "TweakMismatchResolve.Title": string; - "TweakMismatchResolve.Title.TweakResolving": string; - "TweakMismatchResolve.Title.UseRemoteConfig": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use a custom passphrase": string; - "Use a Setup URI (Recommended)": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Verify all": string; - "Verify and repair all files": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/ko.d.ts b/_types/src/lib/src/common/messages/ko.d.ts deleted file mode 100644 index 77eb7a6b..00000000 --- a/_types/src/lib/src/common/messages/ko.d.ts +++ /dev/null @@ -1,802 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly ko: { - "(Active)": string; - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - "Access Key": string; - Activate: string; - "Add default patterns": string; - "Add new connection": string; - "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - "Always prompt merge conflicts": string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Ask a passphrase at every launch": string; - "Automatically Sync all files when opening Obsidian.": string; - Back: string; - "Back to non-configured": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Cancel: string; - "Cancel Garbage Collection": string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compaction in progress on remote database...": string; - "Compaction on remote database completed successfully.": string; - "Compaction on remote database failed.": string; - "Compaction on remote database timed out.": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - "Compute revisions for chunks": string; - "Configuration Encryption": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "Copy Report to clipboard": string; - "CouchDB Connection Tweak": string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync": string; - "Customization Sync (Beta3)": string; - "Data Compression": string; - "Database Adapter": string; - "Database Name": string; - "Database suffix": string; - Default: string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete old metadata of deleted files on start-up": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - desktop: string; - Developer: string; - Device: string; - "Device name": string; - "Device Setup Method": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables all synchronization and restart.": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Display name": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Doctor.Button.DismissThisVersion": string; - "Doctor.Button.Fix": string; - "Doctor.Button.FixButNoRebuild": string; - "Doctor.Button.No": string; - "Doctor.Button.Skip": string; - "Doctor.Button.Yes": string; - "Doctor.Dialogue.Main": string; - "Doctor.Dialogue.MainFix": string; - "Doctor.Dialogue.Title": string; - "Doctor.Dialogue.TitleAlmostDone": string; - "Doctor.Dialogue.TitleFix": string; - "Doctor.Level.Must": string; - "Doctor.Level.Necessary": string; - "Doctor.Level.Optional": string; - "Doctor.Level.Recommended": string; - "Doctor.Message.NoIssues": string; - "Doctor.Message.RebuildLocalRequired": string; - "Doctor.Message.RebuildRequired": string; - "Doctor.Message.SomeSkipped": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - "Failed to connect to remote for compaction.": string; - "Failed to connect to remote for compaction. ${reason}": string; - "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Failed to start replication after Garbage Collection.": string; - Fetch: string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - Filename: string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Forces the file to be synced when opened.": string; - "Fresh Start Wipe": string; - "Garbage Collection cancelled by user.": string; - "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Garbage Collection Confirmation": string; - "Garbage Collection V3 (Beta)": string; - "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Handle files as Case-Sensitive": string; - "Hidden Files": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "Ignore and Proceed": string; - "Ignore files": string; - "Ignore patterns": string; - "Import connection": string; - "Incubate Chunks in Document": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - "Interval (sec)": string; - "K.exp": string; - "K.long_p2p_sync": string; - "K.P2P": string; - "K.Peer": string; - "K.ScanCustomization": string; - "K.short_p2p_sync": string; - "K.title_p2p_sync": string; - "Keep empty folder": string; - lang_def: string; - "lang-de": string; - "lang-def": string; - "lang-es": string; - "lang-fr": string; - "lang-ja": string; - "lang-ko": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - "Local Database Tweak": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - Merge: string; - "Minimum delay for batch database updating": string; - "Minimum interval for syncing": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleMigration.optionNoSetupUri": string; - "moduleMigration.optionRemindNextLaunch": string; - "moduleMigration.optionSetupViaP2P": string; - "moduleMigration.optionSetupWizard": string; - "moduleMigration.optionYesFetchAgain": string; - "moduleMigration.titleCaseSensitivity": string; - "moduleMigration.titleRecommendSetupUri": string; - "moduleMigration.titleWelcome": string; - "moduleObsidianMenu.replicate": string; - "More actions": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Network warning style": string; - "New Remote": string; - "No connected device information found. Cancelling Garbage Collection.": string; - "No limit configured": string; - "No, please take me back": string; - "Node ID": string; - "Node Information Missing": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "Obsidian version": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - Open: string; - "Open the dialog": string; - Overwrite: string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "P2P.AskPassphraseForDecrypt": string; - "P2P.AskPassphraseForShare": string; - "P2P.DisabledButNeed": string; - "P2P.FailedToOpen": string; - "P2P.NoAutoSyncPeers": string; - "P2P.NoKnownPeers": string; - "P2P.Note.description": string; - "P2P.Note.important_note": string; - "P2P.Note.important_note_sub": string; - "P2P.Note.Summary": string; - "P2P.NotEnabled": string; - "P2P.P2PReplication": string; - "P2P.PaneTitle": string; - "P2P.ReplicatorInstanceMissing": string; - "P2P.SeemsOffline": string; - "P2P.SyncAlreadyRunning": string; - "P2P.SyncCompleted": string; - "P2P.SyncStartedWith": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Path Obfuscation": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - "Per-file-saved customization sync": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Periodic Sync interval": string; - "Pick a file to resolve conflict": string; - "Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Please select 'Cancel' explicitly to cancel this operation.": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": string; - "Please set this device name": string; - "Plug-in version": string; - "Prepare the 'report' to create an issue": string; - Presets: string; - "Proceed Garbage Collection": string; - "Proceed with Setup URI": string; - "Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Proceeding with Garbage Collection.": string; - "Process small files in the foreground": string; - Progress: string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - "RedFlag.Fetch.Method.Desc": string; - "RedFlag.Fetch.Method.FetchSafer": string; - "RedFlag.Fetch.Method.FetchSmoother": string; - "RedFlag.Fetch.Method.FetchTraditional": string; - "RedFlag.Fetch.Method.Title": string; - "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - "Remote server type": string; - "Remote Type": string; - Rename: string; - "Replicator.Dialogue.Locked.Action.Dismiss": string; - "Replicator.Dialogue.Locked.Action.Fetch": string; - "Replicator.Dialogue.Locked.Action.Unlock": string; - "Replicator.Dialogue.Locked.Message": string; - "Replicator.Dialogue.Locked.Message.Fetch": string; - "Replicator.Dialogue.Locked.Message.Unlocked": string; - "Replicator.Dialogue.Locked.Title": string; - "Replicator.Message.Cleaned": string; - "Replicator.Message.InitialiseFatalError": string; - "Replicator.Message.Pending": string; - "Replicator.Message.SomeModuleFailed": string; - "Replicator.Message.VersionUpFlash": string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset notification threshold and check the remote database usage": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restore or reconstruct local database from remote.": string; - "Run Doctor": string; - "S3/MinIO/R2 Object Storage": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for Broken files": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram Switches": string; - "Scram!": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setting.TroubleShooting": string; - "Setting.TroubleShooting.Doctor": string; - "Setting.TroubleShooting.Doctor.Desc": string; - "Setting.TroubleShooting.ScanBrokenFiles": string; - "Setting.TroubleShooting.ScanBrokenFiles.Desc": string; - "SettingTab.Message.AskRebuild": string; - "Setup URI dialog cancelled.": string; - "Setup.QRCode": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.ShowQRCode": string; - "Setup.ShowQRCode.Desc": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show full banner": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "The Hash algorithm for chunk IDs": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "Transfer Tweak": string; - "TweakMismatchResolve.Action.Dismiss": string; - "TweakMismatchResolve.Action.UseConfigured": string; - "TweakMismatchResolve.Action.UseMine": string; - "TweakMismatchResolve.Action.UseMineAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseMineWithRebuild": string; - "TweakMismatchResolve.Action.UseRemote": string; - "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseRemoteWithRebuild": string; - "TweakMismatchResolve.Message.Main": string; - "TweakMismatchResolve.Message.MainTweakResolving": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": string; - "TweakMismatchResolve.Table": string; - "TweakMismatchResolve.Table.Row": string; - "TweakMismatchResolve.Title": string; - "TweakMismatchResolve.Title.TweakResolving": string; - "TweakMismatchResolve.Title.UseRemoteConfig": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use a custom passphrase": string; - "Use a Setup URI (Recommended)": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Verify all": string; - "Verify and repair all files": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/ru.d.ts b/_types/src/lib/src/common/messages/ru.d.ts deleted file mode 100644 index b95e153d..00000000 --- a/_types/src/lib/src/common/messages/ru.d.ts +++ /dev/null @@ -1,860 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly ru: { - "(Active)": string; - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - "Access Key": string; - Activate: string; - "Active Remote Configuration": string; - "Add default patterns": string; - "Add new connection": string; - "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - "Always prompt merge conflicts": string; - Analyse: string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Ask a passphrase at every launch": string; - "Automatically Sync all files when opening Obsidian.": string; - Back: string; - "Back to non-configured": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding.": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Cancel: string; - "Cancel Garbage Collection": string; - Check: string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compaction in progress on remote database...": string; - "Compaction on remote database completed successfully.": string; - "Compaction on remote database failed.": string; - "Compaction on remote database timed out.": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - "Compute revisions for chunks": string; - "Configuration Encryption": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "Copy Report to clipboard": string; - "CouchDB Connection Tweak": string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync": string; - "Customization Sync (Beta3)": string; - "Data Compression": string; - "Database Adapter": string; - "Database Name": string; - "Database suffix": string; - Default: string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete old metadata of deleted files on start-up": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - descConnectSetupURI: string; - descCopySetupURI: string; - descEnableLiveSync: string; - descFetchConfigFromRemote: string; - descManualSetup: string; - descTestDatabaseConnection: string; - descValidateDatabaseConfig: string; - desktop: string; - Developer: string; - Device: string; - "Device name": string; - "Device Setup Method": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables all synchronization and restart.": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Display name": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Doctor.Button.DismissThisVersion": string; - "Doctor.Button.Fix": string; - "Doctor.Button.FixButNoRebuild": string; - "Doctor.Button.No": string; - "Doctor.Button.Skip": string; - "Doctor.Button.Yes": string; - "Doctor.Dialogue.Main": string; - "Doctor.Dialogue.MainFix": string; - "Doctor.Dialogue.Title": string; - "Doctor.Dialogue.TitleAlmostDone": string; - "Doctor.Dialogue.TitleFix": string; - "Doctor.Level.Must": string; - "Doctor.Level.Necessary": string; - "Doctor.Level.Optional": string; - "Doctor.Level.Recommended": string; - "Doctor.Message.NoIssues": string; - "Doctor.Message.RebuildLocalRequired": string; - "Doctor.Message.RebuildRequired": string; - "Doctor.Message.SomeSkipped": string; - "Doctor.RULES.E2EE_V02500.REASON": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - "Failed to connect to remote for compaction.": string; - "Failed to connect to remote for compaction. ${reason}": string; - "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Failed to start replication after Garbage Collection.": string; - Fetch: string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - Filename: string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Forces the file to be synced when opened.": string; - "Fresh Start Wipe": string; - "Garbage Collection cancelled by user.": string; - "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Garbage Collection Confirmation": string; - "Garbage Collection V3 (Beta)": string; - "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Handle files as Case-Sensitive": string; - "Hidden Files": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this.": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "Ignore and Proceed": string; - "Ignore files": string; - "Ignore patterns": string; - "Import connection": string; - "Incubate Chunks in Document": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - "Interval (sec)": string; - "K.exp": string; - "K.long_p2p_sync": string; - "K.P2P": string; - "K.Peer": string; - "K.ScanCustomization": string; - "K.short_p2p_sync": string; - "K.title_p2p_sync": string; - "Keep empty folder": string; - lang_def: string; - "lang-de": string; - "lang-def": string; - "lang-es": string; - "lang-fr": string; - "lang-ja": string; - "lang-ko": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - "Local Database Tweak": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - Merge: string; - "Minimum delay for batch database updating": string; - "Minimum interval for syncing": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.fix0256.buttons.checkItLater": string; - "moduleMigration.fix0256.buttons.DismissForever": string; - "moduleMigration.fix0256.buttons.fix": string; - "moduleMigration.fix0256.message": string; - "moduleMigration.fix0256.messageUnrecoverable": string; - "moduleMigration.fix0256.title": string; - "moduleMigration.insecureChunkExist.buttons.fetch": string; - "moduleMigration.insecureChunkExist.buttons.later": string; - "moduleMigration.insecureChunkExist.buttons.rebuild": string; - "moduleMigration.insecureChunkExist.laterMessage": string; - "moduleMigration.insecureChunkExist.message": string; - "moduleMigration.insecureChunkExist.title": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleMigration.optionNoSetupUri": string; - "moduleMigration.optionRemindNextLaunch": string; - "moduleMigration.optionSetupViaP2P": string; - "moduleMigration.optionSetupWizard": string; - "moduleMigration.optionYesFetchAgain": string; - "moduleMigration.titleCaseSensitivity": string; - "moduleMigration.titleRecommendSetupUri": string; - "moduleMigration.titleWelcome": string; - "moduleObsidianMenu.replicate": string; - "More actions": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Network warning style": string; - "New Remote": string; - "No connected device information found. Cancelling Garbage Collection.": string; - "No limit configured": string; - "No, please take me back": string; - "Node ID": string; - "Node Information Missing": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2.": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "Obsidian version": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.serverVersion": string; - "obsidianLiveSyncSettingTab.titleActiveRemoteServer": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - Open: string; - "Open the dialog": string; - Overwrite: string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "P2P.AskPassphraseForDecrypt": string; - "P2P.AskPassphraseForShare": string; - "P2P.DisabledButNeed": string; - "P2P.FailedToOpen": string; - "P2P.NoAutoSyncPeers": string; - "P2P.NoKnownPeers": string; - "P2P.Note.description": string; - "P2P.Note.important_note": string; - "P2P.Note.important_note_sub": string; - "P2P.Note.Summary": string; - "P2P.NotEnabled": string; - "P2P.P2PReplication": string; - "P2P.PaneTitle": string; - "P2P.ReplicatorInstanceMissing": string; - "P2P.SeemsOffline": string; - "P2P.SyncAlreadyRunning": string; - "P2P.SyncCompleted": string; - "P2P.SyncStartedWith": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Path Obfuscation": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - "Per-file-saved customization sync": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Periodic Sync interval": string; - "Pick a file to resolve conflict": string; - "Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Please select 'Cancel' explicitly to cancel this operation.": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": string; - "Please set this device name": string; - "Plug-in version": string; - "Prepare the 'report' to create an issue": string; - Presets: string; - "Proceed Garbage Collection": string; - "Proceed with Setup URI": string; - "Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Proceeding with Garbage Collection.": string; - "Process small files in the foreground": string; - Progress: string; - "Property Encryption": string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - "RedFlag.Fetch.Method.Desc": string; - "RedFlag.Fetch.Method.FetchSafer": string; - "RedFlag.Fetch.Method.FetchSmoother": string; - "RedFlag.Fetch.Method.FetchTraditional": string; - "RedFlag.Fetch.Method.Title": string; - "RedFlag.FetchRemoteConfig.Buttons.Cancel": string; - "RedFlag.FetchRemoteConfig.Buttons.Fetch": string; - "RedFlag.FetchRemoteConfig.Message": string; - "RedFlag.FetchRemoteConfig.Title": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - "Remote server type": string; - "Remote Type": string; - Rename: string; - "Replicator.Dialogue.Locked.Action.Dismiss": string; - "Replicator.Dialogue.Locked.Action.Fetch": string; - "Replicator.Dialogue.Locked.Action.Unlock": string; - "Replicator.Dialogue.Locked.Message": string; - "Replicator.Dialogue.Locked.Message.Fetch": string; - "Replicator.Dialogue.Locked.Message.Unlocked": string; - "Replicator.Dialogue.Locked.Title": string; - "Replicator.Message.Cleaned": string; - "Replicator.Message.InitialiseFatalError": string; - "Replicator.Message.Pending": string; - "Replicator.Message.SomeModuleFailed": string; - "Replicator.Message.VersionUpFlash": string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset notification threshold and check the remote database usage": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restore or reconstruct local database from remote.": string; - "Run Doctor": string; - "S3/MinIO/R2 Object Storage": string; - "Save settings to a markdown file.": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram!": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setting.TroubleShooting": string; - "Setting.TroubleShooting.Doctor": string; - "Setting.TroubleShooting.Doctor.Desc": string; - "Setting.TroubleShooting.ScanBrokenFiles": string; - "Setting.TroubleShooting.ScanBrokenFiles.Desc": string; - "SettingTab.Message.AskRebuild": string; - "Setup URI dialog cancelled.": string; - "Setup.Apply.Buttons.ApplyAndFetch": string; - "Setup.Apply.Buttons.ApplyAndMerge": string; - "Setup.Apply.Buttons.ApplyAndRebuild": string; - "Setup.Apply.Buttons.Cancel": string; - "Setup.Apply.Buttons.OnlyApply": string; - "Setup.Apply.Message": string; - "Setup.Apply.Title": string; - "Setup.Apply.WarningRebuildRecommended": string; - "Setup.Doctor.Buttons.No": string; - "Setup.Doctor.Buttons.Yes": string; - "Setup.Doctor.Message": string; - "Setup.Doctor.Title": string; - "Setup.FetchRemoteConf.Buttons.Fetch": string; - "Setup.FetchRemoteConf.Buttons.Skip": string; - "Setup.FetchRemoteConf.Message": string; - "Setup.FetchRemoteConf.Title": string; - "Setup.QRCode": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.ShowQRCode": string; - "Setup.ShowQRCode.Desc": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show full banner": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "The Hash algorithm for chunk IDs": string; - "The maximum duration for which chunks can be incubated within the document.": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This passphrase will not be copied to another device. It will be set to until you configure it again.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "Transfer Tweak": string; - "TweakMismatchResolve.Action.Dismiss": string; - "TweakMismatchResolve.Action.UseConfigured": string; - "TweakMismatchResolve.Action.UseMine": string; - "TweakMismatchResolve.Action.UseMineAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseMineWithRebuild": string; - "TweakMismatchResolve.Action.UseRemote": string; - "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseRemoteWithRebuild": string; - "TweakMismatchResolve.Message.Main": string; - "TweakMismatchResolve.Message.MainTweakResolving": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": string; - "TweakMismatchResolve.Table": string; - "TweakMismatchResolve.Table.Row": string; - "TweakMismatchResolve.Title": string; - "TweakMismatchResolve.Title.TweakResolving": string; - "TweakMismatchResolve.Title.UseRemoteConfig": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use a custom passphrase": string; - "Use a Setup URI (Recommended)": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Verify all": string; - "Verify and repair all files": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name.": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/zh-tw.d.ts b/_types/src/lib/src/common/messages/zh-tw.d.ts deleted file mode 100644 index d2193760..00000000 --- a/_types/src/lib/src/common/messages/zh-tw.d.ts +++ /dev/null @@ -1,386 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly "zh-tw": { - "(Active)": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - Activate: string; - "Active Remote Configuration": string; - "Add default patterns": string; - "Add new connection": string; - "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - "Always prompt merge conflicts": string; - Analyse: string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Ask a passphrase at every launch": string; - "Automatically Sync all files when opening Obsidian.": string; - Back: string; - "Back to non-configured": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Bucket Name": string; - Cancel: string; - "Cancel Garbage Collection": string; - Check: string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Compaction in progress on remote database...": string; - "Compaction on remote database completed successfully.": string; - "Compaction on remote database failed.": string; - "Compaction on remote database timed out.": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - "Configuration Encryption": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "Copy Report to clipboard": string; - "CouchDB Connection Tweak": string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync": string; - "Customization Sync (Beta3)": string; - "Data Compression": string; - "Database -> Storage": string; - "Database Adapter": string; - "Database Name": string; - "Database suffix": string; - Default: string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete old metadata of deleted files on start-up": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - desktop: string; - Developer: string; - Device: string; - "Device name": string; - "Device Setup Method": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables all synchronization and restart.": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Display name": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Document History": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - "Failed to connect to remote for compaction.": string; - "Failed to connect to remote for compaction. ${reason}": string; - "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Failed to start replication after Garbage Collection.": string; - Fetch: string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - "File to view History": string; - Filename: string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Forces the file to be synced when opened.": string; - "Fresh Start Wipe": string; - "Garbage Collection cancelled by user.": string; - "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Garbage Collection Confirmation": string; - "Garbage Collection V3 (Beta)": string; - "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Handle files as Case-Sensitive": string; - "Hidden Files": string; - "Hide completely": string; - "Highlight diff": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.": string; - "Ignore and Proceed": string; - "Ignore patterns": string; - "Import connection": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - "Initialise journal received history. On the next sync, every item except this device sent will be downloaded again.": string; - "Initialise journal sent history. On the next sync, every item except this device received will be sent again.": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "Minimum interval for syncing": string; - "More actions": string; - "Network warning style": string; - "New Remote": string; - "No connected device information found. Cancelling Garbage Collection.": string; - "No limit configured": string; - "No, please take me back": string; - "Node ID": string; - "Node Information Missing": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Obsidian version": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Pick a file to resolve conflict": string; - "Pick a file to show history": string; - "Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Please select 'Cancel' explicitly to cancel this operation.": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set this device name": string; - "Plug-in version": string; - "Prepare the 'report' to create an issue": string; - "Proceed Garbage Collection": string; - "Proceed with Setup URI": string; - "Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Proceeding with Garbage Collection.": string; - Progress: string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recovery and Repair": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.": string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - Rename: string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset notification threshold and check the remote database usage": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and display may be inconsistent. Are you sure to restart now?": string; - "Restore or reconstruct local database from remote.": string; - "Run Doctor": string; - "S3/MinIO/R2 Object Storage": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan for Broken files": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram Switches": string; - "Scram!": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setup URI dialog cancelled.": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Show full banner": string; - "Show history": string; - "Show icon only": string; - "Show status icon instead of file warnings banner": string; - "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Storage -> Database": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "The IndexedDB adapter often offers superior performance in certain scenarios, but it has been found to cause memory leaks when used with LiveSync mode. When using LiveSync mode, please use IDB adapter instead.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "Use a Setup URI (Recommended)": string; - "Verify all": string; - "Verify and repair all files": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/zh.d.ts b/_types/src/lib/src/common/messages/zh.d.ts deleted file mode 100644 index 473845e4..00000000 --- a/_types/src/lib/src/common/messages/zh.d.ts +++ /dev/null @@ -1,1095 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly zh: { - "(Active)": string; - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - "Access Key": string; - Activate: string; - "Active Remote Configuration": string; - "Add default patterns": string; - "Add new connection": string; - "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - "Always prompt merge conflicts": string; - Analyse: string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Ask a passphrase at every launch": string; - "Automatically Sync all files when opening Obsidian.": string; - Back: string; - "Back to non-configured": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Cancel: string; - "Cancel Garbage Collection": string; - Check: string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compaction in progress on remote database...": string; - "Compaction on remote database completed successfully.": string; - "Compaction on remote database failed.": string; - "Compaction on remote database timed out.": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - "Compute revisions for chunks": string; - "Configuration Encryption": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "Copy Report to clipboard": string; - "CouchDB Connection Tweak": string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync": string; - "Customization Sync (Beta3)": string; - "Data Compression": string; - "Database Adapter": string; - "Database Name": string; - "Database suffix": string; - Default: string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete old metadata of deleted files on start-up": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - desktop: string; - Developer: string; - Device: string; - "Device name": string; - "Device Setup Method": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables all synchronization and restart.": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Display name": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Doctor.Button.DismissThisVersion": string; - "Doctor.Button.Fix": string; - "Doctor.Button.FixButNoRebuild": string; - "Doctor.Button.No": string; - "Doctor.Button.Skip": string; - "Doctor.Button.Yes": string; - "Doctor.Dialogue.Main": string; - "Doctor.Dialogue.MainFix": string; - "Doctor.Dialogue.Title": string; - "Doctor.Dialogue.TitleAlmostDone": string; - "Doctor.Dialogue.TitleFix": string; - "Doctor.Level.Must": string; - "Doctor.Level.Necessary": string; - "Doctor.Level.Optional": string; - "Doctor.Level.Recommended": string; - "Doctor.Message.NoIssues": string; - "Doctor.Message.RebuildLocalRequired": string; - "Doctor.Message.RebuildRequired": string; - "Doctor.Message.SomeSkipped": string; - "Doctor.RULES.E2EE_V02500.REASON": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - "Failed to connect to remote for compaction.": string; - "Failed to connect to remote for compaction. ${reason}": string; - "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Failed to start replication after Garbage Collection.": string; - Fetch: string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - Filename: string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Forces the file to be synced when opened.": string; - "Fresh Start Wipe": string; - "Garbage Collection cancelled by user.": string; - "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Garbage Collection Confirmation": string; - "Garbage Collection V3 (Beta)": string; - "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Handle files as Case-Sensitive": string; - "Hidden Files": string; - "Hide completely": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "Ignore and Proceed": string; - "Ignore files": string; - "Ignore patterns": string; - "Import connection": string; - "Incubate Chunks in Document": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - "Interval (sec)": string; - "K.exp": string; - "K.long_p2p_sync": string; - "K.P2P": string; - "K.Peer": string; - "K.ScanCustomization": string; - "K.short_p2p_sync": string; - "K.title_p2p_sync": string; - "Keep empty folder": string; - lang_def: string; - "lang-de": string; - "lang-def": string; - "lang-es": string; - "lang-fr": string; - "lang-ja": string; - "lang-ko": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - Merge: string; - "Minimum delay for batch database updating": string; - "Minimum interval for syncing": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.fix0256.buttons.checkItLater": string; - "moduleMigration.fix0256.buttons.DismissForever": string; - "moduleMigration.fix0256.buttons.fix": string; - "moduleMigration.fix0256.message": string; - "moduleMigration.fix0256.messageUnrecoverable": string; - "moduleMigration.fix0256.title": string; - "moduleMigration.insecureChunkExist.buttons.fetch": string; - "moduleMigration.insecureChunkExist.buttons.later": string; - "moduleMigration.insecureChunkExist.buttons.rebuild": string; - "moduleMigration.insecureChunkExist.laterMessage": string; - "moduleMigration.insecureChunkExist.message": string; - "moduleMigration.insecureChunkExist.title": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleMigration.optionNoSetupUri": string; - "moduleMigration.optionRemindNextLaunch": string; - "moduleMigration.optionSetupViaP2P": string; - "moduleMigration.optionSetupWizard": string; - "moduleMigration.optionYesFetchAgain": string; - "moduleMigration.titleCaseSensitivity": string; - "moduleMigration.titleRecommendSetupUri": string; - "moduleMigration.titleWelcome": string; - "moduleObsidianMenu.replicate": string; - "More actions": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Network warning style": string; - "New Remote": string; - "No connected device information found. Cancelling Garbage Collection.": string; - "No limit configured": string; - "No, please take me back": string; - "Node ID": string; - "Node Information Missing": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "Obsidian version": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.serverVersion": string; - "obsidianLiveSyncSettingTab.titleActiveRemoteServer": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - Open: string; - "Open the dialog": string; - Overwrite: string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "P2P.AskPassphraseForDecrypt": string; - "P2P.AskPassphraseForShare": string; - "P2P.DisabledButNeed": string; - "P2P.FailedToOpen": string; - "P2P.NoAutoSyncPeers": string; - "P2P.NoKnownPeers": string; - "P2P.Note.description": string; - "P2P.Note.important_note": string; - "P2P.Note.important_note_sub": string; - "P2P.Note.Summary": string; - "P2P.NotEnabled": string; - "P2P.P2PReplication": string; - "P2P.PaneTitle": string; - "P2P.ReplicatorInstanceMissing": string; - "P2P.SeemsOffline": string; - "P2P.SyncAlreadyRunning": string; - "P2P.SyncCompleted": string; - "P2P.SyncStartedWith": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Path Obfuscation": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - "Per-file-saved customization sync": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Periodic Sync interval": string; - "Pick a file to resolve conflict": string; - "Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Please select 'Cancel' explicitly to cancel this operation.": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": string; - "Please set this device name": string; - "Plug-in version": string; - "Prepare the 'report' to create an issue": string; - Presets: string; - "Proceed Garbage Collection": string; - "Proceed with Setup URI": string; - "Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Proceeding with Garbage Collection.": string; - "Process small files in the foreground": string; - Progress: string; - "Property Encryption": string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - "RedFlag.Fetch.Method.Desc": string; - "RedFlag.Fetch.Method.FetchSafer": string; - "RedFlag.Fetch.Method.FetchSmoother": string; - "RedFlag.Fetch.Method.FetchTraditional": string; - "RedFlag.Fetch.Method.Title": string; - "RedFlag.FetchRemoteConfig.Buttons.Cancel": string; - "RedFlag.FetchRemoteConfig.Buttons.Fetch": string; - "RedFlag.FetchRemoteConfig.Message": string; - "RedFlag.FetchRemoteConfig.Title": string; - "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - "Remote server type": string; - "Remote Type": string; - Rename: string; - "Replicator.Dialogue.Locked.Action.Dismiss": string; - "Replicator.Dialogue.Locked.Action.Fetch": string; - "Replicator.Dialogue.Locked.Action.Unlock": string; - "Replicator.Dialogue.Locked.Message": string; - "Replicator.Dialogue.Locked.Message.Fetch": string; - "Replicator.Dialogue.Locked.Message.Unlocked": string; - "Replicator.Dialogue.Locked.Title": string; - "Replicator.Message.Cleaned": string; - "Replicator.Message.InitialiseFatalError": string; - "Replicator.Message.Pending": string; - "Replicator.Message.SomeModuleFailed": string; - "Replicator.Message.VersionUpFlash": string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset notification threshold and check the remote database usage": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restore or reconstruct local database from remote.": string; - "Run Doctor": string; - "S3/MinIO/R2 Object Storage": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for Broken files": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram Switches": string; - "Scram!": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setting.TroubleShooting": string; - "Setting.TroubleShooting.Doctor": string; - "Setting.TroubleShooting.Doctor.Desc": string; - "Setting.TroubleShooting.ScanBrokenFiles": string; - "Setting.TroubleShooting.ScanBrokenFiles.Desc": string; - "SettingTab.Message.AskRebuild": string; - "Setup URI dialog cancelled.": string; - "Setup.Apply.Buttons.ApplyAndFetch": string; - "Setup.Apply.Buttons.ApplyAndMerge": string; - "Setup.Apply.Buttons.ApplyAndRebuild": string; - "Setup.Apply.Buttons.Cancel": string; - "Setup.Apply.Buttons.OnlyApply": string; - "Setup.Apply.Message": string; - "Setup.Apply.Title": string; - "Setup.Apply.WarningRebuildRecommended": string; - "Setup.Doctor.Buttons.No": string; - "Setup.Doctor.Buttons.Yes": string; - "Setup.Doctor.Message": string; - "Setup.Doctor.Title": string; - "Setup.FetchRemoteConf.Buttons.Fetch": string; - "Setup.FetchRemoteConf.Buttons.Skip": string; - "Setup.FetchRemoteConf.Message": string; - "Setup.FetchRemoteConf.Title": string; - "Setup.QRCode": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.ShowQRCode": string; - "Setup.ShowQRCode.Desc": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show full banner": string; - "Show icon only": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "The Hash algorithm for chunk IDs": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "TweakMismatchResolve.Action.Dismiss": string; - "TweakMismatchResolve.Action.UseConfigured": string; - "TweakMismatchResolve.Action.UseMine": string; - "TweakMismatchResolve.Action.UseMineAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseMineWithRebuild": string; - "TweakMismatchResolve.Action.UseRemote": string; - "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseRemoteWithRebuild": string; - "TweakMismatchResolve.Message.Main": string; - "TweakMismatchResolve.Message.MainTweakResolving": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": string; - "TweakMismatchResolve.Table": string; - "TweakMismatchResolve.Table.Row": string; - "TweakMismatchResolve.Title": string; - "TweakMismatchResolve.Title.TweakResolving": string; - "TweakMismatchResolve.Title.UseRemoteConfig": string; - "Ui.Common.Signal.Caution": string; - "Ui.Common.Signal.Danger": string; - "Ui.Common.Signal.Notice": string; - "Ui.Common.Signal.Warning": string; - "Ui.Settings.Advanced.LocalDatabaseTweak": string; - "Ui.Settings.Advanced.MemoryCache": string; - "Ui.Settings.Advanced.TransferTweak": string; - "Ui.Settings.Common.Analyse": string; - "Ui.Settings.Common.Back": string; - "Ui.Settings.Common.Check": string; - "Ui.Settings.Common.Configure": string; - "Ui.Settings.Common.Continue": string; - "Ui.Settings.Common.Delete": string; - "Ui.Settings.Common.Fetch": string; - "Ui.Settings.Common.Lock": string; - "Ui.Settings.Common.Merge": string; - "Ui.Settings.Common.Open": string; - "Ui.Settings.Common.Overwrite": string; - "Ui.Settings.Common.Perform": string; - "Ui.Settings.Common.ResetAll": string; - "Ui.Settings.Common.ResolveAll": string; - "Ui.Settings.Common.Scan": string; - "Ui.Settings.Common.Send": string; - "Ui.Settings.Common.Use": string; - "Ui.Settings.Common.VerifyAll": string; - "Ui.Settings.CustomizationSync.OpenDesc": string; - "Ui.Settings.CustomizationSync.Panel": string; - "Ui.Settings.CustomizationSync.WarnChangeDeviceName": string; - "Ui.Settings.CustomizationSync.WarnSetDeviceName": string; - "Ui.Settings.Hatch.AnalyseDatabaseUsage": string; - "Ui.Settings.Hatch.AnalyseDatabaseUsageDesc": string; - "Ui.Settings.Hatch.BackToNonConfigured": string; - "Ui.Settings.Hatch.ConvertNonObfuscated": string; - "Ui.Settings.Hatch.ConvertNonObfuscatedDesc": string; - "Ui.Settings.Hatch.CopyIssueReport": string; - "Ui.Settings.Hatch.DatabaseLabel": string; - "Ui.Settings.Hatch.DatabaseToStorage": string; - "Ui.Settings.Hatch.DeleteCustomizationSyncData": string; - "Ui.Settings.Hatch.GeneratedReport": string; - "Ui.Settings.Hatch.Missing": string; - "Ui.Settings.Hatch.ModifiedSize": string; - "Ui.Settings.Hatch.ModifiedSizeActual": string; - "Ui.Settings.Hatch.PrepareIssueReport": string; - "Ui.Settings.Hatch.RecoveryAndRepair": string; - "Ui.Settings.Hatch.RecreateAll": string; - "Ui.Settings.Hatch.RecreateMissingChunks": string; - "Ui.Settings.Hatch.RecreateMissingChunksDesc": string; - "Ui.Settings.Hatch.ResetPanel": string; - "Ui.Settings.Hatch.ResetRemoteUsage": string; - "Ui.Settings.Hatch.ResetRemoteUsageDesc": string; - "Ui.Settings.Hatch.ResolveAllConflictedFiles": string; - "Ui.Settings.Hatch.ResolveAllConflictedFilesDesc": string; - "Ui.Settings.Hatch.RunDoctor": string; - "Ui.Settings.Hatch.ScanBrokenFiles": string; - "Ui.Settings.Hatch.ScramSwitches": string; - "Ui.Settings.Hatch.ShowHistory": string; - "Ui.Settings.Hatch.StorageLabel": string; - "Ui.Settings.Hatch.StorageToDatabase": string; - "Ui.Settings.Hatch.VerifyAndRepairAllFiles": string; - "Ui.Settings.Hatch.VerifyAndRepairAllFilesDesc": string; - "Ui.Settings.Maintenance.Cleanup": string; - "Ui.Settings.Maintenance.CleanupDesc": string; - "Ui.Settings.Maintenance.DeleteLocalDatabase": string; - "Ui.Settings.Maintenance.EmergencyRestart": string; - "Ui.Settings.Maintenance.EmergencyRestartDesc": string; - "Ui.Settings.Maintenance.FreshStartWipe": string; - "Ui.Settings.Maintenance.FreshStartWipeDesc": string; - "Ui.Settings.Maintenance.GarbageCollection": string; - "Ui.Settings.Maintenance.GarbageCollectionAction": string; - "Ui.Settings.Maintenance.GarbageCollectionDesc": string; - "Ui.Settings.Maintenance.LockServer": string; - "Ui.Settings.Maintenance.LockServerDesc": string; - "Ui.Settings.Maintenance.OverwriteRemote": string; - "Ui.Settings.Maintenance.OverwriteRemoteDesc": string; - "Ui.Settings.Maintenance.OverwriteServerData": string; - "Ui.Settings.Maintenance.OverwriteServerDataDesc": string; - "Ui.Settings.Maintenance.PurgeAllJournalCounter": string; - "Ui.Settings.Maintenance.PurgeAllJournalCounterDesc": string; - "Ui.Settings.Maintenance.RebuildingOperations": string; - "Ui.Settings.Maintenance.Resend": string; - "Ui.Settings.Maintenance.ResendDesc": string; - "Ui.Settings.Maintenance.Reset": string; - "Ui.Settings.Maintenance.ResetAllJournalCounter": string; - "Ui.Settings.Maintenance.ResetAllJournalCounterDesc": string; - "Ui.Settings.Maintenance.ResetJournalReceived": string; - "Ui.Settings.Maintenance.ResetJournalReceivedDesc": string; - "Ui.Settings.Maintenance.ResetJournalSent": string; - "Ui.Settings.Maintenance.ResetJournalSentDesc": string; - "Ui.Settings.Maintenance.ResetLocalSyncInfo": string; - "Ui.Settings.Maintenance.ResetLocalSyncInfoDesc": string; - "Ui.Settings.Maintenance.ResetReceived": string; - "Ui.Settings.Maintenance.ResetSentHistory": string; - "Ui.Settings.Maintenance.ResetThisDevice": string; - "Ui.Settings.Maintenance.ScheduleAndRestart": string; - "Ui.Settings.Maintenance.Scram": string; - "Ui.Settings.Maintenance.SendChunks": string; - "Ui.Settings.Maintenance.Syncing": string; - "Ui.Settings.Maintenance.WarningLockedReadyAction": string; - "Ui.Settings.Maintenance.WarningLockedReadyText": string; - "Ui.Settings.Maintenance.WarningLockedResolveAction": string; - "Ui.Settings.Maintenance.WarningLockedResolveText": string; - "Ui.Settings.Maintenance.WriteRedFlagAndRestart": string; - "Ui.Settings.Patches.CompatibilityConflict": string; - "Ui.Settings.Patches.CompatibilityDatabase": string; - "Ui.Settings.Patches.CompatibilityInternalApi": string; - "Ui.Settings.Patches.CompatibilityMetadata": string; - "Ui.Settings.Patches.CompatibilityRemote": string; - "Ui.Settings.Patches.CompatibilityTrouble": string; - "Ui.Settings.Patches.CurrentAdapter": string; - "Ui.Settings.Patches.DatabaseAdapter": string; - "Ui.Settings.Patches.DatabaseAdapterDesc": string; - "Ui.Settings.Patches.EdgeCaseBehaviour": string; - "Ui.Settings.Patches.EdgeCaseDatabase": string; - "Ui.Settings.Patches.EdgeCaseProcessing": string; - "Ui.Settings.Patches.IndexedDbWarning": string; - "Ui.Settings.Patches.MigratingToIdb": string; - "Ui.Settings.Patches.MigratingToIndexedDb": string; - "Ui.Settings.Patches.MigrationIdbCompleted": string; - "Ui.Settings.Patches.MigrationIdbCompletedFollowUp": string; - "Ui.Settings.Patches.MigrationIndexedDbCompleted": string; - "Ui.Settings.Patches.MigrationIndexedDbCompletedFollowUp": string; - "Ui.Settings.Patches.MigrationWarning": string; - "Ui.Settings.Patches.OperationToIdb": string; - "Ui.Settings.Patches.OperationToIndexedDb": string; - "Ui.Settings.Patches.Remediation": string; - "Ui.Settings.Patches.RemediationChanged": string; - "Ui.Settings.Patches.RemediationNoLimit": string; - "Ui.Settings.Patches.RemediationRestarting": string; - "Ui.Settings.Patches.RemediationRestartLater": string; - "Ui.Settings.Patches.RemediationRestartMessage": string; - "Ui.Settings.Patches.RemediationRestartNow": string; - "Ui.Settings.Patches.RemediationSuffixChanged": string; - "Ui.Settings.Patches.RemediationWithValue": string; - "Ui.Settings.Patches.RemoteDatabaseSunset": string; - "Ui.Settings.Patches.SwitchToIDB": string; - "Ui.Settings.Patches.SwitchToIndexedDb": string; - "Ui.Settings.PowerUsers.ConfigurationEncryption": string; - "Ui.Settings.PowerUsers.ConnectionTweak": string; - "Ui.Settings.PowerUsers.ConnectionTweakDesc": string; - "Ui.Settings.PowerUsers.Default": string; - "Ui.Settings.PowerUsers.Developer": string; - "Ui.Settings.PowerUsers.EncryptSensitiveConfig": string; - "Ui.Settings.PowerUsers.PromptPassphraseEveryLaunch": string; - "Ui.Settings.PowerUsers.UseCustomPassphrase": string; - "Ui.Settings.Remote.Activate": string; - "Ui.Settings.Remote.ActiveSuffix": string; - "Ui.Settings.Remote.AddConnection": string; - "Ui.Settings.Remote.AddRemoteDefaultName": string; - "Ui.Settings.Remote.ConfigureAndChangeRemote": string; - "Ui.Settings.Remote.ConfigureE2EE": string; - "Ui.Settings.Remote.ConfigureRemote": string; - "Ui.Settings.Remote.DeleteRemoteConfirm": string; - "Ui.Settings.Remote.DeleteRemoteTitle": string; - "Ui.Settings.Remote.DisplayName": string; - "Ui.Settings.Remote.DuplicateRemote": string; - "Ui.Settings.Remote.DuplicateRemoteSuffix": string; - "Ui.Settings.Remote.E2EEConfiguration": string; - "Ui.Settings.Remote.Export": string; - "Ui.Settings.Remote.FetchRemoteSettings": string; - "Ui.Settings.Remote.ImportConnection": string; - "Ui.Settings.Remote.ImportConnectionPrompt": string; - "Ui.Settings.Remote.ImportedCouchDb": string; - "Ui.Settings.Remote.ImportedRemote": string; - "Ui.Settings.Remote.MoreActions": string; - "Ui.Settings.Remote.PeerToPeerPanel": string; - "Ui.Settings.Remote.RemoteConfigurationPrefix": string; - "Ui.Settings.Remote.RemoteDatabases": string; - "Ui.Settings.Remote.RemoteName": string; - "Ui.Settings.Remote.RemoteNameCouchDb": string; - "Ui.Settings.Remote.RemoteNameP2P": string; - "Ui.Settings.Remote.RemoteNameS3": string; - "Ui.Settings.Remote.Rename": string; - "Ui.Settings.Selector.AddDefaultPatterns": string; - "Ui.Settings.Selector.CrossPlatform": string; - "Ui.Settings.Selector.Default": string; - "Ui.Settings.Selector.HiddenFiles": string; - "Ui.Settings.Selector.IgnorePatterns": string; - "Ui.Settings.Selector.NonSynchronisingFiles": string; - "Ui.Settings.Selector.NonSynchronisingFilesDesc": string; - "Ui.Settings.Selector.NormalFiles": string; - "Ui.Settings.Selector.OverwritePatterns": string; - "Ui.Settings.Selector.OverwritePatternsDesc": string; - "Ui.Settings.Selector.SynchronisingFiles": string; - "Ui.Settings.Selector.SynchronisingFilesDesc": string; - "Ui.Settings.Selector.TargetPatterns": string; - "Ui.Settings.Selector.TargetPatternsDesc": string; - "Ui.Settings.Setup.RerunWizardButton": string; - "Ui.Settings.Setup.RerunWizardDesc": string; - "Ui.Settings.Setup.RerunWizardName": string; - "Ui.Settings.SyncSettings.Fetch": string; - "Ui.Settings.SyncSettings.Merge": string; - "Ui.Settings.SyncSettings.Overwrite": string; - "Ui.SetupWizard.Common.Back": string; - "Ui.SetupWizard.Common.Cancel": string; - "Ui.SetupWizard.Common.ProceedSelectOption": string; - "Ui.SetupWizard.Intro.ExistingOption": string; - "Ui.SetupWizard.Intro.ExistingOptionDesc": string; - "Ui.SetupWizard.Intro.Guidance": string; - "Ui.SetupWizard.Intro.NewOption": string; - "Ui.SetupWizard.Intro.NewOptionDesc": string; - "Ui.SetupWizard.Intro.ProceedExisting": string; - "Ui.SetupWizard.Intro.ProceedNew": string; - "Ui.SetupWizard.Intro.Question": string; - "Ui.SetupWizard.Intro.Title": string; - "Ui.SetupWizard.OutroAskUserMode.CompatibleOption": string; - "Ui.SetupWizard.OutroAskUserMode.CompatibleOptionDesc": string; - "Ui.SetupWizard.OutroAskUserMode.ExistingOption": string; - "Ui.SetupWizard.OutroAskUserMode.ExistingOptionDesc": string; - "Ui.SetupWizard.OutroAskUserMode.Guidance": string; - "Ui.SetupWizard.OutroAskUserMode.NewOption": string; - "Ui.SetupWizard.OutroAskUserMode.NewOptionDesc": string; - "Ui.SetupWizard.OutroAskUserMode.ProceedApplySettings": string; - "Ui.SetupWizard.OutroAskUserMode.ProceedNext": string; - "Ui.SetupWizard.OutroAskUserMode.Question": string; - "Ui.SetupWizard.OutroAskUserMode.Title": string; - "Ui.SetupWizard.OutroNewUser.GuidancePrimary": string; - "Ui.SetupWizard.OutroNewUser.GuidanceWarning": string; - "Ui.SetupWizard.OutroNewUser.Important": string; - "Ui.SetupWizard.OutroNewUser.Proceed": string; - "Ui.SetupWizard.OutroNewUser.Question": string; - "Ui.SetupWizard.OutroNewUser.Title": string; - "Ui.SetupWizard.SelectExisting.Guidance": string; - "Ui.SetupWizard.SelectExisting.ManualOption": string; - "Ui.SetupWizard.SelectExisting.ManualOptionDesc": string; - "Ui.SetupWizard.SelectExisting.ProceedManual": string; - "Ui.SetupWizard.SelectExisting.ProceedQr": string; - "Ui.SetupWizard.SelectExisting.ProceedSetupUri": string; - "Ui.SetupWizard.SelectExisting.QrOption": string; - "Ui.SetupWizard.SelectExisting.QrOptionDesc": string; - "Ui.SetupWizard.SelectExisting.Question": string; - "Ui.SetupWizard.SelectExisting.SetupUriOption": string; - "Ui.SetupWizard.SelectExisting.SetupUriOptionDesc": string; - "Ui.SetupWizard.SelectExisting.Title": string; - "Ui.SetupWizard.SelectNew.Guidance": string; - "Ui.SetupWizard.SelectNew.ManualOption": string; - "Ui.SetupWizard.SelectNew.ManualOptionDesc": string; - "Ui.SetupWizard.SelectNew.ProceedManual": string; - "Ui.SetupWizard.SelectNew.ProceedSetupUri": string; - "Ui.SetupWizard.SelectNew.Question": string; - "Ui.SetupWizard.SelectNew.SetupUriOption": string; - "Ui.SetupWizard.SelectNew.SetupUriOptionDesc": string; - "Ui.SetupWizard.SelectNew.Title": string; - "Ui.SetupWizard.SetupRemote.BucketOption": string; - "Ui.SetupWizard.SetupRemote.BucketOptionDesc": string; - "Ui.SetupWizard.SetupRemote.CouchDbOptionDesc": string; - "Ui.SetupWizard.SetupRemote.Guidance": string; - "Ui.SetupWizard.SetupRemote.P2POption": string; - "Ui.SetupWizard.SetupRemote.P2POptionDesc": string; - "Ui.SetupWizard.SetupRemote.ProceedBucket": string; - "Ui.SetupWizard.SetupRemote.ProceedCouchDb": string; - "Ui.SetupWizard.SetupRemote.ProceedP2P": string; - "Ui.SetupWizard.SetupRemote.Title": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use a custom passphrase": string; - "Use a Setup URI (Recommended)": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Verify all": string; - "Verify and repair all files": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/models/auth.type.d.ts b/_types/src/lib/src/common/models/auth.type.d.ts deleted file mode 100644 index ebc33799..00000000 --- a/_types/src/lib/src/common/models/auth.type.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type CouchDBCredentials = BasicCredentials | JWTCredentials; -export type JWTAlgorithm = "HS256" | "HS512" | "ES256" | "ES512" | ""; -export type Credential = { - username: string; - password: string; -}; -export type BasicCredentials = { - username: string; - password: string; - type: "basic"; -}; -export type JWTCredentials = { - jwtAlgorithm: JWTAlgorithm; - jwtKey: string; - jwtKid: string; - jwtSub: string; - jwtExpDuration: number; - type: "jwt"; -}; -export interface JWTHeader { - alg: string; - typ: string; - kid?: string; -} -export interface JWTPayload { - sub: string; - exp: number; - iss?: string; - iat: number; - [key: string]: unknown; -} -export interface JWTParams { - header: JWTHeader; - payload: JWTPayload; - credentials: JWTCredentials; -} -export interface PreparedJWT { - header: JWTHeader; - payload: JWTPayload; - token: string; -} diff --git a/_types/src/lib/src/common/models/db.const.d.ts b/_types/src/lib/src/common/models/db.const.d.ts deleted file mode 100644 index 58f5801f..00000000 --- a/_types/src/lib/src/common/models/db.const.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DocumentID } from "./db.type"; -export declare const VERSIONING_DOCID: DocumentID; -export declare const MILESTONE_DOCID: DocumentID; -export declare const NODEINFO_DOCID: DocumentID; -export declare const SYNCINFO_ID: DocumentID; -export declare const EntryTypes: { - readonly NOTE_LEGACY: "notes"; - readonly NOTE_BINARY: "newnote"; - readonly NOTE_PLAIN: "plain"; - readonly INTERNAL_FILE: "internalfile"; - readonly CHUNK: "leaf"; - readonly CHUNK_PACK: "chunkpack"; - readonly VERSION_INFO: "versioninfo"; - readonly SYNC_INFO: "syncinfo"; - readonly SYNC_PARAMETERS: "sync-parameters"; - readonly MILESTONE_INFO: "milestoneinfo"; - readonly NODE_INFO: "nodeinfo"; -}; -export declare const NoteTypes: ("notes" | "newnote" | "plain")[]; -export declare const ChunkTypes: ("leaf" | "chunkpack")[]; diff --git a/_types/src/lib/src/common/models/db.definition.d.ts b/_types/src/lib/src/common/models/db.definition.d.ts deleted file mode 100644 index b916d2e8..00000000 --- a/_types/src/lib/src/common/models/db.definition.d.ts +++ /dev/null @@ -1,58 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { MILESTONE_DOCID, NODEINFO_DOCID } from "./db.const"; -import type { AnyEntry, ChunkVersionRange, DatabaseEntry, EntryChunkPack, EntryLeaf, EntryTypes, EntryVersionInfo, InternalFileEntry, LoadedEntry, MetaEntry, NewEntry, NoteEntry, PlainEntry } from "./db.type"; -import type { TweakValues } from "./tweak.definition"; -export type NodeKey = string; -export interface DeviceInfo { - /** - * Name of the device (Initially from deviceAndVaultName setting, configurable). - */ - device_name: string; - /** - * Vault name (From vaultName setting). - */ - vault_name: string; - /** - * Obsidian App version of the device. - */ - app_version: string; - /** - * Plugin version of the device. - */ - plugin_version: string; - progress: string; -} -export interface NodeData extends DeviceInfo { - /** - * Epoch time in milliseconds when the device last connected. - */ - last_connected: number; -} -export interface EntryMilestoneInfo extends DatabaseEntry { - _id: typeof MILESTONE_DOCID; - type: EntryTypes["MILESTONE_INFO"]; - created: number; - accepted_nodes: string[]; - node_info: { - [key: NodeKey]: NodeData; - }; - locked: boolean; - cleaned?: boolean; - node_chunk_info: { - [key: NodeKey]: ChunkVersionRange; - }; - tweak_values: { - [key: NodeKey]: TweakValues; - }; -} -export interface EntryNodeInfo extends DatabaseEntry { - _id: typeof NODEINFO_DOCID; - type: EntryTypes["NODE_INFO"]; - nodeid: string; - v20220607?: boolean; -} -export type EntryBody = NoteEntry | NewEntry | PlainEntry | InternalFileEntry; -export type EntryDoc = EntryBody | LoadedEntry | EntryLeaf | EntryVersionInfo | EntryMilestoneInfo | EntryNodeInfo | EntryChunkPack; -export type EntryDocResponse = EntryDoc & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta; -export declare function isMetaEntry(entry: AnyEntry): entry is MetaEntry; diff --git a/_types/src/lib/src/common/models/db.type.d.ts b/_types/src/lib/src/common/models/db.type.d.ts deleted file mode 100644 index 801d979f..00000000 --- a/_types/src/lib/src/common/models/db.type.d.ts +++ /dev/null @@ -1,144 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { TaggedType } from "octagonal-wheels/common/types"; -import type { EntryTypes, SYNCINFO_ID } from "./db.const"; -export type FilePath = TaggedType; -export type FilePathWithPrefixLC = TaggedType; -export type FilePathWithPrefix = TaggedType | FilePath | FilePathWithPrefixLC; -export type DocumentID = TaggedType; -export type EntryType = (typeof EntryTypes)[keyof typeof EntryTypes]; -export type EntryTypes = typeof EntryTypes; -export type EntryTypeNotes = EntryTypes["NOTE_BINARY"] | EntryTypes["NOTE_PLAIN"]; -export type EntryTypeNotesWithLegacy = EntryTypeNotes | EntryTypes["NOTE_LEGACY"]; -/** - * Represents an entry in the database. - */ -export interface DatabaseEntry { - /** - * The ID of the document. - */ - _id: DocumentID; - /** - * The revision of the document. - */ - _rev?: string; - /** - * Deleted flag. - */ - _deleted?: boolean; - /** - * Conflicts (if exists). - */ - _conflicts?: string[]; -} -/** - * Represents the base structure for an entry that represents a file. - */ -export type EntryBase = { - /** - * The creation time of the file. - */ - ctime: number; - /** - * The modification time of the file. - */ - mtime: number; - /** - * The size of the file. - */ - size: number; - /** - * Deleted flag. - */ - deleted?: boolean; -}; -export type EdenChunk = { - data: string; - epoch: number; -}; -export type EntryWithEden = { - eden: Record; -}; -export type NoteEntry = DatabaseEntry & EntryBase & EntryWithEden & { - /** - * The path of the file. - */ - path: FilePathWithPrefix; - /** - * Contents of the file. - */ - data: string | string[]; - /** - * The type of the entry. - */ - type: EntryTypes["NOTE_LEGACY"]; -}; -export type NewEntry = DatabaseEntry & EntryBase & EntryWithEden & { - /** - * The path of the file. - */ - path: FilePathWithPrefix; - /** - * Chunk IDs indicating the contents of the file. - */ - children: string[]; - /** - * The type of the entry. - */ - type: EntryTypes["NOTE_BINARY"]; -}; -export type PlainEntry = DatabaseEntry & EntryBase & EntryWithEden & { - /** - * The path of the file. - */ - path: FilePathWithPrefix; - /** - * Chunk IDs indicating the contents of the file. - */ - children: string[]; - /** - * The type of the entry. - */ - type: EntryTypes["NOTE_PLAIN"]; -}; -export type InternalFileEntry = DatabaseEntry & NewEntry & EntryBase & { - deleted?: boolean; -}; -export type AnyEntry = NoteEntry | NewEntry | PlainEntry | InternalFileEntry; -export type LoadedEntry = AnyEntry & { - data: string | string[]; - datatype: EntryTypeNotes; -}; -export type SavingEntry = AnyEntry & { - data: Blob; - datatype: EntryTypeNotes; -}; -export type MetaEntry = AnyEntry & { - children: string[]; -}; -export type EntryLeaf = DatabaseEntry & { - type: EntryTypes["CHUNK"]; - data: string; - isCorrupted?: boolean; -}; -export type EntryChunkPack = DatabaseEntry & { - type: EntryTypes["CHUNK_PACK"]; - data: string; -}; -export interface EntryVersionInfo extends DatabaseEntry { - type: EntryTypes["VERSION_INFO"]; - version: number; -} -export interface EntryHasPath { - path: FilePathWithPrefix | FilePath; -} -export interface ChunkVersionRange { - min: number; - max: number; - current: number; -} -export interface SyncInfo extends DatabaseEntry { - _id: typeof SYNCINFO_ID; - type: EntryTypes["SYNC_INFO"]; - data: string; -} diff --git a/_types/src/lib/src/common/models/diff.definition.d.ts b/_types/src/lib/src/common/models/diff.definition.d.ts deleted file mode 100644 index 8f5e797c..00000000 --- a/_types/src/lib/src/common/models/diff.definition.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { AUTO_MERGED, CANCELLED, MISSING_OR_ERROR, NOT_CONFLICTED } from "./shared.const.symbols"; -export type diff_result_leaf = { - rev: string; - data: string; - ctime: number; - mtime: number; - deleted?: boolean; -}; -export type dmp_result = Array<[number, string]>; -export type diff_result = { - left: diff_result_leaf; - right: diff_result_leaf; - diff: dmp_result; -}; -export type DIFF_CHECK_RESULT_AUTO = typeof CANCELLED | typeof AUTO_MERGED | typeof NOT_CONFLICTED | typeof MISSING_OR_ERROR; -export type diff_check_result = DIFF_CHECK_RESULT_AUTO | diff_result; diff --git a/_types/src/lib/src/common/models/fileaccess.const.d.ts b/_types/src/lib/src/common/models/fileaccess.const.d.ts deleted file mode 100644 index ea346e21..00000000 --- a/_types/src/lib/src/common/models/fileaccess.const.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const CHeader = "h:"; -export declare const PSCHeader = "ps:"; -export declare const PSCHeaderEnd = "ps;"; -export declare const ICHeader = "i:"; -export declare const ICHeaderEnd = "i;"; -export declare const ICHeaderLength: number; -export declare const ICXHeader = "ix:"; diff --git a/_types/src/lib/src/common/models/fileaccess.type.d.ts b/_types/src/lib/src/common/models/fileaccess.type.d.ts deleted file mode 100644 index 29c97d96..00000000 --- a/_types/src/lib/src/common/models/fileaccess.type.d.ts +++ /dev/null @@ -1,74 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, FilePathWithPrefix } from "./db.type"; -export type UXStat = { - size: number; - mtime: number; - ctime: number; - type: "file" | "folder"; -}; -export type UXFileInfoStub = { - name: string; - path: FilePath | FilePathWithPrefix; - stat: UXStat; - deleted?: boolean; - isFolder?: false; - isInternal?: boolean; -}; -export type UXFileInfo = UXFileInfoStub & { - body: Blob; -}; -export type UXAbstractInfoStub = UXFileInfoStub | UXFolderInfo; -export type UXInternalFileInfoStub = { - name: string; - path: FilePath | FilePathWithPrefix; - deleted?: boolean; - isFolder?: false; - isInternal: true; - stat: undefined; -}; -export type UXFolderInfo = { - name: string; - path: FilePath | FilePathWithPrefix; - deleted?: boolean; - isFolder: true; - children: UXFileInfoStub[]; - parent: FilePath | FilePathWithPrefix | undefined; -}; -export type UXDataWriteOptions = { - /** - * Time of creation, represented as a unix timestamp, in milliseconds. - * Omit this if you want to keep the default behaviour. - * @public - * */ - ctime?: number; - /** - * Time of last modification, represented as a unix timestamp, in milliseconds. - * Omit this if you want to keep the default behaviour. - * @public - */ - mtime?: number; -}; -export type CacheData = string | ArrayBuffer; -export type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL"; -export type FileEventArgs = { - file: UXFileInfoStub | UXInternalFileInfoStub; - cache?: CacheData; - oldPath?: string; - ctx?: unknown; -}; -export type FileEventItem = { - type: FileEventType; - args: FileEventArgs; - key: string; - skipBatchWait?: boolean; - cancelled?: boolean; - batched?: boolean; -}; -export interface FileWithFileStat extends Omit { - path: FilePath; -} -export interface FileWithStatAsProp { - path: FilePath; - stat: Omit; -} diff --git a/_types/src/lib/src/common/models/redflag.const.d.ts b/_types/src/lib/src/common/models/redflag.const.d.ts deleted file mode 100644 index 3453f971..00000000 --- a/_types/src/lib/src/common/models/redflag.const.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath } from "./db.type"; -export declare const PREFIXMD_LOGFILE = "livesync_log_"; -export declare const PREFIXMD_LOGFILE_UC = "LIVESYNC_LOG_"; -export declare const FlagFilesOriginal: { - readonly SUSPEND_ALL: FilePath; - readonly REBUILD_ALL: FilePath; - readonly FETCH_ALL: FilePath; -}; -export declare const FlagFilesHumanReadable: { - readonly REBUILD_ALL: FilePath; - readonly FETCH_ALL: FilePath; -}; -/** - * @deprecated Use `FlagFilesOriginal.SUSPEND_ALL` instead. - */ -export declare const FLAGMD_REDFLAG: FilePath; -/** - * @deprecated Use `FlagFilesHumanReadable.REBUILD_ALL` instead. - */ -export declare const FLAGMD_REDFLAG2: FilePath; -/** - * @deprecated Use `FlagFilesHumanReadable.FETCH_ALL` instead. - */ -export declare const FLAGMD_REDFLAG2_HR: FilePath; -/** - * @deprecated Use `FlagFilesOriginal.FETCH_ALL` instead. - */ -export declare const FLAGMD_REDFLAG3: FilePath; -/** - * @deprecated Use `FlagFilesHumanReadable.FETCH_ALL` instead. - */ -export declare const FLAGMD_REDFLAG3_HR: FilePath; diff --git a/_types/src/lib/src/common/models/setting.const.d.ts b/_types/src/lib/src/common/models/setting.const.d.ts deleted file mode 100644 index b21cd139..00000000 --- a/_types/src/lib/src/common/models/setting.const.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const SETTING_VERSION_INITIAL = 0; -export declare const SETTING_VERSION_SUPPORT_CASE_INSENSITIVE = 10; -export declare const CURRENT_SETTING_VERSION = 10; -export declare const RemoteTypes: { - readonly REMOTE_COUCHDB: ""; - readonly REMOTE_MINIO: "MINIO"; - readonly REMOTE_P2P: "ONLY_P2P"; -}; -export declare const REMOTE_COUCHDB: ""; -export declare const REMOTE_MINIO: "MINIO"; -export declare const REMOTE_P2P: "ONLY_P2P"; -export declare const E2EEAlgorithmNames: { - readonly "": "V1: Legacy"; - readonly v2: "V2: AES-256-GCM With HKDF"; - readonly forceV1: "Force-V1: Force Legacy (Not recommended)"; -}; -export declare const E2EEAlgorithms: { - readonly V1: ""; - readonly V2: "v2"; - readonly ForceV1: "forceV1"; -}; -export declare const HashAlgorithms: { - readonly XXHASH32: "xxhash32"; - readonly XXHASH64: "xxhash64"; - readonly MIXED_PUREJS: "mixed-purejs"; - readonly SHA1: "sha1"; - readonly LEGACY: ""; -}; -export declare const ChunkAlgorithmNames: { - readonly v1: "V1: Legacy"; - readonly v2: "V2: Simple (Default)"; - readonly "v2-segmenter": "V2.5: Lexical chunks"; - readonly "v3-rabin-karp": "V3: Fine deduplication"; -}; -export declare const ChunkAlgorithms: { - readonly V1: "v1"; - readonly V2: "v2"; - readonly V2Segmenter: "v2-segmenter"; - readonly RabinKarp: "v3-rabin-karp"; -}; -export declare const MODE_SELECTIVE = 0; -export declare const MODE_AUTOMATIC = 1; -export declare const MODE_PAUSED = 2; -export declare const MODE_SHINY = 3; -export declare const NetworkWarningStyles: { - readonly BANNER: ""; - readonly ICON: "icon"; - readonly HIDDEN: "hidden"; -}; diff --git a/_types/src/lib/src/common/models/setting.const.defaults.d.ts b/_types/src/lib/src/common/models/setting.const.defaults.d.ts deleted file mode 100644 index d51b78de..00000000 --- a/_types/src/lib/src/common/models/setting.const.defaults.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ObsidianLiveSyncSettings, type P2PSyncSetting } from "./setting.type"; -export declare const P2P_DEFAULT_SETTINGS: P2PSyncSetting; -export declare const DEFAULT_SETTINGS: ObsidianLiveSyncSettings; diff --git a/_types/src/lib/src/common/models/setting.const.preferred.d.ts b/_types/src/lib/src/common/models/setting.const.preferred.d.ts deleted file mode 100644 index bfeb921e..00000000 --- a/_types/src/lib/src/common/models/setting.const.preferred.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettings } from "./setting.type"; -export declare const PREFERRED_BASE: Partial; -export declare const PREFERRED_SETTING_CLOUDANT: Partial; -export declare const PREFERRED_SETTING_SELF_HOSTED: Partial; -export declare const PREFERRED_JOURNAL_SYNC: Partial; diff --git a/_types/src/lib/src/common/models/setting.const.qr.d.ts b/_types/src/lib/src/common/models/setting.const.qr.d.ts deleted file mode 100644 index 1cccc1e3..00000000 --- a/_types/src/lib/src/common/models/setting.const.qr.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettings } from "./setting.type"; -export declare const KeyIndexOfSettings: Record; diff --git a/_types/src/lib/src/common/models/setting.type.d.ts b/_types/src/lib/src/common/models/setting.type.d.ts deleted file mode 100644 index 59c5a925..00000000 --- a/_types/src/lib/src/common/models/setting.type.d.ts +++ /dev/null @@ -1,902 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ChunkAlgorithms, E2EEAlgorithms, HashAlgorithms, MODE_AUTOMATIC, MODE_PAUSED, MODE_SELECTIVE, MODE_SHINY, RemoteTypes } from "./setting.const"; -import type { I18N_LANGS } from "@lib/common/rosetta"; -import type { CustomRegExpSourceList } from "./shared.type.util"; -import type { JWTAlgorithm } from "./auth.type"; -/** - * Represents the connection details required to connect to a CouchDB instance. - */ -export interface CouchDBConnection { - /** - * The URI of the CouchDB instance. - */ - couchDB_URI: string; - /** - * The username to use when connecting to the CouchDB instance. - */ - couchDB_USER: string; - /** - * The password to use when connecting to the CouchDB instance. - */ - couchDB_PASSWORD: string; - /** - * The name of the database to use. - */ - couchDB_DBNAME: string; - /** - * e.g. `x-some-header: some-value\n x-some-header2: some-value2` - */ - couchDB_CustomHeaders: string; - useJWT: boolean; - jwtAlgorithm: JWTAlgorithm; - jwtKey: string; - jwtKid: string; - jwtSub: string; - jwtExpDuration: number; - /** - * Use Request API to avoid `inevitable` CORS problem. - * Seems stable, so promoted to the normal setting. - */ - useRequestAPI: boolean; -} -/** - * Interface representing the settings for periodic replication. - */ -interface PeriodicReplicationSettings { - /** - * Indicates whether periodic replication is enabled. - */ - periodicReplication: boolean; - /** - * The interval, in milliseconds, at which periodic replication occurs. - */ - periodicReplicationInterval: number; -} -export type ConfigPassphraseStore = "" | "LOCALSTORAGE" | "ASK_AT_LAUNCH"; -/** - * Represents the user settings that are encrypted. - */ -interface EncryptedUserSettings { - /** - * The store for the configuration passphrase. - */ - configPassphraseStore: ConfigPassphraseStore; - /** - * The encrypted passphrase used for E2EE. - */ - encryptedPassphrase: string; - /** - * The encrypted connection details for CouchDB. - */ - encryptedCouchDBConnection: string; -} -/** - * Interface representing the settings for different sync invocation methods. - */ -interface SyncMethodSettings { - /** - * Synchronise in Live. This is an exclusive setting against other sync methods. - */ - liveSync: boolean; - /** - * automatically run sync on save. - * File modification will trigger the sync, even if the file is not changed on the editor. - */ - syncOnSave: boolean; - /** - * automatically run sync on starting the plug-in. - */ - syncOnStart: boolean; - /** - * automatically run sync on opening a file. - */ - syncOnFileOpen: boolean; - /** - * automatically run sync on editor save. - * Different from syncOnSave, this is only reacts to the editor save event. - */ - syncOnEditorSave: boolean; - /** - * Desktop only, opt-in. Keep replication running while the window is hidden or minimised, - * instead of suspending it until the window becomes visible again. The trigger is - * document.hidden, not window focus. Applies to the background-capable sync modes (LiveSync - * and Periodic). Ignored on mobile. Default false. - */ - keepReplicationActiveInBackground: boolean; - /** - * The minimum delay between synchronisation operations (in milliseconds). - * If the operation is triggered before this delay, the operation will be delayed until the delay is over, and executed as a single operation. - */ - syncMinimumInterval: number; -} -/** - * Interface representing the settings for file handling. - */ -interface FileHandlingSettings { - /** - * Use trash instead of actually delete. - */ - trashInsteadDelete: boolean; - /** - * Do not delete the folder even if it has got empty. - */ - doNotDeleteFolder: boolean; - /** - * Thinning out the changes and make a single change for the same file. - */ - batchSave: boolean; - batchSaveMinimumDelay: number; - batchSaveMaximumDelay: number; - /** - * Maximum size of the file to be synchronized (in MB). - */ - syncMaxSizeInMB: number; - /** - * Use ignore files. - */ - useIgnoreFiles: boolean; - /** - * Ignore files pattern, i,e, `.gitignore, .obsidianignore` (This should be separated by comma) - */ - ignoreFiles: string; - /** - * Do not prevent write if the size is mismatched. - */ - processSizeMismatchedFiles: boolean; -} -/** - * Interface representing the settings for Hidden File Sync. - */ -interface InternalFileSettings { - /** - * Synchronise internal files. - */ - syncInternalFiles: boolean; - /** - * Scan internal files before replication. - */ - syncInternalFilesBeforeReplication: boolean; - /** - * Interval for scanning internal files (in seconds). - */ - syncInternalFilesInterval: number; - /** - * Ignore patterns for internal files. - * (Comma separated list of regular expressions) - */ - syncInternalFilesIgnorePatterns: CustomRegExpSourceList<",">; - /** - * Limit patterns for internal files. - */ - syncInternalFilesTargetPatterns: CustomRegExpSourceList<",">; - /** - * Enable watch internal file changes (This option uses the unexposed API) - */ - watchInternalFileChanges: boolean; - /** - * Suppress notification of hidden files change. - */ - suppressNotifyHiddenFilesChange: boolean; - /** - * Overwrite instead of merging patterns for internal files. - */ - syncInternalFileOverwritePatterns: CustomRegExpSourceList<",">; -} -export type SYNC_MODE = typeof MODE_SELECTIVE | typeof MODE_AUTOMATIC | typeof MODE_PAUSED | typeof MODE_SHINY; -export interface PluginSyncSettingEntry { - key: string; - mode: SYNC_MODE; - files: string[]; -} -/** - * Interface representing the settings for plugin synchronisation. - */ -interface PluginSyncSettings { - /** - * Indicates whether plugin synchronisation is enabled. - */ - usePluginSync: boolean; - /** - * Indicates whether plugin settings synchronisation is enabled. - */ - usePluginSettings: boolean; - /** - * Indicates whether to show the device's own plugins. - */ - showOwnPlugins: boolean; - /** - * Indicates whether to automatically scan plugins. - */ - autoSweepPlugins: boolean; - /** - * Indicates whether to periodically scan plugins automatically. - */ - autoSweepPluginsPeriodic: boolean; - /** - * Indicates whether to notify when a plugin or setting is updated. - */ - notifyPluginOrSettingUpdated: boolean; - /** - * The name of the device and vault. - * This is used to identify the device and vault among synchronised devices and vaults. - * Hence, this should be unique among devices and vaults. - */ - deviceAndVaultName: string; - /** - * Indicates whether the v2 of plugin synchronisation is enabled. - */ - usePluginSyncV2: boolean; - /** - * Indicates whether additional plugin synchronisation settings are enabled. - * This setting is hidden from the UI. - */ - usePluginEtc: boolean; - /** - * Extended settings for plugin synchronisation. - */ - pluginSyncExtendedSetting: Record; -} -/** - * Interface representing the user interface settings. - */ -interface UISettings { - /** - * Indicates whether verbose logging has been enabled. - */ - showVerboseLog: boolean; - /** - * Indicates whether less information should be shown in the log. - */ - lessInformationInLog: boolean; - /** - * Indicates whether longer status line should be shown inside the editor. - */ - showLongerLogInsideEditor: boolean; - /** - * Indicates whether the status line should be shown on the editor. - */ - showStatusOnEditor: boolean; - /** - * Indicates whether the status line should be shown on the status bar. - */ - showStatusOnStatusbar: boolean; - /** - * Indicates whether only icons instead of status line should be shown on the editor. - */ - showOnlyIconsOnEditor: boolean; - /** - * Hide File warning notice bar. - */ - hideFileWarningNotice: boolean; - /** - * How to display connection error warnings. - * "banner" shows the full banner, "icon" shows only an icon, "hidden" suppresses entirely. - */ - networkWarningStyle: "" | "icon" | "hidden"; - /** - * The language to be used for display. - */ - displayLanguage: I18N_LANGS; -} -/** - * Interface representing the settings for mode of exposing advanced things. - */ -interface ModeSettings { - /** - * Indicates whether the advanced mode is enabled. - */ - useAdvancedMode: boolean; - /** - * Indicates whether the power user mode is enabled. - */ - usePowerUserMode: boolean; - /** - * Indicates whether the edge case mode is enabled. - */ - useEdgeCaseMode: boolean; -} -/** - * Interface representing the settings for debug mode. - */ -interface DebugModeSettings { - /** - * Indicates whether the debug tools of Self-hosted LiveSync are enabled. - */ - enableDebugTools: boolean; - /** - * Indicates whether to write log to the file. - */ - writeLogToTheFile: boolean; -} -/** - * Interface representing additional tweak settings. - */ -interface ExtraTweakSettings { - /** - * The threshold value for notifying about the size of remote storage. - * When the size of the remote storage exceeds this threshold, a notification will be triggered. - */ - notifyThresholdOfRemoteStorageSize: number; -} -/** - * Interface representing the settings for beta tweaks. - */ -interface BetaTweakSettings { - /** - * Indicates whether to disable the WebWorker for generating chunks. - */ - disableWorkerForGeneratingChunks: boolean; - /** - * Indicates whether to process small files in the UI thread. - */ - processSmallFilesInUIThread: boolean; -} -/** - * Interface representing the settings for synchronising settings via file. - */ -interface SettingSyncSettings { - /** - * The file path where the settings is stored. - */ - settingSyncFile: string; - /** - * Indicates whether to write credentials for settings synchronising. - */ - writeCredentialsForSettingSync: boolean; - /** - * Indicates whether to notify all settings synchronising files events. - */ - notifyAllSettingSyncFile: boolean; -} -/** - * Represents settings that are considered obsolete and are not configurable from the UI. - */ -interface ObsoleteSettings { - /** - * Saving delay (in milliseconds). - */ - savingDelay: number; - /** - * Garbage collection delay (in milliseconds). Now, no longer GC is implemented. - */ - gcDelay: number; - /** - * Skip older files on sync. No effect now. - */ - skipOlderFilesOnSync: boolean; - /** - * Use the IndexedDB adapter. Now always true. Should be. - */ - useIndexedDBAdapter: boolean; -} -/** - * Interface representing some data stored in the settings for the plugin. - */ -interface DataOnSettings { - /** - * VersionUp flash message which is shown when some incompatible changes are made during the update. - */ - versionUpFlash: string; - /** - * Setting file version, to migrate the settings. - */ - settingVersion: number; - /** - * Indicates whether the setting of the plug-in is configured once. - */ - isConfigured?: boolean; - /** - * The user-last-read version number. - */ - lastReadUpdates: number; - /** - * The last checked version by the doctor. - */ - doctorProcessedVersion: string; -} -/** - * Interface representing the settings for a safety valve mechanism. - */ -interface SafetyValveSettings { - /** - * Indicates whether file watching should be suspended. - */ - suspendFileWatching: boolean; - /** - * Indicates whether parsing and reflecting of replication results should be suspended. - */ - suspendParseReplicationResult: boolean; - /** - * Indicates whether suspension should be avoided during fetching operations. - */ - doNotSuspendOnFetching: boolean; - /** - * Maximum file modification time applied to reflected file events - */ - maxMTimeForReflectEvents: number; -} -/** - * Represents the settings required to synchronise with a bucket. - */ -export interface BucketSyncSetting { - /** - * The access key to use when connecting to the bucket. - */ - accessKey: string; - /** - * The secret to use when connecting to the bucket. - */ - secretKey: string; - /** - * The name of bucket to use. - */ - bucket: string; - /** - * The region of the bucket. - */ - region: string; - /** - * The endpoint of the bucket. - */ - endpoint: string; - /** - * Indicates whether to use a custom request handler. - * (This is for CORS issue). - */ - useCustomRequestHandler: boolean; - bucketCustomHeaders: string; - /** - * The prefix to use for the bucket (e.g., "my-bucket/", means mostly like a folder). - */ - bucketPrefix: string; - /** - * Indicates whether to force path style access. - */ - forcePathStyle: boolean; -} -export interface LocalDBSettings { - /** - * Indicates whether to use the IndexedDB adapter for the local database. - * @deprecated - */ - useIndexedDBAdapter: boolean; -} -export type RemoteType = (typeof RemoteTypes)[keyof typeof RemoteTypes]; -export declare enum AutoAccepting { - NONE = 0, - ALL = 1 -} -export interface P2PConnectionInfo { - /** - * Indicates whether P2P connection is enabled. - */ - P2P_Enabled: boolean; - /** - * Nostr relay server URL. (Comma separated list) - * This is only for the channelling server to establish for the P2P connection. - * No data is transferred through this server. - */ - P2P_relays: string; - /** - * The room ID for `your devices`. This should be unique among the users. - * (Or, lines will be got mixed up). - */ - P2P_roomID: string; - /** - * The passphrase for your devices. - * It can be empty, but it will help you if you have a duplicate Room ID. - */ - P2P_passphrase: string; - /** - * The Application ID for the P2P connection. - * This is used to identify the application using the P2P network. - * In Self-hosted LiveSync, fixed to "self-hosted-livesync". - */ - P2P_AppID: string; - /** - * Indicates whether to auto-start the P2P connection on launch. - */ - P2P_AutoStart: boolean; - /** - * Indicates whether to automatically broadcast changes to connected peers. - */ - P2P_AutoBroadcast: boolean; - /** - * The name of the device peer (This only for editing-setting purpose, not saved in the actual setting file, due to avoid setting-sync issues). - */ - P2P_DevicePeerName?: string; - /** - * The TURN server URLs for the P2P connection. (Comma separated list) - */ - P2P_turnServers: string; - /** - * The TURN username for the P2P connection. - */ - P2P_turnUsername: string; - /** - * The TURN credential (password, secret, etc...) for the P2P connection. - */ - P2P_turnCredential: string; - /** - * Use Diagnostic Wrapper for RTCPeerConnection to collect statistics. - */ - P2P_useDiagRTC?: boolean; -} -export interface P2PSyncSetting extends P2PConnectionInfo { - P2P_AutoAccepting: AutoAccepting; - P2P_AutoSyncPeers: string; - P2P_AutoWatchPeers: string; - P2P_SyncOnReplication: string; - P2P_RebuildFrom: string; - P2P_AutoAcceptingPeers: string; - P2P_AutoDenyingPeers: string; - P2P_IsHeadless?: boolean; -} -/** - * Interface representing the settings for a remote type. - */ -export interface RemoteTypeSettings { - /** - * The type of the remote. - */ - remoteType: RemoteType; -} -export type E2EEAlgorithm = (typeof E2EEAlgorithms)[keyof typeof E2EEAlgorithms] | ""; -/** - * Represents the settings used for End-to-End encryption. - */ -export interface EncryptionSettings { - /** - * Indicates whether E2EE is enabled. - */ - encrypt: boolean; - /** - * The passphrase used for E2EE. - */ - passphrase: string; - /** - * Indicates whether path obfuscation is used. - * If not, the path will be stored as it is, as the document ID. - */ - usePathObfuscation: boolean; - /** - * The algorithm used for hashing the passphrase. - * This is used for E2EE. - */ - E2EEAlgorithm: E2EEAlgorithm; -} -export type HashAlgorithm = (typeof HashAlgorithms)[keyof typeof HashAlgorithms]; -export type ChunkSplitterVersion = (typeof ChunkAlgorithms)[keyof typeof ChunkAlgorithms] | ""; -/** - * Interface representing the settings for chunk processing. - */ -interface ChunkSettings { - /** - * The algorithm used for hashing chunks. - */ - hashAlg: HashAlgorithm; - /** - * The minimum size of a chunk in chars. - */ - minimumChunkSize: number; - /** - * The custom size of a chunk. - * Note: This value used as a coefficient for the normal chunk size. - */ - customChunkSize: number; - /** - * The threshold for considering a line as long. - * (Not respected in v0.24.x). - */ - longLineThreshold: number; - /** - * Flag indicating whether to use a segmenter for chunking. - * @deprecated use chunkSplitterVersion instead. - */ - useSegmenter: boolean; - /** - * Flag indicating whether to enable version 2 of the chunk splitter. - * @deprecated use chunkSplitterVersion instead. - */ - enableChunkSplitterV2: boolean; - /** - * Flag indicating whether to avoid using a fixed revision for chunks. - */ - doNotUseFixedRevisionForChunks: boolean; - /** - * The version of the chunk splitter to use. - */ - chunkSplitterVersion: ChunkSplitterVersion; -} -/** - * Settings for on-demand chunk fetching. - */ -interface OnDemandChunkSettings { - /** - * Indicates whether chunks should be fetch online. (means replication transfers only metadata). - */ - readChunksOnline: boolean; - /** - * Indicates whether to use only local chunks without fetching online. - */ - useOnlyLocalChunk: boolean; - /** - * The number of concurrent chunk reads allowed when fetching online. - */ - concurrencyOfReadChunksOnline: number; - /** - * The minimum interval (in milliseconds) between consecutive online chunk fetching. - */ - minimumIntervalOfReadChunksOnline: number; -} -/** - * Configuration settings for Eden. - */ -interface EdenSettings { - /** - * Indicates whether Eden is enabled. - */ - useEden: boolean; - /** - * The maximum number of chunks allowed in Eden. - */ - maxChunksInEden: number; - /** - * The maximum total length allowed in Eden. - */ - maxTotalLengthInEden: number; - /** - * The maximum age allowed in Eden. - */ - maxAgeInEden: number; -} -/** - * Interface representing obsolete settings for an remote database. - */ -interface ObsoleteRemoteDBSettings { - /** - * Indicates whether to check the integrity of the data on save. - */ - checkIntegrityOnSave: boolean; - /** - * Indicates whether to use history tracking. - * (Now always true) - */ - useHistory: boolean; - /** - * Indicates whether to disable using API of Obsidian. - * (Now always true: Note: Obsidian cannot handle multiple requests at the same time). - */ - disableRequestURI: boolean; - /** - * Indicates whether to send data in bulk chunks. - */ - sendChunksBulk: boolean; - /** - * The maximum size of the bulk chunks to be sent. - */ - sendChunksBulkMaxSize: number; - /** - * Indicates whether to use a dynamic iteration count. - */ - useDynamicIterationCount: boolean; - /** - * Indicates weather to pace the replication processing interval. - * Now (v0.24.x) not be respected. - */ - doNotPaceReplication: boolean; -} -/** - * Interface representing the settings for beta tweaks for the remote database. - */ -interface BetaRemoteDBSettings { - /** - * Indicates whether compression is enabled for the remote database. - */ - enableCompression: boolean; -} -/** - * Interface representing the some data stored on the settings. - */ -interface DataOnRemoteDBSettings { - /** - * VersionUp flash message which is shown when some incompatible changes are made during the update. - */ - versionUpFlash: string; - /** - * Unix timestamp (ms) of the latest tweak update. - * Used to determine which side has newer tweak values. - */ - tweakModified: number | undefined; -} -/** - * Interface representing the settings for replication. - */ -interface ReplicationSetting { - /** - * The maximum number of documents to be processed in a batch. - */ - batch_size: number; - /** - * The maximum number of batches to be processed. - */ - batches_limit: number; -} -/** - * Interface representing the settings for targetting files. - */ -interface FileHandlingSettings { - /** - * The regular expression for files to be synchronised. - */ - syncOnlyRegEx: CustomRegExpSourceList<"|[]|">; - /** - * The regular expression for files to be ignored during synchronisation. - */ - syncIgnoreRegEx: CustomRegExpSourceList<"|[]|">; -} -/** - * Interface representing the settings for processing behaviour. - */ -interface ProcessingBehaviourSettings { - /** - * Hash cache maximum count. - */ - hashCacheMaxCount: number; - /** - * Hash cache maximum amount. - */ - hashCacheMaxAmount: number; -} -/** - * Interface representing the settings for remote database tweaks. - */ -interface RemoteDBTweakSettings { - /** - * Indicates whether to ignore the version check. - */ - ignoreVersionCheck: boolean; - /** - * Indicates whether to ignore and continue syncing even if the configuration-mismatch is detected. - * (Note: Mismatched settings can lead to inappropriate de-duplication, leading to storage wastage and increased traffic). - */ - disableCheckingConfigMismatch: boolean; - /** - * Automatically accepts compatible-but-lossy tweak mismatches. - * If undefined, the feature is not configured yet. - */ - autoAcceptCompatibleTweak: boolean | undefined; -} -/** - * Interface representing the settings for optional and not exposed remote database settings. - */ -interface OptionalAndNotExposedRemoteDBSettings { - /** - * Indicates whether to accept empty passphrase. - * This not meant to `Not be encrypted`, but `Be encrypted with empty passphrase`. - */ - permitEmptyPassphrase: boolean; -} -/** - * Interface representing the settings for cross-platform interoperability. - */ -interface CrossPlatformInteroperabilitySettings { - /** - * Indicates whether to handle filename case sensitively. - */ - handleFilenameCaseSensitive: boolean; -} -/** - * Interface representing the settings for conflict handling. - */ -interface ConflictHandlingSettings { - /** - * Indicates whether to check conflicts only on file open. - */ - checkConflictOnlyOnOpen: boolean; - /** - * Indicates whether to show the merge dialog only on active file. - */ - showMergeDialogOnlyOnActive: boolean; -} -/** - * Settings that define the behavior of the merge process. - */ -interface MergeBehaviourSettings { - /** - * Indicates whether to synchronise after merging. - */ - syncAfterMerge: boolean; - /** - * Determines if conflicts should be resolved by choosing the newer file. - */ - resolveConflictsByNewerFile: boolean; - /** - * Specifies whether to write documents even if there are conflicts. - */ - writeDocumentsIfConflicted: boolean; - /** - * Disables automatic merging of markdown files. - */ - disableMarkdownAutoMerge: boolean; -} -/** - * Configuration settings for handling edge cases in the application. - */ -interface EdgeCaseHandlingSettings { - /** - * An optional suffix to append to the database name after the vault name. - */ - additionalSuffixOfDatabaseName: string | undefined; - /** - * Flag to disable the worker thread for generating chunks. - */ - disableWorkerForGeneratingChunks: boolean; - /** - * Flag to process small files in the UI thread instead of a worker thread. - */ - processSmallFilesInUIThread: boolean; - /** - * Indicates whether to use timeout for PouchDB replication. - */ - useTimeouts: boolean; -} -/** - * Configuration settings for handling deleted files. - */ -interface DeletedFileMetadataSettings { - /** - * Indicates whether to delete metadata of deleted files. - */ - deleteMetadataOfDeletedFiles: boolean; - /** - * The number of days to wait before automatically deleting metadata of deleted files. - */ - automaticallyDeleteMetadataOfDeletedFiles: number; -} -/** - * Represents a single remote configuration. - */ -export interface RemoteConfiguration { - /** - * Unique identifier for this configuration. - */ - id: string; - /** - * Display name for the configuration. - */ - name: string; - /** - * The connection string (URI) for the remote. - * This may be an encrypted string if configPassphraseStore is set. - */ - uri: string; - /** - * Indicates whether this configuration is encrypted. - */ - isEncrypted: boolean; -} -export interface RemoteConfigurations { - /** - * The list of remote configurations. - */ - remoteConfigurations: Record; - /** - * The ID of the currently active remote configuration. - */ - activeConfigurationId: string; - /** - * The ID of the active remote configuration dedicated for P2P features. - * If empty, P2P features should request explicit selection from the user. - */ - P2P_ActiveRemoteConfigurationId: string; -} -interface ObsidianLiveSyncSettings_PluginSetting extends SyncMethodSettings, UISettings, FileHandlingSettings, MergeBehaviourSettings, EncryptedUserSettings, PeriodicReplicationSettings, InternalFileSettings, PluginSyncSettings, ModeSettings, ExtraTweakSettings, BetaTweakSettings, ObsoleteSettings, DebugModeSettings, SettingSyncSettings, SafetyValveSettings, DataOnSettings, RemoteConfigurations { // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface -} -export type RemoteDBSettings = CouchDBConnection & BucketSyncSetting & RemoteTypeSettings & EncryptionSettings & ChunkSettings & EdenSettings & DataOnRemoteDBSettings & ObsoleteRemoteDBSettings & OnDemandChunkSettings & BetaRemoteDBSettings & ReplicationSetting & RemoteDBTweakSettings & FileHandlingSettings & ProcessingBehaviourSettings & OptionalAndNotExposedRemoteDBSettings & CrossPlatformInteroperabilitySettings & ConflictHandlingSettings & EdgeCaseHandlingSettings & DeletedFileMetadataSettings & P2PSyncSetting & RemoteConfigurations; -export type ObsidianLiveSyncSettings = ObsidianLiveSyncSettings_PluginSetting & RemoteDBSettings & LocalDBSettings; -export interface HasSettings> { - settings: T; -} -export {}; diff --git a/_types/src/lib/src/common/models/shared.const.behabiour.d.ts b/_types/src/lib/src/common/models/shared.const.behabiour.d.ts deleted file mode 100644 index 02c8309a..00000000 --- a/_types/src/lib/src/common/models/shared.const.behabiour.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const MAX_DOC_SIZE = 1000; -export declare const MAX_DOC_SIZE_BIN = 102400; -export declare const VER = 12; -export declare const RECENT_MODIFIED_DOCS_QTY = 30; -export declare const LEAF_WAIT_TIMEOUT = 30000; -export declare const LEAF_WAIT_ONLY_REMOTE = 5000; -export declare const LEAF_WAIT_TIMEOUT_SEQUENTIAL_REPLICATOR = 5000; -export declare const REPLICATION_BUSY_TIMEOUT = 3000000; -export declare const SALT_OF_PASSPHRASE = "rHGMPtr6oWw7VSa3W3wpa8fT8U"; -export declare const SALT_OF_ID = "a83hrf7f\u0003y7sa8g31"; -export declare const SEED_MURMURHASH = 305419896; -export declare const IDPrefixes: { - Obfuscated: string; - Chunk: string; - EncryptedChunk: string; -}; -/** - * @deprecated Use `IDPrefixes.Obfuscated` instead. - */ -export declare const PREFIX_OBFUSCATED = "f:"; -/** - * @deprecated Use `IDPrefixes.Chunk` instead. - */ -export declare const PREFIX_CHUNK = "h:"; -/** - * @deprecated Use `IDPrefixes.EncryptedChunk` instead. - */ -export declare const PREFIX_ENCRYPTED_CHUNK = "h:+"; diff --git a/_types/src/lib/src/common/models/shared.const.d.ts b/_types/src/lib/src/common/models/shared.const.d.ts deleted file mode 100644 index 6ddc7491..00000000 --- a/_types/src/lib/src/common/models/shared.const.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const SETTING_KEY_P2P_DEVICE_NAME = "p2p_device_name"; -export declare const configURIBase = "obsidian://setuplivesync?settings="; -export declare const configURIBaseQR = "obsidian://setuplivesync?settingsQR="; -export declare const SuffixDatabaseName = "-livesync-v2"; -export declare const ExtraSuffixIndexedDB = "-indexeddb"; diff --git a/_types/src/lib/src/common/models/shared.const.symbols.d.ts b/_types/src/lib/src/common/models/shared.const.symbols.d.ts deleted file mode 100644 index d06dcf57..00000000 --- a/_types/src/lib/src/common/models/shared.const.symbols.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const CANCELLED: unique symbol; -export declare const AUTO_MERGED: unique symbol; -export declare const NOT_CONFLICTED: unique symbol; -export declare const MISSING_OR_ERROR: unique symbol; -export declare const LEAVE_TO_SUBSEQUENT: unique symbol; -export declare const TIME_ARGUMENT_INFINITY: unique symbol; -export declare const BASE_IS_NEW: unique symbol; -export declare const TARGET_IS_NEW: unique symbol; -export declare const EVEN: unique symbol; diff --git a/_types/src/lib/src/common/models/shared.definition.configNames.d.ts b/_types/src/lib/src/common/models/shared.definition.configNames.d.ts deleted file mode 100644 index 1d644a98..00000000 --- a/_types/src/lib/src/common/models/shared.definition.configNames.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettings } from "./setting.type"; -export declare const LEVEL_ADVANCED = "ADVANCED"; -export declare const LEVEL_POWER_USER = "POWER_USER"; -export declare const LEVEL_EDGE_CASE = "EDGE_CASE"; -export type ConfigLevel = "" | "ADVANCED" | "POWER_USER" | "EDGE_CASE"; -export type ConfigurationItem = { - name: string; - desc?: string; - placeHolder?: string; - status?: "BETA" | "ALPHA" | "EXPERIMENTAL"; - obsolete?: boolean; - level?: ConfigLevel; - isHidden?: boolean; - isAdvanced?: boolean; -}; -export declare const configurationNames: Partial>; -/** - * Get human readable Configuration stability - * @param status - * @returns - */ -export declare function statusDisplay(status?: string): string; -/** - * Get human readable configuration name. - * @param key configuration key - * @param alt - * @returns - */ -export declare function confName(key: keyof ObsidianLiveSyncSettings, alt?: string): string; -/** - * Get human readable configuration description. - * @param key configuration key - * @param alt - * @returns - */ -export declare function confDesc(key: keyof ObsidianLiveSyncSettings, alt?: string): string | undefined; diff --git a/_types/src/lib/src/common/models/shared.definition.d.ts b/_types/src/lib/src/common/models/shared.definition.d.ts deleted file mode 100644 index 317274f1..00000000 --- a/_types/src/lib/src/common/models/shared.definition.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const DatabaseConnectingStatuses: { - readonly STARTED: "STARTED"; - readonly NOT_CONNECTED: "NOT_CONNECTED"; - readonly PAUSED: "PAUSED"; - readonly CONNECTED: "CONNECTED"; - readonly COMPLETED: "COMPLETED"; - readonly CLOSED: "CLOSED"; - readonly ERRORED: "ERRORED"; - readonly JOURNAL_SEND: "JOURNAL_SEND"; - readonly JOURNAL_RECEIVE: "JOURNAL_RECEIVE"; -}; -export type DatabaseConnectingStatus = (typeof DatabaseConnectingStatuses)[keyof typeof DatabaseConnectingStatuses]; -export type ReplicationStatics = { - sent: number; - arrived: number; - maxPullSeq: number; - maxPushSeq: number; - lastSyncPullSeq: number; - lastSyncPushSeq: number; - syncStatus: DatabaseConnectingStatus; -}; -export declare const DEFAULT_REPLICATION_STATICS: ReplicationStatics; diff --git a/_types/src/lib/src/common/models/shared.type.util.d.ts b/_types/src/lib/src/common/models/shared.type.util.d.ts deleted file mode 100644 index c4a68acc..00000000 --- a/_types/src/lib/src/common/models/shared.type.util.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { TaggedType } from "octagonal-wheels/common/types"; -export type { TaggedType }; -export type CustomRegExpSource = TaggedType; -export type CustomRegExpSourceList = TaggedType; -export type ParsedCustomRegExp = [isInverted: boolean, pattern: string]; -export type Prettify = { - [K in keyof T]: T[K]; -} & {}; // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type diff --git a/_types/src/lib/src/common/models/sync.definition.d.ts b/_types/src/lib/src/common/models/sync.definition.d.ts deleted file mode 100644 index b6c19729..00000000 --- a/_types/src/lib/src/common/models/sync.definition.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { EntryTypes } from "./db.const"; -import type { DatabaseEntry, DocumentID } from "./db.type"; -export declare const ProtocolVersions: { - readonly UNSET: undefined; - readonly LEGACY: 1; - readonly ADVANCED_E2EE: 2; -}; -export type ProtocolVersion = (typeof ProtocolVersions)[keyof typeof ProtocolVersions]; -export declare const DOCID_SYNC_PARAMETERS: DocumentID; -export declare const DOCID_JOURNAL_SYNC_PARAMETERS: DocumentID; -export interface SyncParameters extends DatabaseEntry { - _id: typeof DOCID_SYNC_PARAMETERS; - _rev?: string; - type: (typeof EntryTypes)["SYNC_PARAMETERS"]; - protocolVersion: ProtocolVersion; - pbkdf2salt: string; -} -export declare const DEFAULT_SYNC_PARAMETERS: SyncParameters; diff --git a/_types/src/lib/src/common/models/tweak.definition.d.ts b/_types/src/lib/src/common/models/tweak.definition.d.ts deleted file mode 100644 index 15d03669..00000000 --- a/_types/src/lib/src/common/models/tweak.definition.d.ts +++ /dev/null @@ -1,193 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettings } from "./setting.type"; -export declare const TweakValuesShouldMatchedTemplate: Partial; -type TweakKeys = keyof TweakValues; -export declare const IncompatibleChanges: TweakKeys[]; -export declare const CompatibleButLossyChanges: TweakKeys[]; -type IncompatibleRecommendationPatterns = { - key: T; - isRecommendation?: boolean; -} & ({ - from: TweakValues[T]; - to: TweakValues[T]; -} | { - from: TweakValues[T]; -} | { - to: TweakValues[T]; -}); -export declare const IncompatibleChangesInSpecificPattern: IncompatibleRecommendationPatterns[]; -export declare const TweakValuesRecommendedTemplate: Partial; -export declare const TweakValuesDefault: Partial; -export declare const TweakValuesTemplate: { - tweakModified: number; - liveSync?: boolean | undefined; - syncOnSave?: boolean | undefined; - syncOnStart?: boolean | undefined; - syncOnFileOpen?: boolean | undefined; - syncOnEditorSave?: boolean | undefined; - keepReplicationActiveInBackground?: boolean | undefined; - syncMinimumInterval?: number | undefined; - showVerboseLog?: boolean | undefined; - lessInformationInLog?: boolean | undefined; - showLongerLogInsideEditor?: boolean | undefined; - showStatusOnEditor?: boolean | undefined; - showStatusOnStatusbar?: boolean | undefined; - showOnlyIconsOnEditor?: boolean | undefined; - hideFileWarningNotice?: boolean | undefined; - networkWarningStyle?: "" | "icon" | "hidden" | undefined; - displayLanguage?: import("../rosetta").I18N_LANGS | undefined; - trashInsteadDelete?: boolean | undefined; - doNotDeleteFolder?: boolean | undefined; - batchSave?: boolean | undefined; - batchSaveMinimumDelay?: number | undefined; - batchSaveMaximumDelay?: number | undefined; - syncMaxSizeInMB?: number | undefined; - useIgnoreFiles?: boolean | undefined; - ignoreFiles?: string | undefined; - processSizeMismatchedFiles?: boolean | undefined; - syncOnlyRegEx?: import("./shared.type.util").CustomRegExpSourceList<"|[]|"> | undefined; - syncIgnoreRegEx?: import("./shared.type.util").CustomRegExpSourceList<"|[]|"> | undefined; - syncAfterMerge?: boolean | undefined; - resolveConflictsByNewerFile?: boolean | undefined; - writeDocumentsIfConflicted?: boolean | undefined; - disableMarkdownAutoMerge?: boolean | undefined; - configPassphraseStore?: import("./setting.type").ConfigPassphraseStore | undefined; - encryptedPassphrase?: string | undefined; - encryptedCouchDBConnection?: string | undefined; - periodicReplication?: boolean | undefined; - periodicReplicationInterval?: number | undefined; - syncInternalFiles?: boolean | undefined; - syncInternalFilesBeforeReplication?: boolean | undefined; - syncInternalFilesInterval?: number | undefined; - syncInternalFilesIgnorePatterns?: import("./shared.type.util").CustomRegExpSourceList<","> | undefined; - syncInternalFilesTargetPatterns?: import("./shared.type.util").CustomRegExpSourceList<","> | undefined; - watchInternalFileChanges?: boolean | undefined; - suppressNotifyHiddenFilesChange?: boolean | undefined; - syncInternalFileOverwritePatterns?: import("./shared.type.util").CustomRegExpSourceList<","> | undefined; - usePluginSync?: boolean | undefined; - usePluginSettings?: boolean | undefined; - showOwnPlugins?: boolean | undefined; - autoSweepPlugins?: boolean | undefined; - autoSweepPluginsPeriodic?: boolean | undefined; - notifyPluginOrSettingUpdated?: boolean | undefined; - deviceAndVaultName?: string | undefined; - usePluginSyncV2?: boolean | undefined; - usePluginEtc?: boolean | undefined; - pluginSyncExtendedSetting?: Record | undefined; - useAdvancedMode?: boolean | undefined; - usePowerUserMode?: boolean | undefined; - useEdgeCaseMode?: boolean | undefined; - notifyThresholdOfRemoteStorageSize?: number | undefined; - disableWorkerForGeneratingChunks?: boolean | undefined; - processSmallFilesInUIThread?: boolean | undefined; - savingDelay?: number | undefined; - gcDelay?: number | undefined; - skipOlderFilesOnSync?: boolean | undefined; - useIndexedDBAdapter?: boolean | undefined; - enableDebugTools?: boolean | undefined; - writeLogToTheFile?: boolean | undefined; - settingSyncFile?: string | undefined; - writeCredentialsForSettingSync?: boolean | undefined; - notifyAllSettingSyncFile?: boolean | undefined; - suspendFileWatching?: boolean | undefined; - suspendParseReplicationResult?: boolean | undefined; - doNotSuspendOnFetching?: boolean | undefined; - maxMTimeForReflectEvents?: number | undefined; - versionUpFlash?: string | undefined; - settingVersion?: number | undefined; - isConfigured?: boolean; - lastReadUpdates?: number | undefined; - doctorProcessedVersion?: string | undefined; - remoteConfigurations?: Record | undefined; - activeConfigurationId?: string | undefined; - P2P_ActiveRemoteConfigurationId?: string | undefined; - couchDB_URI?: string | undefined; - couchDB_USER?: string | undefined; - couchDB_PASSWORD?: string | undefined; - couchDB_DBNAME?: string | undefined; - couchDB_CustomHeaders?: string | undefined; - useJWT?: boolean | undefined; - jwtAlgorithm?: import("./auth.type").JWTAlgorithm | undefined; - jwtKey?: string | undefined; - jwtKid?: string | undefined; - jwtSub?: string | undefined; - jwtExpDuration?: number | undefined; - useRequestAPI?: boolean | undefined; - accessKey?: string | undefined; - secretKey?: string | undefined; - bucket?: string | undefined; - region?: string | undefined; - endpoint?: string | undefined; - useCustomRequestHandler?: boolean | undefined; - bucketCustomHeaders?: string | undefined; - bucketPrefix?: string | undefined; - forcePathStyle?: boolean | undefined; - remoteType?: import("./setting.type").RemoteType | undefined; - encrypt?: boolean | undefined; - passphrase?: string | undefined; - usePathObfuscation?: boolean | undefined; - E2EEAlgorithm?: import("./setting.type").E2EEAlgorithm | undefined; - hashAlg?: import("./setting.type").HashAlgorithm | undefined; - minimumChunkSize?: number | undefined; - customChunkSize?: number | undefined; - longLineThreshold?: number | undefined; - useSegmenter?: boolean | undefined; - enableChunkSplitterV2?: boolean | undefined; - doNotUseFixedRevisionForChunks?: boolean | undefined; - chunkSplitterVersion?: import("./setting.type").ChunkSplitterVersion | undefined; - useEden?: boolean | undefined; - maxChunksInEden?: number | undefined; - maxTotalLengthInEden?: number | undefined; - maxAgeInEden?: number | undefined; - checkIntegrityOnSave?: boolean | undefined; - useHistory?: boolean | undefined; - disableRequestURI?: boolean | undefined; - sendChunksBulk?: boolean | undefined; - sendChunksBulkMaxSize?: number | undefined; - useDynamicIterationCount?: boolean | undefined; - doNotPaceReplication?: boolean | undefined; - readChunksOnline?: boolean | undefined; - useOnlyLocalChunk?: boolean | undefined; - concurrencyOfReadChunksOnline?: number | undefined; - minimumIntervalOfReadChunksOnline?: number | undefined; - enableCompression?: boolean | undefined; - batch_size?: number | undefined; - batches_limit?: number | undefined; - ignoreVersionCheck?: boolean | undefined; - disableCheckingConfigMismatch?: boolean | undefined; - autoAcceptCompatibleTweak?: boolean | undefined; - hashCacheMaxCount?: number | undefined; - hashCacheMaxAmount?: number | undefined; - permitEmptyPassphrase?: boolean | undefined; - handleFilenameCaseSensitive?: boolean | undefined; - checkConflictOnlyOnOpen?: boolean | undefined; - showMergeDialogOnlyOnActive?: boolean | undefined; - additionalSuffixOfDatabaseName?: string | undefined; - useTimeouts?: boolean | undefined; - deleteMetadataOfDeletedFiles?: boolean | undefined; - automaticallyDeleteMetadataOfDeletedFiles?: number | undefined; - P2P_AutoAccepting?: import("./setting.type").AutoAccepting | undefined; - P2P_AutoSyncPeers?: string | undefined; - P2P_AutoWatchPeers?: string | undefined; - P2P_SyncOnReplication?: string | undefined; - P2P_RebuildFrom?: string | undefined; - P2P_AutoAcceptingPeers?: string | undefined; - P2P_AutoDenyingPeers?: string | undefined; - P2P_IsHeadless?: boolean; - P2P_Enabled?: boolean | undefined; - P2P_relays?: string | undefined; - P2P_roomID?: string | undefined; - P2P_passphrase?: string | undefined; - P2P_AppID?: string | undefined; - P2P_AutoStart?: boolean | undefined; - P2P_AutoBroadcast?: boolean | undefined; - P2P_DevicePeerName?: string; - P2P_turnServers?: string | undefined; - P2P_turnUsername?: string | undefined; - P2P_turnCredential?: string | undefined; - P2P_useDiagRTC?: boolean; -}; -export type TweakValues = Partial; -export declare const DEVICE_ID_PREFERRED = "PREFERRED"; -export {}; diff --git a/_types/src/lib/src/common/rosetta.d.ts b/_types/src/lib/src/common/rosetta.d.ts deleted file mode 100644 index 80cf769d..00000000 --- a/_types/src/lib/src/common/rosetta.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** -# Rosetta stone -- To localise messages to your language, please write a translation to this file and submit a PR. -- Please order languages in alphabetic order, if you write multiple items. - -## Notice to ensure that your favours are not wasted. - -If you plan to utilise machine translation engines to contribute translated resources, -please ensure the engine's terms of service are compatible with our project's license. -Your diligence in this matter helps maintain compliance and avoid potential licensing issues. -Thank you for your consideration. - -Usually, our projects (Self-hosted LiveSync and its families) are licensed under MIT License. -To see details, please refer to the LICENSES file on each repository. - -## How to internationalise untranslated items? -1. Change the message literal to use `$msg` - "Could not parse YAML" -> $msg('anyKey') -2. Create `ls-debug` folder under the `.obsidian` folder of your vault. -3. Run Self-hosted LiveSync in dev mode (npm run dev). -4. You will get the `missing-translation-YYYY-MM-DD.jsonl` under `ls-debug`. Please copy and paste inside `allMessages` and write the translations. -5. Send me the PR! -*/ -declare const LANG_DE = "de"; -declare const LANG_ES = "es"; -declare const LANG_FR = "fr"; -declare const LANG_HE = "he"; -declare const LANG_JA = "ja"; -declare const LANG_RU = "ru"; -declare const LANG_ZH = "zh"; -declare const LANG_KO = "ko"; -declare const LANG_ZH_TW = "zh-tw"; -declare const LANG_DEF = "def"; -export declare const SUPPORTED_I18N_LANGS: string[]; -export type I18N_LANGS = typeof LANG_DEF | typeof LANG_DE | typeof LANG_ES | typeof LANG_FR | typeof LANG_HE | typeof LANG_JA | typeof LANG_KO | typeof LANG_RU | typeof LANG_ZH | typeof LANG_ZH_TW | ""; -export type MESSAGE = { - [key in I18N_LANGS]?: string; -}; -import { type MessageKeys } from "./messages/combinedMessages.dev"; -export declare function expandKeywords, U extends Record>(message: T, lang: I18N_LANGS, recurseLimit?: number): T; -export type AllMessageKeys = MessageKeys; -export {}; diff --git a/_types/src/lib/src/common/settingConstants.d.ts b/_types/src/lib/src/common/settingConstants.d.ts deleted file mode 100644 index 24a721f1..00000000 --- a/_types/src/lib/src/common/settingConstants.d.ts +++ /dev/null @@ -1,217 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ConfigurationItem, type ObsidianLiveSyncSettings } from "./types.ts"; -type ExtractPropertiesByType = { - [K in keyof T as T[K] extends U ? K : never]: T[K] extends U ? K : never; -}; -export type FilterStringKeys = keyof ExtractPropertiesByType; -export type FilterBooleanKeys = keyof ExtractPropertiesByType; -export type FilterNumberKeys = keyof ExtractPropertiesByType; -import type { FilePath, FilePathWithPrefixLC, FilePathWithPrefix, DocumentID } from "./models/db.type.ts"; -export type { FilePath, FilePathWithPrefixLC, FilePathWithPrefix, DocumentID }; -/** - * Self-hosted LiveSync settings pane items. - */ -export type OnDialogSettings = { - configPassphrase: string; - preset: "" | "PERIODIC" | "LIVESYNC" | "DISABLE"; - syncMode: "ONEVENTS" | "PERIODIC" | "LIVESYNC"; - dummy: number; - deviceAndVaultName: string; -}; -/** - * Default settings for the OnDialogSettings. - * Not used for the common library, but used for the Obsidian plugin. - */ -export declare const OnDialogSettingsDefault: OnDialogSettings; -export declare const AllSettingDefault: { - configPassphrase: string; - preset: "" | "PERIODIC" | "LIVESYNC" | "DISABLE"; - syncMode: "ONEVENTS" | "PERIODIC" | "LIVESYNC"; - dummy: number; - deviceAndVaultName: string; - liveSync: boolean; - syncOnSave: boolean; - syncOnStart: boolean; - syncOnFileOpen: boolean; - syncOnEditorSave: boolean; - keepReplicationActiveInBackground: boolean; - syncMinimumInterval: number; - showVerboseLog: boolean; - lessInformationInLog: boolean; - showLongerLogInsideEditor: boolean; - showStatusOnEditor: boolean; - showStatusOnStatusbar: boolean; - showOnlyIconsOnEditor: boolean; - hideFileWarningNotice: boolean; - networkWarningStyle: "" | "icon" | "hidden"; - displayLanguage: import("./rosetta.ts").I18N_LANGS; - trashInsteadDelete: boolean; - doNotDeleteFolder: boolean; - batchSave: boolean; - batchSaveMinimumDelay: number; - batchSaveMaximumDelay: number; - syncMaxSizeInMB: number; - useIgnoreFiles: boolean; - ignoreFiles: string; - processSizeMismatchedFiles: boolean; - syncOnlyRegEx: import("./types.ts").CustomRegExpSourceList<"|[]|">; - syncIgnoreRegEx: import("./types.ts").CustomRegExpSourceList<"|[]|">; - syncAfterMerge: boolean; - resolveConflictsByNewerFile: boolean; - writeDocumentsIfConflicted: boolean; - disableMarkdownAutoMerge: boolean; - configPassphraseStore: import("./types.ts").ConfigPassphraseStore; - encryptedPassphrase: string; - encryptedCouchDBConnection: string; - periodicReplication: boolean; - periodicReplicationInterval: number; - syncInternalFiles: boolean; - syncInternalFilesBeforeReplication: boolean; - syncInternalFilesInterval: number; - syncInternalFilesIgnorePatterns: import("./types.ts").CustomRegExpSourceList<",">; - syncInternalFilesTargetPatterns: import("./types.ts").CustomRegExpSourceList<",">; - watchInternalFileChanges: boolean; - suppressNotifyHiddenFilesChange: boolean; - syncInternalFileOverwritePatterns: import("./types.ts").CustomRegExpSourceList<",">; - usePluginSync: boolean; - usePluginSettings: boolean; - showOwnPlugins: boolean; - autoSweepPlugins: boolean; - autoSweepPluginsPeriodic: boolean; - notifyPluginOrSettingUpdated: boolean; - usePluginSyncV2: boolean; - usePluginEtc: boolean; - pluginSyncExtendedSetting: Record; - useAdvancedMode: boolean; - usePowerUserMode: boolean; - useEdgeCaseMode: boolean; - notifyThresholdOfRemoteStorageSize: number; - disableWorkerForGeneratingChunks: boolean; - processSmallFilesInUIThread: boolean; - savingDelay: number; - gcDelay: number; - skipOlderFilesOnSync: boolean; - useIndexedDBAdapter: boolean; - enableDebugTools: boolean; - writeLogToTheFile: boolean; - settingSyncFile: string; - writeCredentialsForSettingSync: boolean; - notifyAllSettingSyncFile: boolean; - suspendFileWatching: boolean; - suspendParseReplicationResult: boolean; - doNotSuspendOnFetching: boolean; - maxMTimeForReflectEvents: number; - versionUpFlash: string; - settingVersion: number; - isConfigured?: boolean; - lastReadUpdates: number; - doctorProcessedVersion: string; - remoteConfigurations: Record; - activeConfigurationId: string; - P2P_ActiveRemoteConfigurationId: string; - couchDB_URI: string; - couchDB_USER: string; - couchDB_PASSWORD: string; - couchDB_DBNAME: string; - couchDB_CustomHeaders: string; - useJWT: boolean; - jwtAlgorithm: import("./models/auth.type.ts").JWTAlgorithm; - jwtKey: string; - jwtKid: string; - jwtSub: string; - jwtExpDuration: number; - useRequestAPI: boolean; - accessKey: string; - secretKey: string; - bucket: string; - region: string; - endpoint: string; - useCustomRequestHandler: boolean; - bucketCustomHeaders: string; - bucketPrefix: string; - forcePathStyle: boolean; - remoteType: import("./types.ts").RemoteType; - encrypt: boolean; - passphrase: string; - usePathObfuscation: boolean; - E2EEAlgorithm: import("./types.ts").E2EEAlgorithm; - hashAlg: import("./types.ts").HashAlgorithm; - minimumChunkSize: number; - customChunkSize: number; - longLineThreshold: number; - useSegmenter: boolean; - enableChunkSplitterV2: boolean; - doNotUseFixedRevisionForChunks: boolean; - chunkSplitterVersion: import("./types.ts").ChunkSplitterVersion; - useEden: boolean; - maxChunksInEden: number; - maxTotalLengthInEden: number; - maxAgeInEden: number; - tweakModified: number | undefined; - checkIntegrityOnSave: boolean; - useHistory: boolean; - disableRequestURI: boolean; - sendChunksBulk: boolean; - sendChunksBulkMaxSize: number; - useDynamicIterationCount: boolean; - doNotPaceReplication: boolean; - readChunksOnline: boolean; - useOnlyLocalChunk: boolean; - concurrencyOfReadChunksOnline: number; - minimumIntervalOfReadChunksOnline: number; - enableCompression: boolean; - batch_size: number; - batches_limit: number; - ignoreVersionCheck: boolean; - disableCheckingConfigMismatch: boolean; - autoAcceptCompatibleTweak: boolean | undefined; - hashCacheMaxCount: number; - hashCacheMaxAmount: number; - permitEmptyPassphrase: boolean; - handleFilenameCaseSensitive: boolean; - checkConflictOnlyOnOpen: boolean; - showMergeDialogOnlyOnActive: boolean; - additionalSuffixOfDatabaseName: string | undefined; - useTimeouts: boolean; - deleteMetadataOfDeletedFiles: boolean; - automaticallyDeleteMetadataOfDeletedFiles: number; - P2P_AutoAccepting: import("./types.ts").AutoAccepting; - P2P_AutoSyncPeers: string; - P2P_AutoWatchPeers: string; - P2P_SyncOnReplication: string; - P2P_RebuildFrom: string; - P2P_AutoAcceptingPeers: string; - P2P_AutoDenyingPeers: string; - P2P_IsHeadless?: boolean; - P2P_Enabled: boolean; - P2P_relays: string; - P2P_roomID: string; - P2P_passphrase: string; - P2P_AppID: string; - P2P_AutoStart: boolean; - P2P_AutoBroadcast: boolean; - P2P_DevicePeerName?: string; - P2P_turnServers: string; - P2P_turnUsername: string; - P2P_turnCredential: string; - P2P_useDiagRTC?: boolean; -}; -export type AllSettings = ObsidianLiveSyncSettings & OnDialogSettings; -export type AllStringItemKey = FilterStringKeys; -export type AllNumericItemKey = FilterNumberKeys; -export type AllBooleanItemKey = FilterBooleanKeys; -export type AllSettingItemKey = AllStringItemKey | AllNumericItemKey | AllBooleanItemKey; -export type ValueOf = T extends AllStringItemKey ? string : T extends AllNumericItemKey ? number : T extends AllBooleanItemKey ? boolean : AllSettings[T]; -export declare const SettingInformation: Partial>; -export declare function getConfig(key: AllSettingItemKey): false | { - name: string; - desc?: string; - placeHolder?: string; - status?: "BETA" | "ALPHA" | "EXPERIMENTAL"; - obsolete?: boolean; - level?: import("./types.ts").ConfigLevel; - isHidden?: boolean; - isAdvanced?: boolean; -}; -export declare function getConfName(key: AllSettingItemKey): string; diff --git a/_types/src/lib/src/common/typeUtils.d.ts b/_types/src/lib/src/common/typeUtils.d.ts deleted file mode 100644 index 0b2da7b2..00000000 --- a/_types/src/lib/src/common/typeUtils.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DocumentID, FilePath, FilePathWithPrefix } from "./models/db.type"; -import type { UXFileInfoStub } from "./types"; -/** - * returns is internal chunk of file - * @param id ID - * @returns - */ -export declare function isInternalMetadata(id: FilePath | FilePathWithPrefix | DocumentID): boolean; -export declare function isInternalFile(file: UXFileInfoStub | string | FilePathWithPrefix): boolean; -export declare function stripInternalMetadataPrefix(id: T): T; -export declare function id2InternalMetadataId(id: DocumentID): DocumentID; -export declare function isChunk(str: string): boolean; -export declare function isPluginMetadata(str: string): boolean; -export declare function isCustomisationSyncMetadata(str: string): boolean; -export declare function getPathFromUXFileInfo(file: UXFileInfoStub | string | FilePathWithPrefix): FilePathWithPrefix; -export declare function getStoragePathFromUXFileInfo(file: UXFileInfoStub | string | FilePathWithPrefix): FilePath; -export declare function getDatabasePathFromUXFileInfo(file: UXFileInfoStub | string | FilePathWithPrefix): FilePathWithPrefix; diff --git a/_types/src/lib/src/common/types.d.ts b/_types/src/lib/src/common/types.d.ts deleted file mode 100644 index ba7965d5..00000000 --- a/_types/src/lib/src/common/types.d.ts +++ /dev/null @@ -1,109 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type { TaggedType } from "./models/shared.type.util.ts"; -export { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT, LOG_LEVEL_VERBOSE, } from "octagonal-wheels/common/logger"; -export type { LOG_LEVEL } from "octagonal-wheels/common/logger"; -import { RESULT_NOT_FOUND, RESULT_TIMED_OUT } from "octagonal-wheels/common/const"; -import type { Credential } from "./models/auth.type.ts"; -import type { AnyEntry, ChunkVersionRange, DatabaseEntry, EdenChunk, EntryBase, EntryChunkPack, EntryHasPath, EntryLeaf, EntryType, EntryTypeNotes, EntryTypeNotesWithLegacy, EntryVersionInfo, EntryWithEden, InternalFileEntry, LoadedEntry, MetaEntry, NewEntry, NoteEntry, PlainEntry, SavingEntry, SyncInfo } from "./models/db.type.ts"; -import { ChunkTypes, EntryTypes, MILESTONE_DOCID, NODEINFO_DOCID, NoteTypes, SYNCINFO_ID, VERSIONING_DOCID } from "./models/db.const.ts"; -import { AUTO_MERGED, CANCELLED, LEAVE_TO_SUBSEQUENT, MISSING_OR_ERROR, NOT_CONFLICTED, TIME_ARGUMENT_INFINITY } from "./models/shared.const.symbols.ts"; -import { IDPrefixes, LEAF_WAIT_ONLY_REMOTE, LEAF_WAIT_TIMEOUT, LEAF_WAIT_TIMEOUT_SEQUENTIAL_REPLICATOR, MAX_DOC_SIZE, MAX_DOC_SIZE_BIN, PREFIX_CHUNK, PREFIX_ENCRYPTED_CHUNK, PREFIX_OBFUSCATED, RECENT_MODIFIED_DOCS_QTY, REPLICATION_BUSY_TIMEOUT, SALT_OF_ID, SALT_OF_PASSPHRASE, SEED_MURMURHASH, VER } from "./models/shared.const.behabiour.ts"; -import { AutoAccepting, type BucketSyncSetting, type ChunkSplitterVersion, type ConfigPassphraseStore, type CouchDBConnection, type E2EEAlgorithm, type EncryptionSettings, type HashAlgorithm, type HasSettings, type LocalDBSettings, type ObsidianLiveSyncSettings, type P2PConnectionInfo, type P2PSyncSetting, type PluginSyncSettingEntry, type RemoteDBSettings, type RemoteType, type RemoteTypeSettings, type SYNC_MODE } from "./models/setting.type.ts"; -import { ChunkAlgorithmNames, ChunkAlgorithms, CURRENT_SETTING_VERSION, E2EEAlgorithmNames, E2EEAlgorithms, HashAlgorithms, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P, RemoteTypes, SETTING_VERSION_INITIAL, SETTING_VERSION_SUPPORT_CASE_INSENSITIVE, MODE_AUTOMATIC, MODE_PAUSED, MODE_SELECTIVE, MODE_SHINY } from "./models/setting.const.ts"; -import { PREFERRED_BASE, PREFERRED_JOURNAL_SYNC, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED } from "./models/setting.const.preferred.ts"; -import { P2P_DEFAULT_SETTINGS, DEFAULT_SETTINGS } from "./models/setting.const.defaults.ts"; -import { KeyIndexOfSettings } from "./models/setting.const.qr.ts"; -import type { DeviceInfo, EntryBody, EntryDoc, EntryDocResponse, EntryMilestoneInfo, EntryNodeInfo, NodeData, NodeKey } from "./models/db.definition.ts"; -import { isMetaEntry } from "./models/db.definition.ts"; -import type { CouchDBCredentials, BasicCredentials, JWTCredentials, JWTHeader, JWTPayload, JWTParams, PreparedJWT } from "./models/auth.type.ts"; -import type { CacheData, FileEventArgs, FileEventItem, FileEventType, UXAbstractInfoStub, UXDataWriteOptions, UXFileInfo, UXFileInfoStub, UXFolderInfo, UXInternalFileInfoStub, UXStat } from "./models/fileaccess.type.ts"; -import { SETTING_KEY_P2P_DEVICE_NAME, configURIBase, configURIBaseQR, SuffixDatabaseName, ExtraSuffixIndexedDB } from "./models/shared.const.ts"; -import { configurationNames, LEVEL_ADVANCED, LEVEL_POWER_USER, LEVEL_EDGE_CASE, type ConfigLevel, type ConfigurationItem, statusDisplay, confName, confDesc } from "./models/shared.definition.configNames.ts"; -import type { CustomRegExpSource, CustomRegExpSourceList, ParsedCustomRegExp, Prettify } from "./models/shared.type.util.ts"; -import { ProtocolVersions, type ProtocolVersion, DOCID_SYNC_PARAMETERS, DOCID_JOURNAL_SYNC_PARAMETERS, type SyncParameters, DEFAULT_SYNC_PARAMETERS } from "./models/sync.definition.ts"; -import { TweakValuesShouldMatchedTemplate, IncompatibleChanges, CompatibleButLossyChanges, IncompatibleChangesInSpecificPattern, TweakValuesRecommendedTemplate, TweakValuesDefault, TweakValuesTemplate, type TweakValues, DEVICE_ID_PREFERRED } from "./models/tweak.definition.ts"; -import type { diff_result_leaf, dmp_result, diff_result, DIFF_CHECK_RESULT_AUTO, diff_check_result } from "./models/diff.definition.ts"; -import { PREFIXMD_LOGFILE, PREFIXMD_LOGFILE_UC, FlagFilesOriginal, FlagFilesHumanReadable, FLAGMD_REDFLAG, FLAGMD_REDFLAG2, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3, FLAGMD_REDFLAG3_HR } from "./models/redflag.const.ts"; -import { DatabaseConnectingStatuses, type DatabaseConnectingStatus } from "./models/shared.definition.ts"; -export { RESULT_NOT_FOUND, RESULT_TIMED_OUT }; -export type { FilePath, FilePathWithPrefixLC, FilePathWithPrefix, DocumentID } from "./models/db.type.ts"; -export { MAX_DOC_SIZE, MAX_DOC_SIZE_BIN, VER, RECENT_MODIFIED_DOCS_QTY, LEAF_WAIT_TIMEOUT, LEAF_WAIT_ONLY_REMOTE, LEAF_WAIT_TIMEOUT_SEQUENTIAL_REPLICATOR, REPLICATION_BUSY_TIMEOUT, CANCELLED, AUTO_MERGED, NOT_CONFLICTED, MISSING_OR_ERROR, LEAVE_TO_SUBSEQUENT, TIME_ARGUMENT_INFINITY, VERSIONING_DOCID, MILESTONE_DOCID, NODEINFO_DOCID, }; -export { type CouchDBConnection }; -export type { ConfigPassphraseStore }; -export { MODE_SELECTIVE, MODE_AUTOMATIC, MODE_PAUSED, MODE_SHINY, type SYNC_MODE }; -export { type PluginSyncSettingEntry }; -export { SETTING_VERSION_INITIAL, SETTING_VERSION_SUPPORT_CASE_INSENSITIVE, CURRENT_SETTING_VERSION }; -export type { BucketSyncSetting, LocalDBSettings }; -export { RemoteTypes, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P, type RemoteType, AutoAccepting }; -export type { P2PConnectionInfo, P2PSyncSetting }; -export { P2P_DEFAULT_SETTINGS }; -export type { RemoteTypeSettings }; -export { E2EEAlgorithmNames, E2EEAlgorithms, type E2EEAlgorithm }; -export type { EncryptionSettings }; -export { HashAlgorithms, type HashAlgorithm, ChunkAlgorithmNames, ChunkAlgorithms, type ChunkSplitterVersion }; -export type { RemoteDBSettings }; -export type { ObsidianLiveSyncSettings }; -export { DEFAULT_SETTINGS }; -export { KeyIndexOfSettings }; -export { type HasSettings }; -export { PREFERRED_BASE, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED, PREFERRED_JOURNAL_SYNC }; -export { EntryTypes, NoteTypes, ChunkTypes, type EntryType, type EntryTypeNotes, type EntryTypeNotesWithLegacy, type DatabaseEntry, type EntryBase, type EdenChunk, type EntryWithEden, type NoteEntry, type NewEntry, type PlainEntry, type InternalFileEntry, type AnyEntry, type LoadedEntry, type SavingEntry, type MetaEntry, isMetaEntry, type EntryLeaf, type EntryChunkPack, type EntryVersionInfo, type EntryHasPath, }; -export type { ChunkVersionRange }; -export { TweakValuesShouldMatchedTemplate }; -export { IncompatibleChanges }; -export { CompatibleButLossyChanges }; -export { IncompatibleChangesInSpecificPattern }; -export { TweakValuesRecommendedTemplate }; -export { TweakValuesDefault }; -export { configurationNames }; -export { LEVEL_ADVANCED }; -export { LEVEL_POWER_USER }; -export { LEVEL_EDGE_CASE }; -export type { ConfigLevel }; -export type { ConfigurationItem }; -export { statusDisplay }; -export { confName }; -export { confDesc }; -export { TweakValuesTemplate }; -export type { TweakValues }; -export { DEVICE_ID_PREFERRED }; -export type { NodeKey }; -export type { DeviceInfo }; -export type { NodeData }; -export type { EntryMilestoneInfo }; -export type { EntryNodeInfo }; -export type { EntryBody }; -export type { EntryDoc }; -export type { diff_result_leaf }; -export type { dmp_result }; -export type { diff_result }; -export type { DIFF_CHECK_RESULT_AUTO }; -export type { diff_check_result }; -export type { Credential }; -export type { EntryDocResponse }; -export { DatabaseConnectingStatuses }; -export type { DatabaseConnectingStatus }; -export { PREFIXMD_LOGFILE, PREFIXMD_LOGFILE_UC, FlagFilesOriginal, FlagFilesHumanReadable, FLAGMD_REDFLAG, FLAGMD_REDFLAG2, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3, FLAGMD_REDFLAG3_HR, }; -export { SYNCINFO_ID }; -export type { SyncInfo }; -export { SALT_OF_PASSPHRASE, SALT_OF_ID, SEED_MURMURHASH, IDPrefixes, PREFIX_OBFUSCATED, PREFIX_CHUNK, PREFIX_ENCRYPTED_CHUNK, }; -export type { UXStat, UXFileInfo, UXAbstractInfoStub, UXFileInfoStub, UXInternalFileInfoStub, UXFolderInfo, UXDataWriteOptions, CacheData, FileEventType, FileEventArgs, FileEventItem, }; -export type { Prettify }; -export type { CouchDBCredentials }; -export type { BasicCredentials }; -export type { JWTCredentials }; -export type { JWTHeader }; -export type { JWTPayload }; -export type { JWTParams }; -export type { PreparedJWT }; -export type { CustomRegExpSource }; -export type { CustomRegExpSourceList }; -export type { ParsedCustomRegExp }; -export { ProtocolVersions }; -export type { ProtocolVersion }; -export { DOCID_SYNC_PARAMETERS }; -export { DOCID_JOURNAL_SYNC_PARAMETERS }; -export type { SyncParameters }; -export { DEFAULT_SYNC_PARAMETERS }; -export { SETTING_KEY_P2P_DEVICE_NAME, configURIBase, configURIBaseQR, SuffixDatabaseName, ExtraSuffixIndexedDB }; diff --git a/_types/src/lib/src/common/utils.d.ts b/_types/src/lib/src/common/utils.d.ts deleted file mode 100644 index 286c212a..00000000 --- a/_types/src/lib/src/common/utils.d.ts +++ /dev/null @@ -1,131 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type AnyEntry, type DatabaseEntry, type EntryLeaf, type SyncInfo, type LoadedEntry, type SavingEntry, type NewEntry, type PlainEntry, type CustomRegExpSource, type ParsedCustomRegExp, type CustomRegExpSourceList, type ObsidianLiveSyncSettings, type RemoteDBSettings, type P2PConnectionInfo, type BucketSyncSetting, type CouchDBConnection, type EncryptionSettings } from "./types.ts"; -import { replaceAll, replaceAllPairs } from "octagonal-wheels/string"; -export { replaceAll, replaceAllPairs }; -import { concatUInt8Array } from "octagonal-wheels/binary"; -export { concatUInt8Array }; -import { delay, fireAndForget } from "octagonal-wheels/promises"; -export { delay, fireAndForget }; -import { arrayToChunkedArray, unique } from "octagonal-wheels/collection"; -export { arrayToChunkedArray, unique }; -import { extractObject, isObjectDifferent } from "octagonal-wheels/object"; -export { extractObject, isObjectDifferent }; -import { sendValue, sendSignal, waitForSignal, waitForValue } from "octagonal-wheels/messagepassing/signal"; -export { sendValue, sendSignal, waitForSignal, waitForValue }; -import { throttle } from "octagonal-wheels/function"; -export { throttle }; -import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; -import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "./models/shared.const.symbols.ts"; -export type { SimpleStore }; -export { sizeToHumanReadable } from "octagonal-wheels/number"; -export declare function resolveWithIgnoreKnownError(p: Promise, def: T): Promise; -export declare const Parallels: (ps?: Set>) => { - add: (p: Promise) => Set>; - wait: (limit: number) => false | Promise; - all: () => Promise; -}; -export declare function allSettledWithConcurrencyLimit(processes: Promise[], limit: number): Promise; -export declare function getDocData(doc: string | string[]): string; -export declare function getDocDataAsArray(doc: string | string[]): string[]; -export declare function getDocDataAsArrayBuffer(doc: string | string[] | ArrayBuffer): Uint8Array; -export declare function isTextBlob(blob: Blob): boolean; -export declare function createTextBlob(data: string | string[]): Blob; -export declare function createBinaryBlob(data: Uint8Array | ArrayBuffer): Blob; -export declare function createBlob(data: string | string[] | Uint8Array | ArrayBuffer | Blob): Blob; -export declare function isTextDocument(doc: LoadedEntry): boolean; -export declare function readAsBlob(doc: LoadedEntry): Blob; -export declare function readContent(doc: LoadedEntry): string | ArrayBuffer; -export declare function isDocContentSame(docA: string | string[] | Blob | ArrayBuffer, docB: string | string[] | Blob | ArrayBuffer): Promise; -export declare function isObfuscatedEntry(doc: DatabaseEntry): doc is AnyEntry; -export declare function isEncryptedChunkEntry(doc: DatabaseEntry): doc is EntryLeaf; -export declare function isSyncInfoEntry(doc: DatabaseEntry): doc is SyncInfo; -export declare function memorizeFuncWithLRUCache(func: (key: T) => U): (key: T) => U | undefined; -export declare function memorizeFuncWithLRUCacheMulti(func: (...keys: T) => U): (keys: T) => U | undefined; -/** - * - * @param exclusion return only not exclusion - * @returns - * - * ["something",false,"aaaaa"].filter(onlyNot(false)) => yields ["something","aaaaaa"]. but, as string[]. - */ -export declare function onlyNot(exclusion: B): (item: A | B) => item is Exclude; -/** - * Run task with keeping minimum interval - * @param key waiting key - * @param interval interval (ms) - * @param task task to perform. - * @returns result of task - * @remarks This function is not designed to be concurrent. - */ -export declare function runWithInterval(key: string, interval: number, task: () => Promise): Promise; -/** - * Run task with keeping minimum interval on start - * @param key waiting key - * @param interval interval (ms) - * @param task task to perform. - * @returns result of task - * @remarks This function is not designed to be concurrent. - */ -export declare function runWithStartInterval(key: string, interval: number, task: () => Promise): Promise; -export declare const globalConcurrencyController: import("octagonal-wheels/concurrency/semaphore_v2").SemaphoreObject; -export declare function determineTypeFromBlob(data: Blob): "newnote" | "plain"; -export declare function determineType(path: string, data: string | string[] | Uint8Array | ArrayBuffer | Blob): "newnote" | "plain"; -export declare function isAnyNote(doc: DatabaseEntry): doc is NewEntry | PlainEntry; -export declare function isLoadedEntry(doc: DatabaseEntry): doc is LoadedEntry; -export declare function isDeletedEntry(doc: LoadedEntry): boolean; -export declare function createSavingEntryFromLoadedEntry(doc: LoadedEntry): SavingEntry; -export declare function setAllItems(set: Set, items: T[]): Set; -export declare function escapeNewLineFromString(str: string): string; -export declare function unescapeNewLineFromString(str: string): string; -export declare function escapeMarkdownValue(value: T): T; -export declare function timeDeltaToHumanReadable(delta: number): string; -export declare function wrapException(func: () => Promise>): Promise | Error>; -export declare function toRanges(sorted: number[]): string; -export declare function isDirty(key: string, value: unknown): boolean; -export declare function tryParseJSON(str: string, fallbackValue?: T): T | undefined; -export { mergeObject, applyPatch, generatePatchObj, flattenObject, isObjectMargeApplicable, isSensibleMargeApplicable, } from "./utils.patch.ts"; -export declare function parseHeaderValues(strHeader: string): Record; -/*** - * Parse custom regular expression - * @param regexp - * @returns [negate: boolean, regexp: string] - * @example `!!foo` => [true, "foo"] - * @example `foo` => [false, "foo"] - */ -export declare function parseCustomRegExp(regexp: CustomRegExpSource): ParsedCustomRegExp; -export declare function matchRegExp(regexp: CustomRegExpSource, target: string): boolean; -export declare function isValidRegExp(regexp: CustomRegExpSource): boolean; -export declare function isInvertedRegExp(regexp: CustomRegExpSource): boolean; -export declare function constructCustomRegExpList(items: CustomRegExpSource[], delimiter: D): CustomRegExpSourceList; -export declare function splitCustomRegExpList(list: CustomRegExpSourceList, delimiter: D): CustomRegExpSource[]; -export declare class CustomRegExp { - regexp: RegExp; - negate: boolean; - pattern: string; - constructor(regexp: CustomRegExpSource, flags?: string); - test(str: string): boolean; -} -type RegExpSettingKey = "syncOnlyRegEx" | "syncIgnoreRegEx" | "syncInternalFilesIgnorePatterns" | "syncInternalFilesTargetPatterns" | "syncInternalFileOverwritePatterns"; -export declare function getFileRegExp(settings: ObsidianLiveSyncSettings | RemoteDBSettings, key: RegExpSettingKey): CustomRegExp[]; -/** - * Copies properties from the source object to the target object only if they exist in the target. - * @param source The object to copy properties from. - * @param target The object to copy properties to. - */ -export declare function copyTo(source: U, target: T): void; -export declare function pickBucketSyncSettings(setting: ObsidianLiveSyncSettings): BucketSyncSetting; -export declare function pickCouchDBSyncSettings(setting: ObsidianLiveSyncSettings): CouchDBConnection; -export declare function pickEncryptionSettings(setting: ObsidianLiveSyncSettings | EncryptionSettings): EncryptionSettings; -export declare function pickP2PSyncSettings(setting: Partial & P2PConnectionInfo): P2PConnectionInfo; -export declare function wrapByDefault(func: () => T, onError: (err: Error) => U): T | U; -export declare function compareMTime(baseMTime: number, targetMTime: number): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN; -export declare function displayRev(rev: string): string; -/** - * Generate a random P2P Room ID in the format `123-456-789-abc`. - */ -export declare function generateP2PRoomId(): string; -/** - * Extract the stable suffix (last segment) from a Room ID. - */ -export declare function extractP2PRoomSuffix(roomId: string): string; diff --git a/_types/src/lib/src/common/utils.doc.d.ts b/_types/src/lib/src/common/utils.doc.d.ts deleted file mode 100644 index 9cf61d47..00000000 --- a/_types/src/lib/src/common/utils.doc.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function isErrorOf(ex: unknown, statusCode: number): boolean; -/** - * Checks if the error is effectively a 404 error from CouchDB or PouchDB. - * @param ex some error object, expected to be from CouchDB or PouchDB. - * @returns true if the error is a 404 not found error, false otherwise. - * @throws if the input is not an object or does not have a numeric "status" property. - */ -export declare function isNotFoundError(ex: unknown): boolean; -export declare function isConflictError(ex: unknown): boolean; -export declare function isUnauthorizedError(ex: unknown): boolean; -export declare function tryGetFilePath(entry: unknown): string | undefined; diff --git a/_types/src/lib/src/common/utils.object.d.ts b/_types/src/lib/src/common/utils.object.d.ts deleted file mode 100644 index ea17d4fc..00000000 --- a/_types/src/lib/src/common/utils.object.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function asCopy(obj: T): T; -export declare function ensureError(error: unknown): Error; diff --git a/_types/src/lib/src/common/utils.patch.d.ts b/_types/src/lib/src/common/utils.patch.d.ts deleted file mode 100644 index 432e9433..00000000 --- a/_types/src/lib/src/common/utils.patch.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function generatePatchObj(from: Record, to: Record): Record; -export declare function applyPatch(from: Record, patch: Record): Record; -export declare function mergeObject(objA: Record | [unknown], objB: Record | [unknown]): unknown[] | { - [k: string]: unknown; -}; -export declare function flattenObject(obj: Record, path?: string[]): [string, unknown][]; -export declare function isSensibleMargeApplicable(path: string): boolean; -export declare function isObjectMargeApplicable(path: string): boolean; diff --git a/_types/src/lib/src/common/utils.type.d.ts b/_types/src/lib/src/common/utils.type.d.ts deleted file mode 100644 index a0e11728..00000000 --- a/_types/src/lib/src/common/utils.type.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type Constructor = new (...args: any[]) => T; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration diff --git a/_types/src/lib/src/dataobject/StoredMap.d.ts b/_types/src/lib/src/dataobject/StoredMap.d.ts deleted file mode 100644 index 3962e99f..00000000 --- a/_types/src/lib/src/dataobject/StoredMap.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; -export declare class StoredMapLike { - _store: SimpleStore; - _cache: Map; - _prefix: string; - constructor(store: SimpleStore>, prefix?: string); - addPrefix(key: string): string; - get(key: string): Promise; - set(key: string, value: U): Promise; - delete(key: string): Promise; - has(key: string): Promise; -} diff --git a/_types/src/lib/src/dev/checks.d.ts b/_types/src/lib/src/dev/checks.d.ts deleted file mode 100644 index 2e11adf3..00000000 --- a/_types/src/lib/src/dev/checks.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -interface InstanceHaveOnBindFunction { - onBindFunction: (...params: T[]) => void; -} -export declare function __$checkInstanceBinding>(instance: T): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export {}; diff --git a/_types/src/lib/src/encryption/encryptHKDF.d.ts b/_types/src/lib/src/encryption/encryptHKDF.d.ts deleted file mode 100644 index ff5ff860..00000000 --- a/_types/src/lib/src/encryption/encryptHKDF.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { encryptHKDFWorker, decryptHKDFWorker } from "@lib/worker/bgWorker.ts"; -export declare const encryptHKDF: typeof encryptHKDFWorker; -export declare const decryptHKDF: typeof decryptHKDFWorker; diff --git a/_types/src/lib/src/encryption/stringEncryption.d.ts b/_types/src/lib/src/encryption/stringEncryption.d.ts deleted file mode 100644 index 6a7c7d20..00000000 --- a/_types/src/lib/src/encryption/stringEncryption.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** - * Encrypts a string using a passphrase, unless the string is already encrypted. - * - * If the input string begins with `ENCRYPT_V2_PREFIX` or `HKDF_SALTED_ENCRYPTED_PREFIX`, - * we assume it is already encrypted and return it unchanged. - * Otherwise, we encrypt the string using an ephemeral salt and the provided passphrase. - * - * @param source - The plaintext string to encrypt, or an already encrypted string. - * @param passphrase - The passphrase used for encryption. - * @returns A promise resolving to the encrypted string, or the original string if it is already encrypted. - */ -export declare function encryptString(source: string, passphrase: string): Promise; -/** - * Decrypts an encrypted string using the provided passphrase. - * - * This function determines the encryption format by inspecting the string prefix, then applies - * the appropriate decryption method. It supports several encryption formats, including - * HKDF salted encryption and legacy formats (V1, V2, V3). If the format is not supported, - * an error is thrown. - * - * @param encrypted - The encrypted string to decrypt. - * @param passphrase - The passphrase used for decryption. - * @returns A promise resolving to the decrypted string. - * @throws {Error} If the encryption format is unsupported. - */ -export declare function decryptString(encrypted: string, passphrase: string): Promise; -export declare function tryDecryptString(encrypted: string, passphrase: string | false): Promise; diff --git a/_types/src/lib/src/events/coreEvents.d.ts b/_types/src/lib/src/events/coreEvents.d.ts deleted file mode 100644 index 0602935c..00000000 --- a/_types/src/lib/src/events/coreEvents.d.ts +++ /dev/null @@ -1,47 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePathWithPrefix, ObsidianLiveSyncSettings } from "@lib/common/types"; -export declare const EVENT_LAYOUT_READY = "layout-ready"; -export declare const EVENT_PLUGIN_LOADED = "plugin-loaded"; -export declare const EVENT_PLUGIN_UNLOADED = "plugin-unloaded"; -export declare const EVENT_SETTING_SAVED = "setting-saved"; -export declare const EVENT_FILE_RENAMED = "file-renamed"; -export declare const EVENT_FILE_SAVED = "file-saved"; -export declare const EVENT_LEAF_ACTIVE_CHANGED = "leaf-active-changed"; -export declare const EVENT_DATABASE_REBUILT = "database-rebuilt"; -export declare const EVENT_LOG_ADDED = "log-added"; -export declare const EVENT_REQUEST_OPEN_SETUP_URI = "request-open-setup-uri"; -export declare const EVENT_REQUEST_COPY_SETUP_URI = "request-copy-setup-uri"; -export declare const EVENT_REQUEST_SHOW_SETUP_QR = "request-show-setup-qr"; -export declare const EVENT_REQUEST_RELOAD_SETTING_TAB = "reload-setting-tab"; -export declare const EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG = "request-open-plugin-sync-dialog"; -export declare const EVENT_FILE_CHANGED = "event-file-changed"; -export declare const EVENT_REQUEST_OPEN_P2P_SETTINGS = "request-open-p2p-settings"; -export declare const EVENT_REQUEST_OPEN_P2P = "request-open-p2p"; -export declare const EVENT_REQUEST_CLOSE_P2P = "request-close-p2p"; -export declare const EVENT_PLATFORM_UNLOADED = "platform-unloaded"; -export declare const EVENT_ON_UNRESOLVED_ERROR = "on-unresolved-error"; -export declare const EVENT_REQUEST_CHECK_REMOTE_SIZE = "request-check-remote-size"; -declare global { - interface LSEvents { - [EVENT_FILE_SAVED]: undefined; - [EVENT_SETTING_SAVED]: ObsidianLiveSyncSettings; - [EVENT_LAYOUT_READY]: undefined; - [EVENT_FILE_CHANGED]: { - file: FilePathWithPrefix; - automated: boolean; - }; - [EVENT_FILE_RENAMED]: { - newPath: FilePathWithPrefix; - old: FilePathWithPrefix; - }; - [EVENT_DATABASE_REBUILT]: undefined; - [EVENT_REQUEST_OPEN_P2P_SETTINGS]: undefined; - [EVENT_REQUEST_SHOW_SETUP_QR]: undefined; - [EVENT_REQUEST_OPEN_P2P]: undefined; - [EVENT_REQUEST_CLOSE_P2P]: undefined; - [EVENT_PLATFORM_UNLOADED]: undefined; - [EVENT_ON_UNRESOLVED_ERROR]: undefined; - [EVENT_REQUEST_CHECK_REMOTE_SIZE]: undefined; - } -} diff --git a/_types/src/lib/src/hub/hub.d.ts b/_types/src/lib/src/hub/hub.d.ts deleted file mode 100644 index f608abfa..00000000 --- a/_types/src/lib/src/hub/hub.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { EventHub } from "octagonal-wheels/events"; -declare global { - interface LSEvents { - hello: string; - world: undefined; - } -} -export declare const eventHub: EventHub; diff --git a/_types/src/lib/src/index.d.ts b/_types/src/lib/src/index.d.ts deleted file mode 100644 index cab0387e..00000000 --- a/_types/src/lib/src/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export { DirectFileManipulator, type DirectFileManipulatorOptions } from "./API/DirectFileManipulator.ts"; diff --git a/_types/src/lib/src/interfaces/AsyncActivityRunner.d.ts b/_types/src/lib/src/interfaces/AsyncActivityRunner.d.ts deleted file mode 100644 index 242f990a..00000000 --- a/_types/src/lib/src/interfaces/AsyncActivityRunner.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** Options attached to one bounded asynchronous activity. */ -export interface AsyncActivityOptions { - /** An optional diagnostic label supplied to the activity owner. */ - label?: string; -} -/** - * Runs bounded asynchronous work inside a consumer-owned activity scope. - * - * The common library deliberately does not prescribe what the scope does. A - * browser host may keep the screen awake, while a headless host may omit the - * runner and execute the task directly. - */ -export interface AsyncActivityRunner { - /** Runs the task and returns its result without changing its error semantics. */ - run(task: () => T | PromiseLike, options?: AsyncActivityOptions): Promise; -} -/** Runs a task through the injected activity owner, or directly when none is supplied. */ -export declare function runWithOptionalActivity(runner: AsyncActivityRunner | undefined, task: () => T | PromiseLike, options?: AsyncActivityOptions): Promise; diff --git a/_types/src/lib/src/interfaces/Confirm.d.ts b/_types/src/lib/src/interfaces/Confirm.d.ts deleted file mode 100644 index 7a651178..00000000 --- a/_types/src/lib/src/interfaces/Confirm.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export interface Confirm { - askYesNo(message: string): Promise<"yes" | "no">; - askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise; - askYesNoDialog(message: string, opt: { - title?: string; - defaultOption?: "Yes" | "No"; - timeout?: number; - }): Promise<"yes" | "no">; - askSelectString(message: string, items: string[]): Promise; - askSelectStringDialogue(message: string, buttons: T, opt: { - title?: string; - defaultAction: T[number]; - timeout?: number; - }): Promise; - askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void; - confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false>; -} diff --git a/_types/src/lib/src/interfaces/DatabaseFileAccess.d.ts b/_types/src/lib/src/interfaces/DatabaseFileAccess.d.ts deleted file mode 100644 index 45c910c0..00000000 --- a/_types/src/lib/src/interfaces/DatabaseFileAccess.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePathWithPrefix, LoadedEntry, MetaEntry, UXFileInfo, UXFileInfoStub } from "@lib/common/types"; -export interface DatabaseFileAccess { - delete: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string) => Promise; - store: (file: UXFileInfo, force?: boolean, skipCheck?: boolean) => Promise; - storeAsConflictedRevision: (file: UXFileInfo, currentRev: string, skipCheck?: boolean) => Promise; - storeContent(path: FilePathWithPrefix, content: string): Promise; - createChunks: (file: UXFileInfo, force?: boolean, skipCheck?: boolean) => Promise; - hasContentInRevisionHistory: (file: UXFileInfoStub | FilePathWithPrefix, content: string | string[] | Blob | ArrayBuffer, currentRev?: string) => Promise; - fetch: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean) => Promise; - fetchEntryFromMeta: (meta: MetaEntry, waitForReady?: boolean, skipCheck?: boolean) => Promise; - fetchEntryMeta: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string, skipCheck?: boolean) => Promise; - fetchEntry: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean) => Promise; - getConflictedRevs: (file: UXFileInfoStub | FilePathWithPrefix) => Promise; -} diff --git a/_types/src/lib/src/interfaces/DatabaseRebuilder.d.ts b/_types/src/lib/src/interfaces/DatabaseRebuilder.d.ts deleted file mode 100644 index 2b1adba6..00000000 --- a/_types/src/lib/src/interfaces/DatabaseRebuilder.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export interface Rebuilder { - $performRebuildDB(method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks"): Promise; - $rebuildRemote(): Promise; - $rebuildEverything(): Promise; - $fetchLocal(makeLocalChunkBeforeSync?: boolean, preventMakeLocalFilesBeforeSync?: boolean): Promise; - $fetchLocalDBFast(autoResume: boolean): Promise; - scheduleRebuild(): Promise; - scheduleFetch(): Promise; - /** - * Declares the finish of the rebuild process and unlock remote, resume reflecting the changes. - */ - finishRebuild(): Promise; -} diff --git a/_types/src/lib/src/interfaces/FileHandler.d.ts b/_types/src/lib/src/interfaces/FileHandler.d.ts deleted file mode 100644 index 50d44abb..00000000 --- a/_types/src/lib/src/interfaces/FileHandler.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, FilePathWithPrefix, MetaEntry } from "@lib/common/models/db.type"; -import type { UXFileInfo, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/models/fileaccess.type"; -export interface IFileHandler { - readFileFromStub(file: UXFileInfoStub | UXFileInfo): Promise; - storeFileToDB(info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, force?: boolean, onlyChunks?: boolean): Promise; - deleteFileFromDB(info: UXFileInfoStub | UXInternalFileInfoStub | FilePath): Promise; - renameFileInDB(info: UXFileInfoStub | UXFileInfo, oldPath: FilePath | FilePathWithPrefix): Promise; - deleteRevisionFromDB(info: UXFileInfoStub | FilePath | FilePathWithPrefix, rev: string): Promise; - resolveConflictedByDeletingRevision(info: UXFileInfoStub | FilePath, rev: string): Promise; - dbToStorageWithSpecificRev(info: UXFileInfoStub | UXFileInfo | FilePath | null, rev: string, force?: boolean): Promise; - dbToStorage(entryInfo: MetaEntry | FilePathWithPrefix, info: UXFileInfoStub | UXFileInfo | FilePath | null, force?: boolean): Promise; - createAllChunks(showingNotice?: boolean): Promise; -} diff --git a/_types/src/lib/src/interfaces/KeyValueDatabase.d.ts b/_types/src/lib/src/interfaces/KeyValueDatabase.d.ts deleted file mode 100644 index cd5ab750..00000000 --- a/_types/src/lib/src/interfaces/KeyValueDatabase.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export interface KeyValueDatabase { - get(key: IDBValidKey): Promise; - set(key: IDBValidKey, value: T): Promise; - del(key: IDBValidKey): Promise; - clear(): Promise; - keys(query?: IDBValidKey | IDBKeyRange, count?: number): Promise; - close(): Promise; - destroy(): Promise; -} diff --git a/_types/src/lib/src/interfaces/ServiceModule.d.ts b/_types/src/lib/src/interfaces/ServiceModule.d.ts deleted file mode 100644 index afd68d6d..00000000 --- a/_types/src/lib/src/interfaces/ServiceModule.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess"; -import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder"; -import type { IFileHandler } from "@lib/interfaces/FileHandler"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess"; -import type { LogFunction } from "@lib/services/lib/logUtils"; -import type { ServiceHub } from "@lib/services/ServiceHub"; -import type { IServiceHub } from "@lib/services/base/IService"; -export interface ServiceModules { - storageAccess: StorageAccess; - /** - * Database File Accessor for handling file operations related to the database, such as exporting the database, importing from a file, etc. - */ - databaseFileAccess: DatabaseFileAccess; - /** - * File Handler for handling file operations related to replication, such as resolving conflicts, applying changes from replication, etc. - */ - fileHandler: IFileHandler; - /** - * Rebuilder for handling database rebuilding operations. - */ - rebuilder: Rebuilder; -} -export type RequiredServices = Pick; -export type RequiredServiceModules = Pick; -export type RequiredServicesInterfaces = Pick; -export type RequiredServiceModulesInterfaces = Pick; -export type NecessaryServices = { - services: RequiredServices; - serviceModules: RequiredServiceModules; -}; -export type NecessaryServicesInterfaces = { - services: RequiredServicesInterfaces; - serviceModules: RequiredServiceModulesInterfaces; -}; -export type ServiceFeatureFunction = (host: NecessaryServices) => TR; -type ServiceFeatureContext = T & { - _log: LogFunction; -}; -export type ServiceFeatureFunctionWithContext = (host: NecessaryServices, context: ServiceFeatureContext) => TR; -/** - * Helper function to create a service feature with proper typing. - * @param featureFunction The feature function to be wrapped. - * @returns The same feature function with proper typing. - * @example - * const myFeatureDef = createServiceFeature(({ services: { API }, serviceModules: { storageAccess } }) => { - * // ... - * }); - * const myFeature = myFeatureDef.bind(null, this); // <- `this` may `ObsidianLiveSyncPlugin` or a custom context object - * appLifecycle.onLayoutReady(myFeature); - */ -export declare function createServiceFeature(featureFunction: ServiceFeatureFunction): ServiceFeatureFunction; -type ContextFactory = (host: NecessaryServices) => ServiceFeatureContext; -export declare function serviceFeature(): { - create(featureFunction: ServiceFeatureFunction): ServiceFeatureFunction; - withContext(ContextFactory: ContextFactory): { - create: (featureFunction: ServiceFeatureFunctionWithContext) => (host: NecessaryServices, context: ServiceFeatureContext) => TR; - }; -}; -export {}; diff --git a/_types/src/lib/src/interfaces/StorageAccess.d.ts b/_types/src/lib/src/interfaces/StorageAccess.d.ts deleted file mode 100644 index 23f185ca..00000000 --- a/_types/src/lib/src/interfaces/StorageAccess.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, FilePathWithPrefix, UXDataWriteOptions, UXFileInfo, UXFileInfoStub, UXFolderInfo, UXStat } from "@lib/common/types"; -import type { CustomRegExp } from "@lib/common/utils"; -import type { FileWithFileStat, FileWithStatAsProp } from "@lib/common/models/fileaccess.type"; -export interface IStorageAccessManager { - processWriteFile(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise): Promise; - processReadFile(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise): Promise; - isFileProcessing(file: UXFileInfoStub | FilePathWithPrefix): boolean; - recentlyTouched(file: FileWithStatAsProp | FileWithFileStat): boolean; - touch(file: FileWithStatAsProp | FileWithFileStat): void; - clearTouched(): void; -} -export interface StorageAccess { - normalisePath(path: string): string; - restoreState(): Promise; - deleteVaultItem(file: FilePathWithPrefix | UXFileInfoStub | UXFolderInfo): Promise; - renameFile(file: UXFileInfoStub | FilePathWithPrefix, newPath: FilePathWithPrefix): Promise; - writeFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise; - readFileAuto(path: string): Promise; - readFileText(path: string): Promise; - isExists(path: string): Promise; - writeHiddenFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise; - appendHiddenFile(path: string, data: string, opt?: UXDataWriteOptions): Promise; - stat(path: string): Promise; - statHidden(path: string): Promise; - removeHidden(path: string): Promise; - readHiddenFileAuto(path: string): Promise; - readHiddenFileBinary(path: string): Promise; - readHiddenFileText(path: string): Promise; - isExistsIncludeHidden(path: string): Promise; - ensureDir(path: string): Promise; - triggerFileEvent(event: string, path: string): void; - triggerHiddenFile(path: string): Promise; - getFileStub(path: string): Promise; - readStubContent(stub: UXFileInfoStub): Promise; - getStub(path: string): Promise; - getFiles(): Promise; - getFileNames(): Promise; - touched(file: UXFileInfoStub | FilePathWithPrefix): Promise; - recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): Promise; - clearTouched(): void; - delete(file: FilePathWithPrefix | UXFileInfoStub | string, force: boolean): Promise; - trash(file: FilePathWithPrefix | UXFileInfoStub | string, system: boolean): Promise; - getFilesIncludeHidden(basePath: string, includeFilter?: CustomRegExp[], excludeFilter?: CustomRegExp[], skipFolder?: string[]): Promise; -} diff --git a/_types/src/lib/src/interfaces/StorageEventManager.d.ts b/_types/src/lib/src/interfaces/StorageEventManager.d.ts deleted file mode 100644 index ed9b7ee1..00000000 --- a/_types/src/lib/src/interfaces/StorageEventManager.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FileEventType, FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/types"; -export type FileEvent = { - type: FileEventType; - file: UXFileInfoStub | UXInternalFileInfoStub; - oldPath?: string; - cachedData?: string; - skipBatchWait?: boolean; - cancelled?: boolean; -}; -export declare abstract class StorageEventManager { - abstract beginWatch(): Promise; - abstract appendQueue(items: FileEvent[], ctx?: unknown): Promise; - abstract isWaiting(filename: FilePath): boolean; - abstract waitForIdle(): Promise; - abstract restoreState(): Promise; -} diff --git a/_types/src/lib/src/managers/ChangeManager.d.ts b/_types/src/lib/src/managers/ChangeManager.d.ts deleted file mode 100644 index 6661907a..00000000 --- a/_types/src/lib/src/managers/ChangeManager.d.ts +++ /dev/null @@ -1,63 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { FallbackWeakRef } from "octagonal-wheels/common/polyfill"; -/** - * Options for configuring the ChangeManager. - */ -export interface ChangeManagerOptions { - /** - * The PouchDB database instance to monitor for changes. - */ - database: PouchDB.Database; -} -export type ChangeManagerCallback = (change: PouchDB.Core.ChangesResponseChange) => void | Promise; -/** - * Manages and dispatches changes from a PouchDB database to registered callbacks. - * - * @template T The type of documents stored in the PouchDB database. - */ -export declare class ChangeManager { - /** - * The PouchDB database instance being monitored. - */ - _database: PouchDB.Database; - /** - * Creates a new instance of the ChangeManager. - * - * @param options - Configuration options for the ChangeManager. - */ - constructor(options: ChangeManagerOptions); - /** - * A list of registered callbacks wrapped in WeakRefs to avoid memory leaks. - */ - _callbacks: FallbackWeakRef>[]; - /** - * Registers a new callback to be invoked when a change occurs. - * - * @param callback - The callback function to register. - */ - addCallback(callback: ChangeManagerCallback): () => void; - removeCallback(callback: ChangeManagerCallback): void; - /** - * The PouchDB changes feed instance, if active. - */ - _changes?: PouchDB.Core.Changes; - /** - * Handles a change event from the PouchDB changes feed. - * - * @param changeResponse - The change response object from the PouchDB changes feed. - */ - _onChange(changeResponse: PouchDB.Core.ChangesResponseChange): Promise; - /** - * Sets up the PouchDB changes feed listener to monitor for database changes. - */ - setupListener(): void; - /** - * Tears down the PouchDB changes feed listener and cleans up resources. - */ - teardown(): void; - /** - * Restarts the PouchDB changes feed listener. - */ - restartWatch(): void; -} diff --git a/_types/src/lib/src/managers/ChunkDeliveryCoordinator.d.ts b/_types/src/lib/src/managers/ChunkDeliveryCoordinator.d.ts deleted file mode 100644 index 05d94f73..00000000 --- a/_types/src/lib/src/managers/ChunkDeliveryCoordinator.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive"; -import type { DocumentID } from "@lib/common/types"; -/** - * A last-resort leak fuse for an accepted delivery claim which reports no - * observable progress. It releases logical ownership so a waiter, and an - * entered bounded activity with its Wake Lock and indicator, cannot be retained - * forever by a stalled transport or never-settling Promise. - * - * Five minutes is a conservative operational ceiling, not a normal - * chunk-arrival budget, evidence of remote absence, or a transport deadline. - * The transport contract cannot yet abort the underlying request when it fires. - */ -export declare const DEFAULT_CHUNK_DELIVERY_STALL_TIMEOUT_MS: number; -export type ActivityCountSource = Pick, "value" | "onChanged" | "offChanged">; -export type ChunkDeliveryClaimOptions = { - stallTimeoutMs?: number; - onStalled?: (ids: readonly DocumentID[]) => void; -}; -export type ChunkDeliveryChangeListener = (ids?: readonly DocumentID[]) => void; -/** A finite claim that keeps chunk arrival waiters paused until every owned identifier settles. */ -export interface ChunkDeliveryClaim { - readonly done: Promise; - readonly pendingIds: readonly DocumentID[]; - release(): void; - settle(id: DocumentID): void; - touch(): void; -} -/** - * Coordinates on-demand chunk ownership with broader finite remote activity. - * - * `ChunkFetcher` owns claims. `ArrivalWaitLayer` observes only whether an - * identifier may still be delivered, so it does not depend on a replicator - * service or on fetch scheduling details. - */ -export declare class ChunkDeliveryCoordinator { - private readonly finiteReplicationActivity?; - private readonly activeClaimCounts; - private readonly claimReleases; - private readonly listeners; - private finiteActivityWasActive; - private readonly finiteActivityChanged; - private disposed; - constructor(finiteReplicationActivity?: ActivityCountSource | undefined); - claim(ids: readonly DocumentID[], options?: ChunkDeliveryClaimOptions): ChunkDeliveryClaim; - isActivityActiveFor(id: DocumentID): boolean; - isClaimActiveFor(id: DocumentID): boolean; - isFiniteReplicationActive(): boolean; - onChanged(listener: ChunkDeliveryChangeListener): () => void; - dispose(): void; - private notifyChanged; -} diff --git a/_types/src/lib/src/managers/ChunkFetcher.d.ts b/_types/src/lib/src/managers/ChunkFetcher.d.ts deleted file mode 100644 index f6c0f209..00000000 --- a/_types/src/lib/src/managers/ChunkFetcher.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type DocumentID } from "@lib/common/types.ts"; -import { type ChunkManager } from "./ChunkManager.ts"; -import type { IReplicatorService, ISettingService } from "@lib/services/base/IService.ts"; -export declare const EVENT_MISSING_CHUNKS = "missingChunks"; -export declare const EVENT_MISSING_CHUNK_REMOTE = "missingChunkRemote"; -export declare const EVENT_CHUNK_FETCHED = "chunkFetched"; -export type ChunkFetcherOptions = { - settingService: ISettingService; - chunkManager: ChunkManager; - replicatorService: IReplicatorService; - deliveryStallTimeoutMs?: number; -}; -export declare class ChunkFetcher { - options: ChunkFetcherOptions; - get chunkManager(): ChunkManager; - queue: DocumentID[]; - private readonly pendingClaims; - private destroyed; - get interval(): number; - get concurrency(): number; - abort: AbortController; - constructor(options: ChunkFetcherOptions); - destroy(): void; - onEventHandler: (ids: DocumentID[]) => void; - onEvent(ids: DocumentID[]): void; - private ensureClaims; - private removePendingDelivery; - private onClaimStalled; - private settleClaim; - private touchClaims; - private waitForActivityBoundary; - /** - * Processing requests - */ - currentProcessing: number; - /** - * Time of the last request to the remote server. - * This is used to manage the interval between requests. - * Even if concurrency allows, every start of a request will ensure that the interval is respected. - */ - previousRequestTime: number; - canRequestMore(): boolean; - requestMissingChunks(): Promise; -} diff --git a/_types/src/lib/src/managers/ChunkManager.d.ts b/_types/src/lib/src/managers/ChunkManager.d.ts deleted file mode 100644 index 616e4dab..00000000 --- a/_types/src/lib/src/managers/ChunkManager.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { LayeredChunkManager } from "./LayeredChunkManager"; -export { LayeredChunkManager as ChunkManager }; diff --git a/_types/src/lib/src/managers/ConflictManager.d.ts b/_types/src/lib/src/managers/ConflictManager.d.ts deleted file mode 100644 index 86bbd2fe..00000000 --- a/_types/src/lib/src/managers/ConflictManager.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type Diff } from "diff-match-patch"; -import { type EntryDoc, type FilePathWithPrefix, type diff_result_leaf, type LoadedEntry, type DIFF_CHECK_RESULT_AUTO } from "@lib/common/types.ts"; -import type { EntryManager } from "@lib/managers/EntryManager/EntryManager.ts"; -import type { IPathService } from "@lib/services/base/IService.ts"; -type AutoMergeOutcomeOK = { - ok: DIFF_CHECK_RESULT_AUTO; -}; -type AutoMergeCanBeDoneByDeletingRev = { - result: string; - conflictedRev: string; -}; -type UserActionRequired = { - leftRev: string; - rightRev: string; - leftLeaf: diff_result_leaf | false; - rightLeaf: diff_result_leaf | false; -}; -export type AutoMergeResult = Promise; -export interface ConflictManagerOptions { - entryManager: EntryManager; - pathService: IPathService; - database: PouchDB.Database; -} -export declare class ConflictManager { - options: ConflictManagerOptions; - constructor(options: ConflictManagerOptions); - get database(): PouchDB.Database; - getConflictedDoc(path: FilePathWithPrefix, rev: string): Promise; - mergeSensibly(path: FilePathWithPrefix, baseRev: string, currentRev: string, conflictedRev: string): Promise; - mergeObject(path: FilePathWithPrefix, baseRev: string, currentRev: string, conflictedRev: string): Promise; - tryAutoMergeSensibly(path: FilePathWithPrefix, test: LoadedEntry, conflicts: string[]): Promise; - tryAutoMerge(path: FilePathWithPrefix, enableMarkdownAutoMerge: boolean): AutoMergeResult; -} -export {}; diff --git a/_types/src/lib/src/managers/EntryManager/EntryManager.d.ts b/_types/src/lib/src/managers/EntryManager/EntryManager.d.ts deleted file mode 100644 index 3c94752e..00000000 --- a/_types/src/lib/src/managers/EntryManager/EntryManager.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type FilePathWithPrefix, type FilePath, type LoadedEntry, type EntryDoc, type SavingEntry, type MetaEntry } from "@lib/common/types"; -import type { ChunkManager } from "@lib/managers/ChunkManager"; -import type { ContentSplitter } from "@lib/ContentSplitter/ContentSplitters"; -import type { HashManager } from "@lib/managers/HashManager/HashManager"; -import type { GeneratedChunk } from "@lib/pouchdb/LiveSyncLocalDB"; -import type { IPathService, ISettingService } from "@lib/services/base/IService"; -export interface EntryManagerOptions { - hashManager: HashManager; - chunkManager: ChunkManager; - splitter: ContentSplitter; - database: PouchDB.Database; - settingService: ISettingService; - pathService: IPathService; -} -export declare class EntryManager { - options: EntryManagerOptions; - constructor(options: EntryManagerOptions); - get localDatabase(): PouchDB.Database; - get hashManager(): HashManager; - get chunkManager(): ChunkManager; - get splitter(): ContentSplitter; - get serviceHost(): { - services: { - setting: ISettingService; - path: IPathService; - }; - serviceModules: {}; // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type - }; - get isOnDemandChunkEnabled(): boolean; - isTargetFile(filenameSrc: string): boolean; - prepareChunk(piece: string): Promise; - getDBEntryMeta(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, includeDeleted?: boolean): Promise; - getDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean): Promise; - getDBEntryFromMeta(meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean): Promise; - deleteDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions): Promise; - putDBEntry(note: SavingEntry, onlyChunks?: boolean, conflictBaseRev?: string): Promise; -} diff --git a/_types/src/lib/src/managers/EntryManager/EntryManagerImpls.d.ts b/_types/src/lib/src/managers/EntryManager/EntryManagerImpls.d.ts deleted file mode 100644 index 1dcc5a61..00000000 --- a/_types/src/lib/src/managers/EntryManager/EntryManagerImpls.d.ts +++ /dev/null @@ -1,89 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type SavingEntry, type DocumentID, type EntryDoc, type EntryBase, type FilePath, type FilePathWithPrefix, type LoadedEntry, type ObsidianLiveSyncSettings, type MetaEntry } from "@lib/common/types"; -import type { ContentSplitter } from "@lib/ContentSplitter/ContentSplitters"; -import type { HashManager } from "@lib/managers/HashManager/HashManager"; -import type { LayeredChunkManager as ChunkManager } from "@lib/managers/LayeredChunkManager"; -import type { NecessaryServicesInterfaces } from "@lib/interfaces/ServiceModule"; -import type { GeneratedChunk } from "@lib/pouchdb/LiveSyncLocalDB"; -type Managers = { - hashManager: HashManager; - chunkManager: ChunkManager; - splitter: ContentSplitter; - localDatabase: PouchDB.Database; -}; -type NecessaryManagers = Pick; -export declare function createChunks(managers: NecessaryManagers<"chunkManager" | "hashManager" | "splitter">, dispFilename: string, note: SavingEntry): Promise; -export declare function putDBEntry(host: NecessaryServicesInterfaces<"path" | "setting", never>, managers: NecessaryManagers<"localDatabase" | "chunkManager" | "hashManager" | "splitter">, note: SavingEntry, onlyChunks?: boolean, conflictBaseRev?: string): Promise; -export declare function isTargetFile(host: NecessaryServicesInterfaces<"setting", never>, filenameSrc: string): boolean; -export declare function prepareChunk({ chunkManager, hashManager }: NecessaryManagers<"chunkManager" | "hashManager">, piece: string): Promise; -export declare function getDBEntryMetaByPath(host: NecessaryServicesInterfaces<"path" | "setting", never>, { localDatabase }: NecessaryManagers<"localDatabase">, path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, includeDeleted?: boolean): Promise; -export declare function isLegacyNote(meta: LoadedEntry | MetaEntry): meta is (import("@lib/common/types").DatabaseEntry & EntryBase & import("@lib/common/types").EntryWithEden & { - path: FilePathWithPrefix; - data: string | string[]; - type: import("../../common/models/db.type").EntryTypes["NOTE_LEGACY"]; -} & { - data: string | string[]; - datatype: import("@lib/common/types").EntryTypeNotes; -}) | (import("@lib/common/types").DatabaseEntry & EntryBase & import("@lib/common/types").EntryWithEden & { - path: FilePathWithPrefix; - data: string | string[]; - type: import("../../common/models/db.type").EntryTypes["NOTE_LEGACY"]; -} & { - children: string[]; -}); -export declare function canUseOnDemandChunking(settings: ObsidianLiveSyncSettings): boolean; -/** - * Decide how to retrieve chunks based on settings and waitForReady flag. - * `waitForReady` allows an already-observable finite delivery lifecycle to finish. - */ -export declare function computeChunkRetrievalMethod(waitForReady: boolean, settings: ObsidianLiveSyncSettings): { - waitForDelivery: boolean; - preventRemoteRequest: boolean; -}; -export declare function getDBEntryFromMeta(host: NecessaryServicesInterfaces<"path" | "setting", never>, { localDatabase, chunkManager }: NecessaryManagers<"localDatabase" | "chunkManager">, meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean): Promise; -export declare function getDBEntryByPath(host: NecessaryServicesInterfaces<"path" | "setting", never>, managers: NecessaryManagers<"localDatabase" | "chunkManager">, path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean): Promise; -export declare function deleteDBEntryByPath(host: NecessaryServicesInterfaces<"path" | "setting", never>, { localDatabase }: NecessaryManagers<"localDatabase">, path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions): Promise; -export {}; diff --git a/_types/src/lib/src/managers/HashManager/HashManager.d.ts b/_types/src/lib/src/managers/HashManager/HashManager.d.ts deleted file mode 100644 index 63da548b..00000000 --- a/_types/src/lib/src/managers/HashManager/HashManager.d.ts +++ /dev/null @@ -1,62 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { HashAlgorithm } from "@lib/common/models/setting.type.ts"; -import { HashManagerCore, type HashManagerCoreOptions } from "./HashManagerCore.ts"; -/** - * Class for managing hash managers and performing hash calculations. - * Selects an appropriate manager according to the available hash algorithm. - */ -export declare class HashManager extends HashManagerCore { - /** - * Instance of the hash manager currently in use. - */ - manager: HashManagerCore; - /** - * Checks whether the specified hash algorithm is available. - * - * @param hashAlg The hash algorithm to check - * @returns True if available - */ - static isAvailableFor(hashAlg: HashAlgorithm): boolean; - /** - * Selects and initialises an available hash manager. - * - * @returns True if initialisation is successful - * @throws Throws an error if no available manager exists - */ - setManager(): Promise; - /** - * Constructs a new HashManager. - * - * @param options Initialisation options - */ - constructor(options: HashManagerCoreOptions); - /** - * Initialises the hash manager. - * - * @returns True if initialisation is successful - * @throws Throws an error if initialisation fails - */ - processInitialise(): Promise; - /** - * Computes the hash value for the specified string. - * - * @param piece The string to be hashed - * @returns The hash value (returned as a Promise) - */ - computeHash(piece: string): Promise; - /** - * Computes the hash value without encryption. - * - * @param piece The string to be hashed - * @returns The hash value (returned as a Promise) - */ - computeHashWithoutEncryption(piece: string): Promise; - /** - * Computes the hash value with encryption. - * - * @param piece The string to be hashed - * @returns The hash value (returned as a Promise) - */ - computeHashWithEncryption(piece: string): Promise; -} diff --git a/_types/src/lib/src/managers/HashManager/HashManagerCore.d.ts b/_types/src/lib/src/managers/HashManager/HashManagerCore.d.ts deleted file mode 100644 index 9c7ab7ac..00000000 --- a/_types/src/lib/src/managers/HashManager/HashManagerCore.d.ts +++ /dev/null @@ -1,116 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ISettingService } from "@lib/services/base/IService.ts"; -import type { HashAlgorithm } from "@lib/common/models/setting.type.ts"; -/** - * Prefix for encrypted hashes. - * - * This constant is prepended to hash strings when encryption is enabled. - */ -export declare const HashEncryptedPrefix = "+"; -/** - * Options for initialising {@link HashManagerCore}. - */ -export type HashManagerCoreOptions = { - /** - * Remote database settings used for hash management. - */ - settingService: ISettingService; -}; -/** - * Abstract base class for hash management. - * - * Provides core logic for handling passphrase hashing, encryption toggling, - * and initialisation routines. Subclasses should implement encryption-specific - * hash computation methods. - */ -export declare abstract class HashManagerCore { - protected settingService: ISettingService; - /** - * Indicates whether encryption is enabled for hash computation. - */ - useEncryption: boolean; - /** - * Hashed passphrase as a string, used for hash operations. - */ - hashedPassphrase: string; - /** - * Hashed passphrase as a 32-bit number, used for hash operations. - */ - hashedPassphrase32: number; - /** - * Options used for initialisation and configuration. - */ - options: HashManagerCoreOptions; - /** - * Constructs a new {@link HashManagerCore} instance. - * - * @param options - Configuration options for hash management. - */ - constructor(options: HashManagerCoreOptions); - /** - * Applies the given options to the hash manager. - * - * Updates encryption settings and computes passphrase hashes. - * - * @param options - Optional configuration to apply. - */ - applyOptions(options?: HashManagerCoreOptions): void; - /** - * Performs initialisation logic specific to the hash manager implementation. - * - * Subclasses must implement this method. - * - * @returns Promise resolving to true if initialisation succeeds. - */ - abstract processInitialise(): Promise; - /** - * Task representing the initialisation process. - */ - initialiseTask?: Promise; - /** - * Ensures the hash manager is initialised. - * - * Returns a promise that resolves when initialisation is complete. - * - * @returns Promise resolving to true if initialisation succeeds. - */ - initialise(): Promise; - /** - * Computes a hash for the given string. - * - * If encryption is enabled, the hash is computed with encryption and prefixed. - * Otherwise, a plain hash is computed. - * - * @param piece - The input string to hash. - * @returns Promise resolving to the computed hash string. - */ - computeHash(piece: string): Promise; - /** - * Computes a hash for the given string without encryption. - * - * Subclasses must implement this method. - * - * @param piece - The input string to hash. - * @returns Promise resolving to the computed hash string. - */ - abstract computeHashWithoutEncryption(piece: string): Promise; - /** - * Computes a hash for the given string with encryption. - * - * Subclasses must implement this method. - * - * @param piece - The input string to hash. - * @returns Promise resolving to the computed encrypted hash string. - */ - abstract computeHashWithEncryption(piece: string): Promise; - /** - * Determines whether the hash manager is available for the specified algorithm. - * - * Subclasses should override this method to indicate supported algorithms. - * - * @param hashAlg - The hash algorithm to check. - * @returns True if available, false otherwise. - */ - static isAvailableFor(hashAlg: HashAlgorithm): boolean; -} diff --git a/_types/src/lib/src/managers/HashManager/PureJSHashManager.d.ts b/_types/src/lib/src/managers/HashManager/PureJSHashManager.d.ts deleted file mode 100644 index 37c59c78..00000000 --- a/_types/src/lib/src/managers/HashManager/PureJSHashManager.d.ts +++ /dev/null @@ -1,79 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { HashManagerCore } from "./HashManagerCore.ts"; -import type { HashAlgorithm } from "@lib/common/models/setting.type.ts"; -/** - * Provides hash management using the "mixed-purejs" algorithm. - * - * This manager utilises a pure JavaScript implementation for hashing. - * It is available only when the hash algorithm is set to "mixed-purejs". - */ -export declare class PureJSHashManager extends HashManagerCore { - /** - * Determines whether this manager is available for the specified algorithm. - * @param hashAlg The hash algorithm to check. - * @returns True if the algorithm is "mixed-purejs". - */ - static isAvailableFor(hashAlg: HashAlgorithm): boolean; - /** - * Initialises the hash manager. - * @returns Always resolves to true. - */ - processInitialise(): Promise; - /** - * Computes a hash for the given input, including encryption. - * @param input The input string to hash. - * @returns The computed hash as a promise. - */ - computeHashWithEncryption(input: string): Promise; - /** - * Computes a hash for the given input, without encryption. - * @param input The input string to hash. - * @returns The computed hash as a promise. - */ - computeHashWithoutEncryption(input: string): Promise; -} -/** - * Provides hash management using the "sha1" algorithm. - * - * This manager utilises a pure JavaScript SHA-1 implementation. - * It is available only when the hash algorithm is set to "sha1". - */ -export declare class SHA1HashManager extends HashManagerCore { - /** - * Determines whether this manager is available for the specified algorithm. - * @param hashAlg The hash algorithm to check. - * @returns True if the algorithm is "sha1". - */ - static isAvailableFor(hashAlg: HashAlgorithm): boolean; - /** - * Initialises the hash manager. - * @returns Always resolves to true. - */ - processInitialise(): Promise; - /** - * Computes a SHA-1 hash for the given input, including encryption. - * @param input The input string to hash. - * @returns The computed SHA-1 hash as a promise. - */ - computeHashWithEncryption(input: string): Promise; - /** - * Computes a SHA-1 hash for the given input, without encryption. - * @param input The input string to hash. - * @returns The computed SHA-1 hash as a promise. - */ - computeHashWithoutEncryption(input: string): Promise; -} -/** - * Fallback hash manager using the pure JavaScript implementation. - * - * This manager is always available and acts as a fallback when no specific algorithm matches. - */ -export declare class FallbackPureJSHashManager extends PureJSHashManager { - /** - * Always returns true, indicating this manager is available for any algorithm. - * @param _hashAlg The hash algorithm (ignored). - * @returns True. - */ - static isAvailableFor(_hashAlg: HashAlgorithm): boolean; -} diff --git a/_types/src/lib/src/managers/HashManager/XXHashHashManager.d.ts b/_types/src/lib/src/managers/HashManager/XXHashHashManager.d.ts deleted file mode 100644 index 752efede..00000000 --- a/_types/src/lib/src/managers/HashManager/XXHashHashManager.d.ts +++ /dev/null @@ -1,98 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { HashManagerCore, type HashManagerCoreOptions } from "./HashManagerCore.ts"; -import type { XXHashAPI } from "xxhash-wasm-102"; -import type { HashAlgorithm } from "@lib/common/models/setting.type.ts"; -/** - * Abstract base class for hash managers using XXHash algorithms. - * Provides initialisation and common properties for XXHash-based managers. - */ -export declare abstract class XXHashHashManager extends HashManagerCore { - /** - * Instance of XXHash API used for hashing operations. - */ - xxhash: XXHashAPI; - /** - * Constructs a new XXHashHashManager. - * @param options - Options for the hash manager core. - */ - constructor(options: HashManagerCoreOptions); - /** - * Initialises the XXHash API instance. - * @returns A promise resolving to true when initialisation is complete. - */ - processInitialise(): Promise; -} -/** - * Hash manager for the legacy hash algorithm (empty string). - * Utilises XXHash32 raw hashing. - */ -export declare class XXHash32RawHashManager extends XXHashHashManager { - /** - * Determines whether this manager is available for the specified algorithm. - * @param hashAlg - The hash algorithm to check. - * @returns True if available, false otherwise. - */ - static isAvailableFor(hashAlg: HashAlgorithm): boolean; - /** - * Computes a hash for the given piece using encryption. - * @param piece - The input string to hash. - * @returns A promise resolving to the hash string. - */ - computeHashWithEncryption(piece: string): Promise; - /** - * Computes a hash for the given piece without encryption. - * @param piece - The input string to hash. - * @returns A promise resolving to the hash string. - */ - computeHashWithoutEncryption(piece: string): Promise; -} -/** - * Hash manager for the XXHash64 algorithm ("xxhash64"). - */ -export declare class XXHash64HashManager extends XXHashHashManager { - /** - * Determines whether this manager is available for the specified algorithm. - * @param hashAlg - The hash algorithm to check. - * @returns True if available, false otherwise. - */ - static isAvailableFor(hashAlg: HashAlgorithm): boolean; - /** - * Computes a hash for the given piece using encryption. - * @param piece - The input string to hash. - * @returns A promise resolving to the hash string. - */ - computeHashWithEncryption(piece: string): Promise; - /** - * Computes a hash for the given piece without encryption. - * @param piece - The input string to hash. - * @returns A promise resolving to the hash string. - */ - computeHashWithoutEncryption(piece: string): Promise; -} -/** - * Fallback hash manager utilising XXHash32. - * Used when no specific algorithm is matched. - * Please be careful with this manager, as it is different from XXHash32RawHashManager. - */ -export declare class FallbackWasmHashManager extends XXHashHashManager { - /** - * Determines whether this manager is available for the specified algorithm. - * Always returns true as a fallback. - * @param hashAlg - The hash algorithm to check. - * @returns True. - */ - static isAvailableFor(hashAlg: HashAlgorithm): boolean; - /** - * Computes a hash for the given piece using encryption. - * @param piece - The input string to hash. - * @returns A promise resolving to the hash string. - */ - computeHashWithEncryption(piece: string): Promise; - /** - * Computes a hash for the given piece without encryption. - * @param piece - The input string to hash. - * @returns A promise resolving to the hash string. - */ - computeHashWithoutEncryption(piece: string): Promise; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager.d.ts b/_types/src/lib/src/managers/LayeredChunkManager.d.ts deleted file mode 100644 index 263e9c09..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager.d.ts +++ /dev/null @@ -1,55 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DocumentID, EntryDoc, EntryLeaf } from "@lib/common/types.ts"; -import type { ChangeManager } from "@lib/managers/ChangeManager.ts"; -import type { ChunkManagerEventMap, ChunkManagerOptions, ChunkReadOptions, ChunkWriteOptions, WriteResult } from "./LayeredChunkManager/types.ts"; -import { ChunkDeliveryCoordinator } from "./ChunkDeliveryCoordinator.ts"; -/** - * ChunkManager class that manages chunk operations such as reading, writing, and caching. - * Now uses a middleware layer architecture for read and write operations. - */ -export declare class LayeredChunkManager { - protected options: ChunkManagerOptions; - protected eventTarget: EventTarget; - private cacheLayer; - private databaseReadLayer; - private readLayers; - private writeLayers; - private arrivalWaitLayer; - readonly deliveryCoordinator: ChunkDeliveryCoordinator; - get changeManager(): ChangeManager; - get database(): PouchDB.Database; - get cacheStatistics(): { - size: number; - allocCount: number; - derefCount: number; - }; - addListener(type: K, listener: (this: LayeredChunkManager, ev: ChunkManagerEventMap[K]) => void, options?: boolean | AddEventListenerOptions): () => void; - emitEvent(type: K, detail: ChunkManagerEventMap[K]): void; - protected abort: AbortController; - protected offChangeHandler: ReturnType; - protected initialised: Promise; - _initialise(): Promise; - constructor(options: ChunkManagerOptions); - destroy(): void; - getCachedChunk(id: DocumentID): EntryLeaf | false; - getChunkIDFromCache(data: string): DocumentID | false; - cacheChunk(chunk: EntryLeaf): void; - clearCaches(): void; - read(ids: DocumentID[], options: ChunkReadOptions, preloadedChunks?: Record): Promise<(EntryLeaf | false)[]>; - private executeReadPipeline; - write(chunks: EntryLeaf[], options: ChunkWriteOptions, origin: DocumentID): Promise; - private executeWritePipeline; - private isChunkDoc; - private onChunkArrived; - protected onChunkArrivedHandler: (doc: EntryLeaf, deleted?: boolean) => void; - private onChange; - protected onChangeHandler: (change: PouchDB.Core.ChangesResponseChange) => void; - onMissingChunkRemote(id: DocumentID): void; - protected onMissingChunkRemoteHandler: (id: DocumentID) => void; - protected concurrentTransactions: number; - protected stabilised: Promise; - transaction(callback: () => Promise): Promise; - _stabilise(): Promise; - __stabilise(): Promise; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager/ArrivalWaitLayer.d.ts b/_types/src/lib/src/managers/LayeredChunkManager/ArrivalWaitLayer.d.ts deleted file mode 100644 index cea41834..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager/ArrivalWaitLayer.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DocumentID, EntryLeaf } from "@lib/common/types"; -import type { IReadLayer } from "./ChunkLayerInterfaces"; -import type { ChunkReadOptions } from "./types.ts"; -import { ChunkDeliveryCoordinator } from "@lib/managers/ChunkDeliveryCoordinator.ts"; -export type ChunkAvailabilityRecheck = (ids: readonly DocumentID[]) => Promise; -/** - * Waits only for a delivery lifecycle which is already observable when the - * local miss is handled. It does not guess at an arrival delay. - */ -export declare class ArrivalWaitLayer implements IReadLayer { - private readonly recheckAvailability?; - private readonly waitingMap; - private readonly eventEmitter; - private readonly deliveryCoordinator; - private readonly ownsDeliveryCoordinator; - private readonly stopObservingActivity; - constructor(eventEmitter: (eventName: string, data: DocumentID[]) => void, deliveryCoordinator?: ChunkDeliveryCoordinator, recheckAvailability?: ChunkAvailabilityRecheck | undefined); - private enqueueWaiting; - private settle; - private settleAfterObservedActivity; - private refreshActivity; - /** Handle a chunk document becoming available. */ - onChunkArrived(doc: EntryLeaf, deleted?: boolean): void; - /** Handle an explicit remote-missing result. */ - onMissingChunk(id: DocumentID): void; - read(ids: DocumentID[], options: ChunkReadOptions, next: (remaining: DocumentID[]) => Promise<(EntryLeaf | false)[]>): Promise<(EntryLeaf | false)[]>; - clearWaiting(): void; - tearDown(): void; - getWaitingCount(): number; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager/CacheLayer.d.ts b/_types/src/lib/src/managers/LayeredChunkManager/CacheLayer.d.ts deleted file mode 100644 index ac695273..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager/CacheLayer.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DocumentID, EntryLeaf } from "@lib/common/types"; -import type { IReadLayer, IWriteLayer } from "./ChunkLayerInterfaces"; -import type { ChunkReadOptions, ChunkWriteOptions, WriteResult } from "./types.ts"; -/** - * Cache layer - manages in-memory cache of chunks. - * Implements both IReadLayer and IWriteLayer for unified cache management. - * This layer is self-contained and handles cache operations for both read and write operations. - */ -export declare class CacheLayer implements IReadLayer, IWriteLayer { - private caches; - private maxCacheSize; - allocCount: number; - derefCount: number; - constructor(maxCacheSize: number); - /** - * Get a cached chunk - */ - getCachedChunk(id: DocumentID): EntryLeaf | false; - /** - * Find chunk ID by data content - */ - getChunkIDFromCache(data: string): DocumentID | false; - /** - * Cache a chunk - */ - cacheChunk(chunk: EntryLeaf): void; - /** - * Reorder chunk for LRU (move to end) - */ - reorderChunk(id: DocumentID): void; - /** - * Delete a cached chunk - */ - deleteCachedChunk(id: DocumentID): void; - /** - * Clear all caches - */ - clearCaches(): void; - /** - * Tear down the layer (clear caches on shutdown) - */ - tearDown(): void; - /** - * Get current cache statistics - */ - getStatistics(): { - size: number; - allocCount: number; - derefCount: number; - }; - /** - * IReadLayer implementation - read from cache - */ - read(ids: DocumentID[], options: ChunkReadOptions, next: (remaining: DocumentID[]) => Promise<(EntryLeaf | false)[]>): Promise<(EntryLeaf | false)[]>; - /** - * IWriteLayer implementation - cache chunks after database write - */ - write(chunks: EntryLeaf[], options: ChunkWriteOptions, origin: DocumentID, next: (remaining: EntryLeaf[]) => Promise): Promise; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager/ChunkLayerInterfaces.d.ts b/_types/src/lib/src/managers/LayeredChunkManager/ChunkLayerInterfaces.d.ts deleted file mode 100644 index 72e1d071..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager/ChunkLayerInterfaces.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DocumentID, EntryLeaf } from "@lib/common/types.ts"; -import type { ChunkReadOptions, ChunkWriteOptions, WriteResult } from "./types.ts"; -/** - * Interface for read layers in the chunk reading pipeline. - * Each layer processes a chunk read request and passes it to the next layer. - */ -export interface IReadLayer { - /** - * Process a read request for the given chunk IDs. - * The next layer in the pipeline should be called to continue processing. - * - * @param ids - The chunk IDs to read - * @param options - Read options - * @param next - The next layer to process the remaining IDs - * @returns A promise that resolves to an array of chunks or false values - */ - read(ids: DocumentID[], options: ChunkReadOptions, next: (remaining: DocumentID[]) => Promise<(EntryLeaf | false)[]>): Promise<(EntryLeaf | false)[]>; - tearDown?(): void; -} -/** - * Interface for write layers in the chunk writing pipeline. - * Each layer processes chunk write requests and passes them to the next layer. - */ -export interface IWriteLayer { - /** - * Process a write request for the given chunks. - * The next layer in the pipeline should be called to continue processing. - * - * @param chunks - The chunks to write - * @param options - Write options - * @param origin - The origin of the write request - * @param next - The next layer to process the remaining chunks - * @returns A promise that resolves to the write result - */ - write(chunks: EntryLeaf[], options: ChunkWriteOptions, origin: DocumentID, next: (remaining: EntryLeaf[]) => Promise): Promise; - tearDown?(): void; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager/DatabaseReadLayer.d.ts b/_types/src/lib/src/managers/LayeredChunkManager/DatabaseReadLayer.d.ts deleted file mode 100644 index 13775b21..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager/DatabaseReadLayer.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EntryLeaf, DocumentID, EntryDoc } from "@lib/common/types"; -import type { IReadLayer } from "./ChunkLayerInterfaces"; -import type { ChunkReadOptions } from "./types.ts"; -/** - * Database read layer - reads chunks from the database - */ -export declare class DatabaseReadLayer implements IReadLayer { - private database; - constructor(database: PouchDB.Database); - private isChunkDoc; - private getError; - private isMissingError; - read(ids: DocumentID[], options: ChunkReadOptions, next: (remaining: DocumentID[]) => Promise<(EntryLeaf | false)[]>): Promise<(EntryLeaf | false)[]>; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager/DatabaseWriteLayer.d.ts b/_types/src/lib/src/managers/LayeredChunkManager/DatabaseWriteLayer.d.ts deleted file mode 100644 index cfab49e4..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager/DatabaseWriteLayer.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EntryLeaf, DocumentID, EntryDoc } from "@lib/common/types"; -import type { IWriteLayer } from "./ChunkLayerInterfaces"; -import type { ChunkWriteOptions, WriteResult } from "./types.ts"; -/** - * Database write layer - writes chunks to the database - */ -export declare class DatabaseWriteLayer implements IWriteLayer { - private database; - constructor(database: PouchDB.Database); - write(chunks: EntryLeaf[], options: ChunkWriteOptions | undefined, origin: DocumentID, next: (remaining: EntryLeaf[]) => Promise): Promise; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager/HotPackLayer.d.ts b/_types/src/lib/src/managers/LayeredChunkManager/HotPackLayer.d.ts deleted file mode 100644 index 9b39ef5e..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager/HotPackLayer.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EntryLeaf, DocumentID } from "@lib/common/types"; -import type { IWriteLayer } from "./ChunkLayerInterfaces"; -import type { ChunkWriteOptions, WriteResult } from "./types.ts"; -/** - * Hot pack layer - placeholder for hot pack processing - */ -export declare class HotPackLayer implements IWriteLayer { - write(chunks: EntryLeaf[], options: ChunkWriteOptions, origin: DocumentID, next: (remaining: EntryLeaf[]) => Promise): Promise; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager/types.d.ts b/_types/src/lib/src/managers/LayeredChunkManager/types.d.ts deleted file mode 100644 index caedc001..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager/types.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EntryDoc } from "@lib/common/models/db.definition"; -import type { DocumentID, EntryLeaf } from "@lib/common/models/db.type"; -import type { ISettingService } from "@lib/services/base/IService"; -import type { ChangeManager } from "@lib/managers/ChangeManager"; -import type { EVENT_CHUNK_FETCHED, EVENT_MISSING_CHUNK_REMOTE, EVENT_MISSING_CHUNKS } from "@lib/managers/ChunkFetcher"; -import type { ActivityCountSource } from "@lib/managers/ChunkDeliveryCoordinator"; -export type ChunkManagerOptions = { - database: PouchDB.Database; - changeManager: ChangeManager; - settingService: ISettingService; - /** Finite replication which may still deliver a requested chunk. */ - finiteReplicationActivity?: ActivityCountSource; -}; -export type ChunkReadOptions = { - skipCache?: boolean; - /** Wait for an already-observable finite delivery lifecycle. */ - waitForDelivery?: boolean; - /** @deprecated Use `waitForDelivery`. Positive values no longer represent an arrival duration. */ - timeout?: number; - preventRemoteRequest?: boolean; -}; -export type ChunkWriteOptions = { - skipCache?: boolean; - force?: boolean; -}; -export type WriteResult = { - result: boolean; - processed: { - cached: number; - hotPack: number; - written: number; - duplicated: number; - }; -}; -export type ChunkManagerEventMap = { - [EVENT_MISSING_CHUNK_REMOTE]: DocumentID; - [EVENT_MISSING_CHUNKS]: DocumentID[]; - [EVENT_CHUNK_FETCHED]: EntryLeaf; -}; diff --git a/_types/src/lib/src/managers/LiveSyncManagers.d.ts b/_types/src/lib/src/managers/LiveSyncManagers.d.ts deleted file mode 100644 index d6476441..00000000 --- a/_types/src/lib/src/managers/LiveSyncManagers.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc } from "@lib/common/types"; -import { ContentSplitter } from "@lib/ContentSplitter/ContentSplitters.ts"; -import { ChangeManager } from "@lib/managers/ChangeManager.ts"; -import { ChunkFetcher } from "@lib/managers/ChunkFetcher.ts"; -import { ChunkManager } from "@lib/managers/ChunkManager.ts"; -import { ConflictManager } from "@lib/managers/ConflictManager.ts"; -import { EntryManager } from "@lib/managers/EntryManager/EntryManager.ts"; -import { HashManager } from "@lib/managers/HashManager/HashManager.ts"; -import type { APIService } from "@lib/services/base/APIService.ts"; -import type { IDatabaseService, IPathService, IReplicatorService, ISettingService } from "@lib/services/base/IService.ts"; -import { type LogFunction } from "@lib/services/lib/logUtils.ts"; -export interface LiveSyncManagersOptions { - database: PouchDB.Database; - databaseService: IDatabaseService; - settingService: TSettingService; - pathService: IPathService; - replicatorService: IReplicatorService; - APIService: APIService; -} -export declare class LiveSyncManagers { - protected _pathService: IPathService; - protected _replicatorService: IReplicatorService; - protected _settingService: ISettingService; - protected _APIService: APIService; - hashManager: HashManager; - chunkFetcher: ChunkFetcher; - changeManager: ChangeManager; - chunkManager: ChunkManager; - splitter: ContentSplitter; - entryManager: EntryManager; - conflictManager: ConflictManager; - protected options: LiveSyncManagersOptions; - protected log: LogFunction; - constructor(options: LiveSyncManagersOptions); - teardownManagers(): Promise; - protected getManagerMembers(): { - changeManager: ChangeManager; - hashManager: HashManager; - splitter: ContentSplitter; - chunkManager: ChunkManager; - chunkFetcher: ChunkFetcher; - entryManager: EntryManager; - conflictManager: ConflictManager; - }; - initialise(): Promise; - reinitialise(): Promise; - clearCaches(): void; - prepareHashFunction(): Promise; -} diff --git a/_types/src/lib/src/managers/StorageEventManager.d.ts b/_types/src/lib/src/managers/StorageEventManager.d.ts deleted file mode 100644 index e7dcc1a8..00000000 --- a/_types/src/lib/src/managers/StorageEventManager.d.ts +++ /dev/null @@ -1,137 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type FileEventType, type FilePath, type UXFileInfoStub, type UXFolderInfo, type UXInternalFileInfoStub } from "@lib/common/types.ts"; -import { type FileEventItem } from "@lib/common/types.ts"; -import type { IStorageAccessManager } from "@lib/interfaces/StorageAccess.ts"; -import { type PromiseWithResolvers } from "octagonal-wheels/promises"; -import { StorageEventManager, type FileEvent } from "@lib/interfaces/StorageEventManager.ts"; -import type { IAPIService, IVaultService } from "@lib/services/base/IService.ts"; -import type { SettingService } from "@lib/services/base/SettingService.ts"; -import type { FileProcessingService } from "@lib/services/base/FileProcessingService.ts"; -import { createInstanceLogFunction } from "@lib/services/lib/logUtils"; -import type { IStorageEventManagerAdapter } from "./adapters"; -import { type CompatTimeoutHandle } from "@lib/common/coreEnvFunctions"; -type WaitInfo = { - since: number; - type: FileEventType; - canProceed: PromiseWithResolvers; - timerHandler: CompatTimeoutHandle; - event: FileEventItem; -}; -declare const TYPE_SENTINEL_FLUSH = "SENTINEL_FLUSH"; -type FileEventItemSentinelFlush = { - type: typeof TYPE_SENTINEL_FLUSH; -}; -export type FileEventItemSentinel = FileEventItemSentinelFlush; -export interface StorageEventManagerBaseDependencies { - setting: SettingService; - vaultService: IVaultService; - fileProcessing: FileProcessingService; - storageAccessManager: IStorageAccessManager; - APIService: IAPIService; -} -/** - * Type helper to extract the file type from a storage event manager adapter - */ -export type ExtractFile = T extends IStorageEventManagerAdapter ? F : never; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -/** - * Type helper to extract the folder type from a storage event manager adapter - */ -export type ExtractFolder = T extends IStorageEventManagerAdapter ? D : never; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -/** - * Base class for storage event management - * Uses adapter pattern for platform-specific implementations - * - * @template TAdapter - The storage event manager adapter type - */ -export declare abstract class StorageEventManagerBase> extends StorageEventManager { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - _log: ReturnType; - protected setting: SettingService; - protected vaultService: IVaultService; - protected fileProcessing: FileProcessingService; - protected storageAccess: IStorageAccessManager; - protected adapter: TAdapter; - protected get shouldBatchSave(): boolean; - protected get batchSaveMinimumDelay(): number; - protected get batchSaveMaximumDelay(): number; - get settings(): import("@lib/common/types.ts").ObsidianLiveSyncSettings; - constructor(adapter: TAdapter, dependencies: StorageEventManagerBaseDependencies); - _saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]): Promise; - _loadSnapshot(): Promise<(FileEventItem | FileEventItemSentinel)[] | null>; - isFolder(file: UXFileInfoStub | UXInternalFileInfoStub | UXFolderInfo | ExtractFolder | ExtractFile): boolean; - isFile(file: UXFileInfoStub | UXInternalFileInfoStub | UXFolderInfo | ExtractFolder | ExtractFile): boolean; - protected updateStatus(): void; - /** - * Snapshot restoration promise. - * Snapshot will be restored before starting to watch vault changes. - * In designed time, this has been called from Initialisation process, which has been implemented on `ModuleInitializerFile.ts`. - */ - snapShotRestored: Promise | null; - /** - * Restore the previous snapshot if exists. - * @returns - */ - restoreState(): Promise; - appendQueue(params: FileEvent[], ctx?: unknown): Promise; - protected bufferedQueuedItems: (FileEventItem | FileEventItemSentinel)[]; - enqueue(newItem: FileEventItem): void; - /** - * Immediately take snapshot. - */ - private _triggerTakeSnapshot; - /** - * Trigger taking snapshot after throttled period. - */ - triggerTakeSnapshot: import("octagonal-wheels/function").ThrottledFunction<() => void>; - protected concurrentProcessing: import("octagonal-wheels/concurrency/semaphore_v2").SemaphoreObject; - protected _waitingMap: Map; - private _waitForIdle; - /** - * Wait until all queued events are processed. - * Subsequent new events will not be waited, but new events will not be added. - * @returns - */ - waitForIdle(): Promise; - /** - * Proceed waiting for the given key immediately. - */ - private _proceedWaiting; - /** - * Cancel waiting for the given key. - */ - private _cancelWaiting; - /** - * Add waiting for the given key. - * @param key - * @param event - * @param waitedSince Optional waited since timestamp to calculate the remaining delay. - */ - private _addWaiting; - /** - * Process the given file event. - */ - processFileEvent(fei: FileEventItem): Promise; - _takeSnapshot(): Promise; - _restoreFromSnapshot(): Promise; - protected runQueuedEvents(): Promise; - protected processingCount: number; - protected requestProcessQueue(fei: FileEventItem): Promise; - isWaiting(filename: FilePath): boolean; - protected handleFileEvent(queue: FileEventItem): Promise; - protected cancelRelativeEvent(item: FileEventItem): void; - /** - * Begin watching for storage events - */ - beginWatch(): Promise; - /** - * Platform-agnostic event handlers - */ - protected watchEditorChange(editor: TEditor, info: TInfo): void; - protected watchVaultCreate(file: TFile, ctx?: TCtx): void; - protected watchVaultChange(file: TFile, ctx?: TCtx): void; - protected watchVaultDelete(file: TFile, ctx?: TCtx): void; - protected watchVaultRename(file: TFile, oldPath: string, ctx?: TCtx): void; - protected watchVaultRawEvents(path: FilePath): void; - protected _watchVaultRawEvents(path: FilePath): Promise; -} -export {}; diff --git a/_types/src/lib/src/managers/StorageProcessingManager.d.ts b/_types/src/lib/src/managers/StorageProcessingManager.d.ts deleted file mode 100644 index 8eb8bff1..00000000 --- a/_types/src/lib/src/managers/StorageProcessingManager.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePathWithPrefix } from "@lib/common/models/db.type"; -import type { UXFileInfoStub } from "@lib/common/models/fileaccess.type"; -import type { IStorageAccessManager } from "@lib/interfaces/StorageAccess"; -import type { FileWithFileStat, FileWithStatAsProp } from "@lib/common/models/fileaccess.type"; -export declare class StorageAccessManager implements IStorageAccessManager { - processingFiles: Set; - processWriteFile(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise): Promise; - processReadFile(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise): Promise; - isFileProcessing(file: UXFileInfoStub | FilePathWithPrefix): boolean; - private touchedFiles; - touch(file: FileWithFileStat | FileWithStatAsProp): void; - recentlyTouched(file: FileWithStatAsProp | FileWithFileStat): boolean; - clearTouched(): void; -} diff --git a/_types/src/lib/src/managers/adapters/IStorageEventConverterAdapter.d.ts b/_types/src/lib/src/managers/adapters/IStorageEventConverterAdapter.d.ts deleted file mode 100644 index fb0da087..00000000 --- a/_types/src/lib/src/managers/adapters/IStorageEventConverterAdapter.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/types"; -/** - * Adapter interface for converting platform-specific file types to UX types - * - * @template TFile - Platform-specific file type - */ -export interface IStorageEventConverterAdapter { - /** - * Convert platform-specific file to UXFileInfoStub - */ - toFileInfo(file: TFile, deleted?: boolean): UXFileInfoStub; - /** - * Convert path to UXInternalFileInfoStub - */ - toInternalFileInfo(path: FilePath): UXInternalFileInfoStub; -} diff --git a/_types/src/lib/src/managers/adapters/IStorageEventManagerAdapter.d.ts b/_types/src/lib/src/managers/adapters/IStorageEventManagerAdapter.d.ts deleted file mode 100644 index 3aac1c44..00000000 --- a/_types/src/lib/src/managers/adapters/IStorageEventManagerAdapter.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { IStorageEventTypeGuardAdapter } from "./IStorageEventTypeGuardAdapter"; -import type { IStorageEventPersistenceAdapter } from "./IStorageEventPersistenceAdapter"; -import type { IStorageEventWatchAdapter } from "./IStorageEventWatchAdapter"; -import type { IStorageEventStatusAdapter } from "./IStorageEventStatusAdapter"; -import type { IStorageEventConverterAdapter } from "./IStorageEventConverterAdapter"; -/** - * Composite adapter interface for StorageEventManager - * - * @template TFile - Platform-specific file type - * @template TFolder - Platform-specific folder type - */ -export interface IStorageEventManagerAdapter { - readonly typeGuard: IStorageEventTypeGuardAdapter; - readonly persistence: IStorageEventPersistenceAdapter; - readonly watch: IStorageEventWatchAdapter; - readonly status: IStorageEventStatusAdapter; - readonly converter: IStorageEventConverterAdapter; -} diff --git a/_types/src/lib/src/managers/adapters/IStorageEventPersistenceAdapter.d.ts b/_types/src/lib/src/managers/adapters/IStorageEventPersistenceAdapter.d.ts deleted file mode 100644 index 1c82860d..00000000 --- a/_types/src/lib/src/managers/adapters/IStorageEventPersistenceAdapter.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FileEventItem } from "@lib/common/types"; -import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager"; -/** - * Adapter interface for snapshot persistence operations - */ -export interface IStorageEventPersistenceAdapter { - /** - * Save the snapshot of pending events - */ - saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]): Promise; - /** - * Load the snapshot of pending events - */ - loadSnapshot(): Promise<(FileEventItem | FileEventItemSentinel)[] | null>; -} diff --git a/_types/src/lib/src/managers/adapters/IStorageEventStatusAdapter.d.ts b/_types/src/lib/src/managers/adapters/IStorageEventStatusAdapter.d.ts deleted file mode 100644 index b3a8a892..00000000 --- a/_types/src/lib/src/managers/adapters/IStorageEventStatusAdapter.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** - * Adapter interface for status update operations - */ -export interface IStorageEventStatusAdapter { - /** - * Update the status display - */ - updateStatus(status: { - batched: number; - processing: number; - totalQueued: number; - }): void; -} diff --git a/_types/src/lib/src/managers/adapters/IStorageEventTypeGuardAdapter.d.ts b/_types/src/lib/src/managers/adapters/IStorageEventTypeGuardAdapter.d.ts deleted file mode 100644 index c402f5b3..00000000 --- a/_types/src/lib/src/managers/adapters/IStorageEventTypeGuardAdapter.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** - * Adapter interface for type guard operations in StorageEventManager - * - * @template TFile - Platform-specific file type - * @template TFolder - Platform-specific folder type - */ -export interface IStorageEventTypeGuardAdapter { - /** - * Check if the given item is a file - */ - isFile(file: unknown): file is TFile; - /** - * Check if the given item is a folder - */ - isFolder(item: unknown): item is TFolder; -} diff --git a/_types/src/lib/src/managers/adapters/IStorageEventWatchAdapter.d.ts b/_types/src/lib/src/managers/adapters/IStorageEventWatchAdapter.d.ts deleted file mode 100644 index 07394791..00000000 --- a/_types/src/lib/src/managers/adapters/IStorageEventWatchAdapter.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath } from "@lib/common/types"; -/** - * Event handlers for storage events - */ -export interface IStorageEventWatchHandlers { - onCreate: (file: TFile, ctx?: TCtx) => void; - onChange: (file: TFile, ctx?: TCtx) => void; - onDelete: (file: TFile, ctx?: TCtx) => void; - onRename: (file: TFile, oldPath: string, ctx?: TCtx) => void; - onRaw: (path: FilePath) => void; - onEditorChange?: (editor: TEditor, info: TInfo) => void; -} -/** - * Adapter interface for watching vault/storage events - */ -export interface IStorageEventWatchAdapter { - /** - * Begin watching for storage events - */ - beginWatch(handlers: IStorageEventWatchHandlers): Promise; -} diff --git a/_types/src/lib/src/managers/adapters/index.d.ts b/_types/src/lib/src/managers/adapters/index.d.ts deleted file mode 100644 index a7420210..00000000 --- a/_types/src/lib/src/managers/adapters/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type { IStorageEventTypeGuardAdapter } from "./IStorageEventTypeGuardAdapter"; -export type { IStorageEventPersistenceAdapter } from "./IStorageEventPersistenceAdapter"; -export type { IStorageEventWatchAdapter, IStorageEventWatchHandlers } from "./IStorageEventWatchAdapter"; -export type { IStorageEventStatusAdapter } from "./IStorageEventStatusAdapter"; -export type { IStorageEventConverterAdapter } from "./IStorageEventConverterAdapter"; -export type { IStorageEventManagerAdapter } from "./IStorageEventManagerAdapter"; diff --git a/_types/src/lib/src/mock_and_interop/stores.d.ts b/_types/src/lib/src/mock_and_interop/stores.d.ts deleted file mode 100644 index 810ad2f0..00000000 --- a/_types/src/lib/src/mock_and_interop/stores.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LOG_LEVEL } from "@lib/common/types.ts"; -export type LockStats = { - pending: string[]; - running: string[]; - count: number; -}; -export declare const lockStats: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource<{ - pending: never[]; - running: never[]; - count: number; -}>; -export declare const collectingChunks: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; -export declare const pluginScanningCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; -export declare const hiddenFilesProcessingCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; -export declare const hiddenFilesEventCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; -export type LogEntry = { - message: string | Error; - level?: LOG_LEVEL; - key?: string; -}; -export declare const logMessages: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; diff --git a/_types/src/lib/src/mock_and_interop/wrapper.d.ts b/_types/src/lib/src/mock_and_interop/wrapper.d.ts deleted file mode 100644 index f31cc6b8..00000000 --- a/_types/src/lib/src/mock_and_interop/wrapper.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare class WrappedNotice { - constructor(message: string | DocumentFragment, timeout?: number); - setMessage(message: string | DocumentFragment): this; - hide(): void; -} -export declare function setNoticeClass(notice: typeof WrappedNotice): void; -export declare function NewNotice(message: string | DocumentFragment, timeout?: number): WrappedNotice; diff --git a/_types/src/lib/src/mods.d.ts b/_types/src/lib/src/mods.d.ts deleted file mode 100644 index d70f75ac..00000000 --- a/_types/src/lib/src/mods.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function getWebCrypto(): Promise; diff --git a/_types/src/lib/src/pouchdb/LiveSyncDBFunctions.d.ts b/_types/src/lib/src/pouchdb/LiveSyncDBFunctions.d.ts deleted file mode 100644 index 3c744468..00000000 --- a/_types/src/lib/src/pouchdb/LiveSyncDBFunctions.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc, type EntryMilestoneInfo, type RemoteDBSettings, type ChunkVersionRange, type TweakValues, type DeviceInfo } from "@lib/common/types.ts"; -export type ENSURE_DB_RESULT = "OK" | "INCOMPATIBLE" | "LOCKED" | "NODE_LOCKED" | "NODE_CLEANED" | ["MISMATCHED", TweakValues]; -/** - * Ensures that the remote database is compatible with the current device. - * - * @param infoSrc - The information about the remote database (which retrieved from the remote). - * @param setting - The current settings. - * @param deviceNodeID - The ID of the current device node. - * @param currentVersionRange - The current version range of the database. - * @param updateCallback - The callback function to update the remote milestone. - * @returns A promise that resolves to the result of ensuring compatibility. - */ -export declare function ensureRemoteIsCompatible(infoSrc: EntryMilestoneInfo | false, setting: RemoteDBSettings, deviceNodeID: string, currentVersionRange: ChunkVersionRange, nodeDeviceInfo: DeviceInfo, updateCallback: (info: EntryMilestoneInfo) => Promise): Promise; -export declare function ensureDatabaseIsCompatible(db: PouchDB.Database, setting: RemoteDBSettings, deviceNodeID: string, currentVersionRange: ChunkVersionRange, nodeDeviceInfo: DeviceInfo): Promise; diff --git a/_types/src/lib/src/pouchdb/LiveSyncLocalDB.d.ts b/_types/src/lib/src/pouchdb/LiveSyncLocalDB.d.ts deleted file mode 100644 index f44997e4..00000000 --- a/_types/src/lib/src/pouchdb/LiveSyncLocalDB.d.ts +++ /dev/null @@ -1,195 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc, type EntryLeaf, type Credential, type RemoteDBSettings, type DocumentID, type FilePathWithPrefix, type FilePath, type DatabaseEntry, type LoadedEntry, type MetaEntry, type SavingEntry, type diff_result_leaf } from "@lib/common/types.ts"; -import { eventHub } from "@lib/hub/hub.ts"; -import { LiveSyncManagers } from "@lib/managers/LiveSyncManagers.ts"; -import type { AutoMergeResult } from "@lib/managers/ConflictManager.ts"; -import type { IServiceHub } from "@lib/services/base/IService.ts"; -import { type LogFunction } from "@lib/services/lib/logUtils.ts"; -export declare const REMOTE_CHUNK_FETCHED = "remote-chunk-fetched"; -export type REMOTE_CHUNK_FETCHED = typeof REMOTE_CHUNK_FETCHED; -declare global { - interface LSEvents { - [REMOTE_CHUNK_FETCHED]: EntryLeaf; - } -} -export type ChunkRetrievalResultSuccess = { - _id: DocumentID; - data: string; - type: "leaf"; -}; -export type ChunkRetrievalResultError = { - _id: DocumentID; - error: string; -}; -export type ChunkRetrievalResult = ChunkRetrievalResultSuccess | ChunkRetrievalResultError; -export interface LiveSyncLocalDBEnv { - services: Pick; -} -export declare function getNoFromRev(rev: string): number; -export type GeneratedChunk = { - isNew: boolean; - id: DocumentID; - piece: string; -}; -export declare class LiveSyncLocalDB { - auth: Credential; - dbname: string; - settings: RemoteDBSettings; - localDatabase: PouchDB.Database; - _log: LogFunction; - private _managers?; - get managers(): LiveSyncManagers; - isReady: boolean; - needScanning: boolean; - env: LiveSyncLocalDBEnv; - clearCaches(): void; - _prepareHashFunctions(): Promise; - onunload(): void; - refreshSettings(): void; - offRemoteChunkFetchedHandler?: ReturnType; - constructor(dbname: string, env: LiveSyncLocalDBEnv); - close(): Promise; - onNewLeaf(chunk: EntryLeaf): void; - initializeDatabase(): Promise; - /** - * Retrieve all used and existing chunks in the database. - * @param includeDeleted include deleted chunks in the result. - * @returns {used: Set, existing: Map} used: Set of chunk ids that are used in the database. existing: Map of chunk id and EntryLeaf that are existing in the database. - */ - allChunks(includeDeleted?: boolean): Promise<{ - used: Set; - existing: Map; - }>; - resetDatabase(): Promise; - findEntries(startKey: string, endKey: string, opt: PouchDB.Core.AllDocsWithKeyOptions | PouchDB.Core.AllDocsOptions | PouchDB.Core.AllDocsWithKeysOptions | PouchDB.Core.AllDocsWithinRangeOptions): AsyncGenerator<(DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_PLAIN"]; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - deleted?: boolean; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_PLAIN"]; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - deleted?: boolean; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta), void, unknown>; - findAllDocs(opt?: PouchDB.Core.AllDocsWithKeyOptions | PouchDB.Core.AllDocsOptions | PouchDB.Core.AllDocsWithKeysOptions | PouchDB.Core.AllDocsWithinRangeOptions): AsyncGenerator<(DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_PLAIN"]; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - deleted?: boolean; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_PLAIN"]; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - deleted?: boolean; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta), void, unknown>; - findEntryNames(startKey: string, endKey: string, opt: PouchDB.Core.AllDocsWithKeyOptions | PouchDB.Core.AllDocsOptions | PouchDB.Core.AllDocsWithKeysOptions | PouchDB.Core.AllDocsWithinRangeOptions): AsyncGenerator; - findAllDocNames(opt?: PouchDB.Core.AllDocsWithKeyOptions | PouchDB.Core.AllDocsOptions | PouchDB.Core.AllDocsWithKeysOptions | PouchDB.Core.AllDocsWithinRangeOptions): AsyncGenerator; - findAllNormalDocs(opt?: PouchDB.Core.AllDocsWithKeyOptions | PouchDB.Core.AllDocsOptions | PouchDB.Core.AllDocsWithKeysOptions | PouchDB.Core.AllDocsWithinRangeOptions): AsyncGenerator<(DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_PLAIN"]; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - deleted?: boolean; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_PLAIN"]; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - deleted?: boolean; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta), void, unknown>; - removeRevision(docId: DocumentID, revision: string): Promise; - getRaw(docId: DocumentID, options?: PouchDB.Core.GetOptions): Promise; - removeRaw(docId: DocumentID, revision: string, options?: PouchDB.Core.Options): Promise; - putRaw(doc: T, options?: PouchDB.Core.PutOptions): Promise; - allDocsRaw(options?: PouchDB.Core.AllDocsWithKeyOptions | PouchDB.Core.AllDocsWithKeysOptions | PouchDB.Core.AllDocsWithinRangeOptions | PouchDB.Core.AllDocsOptions): Promise>; - bulkDocsRaw(docs: Array>, options?: PouchDB.Core.BulkDocsOptions): Promise>; - isTargetFile(filenameSrc: string): boolean; - getDBEntryMeta(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, includeDeleted?: boolean): Promise; - getDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean): Promise; - getDBEntryFromMeta(meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean): Promise; - deleteDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions): Promise; - putDBEntry(note: SavingEntry, onlyChunks?: boolean, conflictBaseRev?: string): Promise; - getConflictedDoc(path: FilePathWithPrefix, rev: string): Promise; - tryAutoMerge(path: FilePathWithPrefix, enableMarkdownAutoMerge: boolean): AutoMergeResult; -} diff --git a/_types/src/lib/src/pouchdb/ReplicatorShim.d.ts b/_types/src/lib/src/pouchdb/ReplicatorShim.d.ts deleted file mode 100644 index 29a06cac..00000000 --- a/_types/src/lib/src/pouchdb/ReplicatorShim.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type SomeDocument = PouchDB.Core.ExistingDocument & PouchDB.Core.ChangesMeta; -/** - * Minimal subset of the PouchDB public API required by {@link replicateShim}. - * Both a real `PouchDB.Database` and an {@link RpcPouchDBProxy} satisfy this - * interface, allowing replication across an RPC transport. - */ -export type PouchDBShim = { - info: () => Promise; - changes: (options: PouchDB.Core.ChangesOptions) => PromiseLike>; - revsDiff: (diff: PouchDB.Core.RevisionDiffOptions) => Promise; - bulkDocs: (docs: PouchDB.Core.PutDocument[], options?: PouchDB.Core.BulkDocsOptions) => Promise<(PouchDB.Core.Response | PouchDB.Core.Error)[]>; - bulkGet: (options: PouchDB.Core.BulkGetOptions) => Promise>; - put: (doc: PouchDB.Core.PutDocument, options?: PouchDB.Core.PutOptions) => Promise; - get: (id: string, options?: PouchDB.Core.GetOptions) => Promise; -}; -type CompatibleDatabase = PouchDB.Database> | PouchDBShim>; -/** Upserts a document by `id`, calling `func` to produce the updated version. */ -export declare function upsert = CompatibleDatabase, T extends SomeDocument = SomeDocument>(db: TDB, id: string, func: (doc: T) => T): Promise; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export type ShimReplicationOptionBase = { - rewind?: boolean; - batch_size?: number; -}; -export type ShimReplicationOneShot = { - live?: false; - controller?: AbortController; -} & ShimReplicationOptionBase; -export type ShimReplicationOptionContinuous = { - live: true; - controller: AbortController; -} & ShimReplicationOptionBase; -export type ShimReplicationOption = ShimReplicationOneShot | ShimReplicationOptionContinuous; -export type ProgressInfo = { - lastSeq: number; - maxSeqInBatch: number; -}; -export type ShimReplicationProgressReportFunc = (progress: SomeDocument[], progressInfo: ProgressInfo) => Promise; -/** - * Replicate documents from `sourceDB` into `targetDB` using a CouchDB-style - * checkpoint protocol. - * - * Both parameters accept either a real `PouchDB.Database` or any object - * implementing {@link PouchDBShim} — including {@link RpcPouchDBProxy} — so - * replication can span an RPC transport boundary. - * - * @param targetDB Destination database (usually local). - * @param sourceDB Source database (may be remote / RPC-backed). - * @param progress Called after each batch with the written documents. - * @param option Replication options (live mode, batch size, abort signal). - */ -export declare function replicateShim, U extends CompatibleDatabase, V extends object>(targetDB: T, sourceDB: U, progress: ShimReplicationProgressReportFunc, option?: ShimReplicationOption): Promise; -export {}; diff --git a/_types/src/lib/src/pouchdb/StreamingFetch.d.ts b/_types/src/lib/src/pouchdb/StreamingFetch.d.ts deleted file mode 100644 index e8d7e18c..00000000 --- a/_types/src/lib/src/pouchdb/StreamingFetch.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EntryDoc } from "@lib/common/models/db.definition"; -import type { AnyEntry, EntryLeaf } from "@lib/common/models/db.type"; -type DBSequence = number | string; -export type FetchChangesForInitialSyncProgress = { - totalFetched: number; - totalValidFetched: number; - targetSeq: number | string; - docsToFetch: number; - totalBytes: number; -}; -/** - * Fetches initial data from CouchDB as a stream and writes it into PouchDB. - * @param downloadToDB PouchDB instance. - * @param remoteDbUrl CouchDB database URL (for example: 'https://xxx.com/mydb'). - * @param decryptFunction Function to decrypt each document. - * @param since Sequence ID to start fetching changes from (default is '0'). - */ -export declare function fetchChangesForInitialSync(downloadToDB: PouchDB.Database, remoteDbUrl: string, authHeader: string, decryptFunction: (doc: EntryDoc) => Promise, since?: number | string, onProgress?: (progress: FetchChangesForInitialSyncProgress) => void, onCheckpoint?: (sequence: DBSequence) => void | Promise): Promise; -export {}; diff --git a/_types/src/lib/src/pouchdb/StreamingFetch.integration.spec.d.ts b/_types/src/lib/src/pouchdb/StreamingFetch.integration.spec.d.ts deleted file mode 100644 index 467855c2..00000000 --- a/_types/src/lib/src/pouchdb/StreamingFetch.integration.spec.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export {}; diff --git a/_types/src/lib/src/pouchdb/chunks.d.ts b/_types/src/lib/src/pouchdb/chunks.d.ts deleted file mode 100644 index 033d0865..00000000 --- a/_types/src/lib/src/pouchdb/chunks.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { CouchDBConnection } from "@lib/common/types"; -export declare function purgeUnreferencedChunks(db: PouchDB.Database, dryRun: boolean, connSetting?: CouchDBConnection, performCompact?: boolean): Promise; -export declare function transferChunks(key: string, label: string, dbFrom: PouchDB.Database, dbTo: PouchDB.Database, items: { - id: string; - rev: string; -}[]): Promise; -export declare function balanceChunkPurgedDBs(local: PouchDB.Database, remote: PouchDB.Database): Promise; -export declare function fetchAllUsedChunks(local: PouchDB.Database, remote: PouchDB.Database): Promise; -export declare function purgeChunksLocal(db: PouchDB.Database, docs: { - id: string; - rev: string; -}[]): Promise; -export declare function collectUnbalancedChunkIDs(local: PouchDB.Database, remote: PouchDB.Database): Promise<{ - onlyOnLocal: { - id: string; - rev: string; - }[]; - onlyOnRemote: { - id: string; - rev: string; - }[]; -}>; -export declare function collectChunks(db: PouchDB.Database, type: "INUSE" | "DANGLING" | "ALL"): Promise<{ - id: string; - rev: string; -}[]>; -export declare function collectChunksUsage(db: PouchDB.Database): Promise<{ - value: number; - key: string[]; -}[]>; -export declare function collectUnreferencedChunks(db: PouchDB.Database): Promise<{ - id: string; - rev: string; -}[]>; -export declare function purgeChunksRemote(setting: CouchDBConnection, docs: { - id: string; - rev: string; -}[]): Promise; diff --git a/_types/src/lib/src/pouchdb/compress.d.ts b/_types/src/lib/src/pouchdb/compress.d.ts deleted file mode 100644 index 218643b9..00000000 --- a/_types/src/lib/src/pouchdb/compress.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import * as fflate from "fflate"; -import type { EntryDoc } from "@lib/common/types"; -export declare function _compressText(text: string): Promise; -export declare const wrappedInflate: (data: Uint8Array, opts: fflate.AsyncInflateOptions) => Promise; -export declare const wrappedDeflate: (data: Uint8Array, opts: fflate.AsyncDeflateOptions) => Promise; -export declare function _decompressText(compressed: string, _useUTF16?: boolean): Promise; -export declare function compressDoc(doc: EntryDoc): Promise; -export declare function decompressDoc(doc: EntryDoc): Promise; -export declare function wrapFflateFunc(func: (data: T, opts: U, cb: fflate.FlateCallback) => unknown): (data: T, opts: U) => Promise; -export declare const replicationFilter: (db: PouchDB.Database, compress: boolean) => void; -export declare const MARK_SHIFT_COMPRESSED = "\u000ELZ\u001D"; diff --git a/_types/src/lib/src/pouchdb/encryption.d.ts b/_types/src/lib/src/pouchdb/encryption.d.ts deleted file mode 100644 index c509c9a3..00000000 --- a/_types/src/lib/src/pouchdb/encryption.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc, type AnyEntry, type EntryLeaf, type DocumentID, type E2EEAlgorithm } from "@lib/common/types"; -import { encryptWorker, decryptWorker, encryptHKDFWorker, decryptHKDFWorker } from "@lib/worker/bgWorker.ts"; -export declare const encrypt: typeof encryptWorker; -export declare const decrypt: typeof decryptWorker; -export declare const encryptHKDF: typeof encryptHKDFWorker; -export declare const decryptHKDF: typeof decryptHKDFWorker; -export declare let preprocessOutgoing: (doc: AnyEntry | EntryLeaf) => Promise; -export declare let preprocessIncoming: (doc: EntryDoc) => Promise; -export declare function getConfiguredFunctionsForEncryption(passphrase: string, useDynamicIterationCount: boolean, migrationDecrypt: boolean, getPBKDF2Salt: () => Promise, algorithm: E2EEAlgorithm): { - incoming: (doc: AnyEntry | EntryLeaf) => Promise; - outgoing: (doc: EntryDoc) => Promise; -}; -export declare const enableEncryption: (db: PouchDB.Database, passphrase: string, useDynamicIterationCount: boolean, migrationDecrypt: boolean, getPBKDF2Salt: () => Promise, algorithm: E2EEAlgorithm) => void; -export declare function disableEncryption(): void; -export declare const EDEN_ENCRYPTED_KEY: DocumentID; -export declare const EDEN_ENCRYPTED_KEY_HKDF: DocumentID; -export declare function shouldEncryptEden(doc: AnyEntry | EntryLeaf): doc is AnyEntry; -export declare function shouldEncryptEdenHKDF(doc: AnyEntry | EntryLeaf): doc is AnyEntry; -export declare function shouldDecryptEden(doc: AnyEntry | EntryLeaf): doc is AnyEntry; -export declare function shouldDecryptEdenHKDF(doc: AnyEntry | EntryLeaf): doc is AnyEntry; diff --git a/_types/src/lib/src/pouchdb/negotiation.d.ts b/_types/src/lib/src/pouchdb/negotiation.d.ts deleted file mode 100644 index c2c5782f..00000000 --- a/_types/src/lib/src/pouchdb/negotiation.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const checkRemoteVersion: (db: PouchDB.Database, migrate: (from: number, to: number) => Promise, barrier?: number) => Promise; -export declare const bumpRemoteVersion: (db: PouchDB.Database, barrier?: number) => Promise; -export declare const checkSyncInfo: (db: PouchDB.Database) => Promise; -/** - * Counts the number of remote (potentially) compromised chunks in the database. - * @param db The PouchDB database instance. - * @returns The number of compromised chunks or false if an error occurs. - */ -export declare function countCompromisedChunks(db: PouchDB.Database): Promise; diff --git a/_types/src/lib/src/pouchdb/pouchdb-browser.d.ts b/_types/src/lib/src/pouchdb/pouchdb-browser.d.ts deleted file mode 100644 index 0405eca3..00000000 --- a/_types/src/lib/src/pouchdb/pouchdb-browser.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import PouchDB from "pouchdb-core"; -export { PouchDB }; diff --git a/_types/src/lib/src/pouchdb/pouchdb-http.d.ts b/_types/src/lib/src/pouchdb/pouchdb-http.d.ts deleted file mode 100644 index 0405eca3..00000000 --- a/_types/src/lib/src/pouchdb/pouchdb-http.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import PouchDB from "pouchdb-core"; -export { PouchDB }; diff --git a/_types/src/lib/src/pouchdb/pouchdb-test.d.ts b/_types/src/lib/src/pouchdb/pouchdb-test.d.ts deleted file mode 100644 index 0405eca3..00000000 --- a/_types/src/lib/src/pouchdb/pouchdb-test.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import PouchDB from "pouchdb-core"; -export { PouchDB }; diff --git a/_types/src/lib/src/pouchdb/utils_couchdb.d.ts b/_types/src/lib/src/pouchdb/utils_couchdb.d.ts deleted file mode 100644 index 584f3a75..00000000 --- a/_types/src/lib/src/pouchdb/utils_couchdb.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const isValidRemoteCouchDBURI: (uri: string) => boolean; -export declare function isCloudantURI(uri: string): boolean; -export declare function isErrorOfMissingDoc(ex: unknown): boolean; -export declare const _requestToCouchDBFetch: (baseUri: string, username: string, password: string, path?: string, body?: unknown, method?: string) => Promise; diff --git a/_types/src/lib/src/replication/LiveSyncAbstractReplicator.d.ts b/_types/src/lib/src/replication/LiveSyncAbstractReplicator.d.ts deleted file mode 100644 index 2decc291..00000000 --- a/_types/src/lib/src/replication/LiveSyncAbstractReplicator.d.ts +++ /dev/null @@ -1,69 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc, type DatabaseConnectingStatus, type RemoteDBSettings, type EntryLeaf, type TweakValues, type NodeData } from "@lib/common/types.ts"; -import type { RequiredServices } from "@lib/interfaces/ServiceModule"; -export type ReplicationCallback = (e: PouchDB.Core.ExistingDocument[]) => Promise | boolean; -export type ReplicationStat = { - sent: number; - arrived: number; - maxPullSeq: number; - maxPushSeq: number; - lastSyncPullSeq: number; - lastSyncPushSeq: number; - syncStatus: DatabaseConnectingStatus; -}; -export interface LiveSyncReplicatorEnv { - services: RequiredServices<"API" | "appLifecycle" | "setting" | "vault" | "database" | "databaseEvents" | "keyValueDB" | "replication" | "config" | "UI" | "replicator" | "remote">; -} -export type RemoteDBStatus = { - [key: string]: unknown; - estimatedSize?: number; -}; -export declare abstract class LiveSyncAbstractReplicator { - syncStatus: DatabaseConnectingStatus; - docArrived: number; - docSent: number; - lastSyncPullSeq: number; - maxPullSeq: number; - lastSyncPushSeq: number; - maxPushSeq: number; - controller?: AbortController; - originalSetting: RemoteDBSettings; - nodeid: string; - remoteLocked: boolean; - remoteCleaned: boolean; - remoteLockedAndDeviceNotAccepted: boolean; - tweakSettingsMismatched: boolean; - preferredTweakValue?: TweakValues; - abstract get isChunkSendingSupported(): boolean; - get database(): import("../pouchdb/LiveSyncLocalDB").LiveSyncLocalDB; - get rawDatabase(): PouchDB.Database; - get currentSettings(): import("@lib/common/types.ts").ObsidianLiveSyncSettings; - sendChunks(setting: RemoteDBSettings, remoteDB: PouchDB.Database | undefined, showResult: boolean, fromSeq?: number | string): Promise; - abstract getReplicationPBKDF2Salt(setting: RemoteDBSettings, refresh?: boolean): Promise; - ensurePBKDF2Salt(setting: RemoteDBSettings, showMessage?: boolean, useCache?: boolean): Promise; - env: LiveSyncReplicatorEnv; - initializeDatabaseForReplication(): Promise; - constructor(env: LiveSyncReplicatorEnv); - abstract terminateSync(): void; - abstract openReplication(setting: RemoteDBSettings, keepAlive: boolean, showResult: boolean, ignoreCleanLock: boolean): Promise; - updateInfo: () => void; - abstract tryConnectRemote(setting: RemoteDBSettings, showResult?: boolean): Promise; - abstract replicateAllToServer(setting: RemoteDBSettings, showingNotice?: boolean, sendChunksInBulkDisabled?: boolean): Promise; - abstract replicateAllFromServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise; - abstract closeReplication(): void; - abstract tryResetRemoteDatabase(setting: RemoteDBSettings): Promise; - abstract tryCreateRemoteDatabase(setting: RemoteDBSettings): Promise; - abstract markRemoteLocked(setting: RemoteDBSettings, locked: boolean, lockByClean: boolean): Promise; - abstract markRemoteResolved(setting: RemoteDBSettings): Promise; - abstract resetRemoteTweakSettings(setting: RemoteDBSettings): Promise; - abstract setPreferredRemoteTweakSettings(setting: RemoteDBSettings): Promise; - abstract fetchRemoteChunks(missingChunks: string[], showResult: boolean): Promise; - abstract getRemoteStatus(setting: RemoteDBSettings): Promise; - abstract getRemotePreferredTweakValues(setting: RemoteDBSettings): Promise; - abstract countCompromisedChunks(setting?: RemoteDBSettings): Promise; - abstract getConnectedDeviceList(setting?: RemoteDBSettings): Promise; - accepted_nodes: string[]; - }>; -} diff --git a/_types/src/lib/src/replication/SyncParamsHandler.d.ts b/_types/src/lib/src/replication/SyncParamsHandler.d.ts deleted file mode 100644 index 73d6f363..00000000 --- a/_types/src/lib/src/replication/SyncParamsHandler.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { SyncParameters } from "@lib/common/types.ts"; -import { LiveSyncError } from "@lib/common/LSError.ts"; -/** - * Creates a SyncParamsHandler for managing synchronisation parameters. - */ -type putFunc = (params: SyncParameters) => Promise; -/** - * Fetches synchronisation parameters from the server. - */ -type getFunc = () => Promise; -/** - * The function to create new synchronisation parameters. - * Note that this function should not return `pbkdf2salt` in the result; it should be generated and stored in the handler. - */ -type createFunc = () => Promise; -type CreateSyncParamsHanderOptions = { - put: putFunc; - get: getFunc; - create: createFunc; -}; -export type SyncParamsHandler = { - fetch: (refresh?: boolean) => Promise; - getPBKDF2Salt: (refresh?: boolean) => Promise; -}; -export declare function createSyncParamsHanderForServer(key: string, options: CreateSyncParamsHanderOptions): SyncParamsHandler; -export declare function clearHandlers(): void; -export declare class SyncParamsHandlerError extends LiveSyncError { -} -export declare class SyncParamsFetchError extends SyncParamsHandlerError { -} -export declare class SyncParamsNotFoundError extends SyncParamsHandlerError { -} -export declare class SyncParamsUpdateError extends SyncParamsHandlerError { -} -export {}; diff --git a/_types/src/lib/src/replication/couchdb/LiveSyncReplicator.d.ts b/_types/src/lib/src/replication/couchdb/LiveSyncReplicator.d.ts deleted file mode 100644 index 8a9fb4df..00000000 --- a/_types/src/lib/src/replication/couchdb/LiveSyncReplicator.d.ts +++ /dev/null @@ -1,87 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc, type RemoteDBSettings, type EntryLeaf, type TweakValues, type SyncParameters, type DatabaseEntry, type NodeData } from "@lib/common/types.ts"; -import { LiveSyncAbstractReplicator, type LiveSyncReplicatorEnv, type RemoteDBStatus } from "@lib/replication/LiveSyncAbstractReplicator.ts"; -import type { ServiceHub } from "@lib/services/ServiceHub.ts"; -export interface LiveSyncCouchDBReplicatorEnv extends LiveSyncReplicatorEnv { - services: ServiceHub; -} -export declare class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicator { - get isChunkSendingSupported(): boolean; - isMobile(): boolean; - constructor(env: LiveSyncCouchDBReplicatorEnv); - getInitialSyncParameters(setting: RemoteDBSettings): Promise; - getSyncParameters(setting: RemoteDBSettings): Promise; - putSyncParameters(setting: RemoteDBSettings, params: SyncParameters): Promise; - getReplicationPBKDF2Salt(setting: RemoteDBSettings, refresh?: boolean): Promise; - migrate(from: number, to: number): Promise; - terminateSync(): void; - openReplication(setting: RemoteDBSettings, keepAlive: boolean, showResult: boolean, ignoreCleanLock: boolean): Promise; - replicationActivated(showResult: boolean): void; - replicationChangeDetected(e: PouchDB.Replication.SyncResult, showResult: boolean, docSentOnStart: number, docArrivedOnStart: number): Promise; - replicationCompleted(showResult: boolean): void; - replicationDenied(e: unknown): void; - replicationErrored(e: unknown): void; - replicationPaused(): void; - processSync(syncHandler: PouchDB.Replication.Sync | PouchDB.Replication.Replication, showResult: boolean, docSentOnStart: number, docArrivedOnStart: number, syncMode: "sync" | "pullOnly" | "pushOnly", retrying: boolean, reportCancelledAsDone?: boolean): Promise<"DONE" | "NEED_RETRY" | "NEED_RESURRECT" | "FAILED" | "CANCELLED">; - getEmptyMaxEntry(remoteID: number): { - _id: string; - maxSeq: number | string; - remoteID: number; - seqStatusMap: Record; - _rev: string | undefined; - }; - getLastTransferredSeqOfChunks(localDB: PouchDB.Database, remoteID: number): Promise>; - updateMaxTransferredSeqOnChunks(localDB: PouchDB.Database, remoteID: number, seqStatusMap: Record): Promise>; - sendChunks(setting: RemoteDBSettings, remoteDB: PouchDB.Database | undefined, showResult: boolean, fromSeq?: number | string): Promise; - openOneShotReplication(setting: RemoteDBSettings, showResult: boolean, retrying: boolean, syncMode: "sync" | "pullOnly" | "pushOnly", ignoreCleanLock?: boolean): Promise; - replicateAllToServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise; - replicateAllFromServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise; - checkReplicationConnectivity(setting: RemoteDBSettings, keepAlive: boolean, skipCheck: boolean, showResult: boolean, ignoreCleanLock?: boolean): Promise; - info: PouchDB.Core.DatabaseInfo; - syncOptionBase: PouchDB.Replication.SyncOptions; - syncOption: PouchDB.Replication.SyncOptions; - }>; - openContinuousReplication(setting: RemoteDBSettings, showResult: boolean, retrying: boolean): Promise; - closeReplication(): void; - tryResetRemoteDatabase(setting: RemoteDBSettings): Promise; - tryCreateRemoteDatabase(setting: RemoteDBSettings): Promise; - markRemoteLocked(setting: RemoteDBSettings, locked: boolean, lockByClean: boolean): Promise; - markRemoteResolved(setting: RemoteDBSettings): Promise; - connectRemoteCouchDBWithSetting(settings: RemoteDBSettings, isMobile: boolean, performSetup?: boolean, skipInfo?: boolean): Promise; - info: PouchDB.Core.DatabaseInfo; - }> | "Empty passphrases cannot be used without explicit permission"; - _ensureConnection(settings: RemoteDBSettings, performSetup?: boolean): Promise>; - /** - * Fetch a document from the remote database directly. - * @param settings RemoteDBSettings for the connection. - * @param id Document ID to fetch. - * @param db Optional PouchDB instance to use. If provided, it will use this instance instead of creating a new connection (then settings will be ignored). - * @returns The fetched document or false if the document does not exist. - * @throws {Error} Other errors that may occur during the fetch operation. - */ - fetchRemoteDocument(settings: RemoteDBSettings, id: string, db?: PouchDB.Database): Promise; - /** - * Puts a document to the remote database directly - * @param settings RemoteDBSettings for the connection. - * @param doc Document to put. - * @param db Optional PouchDB instance to use. If provided, it will use this instance instead of creating a new connection (then settings will be ignored). - * @returns Response from the remote database or false if an error occurred. - * @throws {Error} If the document could not be put. - */ - putRemoteDocument(settings: RemoteDBSettings, doc: T, db?: PouchDB.Database): Promise; - fetchRemoteChunks(missingChunks: string[], showResult: boolean): Promise; - tryConnectRemote(setting: RemoteDBSettings, showResult?: boolean): Promise; - resetRemoteTweakSettings(setting: RemoteDBSettings): Promise; - setPreferredRemoteTweakSettings(setting: RemoteDBSettings): Promise; - getRemotePreferredTweakValues(setting: RemoteDBSettings): Promise; - compactRemote(setting: RemoteDBSettings): Promise; - getRemoteStatus(setting: RemoteDBSettings): Promise; - countCompromisedChunks(setting?: RemoteDBSettings): Promise; - getConnectedDeviceList(setting?: RemoteDBSettings): Promise; - accepted_nodes: string[]; - }>; -} diff --git a/_types/src/lib/src/replication/httplib.d.ts b/_types/src/lib/src/replication/httplib.d.ts deleted file mode 100644 index 1561073d..00000000 --- a/_types/src/lib/src/replication/httplib.d.ts +++ /dev/null @@ -1,77 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { CouchDBCredentials, JWTCredentials, JWTHeader, JWTParams, JWTPayload, PreparedJWT, RemoteDBSettings } from "@lib/common/types"; -import { Computed } from "octagonal-wheels/dataobject/Computed"; -/** - * Generates a credential object based on the provided settings. - * @param settings - RemoteDBSettings - * @returns {CouchDBCredentials} credentials object - */ -export declare function generateCredentialObject(settings: RemoteDBSettings): { - jwtAlgorithm: import("../common/models/auth.type").JWTAlgorithm; - jwtKey: string; - jwtKid: string; - jwtSub: string; - jwtExpDuration: number; - type: "jwt"; - username?: undefined; - password?: undefined; -} | { - username: string; - password: string; - type: "basic"; - jwtAlgorithm?: undefined; - jwtKey?: undefined; - jwtKid?: undefined; - jwtSub?: undefined; - jwtExpDuration?: undefined; -}; -/** - * Generates a basic authentication header for CouchDB credentials using the provided username and password. - * And it caches the result for performance if the credentials are not changed. - */ -export declare class BasicHeaderGenerator { - _header: Computed<[source: CouchDBCredentials], string>; - /** - * Generates a basic authentication header for CouchDB credentials using the provided username and password. - * @param auth - CouchDBCredentials - * @returns {Promise} The basic authentication header (without "Basic" prefix). - */ - getBasicHeader(auth: CouchDBCredentials): Promise; -} -/** - * Generates a JWT token based on the provided credentials and parameters. - * And it caches the result for performance if the credentials are not changed. - */ -export declare class JWTTokenGenerator { - _importKey(auth: JWTCredentials): Promise; - _currentCryptoKey: Computed<[auth: JWTCredentials], CryptoKey>; - _jwt: Computed<[params: JWTParams], { - token: string; - header: JWTHeader; - payload: JWTPayload; - credentials: JWTCredentials; - }>; - _jwtParams: Computed<[source: JWTCredentials], { - header: JWTHeader; - payload: { - exp: number; - iat: number; - sub: string; - "_couchdb.roles": string[]; - }; - credentials: JWTCredentials; - }>; - getJWT(auth: JWTCredentials): Promise; - /** - * Generates a JWT token based on the provided credentials and parameters. - * @param auth - JWTCredentials - * @returns {Promise} The JWT token (with "Bearer" prefix). - */ - getBearerToken(auth: JWTCredentials): Promise; -} -export declare class AuthorizationHeaderGenerator { - _basicHeader: BasicHeaderGenerator; - _jwtHeader: JWTTokenGenerator; - getAuthorizationHeader(auth: CouchDBCredentials): Promise; -} diff --git a/_types/src/lib/src/replication/journal/JournalSyncCore.d.ts b/_types/src/lib/src/replication/journal/JournalSyncCore.d.ts deleted file mode 100644 index 82fe710f..00000000 --- a/_types/src/lib/src/replication/journal/JournalSyncCore.d.ts +++ /dev/null @@ -1,72 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc, type SyncParameters, type BucketSyncSetting, type RemoteDBSettings } from "@lib/common/types.ts"; -import type { ReplicationCallback, ReplicationStat } from "@lib/replication/LiveSyncAbstractReplicator.ts"; -import { type SimpleStore } from "@lib/common/utils.ts"; -import { type CheckPointInfo } from "./JournalSyncTypes.ts"; -import type { LiveSyncJournalReplicatorEnv } from "./LiveSyncJournalReplicatorEnv.ts"; -import type { IJournalStorage } from "./objectstore/JournalStorageAdapter.ts"; -type ProcessingEntry = PouchDB.Core.PutDocument & PouchDB.Core.GetMeta; -export declare class JournalSyncCore { - _settings: BucketSyncSetting; - storage: IJournalStorage; - get db(): PouchDB.Database; - get currentSettings(): import("@lib/common/types.ts").ObsidianLiveSyncSettings; - hash: string; - processReplication: ReplicationCallback; - batchSize: number; - env: LiveSyncJournalReplicatorEnv; - store: SimpleStore; - requestedStop: boolean; - getInitialSyncParameters(): Promise; - getSyncParameters(): Promise; - putSyncParameters(params: SyncParameters): Promise; - getHash(settings: BucketSyncSetting): string; - constructor(settings: BucketSyncSetting, store: SimpleStore, env: LiveSyncJournalReplicatorEnv, storage: IJournalStorage); - downloadJson(key: string): Promise; - uploadJson(key: string, body: T): Promise; - applyNewConfig(settings: BucketSyncSetting, store: SimpleStore, env: LiveSyncJournalReplicatorEnv): void; - updateInfo(info: Partial): void; - updateCheckPointInfo(func: (infoFrom: CheckPointInfo) => CheckPointInfo): Promise; - _currentCheckPointInfo: { - lastLocalSeq: number | string; - journalEpoch: string; - knownIDs: Set; - sentIDs: Set; - receivedFiles: Set; - sentFiles: Set; - }; - getCheckpointInfo(): Promise; - resetAllCaches(): void; - resetCheckpointInfo(): Promise; - private getJournalEpochFromSyncParams; - ensureCheckpointCachesAreFresh(): Promise; - isAvailable(): Promise; - resetBucket(): Promise; - getRemoteKey(): string; - getReplicationPBKDF2Salt(refresh?: boolean): Promise; - isEncryptionPrevented(fileName: string): boolean; - private decryptDataV2; - private decryptDataV1; - decryptDownloaded(key: string, encrypted: Uint8Array, set: RemoteDBSettings): Promise; - encryptForUpload(key: string, data: Uint8Array, set: RemoteDBSettings): Promise; - getDocKey(doc: EntryDoc): string; - _createJournalPack(override?: number | string): Promise<{ - changes: EntryDoc[]; - hasNext: boolean; - packLastSeq: string | number; - }>; - private _createSendReadableStream; - private _createSendCompressTransformStream; - private _createSendUploadWritableStream; - sendLocalJournal(showMessage?: boolean): Promise; - _getRemoteJournals(): Promise; - processDocuments(allDocs: ProcessingEntry[]): Promise; - private _createReceiveReadableStream; - private _createReceiveTransformStream; - private _createReceiveWritableStream; - receiveRemoteJournal(showMessage?: boolean): Promise; - sync(showResult?: boolean): Promise; - requestStop(): void; -} -export {}; diff --git a/_types/src/lib/src/replication/journal/JournalSyncTypes.d.ts b/_types/src/lib/src/replication/journal/JournalSyncTypes.d.ts deleted file mode 100644 index d5123da4..00000000 --- a/_types/src/lib/src/replication/journal/JournalSyncTypes.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type CheckPointInfo = { - lastLocalSeq: number | string; - journalEpoch: string; - knownIDs: Set; - sentIDs: Set; - receivedFiles: Set; - sentFiles: Set; -}; -export declare const CheckPointInfoDefault: CheckPointInfo; diff --git a/_types/src/lib/src/replication/journal/LiveSyncJournalReplicator.d.ts b/_types/src/lib/src/replication/journal/LiveSyncJournalReplicator.d.ts deleted file mode 100644 index e682e2ff..00000000 --- a/_types/src/lib/src/replication/journal/LiveSyncJournalReplicator.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type RemoteDBSettings, type EntryLeaf, type ChunkVersionRange, type TweakValues, type NodeData } from "@lib/common/types.ts"; -import { JournalSyncCore } from "./JournalSyncCore.ts"; -import { LiveSyncAbstractReplicator, type RemoteDBStatus } from "@lib/replication/LiveSyncAbstractReplicator.ts"; -import { type ENSURE_DB_RESULT } from "@lib/pouchdb/LiveSyncDBFunctions.ts"; -import type { CheckPointInfo } from "./JournalSyncTypes.ts"; -import { type SimpleStore } from "@lib/common/utils.ts"; -import type { LiveSyncJournalReplicatorEnv } from "./LiveSyncJournalReplicatorEnv.ts"; -export declare class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator { - env: LiveSyncJournalReplicatorEnv; - get isChunkSendingSupported(): boolean; - get client(): JournalSyncCore; - get simpleStore(): SimpleStore; - _client: JournalSyncCore; - getReplicationPBKDF2Salt(setting: RemoteDBSettings, refresh?: boolean): Promise; - setupJournalSyncClient(): JournalSyncCore; - ensureBucketIsCompatible(deviceNodeID: string, currentVersionRange: ChunkVersionRange): Promise; - constructor(env: LiveSyncJournalReplicatorEnv); - migrate(from: number, to: number): Promise; - terminateSync(): void; - openReplication(setting: RemoteDBSettings, _: boolean, showResult: boolean, ignoreCleanLock?: boolean): Promise; - replicateAllToServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise; - replicateAllFromServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise; - checkReplicationConnectivity(skipCheck: boolean, ignoreCleanLock?: boolean, showMessage?: boolean): Promise; - fetchRemoteChunks(missingChunks: string[], showResult: boolean): Promise; - closeReplication(): void; - tryResetRemoteDatabase(setting: RemoteDBSettings): Promise; - tryCreateRemoteDatabase(setting: RemoteDBSettings): Promise; - markRemoteLocked(setting: RemoteDBSettings, locked: boolean, lockByClean: boolean): Promise; - markRemoteResolved(setting: RemoteDBSettings): Promise; - tryConnectRemote(setting: RemoteDBSettings, showResult?: boolean): Promise; - resetRemoteTweakSettings(setting: RemoteDBSettings): Promise; - setPreferredRemoteTweakSettings(setting: RemoteDBSettings): Promise; - getRemotePreferredTweakValues(setting: RemoteDBSettings): Promise; - getRemoteStatus(setting: RemoteDBSettings): Promise; - countCompromisedChunks(): Promise; - getConnectedDeviceList(setting?: RemoteDBSettings): Promise; - accepted_nodes: string[]; - }>; -} diff --git a/_types/src/lib/src/replication/journal/LiveSyncJournalReplicatorEnv.d.ts b/_types/src/lib/src/replication/journal/LiveSyncJournalReplicatorEnv.d.ts deleted file mode 100644 index 22c7f1c9..00000000 --- a/_types/src/lib/src/replication/journal/LiveSyncJournalReplicatorEnv.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LiveSyncReplicatorEnv } from "@lib/replication/LiveSyncAbstractReplicator"; -export interface LiveSyncJournalReplicatorEnv extends LiveSyncReplicatorEnv { // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface -} diff --git a/_types/src/lib/src/replication/journal/objectstore/JournalStorageAdapter.d.ts b/_types/src/lib/src/replication/journal/objectstore/JournalStorageAdapter.d.ts deleted file mode 100644 index f4cdd0ed..00000000 --- a/_types/src/lib/src/replication/journal/objectstore/JournalStorageAdapter.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { RemoteDBStatus } from "@lib/replication/LiveSyncAbstractReplicator.ts"; -import type { BucketSyncSetting } from "@lib/common/types.ts"; -export interface IJournalStorage { - upload(key: string, data: Uint8Array, mime: string): Promise; - download(key: string, ignoreCache?: boolean): Promise; - listFiles(from: string, limit?: number): Promise; - deleteFiles(keys: string[]): Promise; - isAvailable(): Promise; - getUsage(): Promise; - applyNewConfig(settings: BucketSyncSetting): void; -} -import type { LiveSyncJournalReplicatorEnv } from "@lib/replication/journal/LiveSyncJournalReplicatorEnv.ts"; -export interface IJournalStorageAdapterClass { - new (settings: BucketSyncSetting, env: LiveSyncJournalReplicatorEnv): IJournalStorage; -} diff --git a/_types/src/lib/src/replication/journal/objectstore/MinioStorageAdapter.d.ts b/_types/src/lib/src/replication/journal/objectstore/MinioStorageAdapter.d.ts deleted file mode 100644 index 02effd95..00000000 --- a/_types/src/lib/src/replication/journal/objectstore/MinioStorageAdapter.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { S3 } from "@aws-sdk/client-s3"; -import { type BucketSyncSetting } from "@lib/common/types.ts"; -import type { RemoteDBStatus } from "@lib/replication/LiveSyncAbstractReplicator.ts"; -import type { IJournalStorage } from "./JournalStorageAdapter.ts"; -import type { LiveSyncJournalReplicatorEnv } from "@lib/replication/journal/LiveSyncJournalReplicatorEnv.ts"; -export declare class MinioStorageAdapter implements IJournalStorage { - _instance?: S3; - _settings: BucketSyncSetting; - _env: LiveSyncJournalReplicatorEnv; - constructor(settings: BucketSyncSetting, env: LiveSyncJournalReplicatorEnv); - private runTrackedRequest; - applyNewConfig(settings: BucketSyncSetting): void; - get customHeaders(): [string, string][]; - _getClient(): S3; - upload(key: string, data: Uint8Array, mime: string): Promise; - download(key: string, ignoreCache?: boolean): Promise; - listFiles(from: string, limit?: number): Promise; - deleteFiles(keys: string[]): Promise; - isAvailable(): Promise; - getUsage(): Promise; -} diff --git a/_types/src/lib/src/replication/journal/objectstore/MinioStorageAdapter.integration.spec.d.ts b/_types/src/lib/src/replication/journal/objectstore/MinioStorageAdapter.integration.spec.d.ts deleted file mode 100644 index 467855c2..00000000 --- a/_types/src/lib/src/replication/journal/objectstore/MinioStorageAdapter.integration.spec.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export {}; diff --git a/_types/src/lib/src/replication/trystero/LiveSyncTrysteroReplicator.d.ts b/_types/src/lib/src/replication/trystero/LiveSyncTrysteroReplicator.d.ts deleted file mode 100644 index 8b43daa5..00000000 --- a/_types/src/lib/src/replication/trystero/LiveSyncTrysteroReplicator.d.ts +++ /dev/null @@ -1,92 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type RemoteDBSettings, type EntryLeaf, type TweakValues, type LOG_LEVEL, type NodeData } from "@lib/common/types"; -import { LiveSyncAbstractReplicator, type LiveSyncReplicatorEnv, type RemoteDBStatus } from "@lib/replication/LiveSyncAbstractReplicator"; -import { TrysteroReplicator } from "./TrysteroReplicator"; -import { P2PHost, type AcceptanceDecision, type RevokeAcceptanceDecision } from "./TrysteroReplicatorP2PServer"; -import type { Advertisement } from "./types"; -export interface LiveSyncTrysteroReplicatorEnv extends LiveSyncReplicatorEnv { - /** - * Injected by the host platform (e.g. Obsidian) to show a UI for peer selection. - * When not set, openReplication falls back to replicateFromCommand (CLI-safe). - */ - openReplicationUI?: (showResult: boolean) => Promise; - /** - * Injected by the host platform to show a UI for selecting a peer to rebuild from. - * When not set, replicateAllFromServer falls back to the headless selectPeer dialog. - */ - openRebuildUI?: (showResult: boolean) => Promise; -} -export declare class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator { - private _p2pHost?; - private _replicator?; - get openReplicationUI(): ((showResult: boolean) => Promise) | undefined; - get rawReplicator(): TrysteroReplicator | undefined; - get rawHost(): P2PHost | undefined; - get isChunkSendingSupported(): boolean; - getReplicationPBKDF2Salt(_setting: RemoteDBSettings, _refresh?: boolean): Promise; - terminateSync(): void; - private _buildEnv; - open(): Promise; - close(): Promise; - closeReplication(): void; - get server(): P2PHost | undefined; - get knownAdvertisements(): Advertisement[]; - enableBroadcastChanges(): void; - disableBroadcastChanges(): void; - requestStatus(): void; - onNewPeer(peer: Advertisement): Promise | undefined; - onPeerLeaved(peerId: string): void; - replicateFromCommand(showResult?: boolean): Promise; - replicateFrom(peerId: string, showNotice?: boolean): Promise<{ - error: unknown; - ok?: undefined; - } | { - ok: boolean; - error?: undefined; - }>; - requestSynchroniseToPeer(peerId: string): Promise<{ - error: unknown; - ok?: undefined; - } | { - ok: boolean; - error?: undefined; - }>; - getRemoteConfig(peerId: string): Promise; - watchPeer(peerId: string): void; - unwatchPeer(peerId: string): void; - sync(peerId: string, showNotice?: boolean): Promise<{ - error: unknown; - ok?: undefined; - } | { - ok: boolean; - error?: undefined; - } | undefined>; - setOnSetup(): void; - clearOnSetup(): void; - makeDecision(decision: AcceptanceDecision): Promise; - revokeDecision(decision: RevokeAcceptanceDecision): Promise; - makeSureOpened(): Promise; - openReplication(_setting: RemoteDBSettings, _keepAlive: boolean, showResult: boolean, _ignoreCleanLock: boolean): Promise; - tryConnectRemote(_setting: RemoteDBSettings, _showResult?: boolean): Promise; - replicateAllToServer(_setting: RemoteDBSettings, _showingNotice?: boolean, _sendChunksInBulkDisabled?: boolean): Promise; - selectPeer(settingPeerName: string, r: TrysteroReplicator, logLevel: LOG_LEVEL): Promise; - tryUntilSuccess(func: () => Promise, repeat: number, logLevel: LOG_LEVEL): Promise; - replicateAllFromServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise; - tryResetRemoteDatabase(_setting: RemoteDBSettings): Promise; - tryCreateRemoteDatabase(_setting: RemoteDBSettings): Promise; - markRemoteLocked(_setting: RemoteDBSettings, _locked: boolean, _lockByClean: boolean): Promise; - markRemoteResolved(_setting: RemoteDBSettings): Promise; - resetRemoteTweakSettings(_setting: RemoteDBSettings): Promise; - setPreferredRemoteTweakSettings(_setting: RemoteDBSettings): Promise; - fetchRemoteChunks(_missingChunks: string[], _showResult: boolean): Promise; - getRemoteStatus(_setting: RemoteDBSettings): Promise; - getRemotePreferredTweakValues(_setting: RemoteDBSettings): Promise; - countCompromisedChunks(): Promise; - getConnectedDeviceList(_setting?: RemoteDBSettings): Promise; - accepted_nodes: string[]; - }>; - env: LiveSyncTrysteroReplicatorEnv; - constructor(env: LiveSyncTrysteroReplicatorEnv); -} diff --git a/_types/src/lib/src/replication/trystero/P2PLogCollector.d.ts b/_types/src/lib/src/replication/trystero/P2PLogCollector.d.ts deleted file mode 100644 index 9212edf9..00000000 --- a/_types/src/lib/src/replication/trystero/P2PLogCollector.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { P2PReplicationProgress } from "./TrysteroReplicator"; -export declare class P2PLogCollector { - constructor(); - p2pReplicationResult: Map; - updateP2PReplicationLine(): void; - p2pReplicationLine: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; -} diff --git a/_types/src/lib/src/replication/trystero/P2PReplicatorBase.d.ts b/_types/src/lib/src/replication/trystero/P2PReplicatorBase.d.ts deleted file mode 100644 index 3eb6750f..00000000 --- a/_types/src/lib/src/replication/trystero/P2PReplicatorBase.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LOG_LEVEL } from "octagonal-wheels/common/logger"; -import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; -import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2"; -import type { P2PSyncSetting, EntryDoc } from "@lib/common/types"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices"; -export interface P2PReplicatorBase { - storeP2PStatusLine: ReactiveSource; - settings: P2PSyncSetting; - _log(msg: unknown, level?: LOG_LEVEL): void; - _notice(msg: unknown, key?: string): void; - getSettings(): P2PSyncSetting; - getDB: () => PouchDB.Database; - confirm: Confirm; - simpleStore(): SimpleStore; - handleReplicatedDocuments(docs: EntryDoc[]): Promise; - init(): Promise; - services: InjectableServiceHub; -} diff --git a/_types/src/lib/src/replication/trystero/P2PReplicatorCore.d.ts b/_types/src/lib/src/replication/trystero/P2PReplicatorCore.d.ts deleted file mode 100644 index 3414b1af..00000000 --- a/_types/src/lib/src/replication/trystero/P2PReplicatorCore.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import type { P2PPaneParams } from "./UseP2PReplicatorResult"; -export type P2PViewFactory = (leaf: unknown) => unknown; -/** - * ServiceFeature: P2P Replicator lifecycle management. - * Binds a LiveSyncTrysteroReplicator to the host's lifecycle events, - * following the same middleware style as useOfflineScanner. - * - * @param viewTypeAndFactory Optional [viewType, factory] pair for registering the P2P pane view. - * When provided, also registers commands and ribbon icon via services.API. - */ -export declare function useP2PReplicator(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "database" | "databaseEvents" | "keyValueDB" | "replication" | "config" | "UI" | "replicator" | "remote", never>, viewTypeAndFactory?: [viewType: string, factory: P2PViewFactory]): P2PPaneParams; diff --git a/_types/src/lib/src/replication/trystero/P2PReplicatorPaneCommon.d.ts b/_types/src/lib/src/replication/trystero/P2PReplicatorPaneCommon.d.ts deleted file mode 100644 index e4ec7234..00000000 --- a/_types/src/lib/src/replication/trystero/P2PReplicatorPaneCommon.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts"; -export declare const EVENT_P2P_PEER_SHOW_EXTRA_MENU = "p2p-peer-show-extra-menu"; -export declare enum AcceptedStatus { - UNKNOWN = "Unknown", - ACCEPTED = "Accepted", - DENIED = "Denied", - ACCEPTED_IN_SESSION = "Accepted in session", - DENIED_IN_SESSION = "Denied in session" -} -export type PeerExtraMenuEvent = { - peer: PeerStatus; - event: MouseEvent; -}; -export declare enum ConnectionStatus { - CONNECTED = "Connected", - CONNECTED_LIVE = "Connected(live)", - DISCONNECTED = "Disconnected" -} -export type PeerStatus = { - name: string; - peerId: string; - syncOnConnect: boolean; - watchOnConnect: boolean; - syncOnReplicationCommand: boolean; - accepted: AcceptedStatus; - status: ConnectionStatus; - isFetching: boolean; - isSending: boolean; - isWatching: boolean; -}; -declare global { - interface LSEvents { - [EVENT_P2P_PEER_SHOW_EXTRA_MENU]: PeerExtraMenuEvent; - } -} -export interface PluginShim { - services: InjectableServiceHub; - core: { - services: InjectableServiceHub; - }; -} diff --git a/_types/src/lib/src/replication/trystero/ProxiedDB.d.ts b/_types/src/lib/src/replication/trystero/ProxiedDB.d.ts deleted file mode 100644 index baa175d7..00000000 --- a/_types/src/lib/src/replication/trystero/ProxiedDB.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ReplicatorHostEnv } from "./types"; -import type { EntryDoc } from "@lib/common/models/db.definition"; -export declare function createHostingDB(env: ReplicatorHostEnv): { - info: () => Promise; - changes: (options: PouchDB.Core.ChangesOptions) => PouchDB.Core.Changes; - revsDiff: (diff: PouchDB.Core.RevisionDiffOptions) => Promise; - bulkDocs: (docs: PouchDB.Core.PutDocument>[], options?: PouchDB.Core.BulkDocsOptions) => Promise<(PouchDB.Core.Response | PouchDB.Core.Error)[]>; - bulkGet: (options: PouchDB.Core.BulkGetOptions) => Promise>; - put: (doc: PouchDB.Core.PutDocument>, options?: PouchDB.Core.PutOptions) => Promise; - get: (id: string, options?: PouchDB.Core.GetOptions) => Promise; - _stopHosting: () => void; -}; -export type HostingDB = ReturnType; diff --git a/_types/src/lib/src/replication/trystero/TrysteroReplicator.d.ts b/_types/src/lib/src/replication/trystero/TrysteroReplicator.d.ts deleted file mode 100644 index 440a7dba..00000000 --- a/_types/src/lib/src/replication/trystero/TrysteroReplicator.d.ts +++ /dev/null @@ -1,153 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc, type ObsidianLiveSyncSettings } from "@lib/common/types"; -import { type ProgressInfo } from "@lib/pouchdb/ReplicatorShim"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import { type Advertisement, type ReplicatorHostEnv } from "./types"; -import { EVENT_P2P_REPLICATOR_PROGRESS, EVENT_P2P_REPLICATOR_STATUS, P2PHost } from "./TrysteroReplicatorP2PServer"; -export type P2PReplicatorStatus = { - isBroadcasting: boolean; - replicatingTo: string[]; - replicatingFrom: string[]; - watchingPeers: string[]; -}; -export type P2PReplicationProgress = { - peerId: string; - peerName: string; - fetching: { - max: number; - current: number; - isActive: boolean; - }; - sending: { - max: number; - current: number; - isActive: boolean; - }; -}; -export type P2PReplicationReport = { - peerId: string; - peerName: string; -} & ({ - fetching: { - max: number; - current: number; - isActive: boolean; - }; -} | { - sending: { - max: number; - current: number; - isActive: boolean; - }; -}); -declare global { - interface LSEvents { - [EVENT_P2P_REPLICATOR_STATUS]: P2PReplicatorStatus; - [EVENT_P2P_REPLICATOR_PROGRESS]: P2PReplicationReport; - } -} -export type AllReplicationClientStatus = { - [peerId: string]: { - isReplicatingTo: boolean; - isReplicatingFrom: boolean; - isWatching: boolean; - stats: P2PReplicationProgress; - }; -}; -export declare class TrysteroReplicator { - _env: ReplicatorHostEnv; - server?: P2PHost; - replicationStatus(): {}; // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type - get settings(): import("@lib/common/types").P2PSyncSetting; - get db(): PouchDB.Database; - get deviceName(): string; - get platform(): string; - get confirm(): Confirm; - private runFiniteReplicationActivity; - constructor(env: ReplicatorHostEnv, server?: P2PHost); - close(): Promise; - open(): Promise; - makeSureOpened(): Promise; - get autoSyncPeers(): RegExp[]; - get autoWatchPeers(): RegExp[]; - onNewPeer(peer: Advertisement): Promise; - onPeerLeaved(peerId: string): void; - _onSetup: boolean; - setOnSetup(): void; - clearOnSetup(): void; - getTweakSettings(fromPeerId: string): Promise>; - getCommands(): { - reqSync: (fromPeerId: string) => Promise<{ - error: unknown; - ok?: undefined; - } | { - ok: boolean; - error?: undefined; - }>; - "!reqAuth": (fromPeerId: string) => Promise; - getTweakSettings: (fromPeerId: string) => Promise>; - onProgress: (fromPeerId: string) => Promise<{ - error: Error; - } | undefined>; - getAllConfig: (fromPeerId: string) => Promise; - onProgressAcknowledged: (fromPeerId: string, info: ProgressInfo) => Promise; - getIsBroadcasting: () => Promise; - requestBroadcasting: (peerId: string) => Promise; - }; - requestAuthenticate(peerId: string): Promise; - lastSeq: string | number; - requestSynchroniseToPeer(peerId: string): Promise["reqSync"]>>>; - requestSynchroniseToAllAvailablePeers(): Promise; - dispatchStatus(): void; - requestStatus(): void; - changes?: PouchDB.Core.Changes; - _isBroadcasting: boolean; - disableBroadcastChanges(): void; - enableBroadcastChanges(): void; - get knownAdvertisements(): Advertisement[]; - availableReplicationPairs: Set; - sync(remotePeer: string, showNotice?: boolean): Promise<{ - error: unknown; - ok?: undefined; - } | { - ok: boolean; - error?: undefined; - } | undefined>; - _replicateToPeers: Set; - _replicateFromPeers: Set; - dispatchReplicationProgress(peerId: string, info?: ProgressInfo): void; - onReplicationProgress(peerId: string, info?: ProgressInfo): boolean; - onProgressAcknowledged(peerId: string, info?: ProgressInfo): boolean; - acknowledgeProgress(remotePeerId: string, info?: ProgressInfo): void; - replicateFrom(remotePeer: string, showNotice?: boolean, fromStart?: boolean): Promise<{ - error: unknown; - ok?: undefined; - } | { - ok: boolean; - error?: undefined; - }>; - notifyProgress(excludePeerId?: string): Promise | undefined; - requestBroadcastChanges(peerId: string): Promise; - getRemoteIsBroadcasting(peerId: string): Promise; - _watchingPeers: Set; - watchPeer(peerId: string): void; - unwatchPeer(peerId: string): void; - onUpdateDatabase(fromPeerId: string): Promise; - getRemoteConfig(peerId: string): Promise; - checkTweakValues(peerId: string): Promise; - replicateFromCommand(showResult?: boolean): Promise; - disconnectFromServer(): void; - pauseServe(): Promise; - allowReconnection(): void; -} diff --git a/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PClient.d.ts b/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PClient.d.ts deleted file mode 100644 index 693f9aac..00000000 --- a/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PClient.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { PouchDBShim, SomeDocument } from "@lib/pouchdb/ReplicatorShim"; -import type { TrysteroReplicatorP2PServer } from "./TrysteroReplicatorP2PServer"; -import { type BindableObject, type NonPrivateMethodKeys, type Response } from "./types"; -import type { JsonLike } from "@lib/rpc"; -export declare class TrysteroReplicatorP2PClient { - _server: TrysteroReplicatorP2PServer; - _connectedPeerId: string; - _remoteDB: PouchDBShim>; - get remoteDB(): PouchDBShim>; - constructor(server: TrysteroReplicatorP2PServer, connectedPeerId: string); - _bindRemoteDB(): PouchDBShim>; - _sendRPC(type: string, args: JsonLike[], timeout?: number): Promise; - __onResponse(_data: Response): void; - bindRemoteFunction(type: string, timeout?: number): (...args: T) => Promise; - invokeRemoteFunction(type: string, args: T, timeout?: number): Promise; - bindRemoteObjectFunctions, U extends keyof T>(key: U, timeout?: number): (...args: Parameters) => Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - invokeRemoteObjectFunction, U extends NonPrivateMethodKeys>(key: U, args: Parameters, timeout?: number): Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - close(): void; -} diff --git a/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PConnection.d.ts b/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PConnection.d.ts deleted file mode 100644 index 482fca67..00000000 --- a/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PConnection.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { TrysteroReplicatorP2PServer } from "./TrysteroReplicatorP2PServer"; -export { TrysteroReplicatorP2PServer as TrysteroConnection }; diff --git a/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PServer.d.ts b/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PServer.d.ts deleted file mode 100644 index 25fd4b34..00000000 --- a/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PServer.d.ts +++ /dev/null @@ -1,119 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ActionSender, type Room } from "@trystero-p2p/nostr"; -import { type P2PSyncSetting } from "@lib/common/types"; -import { type ReplicatorHostEnv, type FullFilledDeviceInfo, type Request, type Response, type Payload, type Advertisement, type BindableObject, type BindableFunction } from "./types"; -import { StoredMapLike } from "@lib/dataobject/StoredMap"; -import { TrysteroReplicatorP2PClient } from "./TrysteroReplicatorP2PClient"; -import { Computed } from "octagonal-wheels/dataobject/Computed"; -import { RpcRoom, type JsonLike } from "@lib/rpc"; -import { type DiagRTCStats } from "@lib/rpc/transports/DiagRTCPeerConnections.types"; -export type PeerInfo = Advertisement & { - isAccepted: boolean | undefined; - isTemporaryAccepted: boolean | undefined; -}; -export type AcceptanceDecision = { - peerId: string; - name: string; - decision: boolean; - isTemporary: boolean; -}; -export type RevokeAcceptanceDecision = { - peerId: string; - name: string; -}; -export type P2PServerInfo = { - isConnected: boolean; - knownAdvertisements: PeerInfo[]; - serverPeerId: string; - roomId: string; - diag: DiagRTCStats; -}; -export declare const EVENT_SERVER_STATUS = "p2p-server-status"; -export declare const EVENT_MAKE_DECISION = "make-decision-p2p-peer"; -export declare const EVENT_REVOKE_DECISION = "revoke-decision-p2p-peer"; -export declare const EVENT_ADVERTISEMENT_RECEIVED = "p2p-advertisement-received"; -export declare const EVENT_DEVICE_LEAVED = "p2p-device-leaved"; -export declare const EVENT_REQUEST_STATUS = "p2p-request-status"; -export declare const EVENT_P2P_REQUEST_FORCE_OPEN = "p2p-request-force-open"; -export declare const EVENT_P2P_CONNECTED = "p2p-connected"; -export declare const EVENT_P2P_DISCONNECTED = "p2p-disconnected"; -export declare const EVENT_P2P_REPLICATOR_STATUS = "p2p-replicator-status"; -export declare const EVENT_P2P_REPLICATOR_PROGRESS = "p2p-replicator-progress"; -declare global { - interface LSEvents { - [EVENT_SERVER_STATUS]: P2PServerInfo; - [EVENT_MAKE_DECISION]: AcceptanceDecision; - [EVENT_REVOKE_DECISION]: RevokeAcceptanceDecision; - [EVENT_ADVERTISEMENT_RECEIVED]: Advertisement; - [EVENT_DEVICE_LEAVED]: string; - [EVENT_REQUEST_STATUS]: undefined; - [EVENT_P2P_REQUEST_FORCE_OPEN]: undefined; - [EVENT_P2P_CONNECTED]: undefined; - [EVENT_P2P_DISCONNECTED]: undefined; - } -} -export declare class TrysteroReplicatorP2PServer { - _env: ReplicatorHostEnv; - _room?: Room; - _serverPeerId: string; - _activeRoomId: string; - ___send?: ActionSender; - assignedFunctions: Map; - clients: Map; - _bindingObjects: BindableObject[]; - _rpcRoom?: RpcRoom; - protected _peerStatusEventCleanup: (() => void) | undefined; - protected _peerFailureAnalysisCleanup: (() => void) | undefined; - protected _peerConnectionEventCleanup(): void; - _diagStats: DiagRTCStats; - get isDisposed(): boolean; - get isServing(): boolean; - ensureLeaved(): Promise; - setRoom(room: Room): Promise; - shutdown(): Promise; - dispatchConnectionStatus(): Promise; - constructor(env: ReplicatorHostEnv, _serverPeerId?: string); - makeDecision(decision: AcceptanceDecision): Promise; - revokeDecision(decision: RevokeAcceptanceDecision): Promise; - get room(): Room | undefined; - get serverPeerId(): string; - get db(): PouchDB.Database; - get confirm(): import("../../interfaces/Confirm").Confirm; - get settings(): P2PSyncSetting; - get isEnabled(): boolean; - get deviceInfo(): FullFilledDeviceInfo; - _sendAdvertisement?: ActionSender; - sendAdvertisement(peerId?: string): void; - _knownAdvertisements: Map; - get knownAdvertisements(): Advertisement[]; - onAdvertisement(data: Advertisement, peerId: string): void; - acceptedPeers: StoredMapLike; - temporaryAcceptedPeers: Map; - confirmUserToAccept(peerId: string): Promise; - _confirmUserToAccept(peerId: string): Promise; - _acceptablePeers: Computed<[settings: P2PSyncSetting], RegExp[]>; - _shouldDenyPeers: Computed<[settings: P2PSyncSetting], RegExp[]>; - isAcceptablePeer(peerId: string): Promise; - __send(data: Payload, peerId: string): Promise; - processArrivedRPC(data: Payload, peerId: string): Promise; - private _onPeerJoin; - private _onPeerLeave; - activePeer: Map; - onAfterJoinRoom(): void; - startService(bindings?: BindableObject[]): Promise; - start(bindings?: BindableObject[]): Promise; - /** - * @deprecated Use serveFunction or serveObject instead. This is only for backward compatibility and may be removed in the future. - * @param type - * @param func - */ - serveFunction(type: string, func: (peerId: string, ...args: T) => U | Promise): void; - serveObject(obj: BindableObject): void; - __onResponse(data: Response, peerId: string): void; - __onRequest(data: Request, peerId: string): Promise; - close(): Promise; - getConnection(peerId: string): TrysteroReplicatorP2PClient; - get rpcRoom(): RpcRoom | undefined; -} -export { TrysteroReplicatorP2PServer as P2PHost }; diff --git a/_types/src/lib/src/replication/trystero/UseP2PReplicatorResult.d.ts b/_types/src/lib/src/replication/trystero/UseP2PReplicatorResult.d.ts deleted file mode 100644 index 3fe87433..00000000 --- a/_types/src/lib/src/replication/trystero/UseP2PReplicatorResult.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2"; -import type { LiveSyncTrysteroReplicator } from "./LiveSyncTrysteroReplicator"; -import type { P2PLogCollector } from "./P2PLogCollector"; -export type UseP2PReplicatorResult = { - replicator: LiveSyncTrysteroReplicator; -}; -export type P2PPaneParams = { - replicator: LiveSyncTrysteroReplicator; - p2pLogCollector: P2PLogCollector; - storeP2PStatusLine: ReactiveSource; -}; diff --git a/_types/src/lib/src/replication/trystero/addP2PEventHandlers.d.ts b/_types/src/lib/src/replication/trystero/addP2PEventHandlers.d.ts deleted file mode 100644 index 7eab73e0..00000000 --- a/_types/src/lib/src/replication/trystero/addP2PEventHandlers.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LiveSyncTrysteroReplicator } from "./LiveSyncTrysteroReplicator"; -import type { Advertisement } from "./types"; -/** - * Minimal interface that a P2P replicator instance should satisfy for addP2PEventHandlers to work. - */ -export interface P2PReplicatorLike { - onNewPeer(peer: Advertisement): Promise | void; - onPeerLeaved(peerId: string): void; - requestStatus(): void; - open(): Promise; - close(): Promise; - /** Indicates whether the room is currently active. */ - readonly isServing?: boolean; - /** Legacy: host object that may carry isServing (LiveSyncTrysteroReplicator). */ - readonly server?: { - isServing?: boolean; - }; -} -/** - * Add event handlers for P2P replication related events. - * @param instance P2PReplicatorLike instance - */ -export declare function addP2PEventHandlers(instance: P2PReplicatorLike): void; -/** - * open P2P replicator if not opened yet. - * @param instance - */ -export declare function openP2PReplicator(instance: P2PReplicatorLike): Promise; -/** - * close P2P replicator - * @param instance - */ -export declare function closeP2PReplicator(instance: P2PReplicatorLike): Promise; -export type { LiveSyncTrysteroReplicator }; diff --git a/_types/src/lib/src/replication/trystero/rpcCompat.d.ts b/_types/src/lib/src/replication/trystero/rpcCompat.d.ts deleted file mode 100644 index 1e3e69a0..00000000 --- a/_types/src/lib/src/replication/trystero/rpcCompat.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function toRpcMethodName(method: string): string; diff --git a/_types/src/lib/src/replication/trystero/types.d.ts b/_types/src/lib/src/replication/trystero/types.d.ts deleted file mode 100644 index 2efa7d8c..00000000 --- a/_types/src/lib/src/replication/trystero/types.d.ts +++ /dev/null @@ -1,91 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { JsonLike } from "@lib/rpc"; -import type { P2PSyncSetting, EntryDoc } from "@lib/common/types"; -import type { SimpleStore } from "@lib/common/utils"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { AsyncActivityRunner } from "@lib/interfaces/AsyncActivityRunner"; -export declare const DIRECTION_REQUEST = "request"; -export type DIRECTION_REQUEST = typeof DIRECTION_REQUEST; -export declare const DIRECTION_RESPONSE = "response"; -export type DIRECTION_RESPONSE = typeof DIRECTION_RESPONSE; -export declare const DEFAULT_RPC_TIMEOUT = 30000; -export declare const BULK_GET_RPC_TIMEOUT = 40000; -export type BindableFunction = (...args: any[]) => any; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export type NonPrivateMethodKeys = { - [K in keyof T]: K extends `_${string}` ? never : K extends `constructor` ? never : T[K] extends BindableFunction ? K : never; -}[keyof T]; -export type BindableObject = { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - [k in NonPrivateMethodKeys]: T[k] extends BindableFunction ? T[k] : never; -}; -export type ConnectionInfo = { - relayURIs: string[]; - roomId: string; - password: string; - appId: string; -}; -export declare class ResponsePreventedError extends Error { - constructor(message: string); -} -export type Request = { - type: string; - direction: DIRECTION_REQUEST; - seq: number; - args: T; -}; -export type Response = { - type: string; - direction: DIRECTION_RESPONSE; - seq: number; - data?: T; - error?: JsonLike; -}; -export type DeviceInfo = { - currentPeerId: string; - name?: string; - version?: string; - platform?: string; - decision?: DeviceDecisions; -}; -export type DeviceInfoForRequest = { - currentPeerId: string; - name: string; -}; -export type FullFilledDeviceInfo = { - currentPeerId: string; - name: string; - version: string; - platform: string; - decision?: DeviceDecisions; -}; -export declare enum DeviceDecisions { - ACCEPT = "accepted", - REJECT = "rejected", - IGNORE = "ignore" -} -export declare const ID_P2PKnownDevices = "_local/P2PKnownDevices"; -export type KnownDevices = { - _id: typeof ID_P2PKnownDevices; - devices: { - [deviceName: string]: DeviceDecisions; - }; -}; -export type Payload = Request | Response; -export interface ReplicatorHost { - deviceName: string; - platform: string; - confirm: Confirm; -} -export interface ReplicatorHostEnv extends ReplicatorHost { - settings: P2PSyncSetting; - db: PouchDB.Database; - simpleStore: SimpleStore; - runFiniteReplicationActivity?: AsyncActivityRunner["run"]; - processReplicatedDocs(docs: Array>): void | Promise; -} -export type Advertisement = { - peerId: string; - name: string; - platform: string; -}; -export declare const KEY_DEVICE_DECISIONS = "p2p-device-decisions"; diff --git a/_types/src/lib/src/replication/trystero/useP2PReplicatorCommands.d.ts b/_types/src/lib/src/replication/trystero/useP2PReplicatorCommands.d.ts deleted file mode 100644 index 28df7f32..00000000 --- a/_types/src/lib/src/replication/trystero/useP2PReplicatorCommands.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import type { UseP2PReplicatorResult } from "./UseP2PReplicatorResult"; -/** - * ServiceFeature: Registers event handlers for P2P replication and manages the lifecycle of a LiveSyncTrysteroReplicator instance. - * @param host - */ -export declare function useP2PReplicatorCommands(host: NecessaryServices<"API" | "setting", never>, { replicator }: UseP2PReplicatorResult): void; diff --git a/_types/src/lib/src/replication/trystero/useP2PReplicatorFeature.d.ts b/_types/src/lib/src/replication/trystero/useP2PReplicatorFeature.d.ts deleted file mode 100644 index 9fb24c80..00000000 --- a/_types/src/lib/src/replication/trystero/useP2PReplicatorFeature.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { LiveSyncTrysteroReplicator } from "./LiveSyncTrysteroReplicator"; -import { type UseP2PReplicatorResult } from "./UseP2PReplicatorResult"; -/** - * Factory type: given a replicator instance, returns the openReplicationUI callback for that instance. - * Injected by the host platform (e.g. Obsidian). CLI/headless environments omit this. - */ -export type OpenReplicationUIFactory = (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise; -/** Same shape as OpenReplicationUIFactory, used for the rebuild/replicateAllFromServer flow. */ -export type OpenRebuildUIFactory = OpenReplicationUIFactory; -/** - * ServiceFeature: P2P Replicator integration and lifecycle management. - * Registers a LiveSyncTrysteroReplicator instance as the active replicator when P2P is enabled in settings, - * and binds it to lifecycle events for proper initialization and cleanup. - * @param host - */ -export declare function useP2PReplicatorFeature(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "database" | "databaseEvents" | "keyValueDB" | "replication" | "config" | "UI" | "replicator" | "remote", never>, openReplicationUIFactory?: OpenReplicationUIFactory, openRebuildUIFactory?: OpenRebuildUIFactory): UseP2PReplicatorResult; diff --git a/_types/src/lib/src/rpc/RpcRoom.d.ts b/_types/src/lib/src/rpc/RpcRoom.d.ts deleted file mode 100644 index 2a64ed85..00000000 --- a/_types/src/lib/src/rpc/RpcRoom.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { RpcSession } from "./RpcSession"; -import { type JsonLike, type RpcMethodHandler, type RpcRegisterOptions, type RpcRoomOptions } from "./types"; -export declare class RpcRoom { - private options; - private pending; - private inboundCalls; - private methods; - private sessions; - private outgoingChunkMap; - private incomingChunkMap; - private incomingChunkTimers; - private peerVersion; - private disposer; - constructor(options: RpcRoomOptions); - close(): void; - session(peerId: string): RpcSession; - register(method: string, handler: RpcMethodHandler, options?: RpcRegisterOptions): void; - invoke(peerId: string, method: string, args: JsonLike[], timeoutMs?: number): Promise; - cancel(peerId: string, requestId: string): Promise; - private sendEnvelope; - private scheduleMissingAck; - private onWireMessage; - private onEnvelopePayload; -} diff --git a/_types/src/lib/src/rpc/RpcSession.d.ts b/_types/src/lib/src/rpc/RpcSession.d.ts deleted file mode 100644 index ceee44e0..00000000 --- a/_types/src/lib/src/rpc/RpcSession.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { JsonLike } from "./types"; -import type { RpcRoom } from "./RpcRoom"; -export declare class RpcSession { - readonly peerId: string; - private room; - constructor(room: RpcRoom, peerId: string); - call(method: string, args?: JsonLike[], timeoutMs?: number): Promise; - createProxy(namespace: string): T; -} diff --git a/_types/src/lib/src/rpc/chunking.d.ts b/_types/src/lib/src/rpc/chunking.d.ts deleted file mode 100644 index fbbc2b53..00000000 --- a/_types/src/lib/src/rpc/chunking.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function estimateBytes(text: string): number; -export declare function splitIntoChunks(payload: string, maxBytes: number): string[]; -export declare class IncomingChunkBuffer { - total: number; - parts: Map; - constructor(total: number); - add(index: number, payload: string): void; - missingIndices(): number[]; - isComplete(): boolean; - toPayload(): string; -} diff --git a/_types/src/lib/src/rpc/errors.d.ts b/_types/src/lib/src/rpc/errors.d.ts deleted file mode 100644 index 2673f539..00000000 --- a/_types/src/lib/src/rpc/errors.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { JsonLike, RpcErrorCode, RpcErrorShape } from "./types"; -export declare class RpcError extends Error { - code: RpcErrorCode; - details?: JsonLike; - constructor(code: RpcErrorCode, message: string, details?: JsonLike); - toShape(): RpcErrorShape; -} -export declare function asRpcErrorShape(ex: unknown): RpcErrorShape; diff --git a/_types/src/lib/src/rpc/index.d.ts b/_types/src/lib/src/rpc/index.d.ts deleted file mode 100644 index 6df2bb5a..00000000 --- a/_types/src/lib/src/rpc/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export { RpcRoom } from "./RpcRoom"; -export { RpcSession } from "./RpcSession"; -export { RpcError } from "./errors"; -export { exposeDB } from "./pouchdb/RpcPouchDBServer"; -export { RpcPouchDBProxy } from "./pouchdb/RpcPouchDBProxy"; -export type { JsonLike, RpcEnvelope, RpcErrorCode, RpcErrorShape, RpcMethodHandler, RpcRegisterOptions, RpcRoomOptions, RpcWireMessage, TransportAdapter, } from "./types"; diff --git a/_types/src/lib/src/rpc/pouchdb/RpcPouchDBProxy.d.ts b/_types/src/lib/src/rpc/pouchdb/RpcPouchDBProxy.d.ts deleted file mode 100644 index d1e2a0fd..00000000 --- a/_types/src/lib/src/rpc/pouchdb/RpcPouchDBProxy.d.ts +++ /dev/null @@ -1,86 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { RpcSession } from "@lib/rpc/RpcSession"; -/** - * A PouchDB-compatible proxy that forwards all database operations to a remote - * peer via an {@link RpcSession}. - * - * The proxy exposes the same public interface as a PouchDB instance and can be - * passed directly to `PouchDB.replicate()` or `PouchDB.sync()` as either the - * source or the target database. It can also be used with {@link replicateShim} - * from `ReplicatorShim.ts`. - * - * The remote side must have called {@link exposeDB} to register the matching - * RPC handlers. - * - * ### Changes feed - * `changes()` returns an object that satisfies both the EventEmitter interface - * required by `pouchdb-replication` and the Promise interface required by - * `replicateShim` (i.e. it can be `await`ed directly). - * - * ### Error propagation - * PouchDB-specific error properties (`status`, `name`, `reason`) are preserved - * across the RPC transport and reconstructed on the proxy side so that callers - * such as `pouchdb-checkpointer` can inspect `err.status === 404` correctly. - */ -export declare class RpcPouchDBProxy extends EventEmitter { - /** The logical name of the remote database. */ - readonly name: string; - /** - * Stub ActiveTasks object required by `pouchdb-replication`. All - * operations are no-ops; task state is not tracked across the RPC boundary. - */ - readonly activeTasks: { - add: (_task: object) => unknown; - get: (_id: unknown) => unknown; - update: (_id: unknown, _update: object) => void; - remove: (_id: unknown, _err?: Error) => void; - list: () => unknown[]; - }; - private readonly session; - private readonly ns; - constructor(session: RpcSession, name: string, ns?: string); - /** - * Invoke an RPC method and reconstruct PouchDB error shapes on the response. - * - * When the remote handler wraps a PouchDB error via {@link exposeDB}'s - * `runDB` helper, the `RpcError.details` object carries `status`, `name`, - * and `reason`. This method rebuilds a plain `Error` with those properties - * so that callers (e.g. pouchdb-checkpointer) can use `err.status` / - * `err.name` as expected. - */ - private callDB; - info(): Promise; - id(): Promise; - /** - * Returns a `Changes`-compatible object that is simultaneously: - * - An **EventEmitter** with `change`, `complete`, and `error` events, plus - * a `cancel()` method — satisfying the interface consumed by - * `PouchDB.replicate()` / `PouchDB.sync()`. - * - A **thenable** (`then` / `catch`) — allowing `await db.changes(opts)` - * as used by `replicateShim`. - * - * The remote changes feed is always fetched as a one-shot snapshot - * (`live: false`). - */ - changes(opts: PouchDB.Core.ChangesOptions): PouchDB.Core.Changes; - get(id: string, opts?: PouchDB.Core.GetOptions): Promise; - put(doc: PouchDB.Core.PutDocument, opts?: PouchDB.Core.PutOptions): Promise; - bulkGet(opts: PouchDB.Core.BulkGetOptions): Promise>; - bulkDocs(docs: PouchDB.Core.PostDocument[] | { - docs: PouchDB.Core.PostDocument[]; - new_edits?: boolean; - }, opts?: PouchDB.Core.BulkDocsOptions): Promise<(PouchDB.Core.Response | PouchDB.Core.Error)[]>; - revsDiff(diff: PouchDB.Core.RevisionDiffOptions): Promise; - allDocs(opts?: PouchDB.Core.AllDocsOptions): Promise>; -} - -class EventEmitter { - on(event: string | symbol, listener: (...args: any[]) => void): this; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - once(event: string | symbol, listener: (...args: any[]) => void): this; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - off(event: string | symbol, listener: (...args: any[]) => void): this; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - emit(event: string | symbol, ...args: any[]): boolean; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - addListener(event: string | symbol, listener: (...args: any[]) => void): this; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - removeListener(event: string | symbol, listener: (...args: any[]) => void): this; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - removeAllListeners(event: string | symbol): this; -} diff --git a/_types/src/lib/src/rpc/pouchdb/RpcPouchDBServer.d.ts b/_types/src/lib/src/rpc/pouchdb/RpcPouchDBServer.d.ts deleted file mode 100644 index d4c617dd..00000000 --- a/_types/src/lib/src/rpc/pouchdb/RpcPouchDBServer.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { RpcRoom } from "@lib/rpc/RpcRoom"; -/** - * Exposes a PouchDB database as a set of RPC methods registered on an - * {@link RpcRoom}. The remote peer can access the database via - * {@link RpcPouchDBProxy}. - * - * All methods are registered under the given namespace prefix `ns` (default: - * `'pdb'`). For example, with the default namespace the method names are - * `pdb.info`, `pdb.id`, `pdb.changes`, `pdb.get`, `pdb.put`, `pdb.bulkGet`, - * `pdb.bulkDocs`, `pdb.revsDiff`, and `pdb.allDocs`. - * - * @param room The {@link RpcRoom} on which to register handlers. - * @param db The PouchDB database instance to expose. - * @param ns Method namespace prefix (default: `'pdb'`). - */ -export declare function exposeDB(room: RpcRoom, db: PouchDB.Database, ns?: string): void; diff --git a/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.d.ts b/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.d.ts deleted file mode 100644 index bbb19f4d..00000000 --- a/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DiagRTCStats, DiagRTCFailureDiagnosis } from "./DiagRTCPeerConnections.types"; -/** - * Subscribes to connection status updates. The callback will be called with the latest connection statistics whenever there is a change in the connection status of any RTCPeerConnection instance. - * Returns an unsubscribe function to stop receiving updates. - * - * @param callback - The function to call with the latest connection statistics. - * @returns A function that can be called to unsubscribe from updates. - */ -export declare function subscribeConnectionStatus(callback: (status: DiagRTCStats) => void): () => void; -/** - * Subscribes to failure diagnosis updates. The callback will be called with the diagnosis information whenever a connection failure is detected in any RTCPeerConnection instance. - * Returns an unsubscribe function to stop receiving updates. - * @param callback - The function to call with the diagnosis information. - * @returns A function that can be called to unsubscribe from updates. - */ -export declare function subscribeFailureDiagnosis(callback: (diagnosis: DiagRTCFailureDiagnosis) => void): () => void; -export type DiagRTCPeerConnectionConstructor = typeof RTCPeerConnection; -/** - * A wrapper around RTCPeerConnection to collect statistics for diagnostics. - * It extends the native (or globally-polyfilled) RTCPeerConnection and overrides its constructor to add event listeners for connection state changes, - * ice connection state changes, ice gathering state changes, and signaling state changes. It maintains a history of these states and logs the progress. - * It also tracks the number of new connections, failed connections, successful connections, and closed connections, and dispatches this information to subscribers. - */ -export declare function createDiagRTCPeerConnectionConstructor(): DiagRTCPeerConnectionConstructor; diff --git a/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.types.d.ts b/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.types.d.ts deleted file mode 100644 index 767d1238..00000000 --- a/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.types.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type DiagRTCConnectionStatus = { - connectionState: RTCPeerConnection["connectionState"]; - iceConnectionState: RTCPeerConnection["iceConnectionState"]; -}; -export type DiagRTCStats = { - totalNewConnections: number; - totalFailedConnections: number; - totalSuccessfulConnections: number; - totalClosedConnections: number; - details: Record; -}; -export type DiagRTCPeerConnectionInternalStateHistory = { - connectionHistory: RTCPeerConnection["connectionState"][]; - iceConnectionHistory: RTCPeerConnection["iceConnectionState"][]; - iceGatheringHistory: RTCPeerConnection["iceGatheringState"][]; - signalingHistory: RTCPeerConnection["signalingState"][]; -}; -export declare const DiagRTCFailureReasonCodes: { - readonly ICE_GATHERING_NOT_COMPLETED: "ICE_GATHERING_NOT_COMPLETED"; - readonly ICE_CONNECTIVITY_FAILED: "ICE_CONNECTIVITY_FAILED"; - readonly STUN_REQUEST_TIMEOUT: "STUN_REQUEST_TIMEOUT"; - readonly SIGNALING_NOT_STABLE: "SIGNALING_NOT_STABLE"; - readonly CONNECTION_DROPPED_AFTER_ESTABLISHED: "CONNECTION_DROPPED_AFTER_ESTABLISHED"; - readonly NETWORK_INTERRUPTED: "NETWORK_INTERRUPTED"; - readonly UNKNOWN: "UNKNOWN"; -}; -export type DiagRTCFailureDiagnosis = { - reasonCode: keyof typeof DiagRTCFailureReasonCodes; - userMessage: string; -}; -export type DiagRTCFailureStats = { - diagnosis: DiagRTCFailureDiagnosis; - stats: { - reportsCount: number; - selectedPair: { - id: string; - state: string; - localCandidateId: string; - remoteCandidateId: string; - currentRoundTripTime: number | "unknown"; - totalRoundTripTime: number | "unknown"; - requestsSent: number | "unknown"; - responsesReceived: number | "unknown"; - packetsDiscardedOnSend: number | "unknown"; - bytesSent: number | "unknown"; - bytesReceived: number | "unknown"; - }; - }; -}; -export type DiagRTCPeerConnectionMetrics = { - selectedPair: Record | undefined; - selectedPairId: string; - state: string; - localCandidateId: string; - remoteCandidateId: string; - currentRoundTripTime: number | "unknown"; - totalRoundTripTime: number | "unknown"; - requestsSent: number | "unknown"; - responsesReceived: number | "unknown"; - packetsDiscardedOnSend: number | "unknown"; - bytesSent: number | "unknown"; - bytesReceived: number | "unknown"; - reports: unknown[]; -}; diff --git a/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.utils.d.ts b/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.utils.d.ts deleted file mode 100644 index bd11f674..00000000 --- a/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.utils.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type DiagRTCPeerConnectionInternalStateHistory, type DiagRTCFailureDiagnosis, type DiagRTCPeerConnectionMetrics, type DiagRTCFailureStats } from "./DiagRTCPeerConnections.types"; -/** - * Diagnoses the failure reason of a failed RTCPeerConnection based on its internal state history and selected candidate pair information. - * @param internalStateHistory The internal state history of the RTCPeerConnection. - * @param selectedPair The selected candidate pair information. - * @returns The diagnosis of the RTC failure. - */ -export declare function diagnoseRtcFailure(internalStateHistory: DiagRTCPeerConnectionInternalStateHistory, selectedPair: Record | undefined): DiagRTCFailureDiagnosis; -/** - * Describes the current progress of the RTCPeerConnection based on its internal state history and selected candidate pair information. - * This is useful for providing user-friendly status messages during the connection process. - * @param internalStateHistory The internal state history of the RTCPeerConnection. - * @param selectedPair The selected candidate pair information. - * @returns A user-friendly description of the current connection progress. - */ -export declare function describeRTCProgress(internalStateHistory: DiagRTCPeerConnectionInternalStateHistory, selectedPair: Record | undefined): string; -/** - * Fetches the RTCPeerConnection statistics and returns a structured metrics object. - * This is useful for diagnosing connection issues and understanding the connection performance. - * @param instanceId An identifier for the RTCPeerConnection instance, used for logging. - * @param peer The RTCPeerConnection instance to fetch stats from. - * @returns A structured object containing the connection metrics, or undefined if fetching stats failed. - */ -export declare function getPeerConnectionStats(instanceId: string, peer: RTCPeerConnection): Promise; -/** - * Audits the RTCPeerConnection for failures and returns a structured failure report. - * This is useful for diagnosing connection issues and understanding the reasons for failure. - * @param instanceId An identifier for the RTCPeerConnection instance, used for logging. - * @param internalStateHistory The internal state history of the RTCPeerConnection. - * @param peer The RTCPeerConnection instance to audit. - * @returns A structured object containing the failure diagnosis and metrics, or undefined if auditing failed. - */ -export declare function auditRtcConnectionFailures(instanceId: string, internalStateHistory: DiagRTCPeerConnectionInternalStateHistory, peer: RTCPeerConnection): Promise; diff --git a/_types/src/lib/src/rpc/transports/TrysteroTransport.d.ts b/_types/src/lib/src/rpc/transports/TrysteroTransport.d.ts deleted file mode 100644 index c5896447..00000000 --- a/_types/src/lib/src/rpc/transports/TrysteroTransport.d.ts +++ /dev/null @@ -1,218 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type Room } from "@trystero-p2p/nostr"; -import { RpcRoom } from "@lib/rpc/RpcRoom"; -import { RpcPouchDBProxy } from "@lib/rpc/pouchdb/RpcPouchDBProxy"; -import type { RpcRoomOptions, TransportAdapter } from "@lib/rpc/types"; -import type { P2PConnectionInfo } from "@lib/common/models/setting.type"; -/** - * Lightweight presence record broadcast by each peer when it joins the room. - * The `peerId` field MUST match the Trystero sender ID to prevent spoofing. - */ -export type TrysteroAdvertisement = { - /** The Trystero `selfId` of the sending peer. */ - peerId: string; - /** Human-readable device / vault name. */ - name: string; - /** Optional platform tag (e.g. `"desktop"`, `"mobile"`). */ - platform: string; -}; -/** The handle returned by {@link attachAdvertisement}. */ -export type AdvertisementHandle = { - /** - * All currently known peers, keyed by `peerId`. - * Updated in real-time as advertisements arrive or peers leave. - */ - readonly peers: ReadonlyMap; - /** - * (Re-)broadcast your own advertisement. - * Pass a `toPeerId` to send only to that peer; omit to broadcast to all. - */ - sendAdvertisement(toPeerId?: string): Promise; - /** Stop listening for advertisements and clear the peer map. */ - stop(): void; -}; -/** - * Attach advertisement-based peer discovery to an already-joined Trystero room. - * - * - Sends your advertisement to every peer that joins after this call. - * - Stores incoming advertisements in `handle.peers`. - * - Removes entries when peers leave. - * - * @param room An active Trystero room. - * @param localPeerId Your own Trystero `selfId`. - * @param name Human-readable name broadcast to other peers. - * @param platform Optional platform string (default `"unknown"`). - */ -export declare function attachAdvertisement(room: Room, localPeerId: string, name: string, platform?: string): AdvertisementHandle; -/** - * Options forwarded to the `RpcRoom` constructor when creating a room through - * the high-level helpers (`serveTrysteroDB`, `connectTrysteroDBClient`, or by - * calling `joinTrysteroRoom` and then constructing `RpcRoom` manually). - * - * Trystero internally chunks at ~16 348 bytes (16 KiB minus a 36-byte header). - * Setting `maxWirePayloadBytes` above that threshold causes double-chunking. - * The Trystero-appropriate default is therefore **15 360 bytes** (15 KiB), - * which leaves ~1 KiB of headroom for the JSON wrapper overhead. - * - * Trystero uses WebRTC SCTP data channels which guarantee delivery and order, - * so missing-chunk retransmission virtually never triggers. `chunkMissingRetryMs` - * can safely be lowered from the generic default of 350 ms to **150 ms**. - */ -export type TrysteroRoomOptions = Pick & { - maxWirePayloadBytes?: number; - chunkMissingRetryMs?: number; -}; -/** Trystero-appropriate defaults for `RpcRoom`. */ -export declare const TRYSTERO_RPC_DEFAULTS: { - /** Stay below Trystero's internal ~16 348-byte chunk boundary. */ - readonly maxWirePayloadBytes: number; - /** SCTP guarantees delivery; retransmission is a last-resort safety net. */ - readonly chunkMissingRetryMs: 150; -}; -/** - * Wrap an already-joined Trystero `Room` as a `TransportAdapter` for `RpcRoom`. - * - * A dedicated Trystero action channel named `"rpc2"` is created on the room. - * Note: Trystero does not expose per-handler unsubscription for `onPeerJoin` - * / `onPeerLeave`, so the returned cleanup stubs are no-ops. Close the - * Trystero room via `leave()` when done. - * - * @param room An active Trystero `Room` returned by `joinRoom()`. - * @returns A `TransportAdapter` that can be passed to `new RpcRoom(...)`. - */ -export declare function wrapTrysteroRoom(room: Room): TransportAdapter; -/** The result of joining a Trystero room. */ -export type TrysteroRoomHandle = { - /** `TransportAdapter` ready to pass to `new RpcRoom(...)`. */ - transport: TransportAdapter; - /** This peer's own ID (Trystero `selfId`). */ - peerId: string; - /** Leave the Trystero room and release resources. */ - leave: () => Promise; - /** - * Attach advertisement-based peer discovery to this room. - * Call once after joining; returns a handle to read and re-broadcast. - */ - advertise: (name: string, platform?: string) => AdvertisementHandle; - /** - * Returns the current WebRTC peer connections keyed by peer ID. - * Equivalent to the Trystero `room.getPeers()` call. - */ - getPeers: () => Record; - /** - * The underlying Trystero `Room` instance. - * Use when you need raw access to Trystero APIs such as `makeAction`, - * `onPeerJoin`, or `onPeerLeave` beyond what the helpers expose. - */ - room: Room; -}; -/** - * Join a Trystero room from a {@link P2PConnectionInfo} and return a - * `TransportAdapter` together with this peer's own ID. - * - * The raw passphrase is hashed with `mixedHash` before being passed to - * Trystero, matching the convention used by `TrysteroReplicatorP2PServer`. - */ -export declare function joinTrysteroRoom(settings: P2PConnectionInfo): TrysteroRoomHandle; -/** - * Join a Trystero room from a `sls+p2p://` connection string and return a - * `TransportAdapter` together with this peer's own ID. - * - * The URL must be parseable by {@link ConnectionStringParser.parse} as type - * `"p2p"`, i.e. it must start with `sls+p2p://`. - * - * @example - * ```ts - * const handle = joinTrysteroRoomFromUrl( - * "sls+p2p://my-room?relays=wss://relay.example.com&appId=my-app" - * ); - * const rpcRoom = new RpcRoom({ transport: handle.transport }); - * ``` - */ -export declare function joinTrysteroRoomFromUrl(url: string): TrysteroRoomHandle; -/** The result of starting a DB server over Trystero. */ -export type TrysteroDBServerHandle = { - /** This peer's own ID. Share it with clients so they can call `session()`. */ - peerId: string; - /** The `RpcRoom` that hosts the DB methods. */ - rpcRoom: RpcRoom; - /** Stop serving and leave the room. */ - close: () => Promise; - /** - * Attach advertisement-based peer discovery so that clients can find this - * server without needing the peerId passed out-of-band. - */ - advertise: (name: string, platform?: string) => AdvertisementHandle; -}; -/** - * **Server side.** Join a Trystero room and expose `db` as a set of RPC - * methods. The caller's `peerId` is returned so that it can be shared with - * clients (e.g. via a separate signalling channel or advertisement). - * - * @param settings P2P connection settings (from `P2PConnectionInfo` or parsed - * from a `sls+p2p://` URL via {@link ConnectionStringParser}). - * @param db The PouchDB database to expose. - * @param ns Method namespace (default `"pdb"`). - * - * @example - * ```ts - * const server = serveTrysteroDB(settings, db); - * console.log("server peer ID:", server.peerId); - * // Share server.peerId with the client out-of-band. - * ``` - */ -export declare function serveTrysteroDB(settings: P2PConnectionInfo, db: PouchDB.Database, ns?: string, options?: TrysteroRoomOptions): TrysteroDBServerHandle; -/** The result of connecting to a DB server over Trystero. */ -export type TrysteroDBClientHandle = { - /** Proxy object usable with `PouchDB.replicate()` or `replicateShim()`. */ - proxy: RpcPouchDBProxy; - /** The `RpcRoom` used to communicate with the server. */ - rpcRoom: RpcRoom; - /** Disconnect and leave the room. */ - close: () => Promise; - /** - * Attach advertisement-based peer discovery so this client can locate the - * server peer without needing its peerId passed statically. - */ - advertise: (name: string, platform?: string) => AdvertisementHandle; -}; -/** - * **Client side.** Join a Trystero room and create an {@link RpcPouchDBProxy} - * pointing at `serverPeerId`. The proxy can be passed directly to - * `PouchDB.replicate()` or `replicateShim()`. - * - * @param settings P2P connection settings. - * @param serverPeerId The `peerId` of the server peer (returned by - * {@link serveTrysteroDB}). - * @param dbName Logical name for the remote database. - * @param ns Method namespace, must match the server's `ns` - * (default `"pdb"`). - * - * @example - * ```ts - * const client = connectTrysteroDBClient(settings, serverPeerId, "my-db"); - * await PouchDB.replicate(client.proxy, localDb, { live: false }); - * await client.close(); - * ``` - */ -export declare function connectTrysteroDBClient(settings: P2PConnectionInfo, serverPeerId: string, dbName: string, ns?: string, options?: TrysteroRoomOptions): TrysteroDBClientHandle; -/** - * Join a Trystero room, advertise as `name`, wait `timeoutMs`, collect all - * received peer advertisements, then leave the room and return the results. - * - * Useful for CLI-style peer discovery where you need a one-shot list of - * present peers before deciding how to connect. - * - * @param settings P2P connection settings. - * @param name Your local device / vault name advertised to others. - * @param timeoutMs How long to listen before returning (milliseconds). - * @param platform Optional platform tag (default `"unknown"`). - * - * @example - * ```ts - * const peers = await collectTrysteroAdvertisements(settings, "my-vault", 5000); - * console.log(peers.map(p => `${p.name} (${p.peerId})`)); - * ``` - */ -export declare function collectTrysteroAdvertisements(settings: P2PConnectionInfo, name: string, timeoutMs: number, platform?: string): Promise; diff --git a/_types/src/lib/src/rpc/transports/trysteroUtils.d.ts b/_types/src/lib/src/rpc/transports/trysteroUtils.d.ts deleted file mode 100644 index af185119..00000000 --- a/_types/src/lib/src/rpc/transports/trysteroUtils.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { BaseRoomConfig } from "@trystero-p2p/nostr"; -import type { P2PConnectionInfo } from "@lib/common/models/setting.type"; -export declare function generateJoinRoomOptions(settings: P2PConnectionInfo): BaseRoomConfig; diff --git a/_types/src/lib/src/rpc/types.d.ts b/_types/src/lib/src/rpc/types.d.ts deleted file mode 100644 index fac0f406..00000000 --- a/_types/src/lib/src/rpc/types.d.ts +++ /dev/null @@ -1,77 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const RPC_VERSION_MAJOR = 1; -export declare const RPC_VERSION_MINOR = 0; -export type JsonLike = null | boolean | number | string | JsonLike[] | { - [key: string]: JsonLike; -}; -export type RpcErrorCode = "TIMEOUT" | "NOT_CONNECTED" | "REMOTE_ERROR" | "CANCELLED" | "PROTOCOL_ERROR"; -export type RpcErrorShape = { - code: RpcErrorCode; - message: string; - details?: JsonLike; -}; -export type RpcRequestEnvelope = { - kind: "request"; - requestId: string; - method: string; - args: JsonLike[]; -}; -export type RpcResponseEnvelope = { - kind: "response"; - requestId: string; - ok: true; - data: JsonLike; -}; -export type RpcResponseErrorEnvelope = { - kind: "response"; - requestId: string; - ok: false; - error: RpcErrorShape; -}; -export type RpcCancelEnvelope = { - kind: "cancel"; - requestId: string; -}; -export type RpcHandshakeEnvelope = { - kind: "handshake"; - versionMajor: number; - versionMinor: number; -}; -export type RpcEnvelope = RpcRequestEnvelope | RpcResponseEnvelope | RpcResponseErrorEnvelope | RpcCancelEnvelope | RpcHandshakeEnvelope; -export type RpcWireMessageRaw = { - wire: "raw"; - payload: string; -}; -export type RpcWireMessageChunk = { - wire: "chunk"; - streamId: string; - index: number; - total: number; - payload: string; -}; -export type RpcWireMessageChunkAck = { - wire: "chunk-ack"; - streamId: string; - missing: number[]; -}; -export type RpcWireMessage = RpcWireMessageRaw | RpcWireMessageChunk | RpcWireMessageChunkAck; -export type TransportOnMessage = (message: RpcWireMessage, peerId: string) => void; -export type TransportOnPeer = (peerId: string) => void; -export interface TransportAdapter { - send(message: RpcWireMessage, peerId: string): void | Promise; - onMessage(handler: TransportOnMessage): () => void; - onPeerJoin?(handler: TransportOnPeer): () => void; - onPeerLeave?(handler: TransportOnPeer): () => void; -} -export type RpcRoomOptions = { - transport: TransportAdapter; - maxWirePayloadBytes?: number; - chunkMissingRetryMs?: number; - canAcceptRequest?: (peerId: string, method: string) => boolean | Promise; - onProtocolWarning?: (message: string, peerId?: string) => void; -}; -export type RpcMethodHandler = (peerId: string, ...args: T) => U | Promise; -export type RpcRegisterOptions = { - serial?: boolean; -}; diff --git a/_types/src/lib/src/serviceFeatures/checkRemoteSize.d.ts b/_types/src/lib/src/serviceFeatures/checkRemoteSize.d.ts deleted file mode 100644 index f694406f..00000000 --- a/_types/src/lib/src/serviceFeatures/checkRemoteSize.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { createInstanceLogFunction, type LogFunction } from "@lib/services/lib/logUtils"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -/** - * Notify when checking remote storage size is not configured. - * @returns true if the check is passed or user has configured the notification, false to block subsequent processes. (always true). - */ -export declare function onNotifyRemoteSizeNotConfiguredFactory(host: NecessaryServices<"appLifecycle" | "API" | "setting", never>, log: ReturnType): () => Promise; -/** - * Notify when the remote storage size exceed the threshold. - * @returns true if the check is passed or user has chosen to ignore the warning, false to block subsequent processes. - * @param host - * @param log - * @returns - */ -export declare function onNotifyRemoteSizeExceedFactory(host: NecessaryServices<"API" | "setting" | "replicator", "rebuilder">, log: ReturnType): () => Promise; -/** - * Scan the remote storage size and notify if it is not configured or exceed the threshold. - * @param host The necessary services required for the operation. - * @param log The logging function to use for logging messages. - * @param resetThreshold Whether to reset the notification threshold before scanning. This is useful when you want to force the notification to show up again. - * @returns A promise that resolves to true if all checks pass or user has configured the notification. - */ -export declare function scanAllStat(host: NecessaryServices<"API" | "setting" | "replicator" | "appLifecycle", "rebuilder">, log: LogFunction, resetThreshold?: boolean): Promise; -/** - * Associate the remote storage size check feature with the app lifecycle events. - * @param host - */ -export declare function useCheckRemoteSize(host: NecessaryServices<"API" | "setting" | "replicator" | "appLifecycle", "rebuilder">): void; diff --git a/_types/src/lib/src/serviceFeatures/offlineScanner.d.ts b/_types/src/lib/src/serviceFeatures/offlineScanner.d.ts deleted file mode 100644 index e1af96ff..00000000 --- a/_types/src/lib/src/serviceFeatures/offlineScanner.d.ts +++ /dev/null @@ -1,130 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type FilePathWithPrefix, type FilePathWithPrefixLC, type MetaEntry, type UXFileInfoStub, type ObsidianLiveSyncSettings, type LOG_LEVEL } from "@lib/common/types"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager"; -/** - * Collect deleted files that have expired according to retention policy. - * @param host Services container - * @param log Logging function - * @returns Array of expired deletion history - */ -export declare function collectDeletedFiles(host: NecessaryServices<"setting" | "database", never>, log: LogFunction): Promise; -/** - * Get the file path from a meta entry. - * This is a helper function to extract path from various document types. - * @param doc Meta entry document - * @returns Path string - */ -export declare function getPathFromEntry(host: NecessaryServices<"path", never>, doc: MetaEntry): FilePathWithPrefix; -/** - * Synchronise a single file between database and storage based on freshness comparison. - * @param host Services container - * @param log Logging function - * @param file Storage file information - * @param doc Database entry - */ -export declare function syncFileBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, doc: MetaEntry): Promise; -export declare function canProceedScan(host: NecessaryServices<"keyValueDB" | "setting", never>, errorManager: UnresolvedErrorManager, log: LogFunction, showingNotice?: boolean, ignoreSuspending?: boolean): boolean; -/** - * Convert file path to lower case if the settings indicate that filename case should be handled insensitively. - * @param settings - * @param path - * @returns - */ -export declare function convertCase(settings: ObsidianLiveSyncSettings, path: T): FilePathWithPrefixLC; -export declare function collectFilesOnStorage(host: NecessaryServices<"vault", "storageAccess">, settings: ObsidianLiveSyncSettings, log: LogFunction): Promise<{ - storageFileNameMap: { - [k: string]: UXFileInfoStub; - }; - storageFileNames: FilePathWithPrefix[]; - storageFileNameCI2CS: Record; -}>; -export declare function collectDatabaseFiles(host: NecessaryServices<"database" | "vault" | "path", never>, settings: ObsidianLiveSyncSettings, log: LogFunction, showingNotice: boolean): Promise<{ - databaseFileNameMap: { - [k: string]: MetaEntry; - }; - databaseFileNames: FilePathWithPrefix[]; - databaseFileNameCI2CS: Record; -}>; -export declare function updateToDatabase(host: NecessaryServices<"vault", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, file: UXFileInfoStub): Promise; -export declare function updateToStorage(host: NecessaryServices<"vault" | "path", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, w: MetaEntry): Promise; -export declare function syncStorageAndDatabase(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, logLevel: LOG_LEVEL, doc: MetaEntry): Promise; -export declare const FullScanModes: { - readonly DB_APPLY: "db-apply"; - readonly NEWER_WINS: "newer-wins"; -}; -export declare const ExtraOnRemote: { - /** - * Delete database entries if they are missing on storage. - */ - readonly DELETE_LOCAL_MISSING: "delete-local-missing"; -}; -export declare const ExtraOnLocal: { - /** - * Delete local files if they were deleted on database. - */ - readonly DELETE_DB_DELETED: "delete-db-deleted"; - /** - * Delete local files if they are missing on database or were deleted on database. - */ - readonly DELETE_DB_MISSING: "delete-db-missing"; - /** - * Merge local files to database - */ - readonly APPEND_STORAGE_ONLY: "append-storage-only"; -}; -export interface FullScanOptions { - mode: FullScanMode; - extraOnLocal?: (typeof ExtraOnLocal)[keyof typeof ExtraOnLocal]; - extraOnRemote?: (typeof ExtraOnRemote)[keyof typeof ExtraOnRemote]; - omitEvents?: boolean; - showingNotice?: boolean; - ignoreSuspending?: boolean; -} -export type FullScanMode = (typeof FullScanModes)[keyof typeof FullScanModes]; -type FilePair = { - file: UXFileInfoStub; - doc: MetaEntry; -} | { - file: undefined; - doc: MetaEntry; -} | { - file: UXFileInfoStub; - doc: undefined; -}; -type FilePairState = "storage-only" | "db-only" | "db-only-deleted" | "both" | "both-db-deleted"; -type FilePairAction = "update-db" | "update-storage" | "sync-newer" | "delete-local" | "delete-db" | "skip"; -export declare function getFilePairState(pair: FilePair): FilePairState; -/** - * Determine the action to be taken for a file pair based on its state and the selected scan options. - */ -export declare function resolveFilePairAction(state: FilePairState, options: FullScanOptions): FilePairAction; -/** - * Synchronise all files between database and storage based on the selected mode and options. - * @param host Core - * @param log Logging function - * @param errorManager Error manager - * @param options Full scan options - */ -export declare function synchroniseAllFilesBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options: FullScanOptions): Promise; -export declare function normaliseFullScanOptions(showingNoticeOrOptions: Partial | boolean | undefined, ignoreSuspending?: boolean): FullScanOptions; -/** - * Perform a full scan and synchronisation between database and storage. - * @param host Services container - * @param log Logging function - * @param errorManager Error manager - * @param showingNotice Whether to show notices during scanning - * @param ignoreSuspending Whether to ignore suspension settings - * @returns True if scan completed successfully - */ -export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options?: Partial): Promise; -export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, ignoreSuspending?: boolean): Promise; -/** - * Associate the initialiser file feature with the app lifecycle events. - * This function binds initialization handlers to the appropriate lifecycle events. - * @param host Services container with required dependencies - */ -export declare function useOfflineScanner(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "keyValueDB" | "replicator", "storageAccess" | "fileHandler">): void; -export {}; diff --git a/_types/src/lib/src/serviceFeatures/prepareDatabaseForUse.d.ts b/_types/src/lib/src/serviceFeatures/prepareDatabaseForUse.d.ts deleted file mode 100644 index 5e3b84b7..00000000 --- a/_types/src/lib/src/serviceFeatures/prepareDatabaseForUse.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -/** - * Initialise the database and trigger a full vault scan. - * @param host Services container - * @param log Logging function - * @param errorManager Error manager - * @param showingNotice Whether to show notices during initialisation - * @param reopenDatabase Whether to reopen the database connection - * @param ignoreSuspending Whether to ignore suspension settings - * @returns True if initialisation succeeded - */ -export declare function prepareDatabaseForUse(host: NecessaryServices<"appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean): Promise; -/** - * Associate the initialiser file feature with the app lifecycle events. - * This function binds initialization handlers to the appropriate lifecycle events. - * @param host Services container with required dependencies - */ -export declare function usePrepareDatabaseForUse(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>): void; diff --git a/_types/src/lib/src/serviceFeatures/remoteConfig.d.ts b/_types/src/lib/src/serviceFeatures/remoteConfig.d.ts deleted file mode 100644 index 5fa4781e..00000000 --- a/_types/src/lib/src/serviceFeatures/remoteConfig.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type LOG_LEVEL } from "@lib/common/logger"; -import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -export type RemoteConfigHost = NecessaryServices<"setting" | "UI" | "replication" | "control" | "appLifecycle" | "API", never>; -export declare function migrateLegacyRemoteConfigurationsInPlace(settings: ObsidianLiveSyncSettings, log?: (message: string, level?: LOG_LEVEL) => void): boolean; -/** - * Generate a unique ID for a new remote configuration. - * @returns A unique string identifier. - */ -export declare function createRemoteConfigurationId(): string; -/** - * Keep compatibility for users who were already using P2P as their main active remote. - */ -export declare function migrateP2PActiveRemoteConfigurationIdInPlace(settings: ObsidianLiveSyncSettings): boolean; -/** - * SF:RemoteConfig - Service Feature for Remote Configuration Management - */ -/** - * Migrates existing flat settings to the new multiple remote configurations list. - */ -export declare function migrateToMultipleRemoteConfigurations(host: RemoteConfigHost): Promise; -/** - * Logic to switch the active configuration. - */ -export declare function activateRemoteConfiguration(settings: ObsidianLiveSyncSettings, id: string): ObsidianLiveSyncSettings | false; -/** - * Apply a dedicated P2P remote configuration onto runtime P2P-related fields, - * while keeping the current `remoteType` unchanged. - */ -export declare function activateP2PRemoteConfiguration(settings: ObsidianLiveSyncSettings, id: string): ObsidianLiveSyncSettings | false; -/** - * Command: Switch Active Remote - */ -export declare function commandSwitchActiveRemote(host: RemoteConfigHost): Promise; -/** - * Command: Replicate with specific remote - */ -export declare function commandReplicateWithSpecificRemote(host: RemoteConfigHost): Promise; -/** - * Migration feature to be used during initialisation. - */ -export declare function useRemoteConfigurationMigration(host: RemoteConfigHost): void; -/** - * Hook to set up remote configuration features (Commands). - */ -export declare function useRemoteConfiguration(host: RemoteConfigHost): boolean; diff --git a/_types/src/lib/src/serviceFeatures/setupObsidian/qrCode.d.ts b/_types/src/lib/src/serviceFeatures/setupObsidian/qrCode.d.ts deleted file mode 100644 index 2caa30c0..00000000 --- a/_types/src/lib/src/serviceFeatures/setupObsidian/qrCode.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import type { SetupFeatureHost } from "./types"; -export declare function encodeSetupSettingsAsQR(host: SetupFeatureHost): Promise; -export declare function useSetupQRCodeFeature(host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>): void; diff --git a/_types/src/lib/src/serviceFeatures/setupObsidian/setupUri.d.ts b/_types/src/lib/src/serviceFeatures/setupObsidian/setupUri.d.ts deleted file mode 100644 index af1aa8d8..00000000 --- a/_types/src/lib/src/serviceFeatures/setupObsidian/setupUri.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LogFunction } from "@lib/services/lib/logUtils"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import type { SetupFeatureHost } from "./types"; -export declare function askEncryptingPassphrase(host: SetupFeatureHost): Promise; -export declare function copySetupURI(host: SetupFeatureHost, log: LogFunction, stripExtra?: boolean): Promise; -export declare function copySetupURIFull(host: SetupFeatureHost, log: LogFunction): Promise; -export declare function useSetupURIFeature(host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>): void; diff --git a/_types/src/lib/src/serviceFeatures/setupObsidian/types.d.ts b/_types/src/lib/src/serviceFeatures/setupObsidian/types.d.ts deleted file mode 100644 index b45c29eb..00000000 --- a/_types/src/lib/src/serviceFeatures/setupObsidian/types.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -export type SetupFeatureHost = NecessaryServices<"API" | "UI" | "setting", never>; diff --git a/_types/src/lib/src/serviceFeatures/targetFilter.d.ts b/_types/src/lib/src/serviceFeatures/targetFilter.d.ts deleted file mode 100644 index 615f71df..00000000 --- a/_types/src/lib/src/serviceFeatures/targetFilter.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type UXFileInfoStub } from "@lib/common/types"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -/** - * This is a simple handler that accepts all files. - */ -export declare function isAcceptedAlwaysFactory(host: NecessaryServices, log: LogFunction): (file: string | UXFileInfoStub) => Promise; -/** - * Check if a file is accepted based on filename duplication in the vault. - */ -export declare function isAcceptedInFilenameDuplicationFactory(host: NecessaryServices<"vault" | "fileProcessing", "storageAccess">, log: LogFunction): (file: string | UXFileInfoStub) => Promise; -/** - * Check if a file is accepted by the local database (e.g., not rejected by the local DB's target file check). - * Local database responsible for non-internal files, syncOnlyRegEx, syncIgnoreRegEx - * This possibly should be separated. - */ -export declare function isAcceptedByLocalDBFactory(host: NecessaryServices<"database" | "databaseEvents", never>, log: LogFunction): (file: string | UXFileInfoStub) => Promise; -/** - * Factory function to create the isAcceptedByIgnoreFiles handler. - * This handler checks if a file is ignored based on the ignore files specified in the settings. - * It also caches the ignore file contents for performance and listens to settings changes to invalidate the cache. - */ -export declare function isAcceptedByIgnoreFilesFactory(host: NecessaryServices<"setting" | "appLifecycle", "storageAccess">, log: LogFunction): (file: string | UXFileInfoStub) => Promise; -export declare function useTargetFilters(host: NecessaryServices<"API" | "vault" | "fileProcessing" | "setting" | "appLifecycle" | "database" | "databaseEvents", "storageAccess">): void; diff --git a/_types/src/lib/src/serviceModules/FileAccessBase.d.ts b/_types/src/lib/src/serviceModules/FileAccessBase.d.ts deleted file mode 100644 index 4cb02b36..00000000 --- a/_types/src/lib/src/serviceModules/FileAccessBase.d.ts +++ /dev/null @@ -1,95 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, UXDataWriteOptions, UXFileInfoStub, UXFolderInfo } from "@lib/common/types.ts"; -import type { IStorageAccessManager } from "@lib/interfaces/StorageAccess.ts"; -import type { IAPIService, IPathService, ISettingService, IVaultService } from "@lib/services/base/IService.ts"; -import { createInstanceLogFunction } from "@lib/services/lib/logUtils.ts"; -import type { FileWithFileStat } from "@lib/common/models/fileaccess.type"; -import type { IFileSystemAdapter } from "./adapters"; -export declare function toArrayBuffer(arr: Uint8Array | ArrayBuffer | DataView): ArrayBuffer; -export interface FileAccessBaseDependencies { - vaultService: IVaultService; - storageAccessManager: IStorageAccessManager; - settingService: ISettingService; - pathService: IPathService; - APIService: IAPIService; -} -/** - * Type helper to extract the abstract file type from a file system adapter - */ -export type ExtractAbstractFile = T extends IFileSystemAdapter ? A : never; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -/** - * Type helper to extract the file type from a file system adapter - */ -export type ExtractFile = T extends IFileSystemAdapter ? F : never; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -/** - * Type helper to extract the folder type from a file system adapter - */ -export type ExtractFolder = T extends IFileSystemAdapter ? D : never; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -/** - * Type helper to extract the stat type from a file system adapter - */ -export type ExtractStat = T extends IFileSystemAdapter ? S : never; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -/** - * Base class for file access operations - * Uses adapter pattern for platform-specific implementations - * - * @template TAdapter - The file system adapter type, which determines all native file types - */ -export declare class FileAccessBase> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - protected storageAccessManager: IStorageAccessManager; - protected vaultService: IVaultService; - protected settingService: ISettingService; - protected APIService: IAPIService; - protected path: IPathService; - protected adapter: TAdapter; - _log: ReturnType; - constructor(adapter: TAdapter, dependencies: FileAccessBaseDependencies); - isFile(file: UXFileInfoStub | ExtractAbstractFile | FilePath | ExtractFolder | ExtractFile | null): file is ExtractFile; - isFolder(item: UXFileInfoStub | ExtractAbstractFile | FilePath | ExtractFolder | ExtractFile | null): item is ExtractFolder; - getPath(file: ExtractAbstractFile | string): FilePath; - nativeFileToUXFileInfoStub(file: ExtractFile): UXFileInfoStub; - nativeFolderToUXFolder(file: ExtractFolder): UXFolderInfo; - normalisePath(path: string): string; - protected _writeOp | string, U>(file: T, callback: (path: FilePath, file: T) => Promise): Promise; - protected _readOp | string, U>(file: T, callback: (path: FilePath, file: T) => Promise): Promise; - tryAdapterStat(file: ExtractFile | string): Promise; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - adapterStat(file: ExtractFile | string): Promise | null>; - adapterExists(file: ExtractFile | string): Promise; - adapterRemove(file: ExtractFile | string): Promise; - adapterRead(file: ExtractFile | string): Promise; - adapterReadBinary(file: ExtractFile | string): Promise; - adapterReadAuto(file: ExtractFile | string): Promise; - adapterWrite(file: ExtractFile | string, data: string | ArrayBuffer | Uint8Array, options?: UXDataWriteOptions): Promise; - adapterList(basePath: string): Promise<{ - files: string[]; - folders: string[]; - }>; - vaultCacheRead(file: ExtractFile): Promise; - vaultRead(file: ExtractFile): Promise; - vaultReadBinary(file: ExtractFile): Promise; - vaultReadAuto(file: ExtractFile): Promise; - vaultModify(file: ExtractFile, data: string | ArrayBuffer | Uint8Array, options?: UXDataWriteOptions): Promise; - vaultCreate(path: string, data: string | ArrayBuffer | Uint8Array, options?: UXDataWriteOptions): Promise>; - vaultRename(file: ExtractFile, newPath: string): Promise>; - trigger(name: string, ...data: unknown[]): void; - reconcileInternalFile(path: string): Promise; - /** - * Append data to a file using the adapter's append method. This is useful for large files that cannot be read into memory. - * Please note that this method does not check concurrent modifications. - * @param normalizedPath - * @param data - * @param options - * @returns - */ - adapterAppend(normalizedPath: string, data: string, options?: UXDataWriteOptions): Promise; - delete(file: ExtractAbstractFile | ExtractFolder, force?: boolean): Promise; - trash(file: ExtractAbstractFile | ExtractFolder, force?: boolean): Promise; - protected isStorageInsensitive(): boolean; - getAbstractFileByPath(path: FilePath | string): Promise | null>; - getFiles(): Promise[]>; - ensureDirectory(fullPath: string): Promise; - touch(file: ExtractFile | FilePath): Promise; - recentlyTouched(file: ExtractFile | UXFileInfoStub | FileWithFileStat): boolean; - clearTouched(): void; -} diff --git a/_types/src/lib/src/serviceModules/Rebuilder.d.ts b/_types/src/lib/src/serviceModules/Rebuilder.d.ts deleted file mode 100644 index 30c8178f..00000000 --- a/_types/src/lib/src/serviceModules/Rebuilder.d.ts +++ /dev/null @@ -1,91 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { IFileHandler } from "@lib/interfaces/FileHandler"; -import type { APIService } from "@lib/services/base/APIService"; -import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; -import type { DatabaseEventService } from "@lib/services/base/DatabaseEventService"; -import type { DatabaseService } from "@lib/services/base/DatabaseService"; -import type { RemoteService } from "@lib/services/base/RemoteService"; -import type { ReplicationService } from "@lib/services/base/ReplicationService"; -import type { ReplicatorService } from "@lib/services/base/ReplicatorService"; -import type { SettingService } from "@lib/services/base/SettingService"; -import type { VaultService } from "@lib/services/base/VaultService"; -import type { UIService } from "@lib/services/implements/base/UIService"; -import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess"; -import { ServiceModuleBase } from "@lib/serviceModules/ServiceModuleBase"; -import type { ControlService } from "@lib/services/base/ControlService"; -export interface ServiceRebuilderDependencies { - appLifecycle: AppLifecycleService; - API: APIService; - UI: UIService; - setting: SettingService; - remote: RemoteService; - databaseEvents: DatabaseEventService; - storageAccess: StorageAccess; - replicator: ReplicatorService; - vault: VaultService; - replication: ReplicationService; - database: DatabaseService; - fileHandler: IFileHandler; - control: ControlService; -} -export declare class ServiceRebuilder extends ServiceModuleBase implements Rebuilder { - private appLifecycle; - private API; - private UI; - private setting; - private remote; - private databaseEvents; - private storageAccess; - private replicator; - private vault; - private replication; - private database; - private fileHandler; - private control; - constructor(services: ServiceRebuilderDependencies); - $performRebuildDB(method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks"): Promise; - informOptionalFeatures(): Promise; - askUsingOptionalFeature(opt: { - enableFetch?: boolean; - enableOverwrite?: boolean; - }): Promise; - rebuildRemote(): Promise; - private performRemoteRebuild; - $rebuildRemote(): Promise; - rebuildEverything(): Promise; - private performRebuildEverything; - $rebuildEverything(): Promise; - $fetchLocal(makeLocalChunkBeforeSync?: boolean, preventMakeLocalFilesBeforeSync?: boolean): Promise; - $fetchLocalDBFast(autoResume: boolean): Promise; - scheduleRebuild(): Promise; - scheduleFetch(): Promise; - private _tryResetRemoteDatabase; - private _onResetLocalDatabase; - suspendAllSync(): Promise; - suspendReflectingDatabase(ignoreMinIO?: boolean): Promise; - resumeReflectingDatabase(ignoreMinIO?: boolean): Promise; - fetchLocal(makeLocalChunkBeforeSync?: boolean, preventMakeLocalFilesBeforeSync?: boolean, autoResume?: boolean): Promise; - private performFetchLocal; - fetchLocalDBFast(autoResume: boolean): Promise; - private performFetchLocalDBFast; - /** - * Finish rebuild process with resuming the reflection. - * - * @param ignoreMinIO Whether to ignore minio for resuming the reflection. - */ - finishRebuild(ignoreMinIO?: boolean): Promise; - /** - * Fetch local database with making all chunks. - * This is a wrapper for {@link fetchLocal} with makeLocalChunkBeforeSync = true. - * - * @returns - */ - fetchLocalWithRebuild(): Promise; - private _allSuspendAllSync; - resetLocalDatabase(): Promise; - private getFastFetchCheckpoint; - private saveFastFetchCheckpoint; - private clearFastFetchCheckpoint; -} diff --git a/_types/src/lib/src/serviceModules/ServiceDatabaseFileAccessBase.d.ts b/_types/src/lib/src/serviceModules/ServiceDatabaseFileAccessBase.d.ts deleted file mode 100644 index 9e1b60e2..00000000 --- a/_types/src/lib/src/serviceModules/ServiceDatabaseFileAccessBase.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UXFileInfoStub, FilePathWithPrefix, UXFileInfo, MetaEntry, LoadedEntry, FilePath } from "@lib/common/types"; -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess"; -import type { APIService } from "@lib/services/base/APIService"; -import type { DatabaseService } from "@lib/services/base/DatabaseService"; -import type { PathService } from "@lib/services/base/PathService"; -import type { VaultService } from "@lib/services/base/VaultService"; -import { ServiceModuleBase } from "@lib/serviceModules/ServiceModuleBase"; -export interface ServiceDatabaseFileAccessDependencies { - API: APIService; - vault: VaultService; - storageAccess: StorageAccess; - path: PathService; - database: DatabaseService; -} -export declare class ServiceDatabaseFileAccessBase extends ServiceModuleBase implements DatabaseFileAccess { - private vault; - private storageAccess; - private path; - private database; - constructor(services: ServiceDatabaseFileAccessDependencies); - checkIsTargetFile(file: UXFileInfoStub | FilePathWithPrefix): Promise; - delete(file: UXFileInfoStub | FilePathWithPrefix, rev?: string): Promise; - createChunks(file: UXFileInfo, force?: boolean, skipCheck?: boolean): Promise; - store(file: UXFileInfo, force?: boolean, skipCheck?: boolean): Promise; - storeAsConflictedRevision(file: UXFileInfo, currentRev: string, skipCheck?: boolean): Promise; - storeContent(path: FilePathWithPrefix, content: string): Promise; - private __store; - private getParentRev; - hasContentInRevisionHistory(file: UXFileInfoStub | FilePathWithPrefix, content: string | string[] | Blob | ArrayBuffer, currentRev?: string): Promise; - getConflictedRevs(file: UXFileInfoStub | FilePathWithPrefix): Promise; - fetch(file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean): Promise; - fetchEntryMeta(file: UXFileInfoStub | FilePathWithPrefix, rev?: string, skipCheck?: boolean): Promise; - fetchEntryFromMeta(meta: MetaEntry, waitForReady?: boolean, skipCheck?: boolean): Promise; - fetchEntry(file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean): Promise; - deleteFromDBbyPath(fullPath: FilePath | FilePathWithPrefix, rev?: string): Promise; -} diff --git a/_types/src/lib/src/serviceModules/ServiceFileAccessBase.d.ts b/_types/src/lib/src/serviceModules/ServiceFileAccessBase.d.ts deleted file mode 100644 index 358ab54f..00000000 --- a/_types/src/lib/src/serviceModules/ServiceFileAccessBase.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, FilePathWithPrefix, UXDataWriteOptions, UXFileInfo, UXFileInfoStub, UXFolderInfo, UXStat } from "@lib/common/types"; -import { ServiceModuleBase } from "@lib/serviceModules/ServiceModuleBase"; -import type { APIService } from "@lib/services/base/APIService"; -import type { IStorageAccessManager, StorageAccess } from "@lib/interfaces/StorageAccess.ts"; -import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; -import type { FileProcessingService } from "@lib/services/base/FileProcessingService"; -import { StorageEventManager } from "@lib/interfaces/StorageEventManager.ts"; -import { type CustomRegExp } from "@lib/common/utils"; -import type { VaultService } from "@lib/services/base/VaultService"; -import type { SettingService } from "@lib/services/base/SettingService"; -import type { FileAccessBase, ExtractFile, ExtractFolder } from "@lib/serviceModules/FileAccessBase"; -import type { IFileSystemAdapter } from "@lib/serviceModules/adapters"; -export interface StorageAccessBaseDependencies> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - API: APIService; - appLifecycle: AppLifecycleService; - fileProcessing: FileProcessingService; - vault: VaultService; - setting: SettingService; - storageEventManager: StorageEventManager; - storageAccessManager: IStorageAccessManager; - vaultAccess: FileAccessBase; -} -export declare class ServiceFileAccessBase> extends ServiceModuleBase> implements StorageAccess { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - private vaultAccess; - private vaultManager; - private vault; - private setting; - constructor(services: StorageAccessBaseDependencies); - restoreState(): Promise; - _everyOnFirstInitialize(): Promise; - _everyCommitPendingFileEvent(): Promise; - normalisePath(path: string): string; - writeFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise; - readFileAuto(path: string): Promise; - readFileText(path: string): Promise; - isExists(path: string): Promise; - renameFile(file: UXFileInfoStub | FilePathWithPrefix, newPath: FilePathWithPrefix): Promise; - writeHiddenFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise; - appendHiddenFile(path: string, data: string, opt?: UXDataWriteOptions): Promise; - stat(path: string): Promise; - statHidden(path: string): Promise; - removeHidden(path: string): Promise; - readHiddenFileAuto(path: string): Promise; - readHiddenFileText(path: string): Promise; - readHiddenFileBinary(path: string): Promise; - isExistsIncludeHidden(path: string): Promise; - ensureDir(path: string): Promise; - _triggerFileEvent(event: string, path: string): Promise; - triggerFileEvent(event: string, path: string): void; - triggerHiddenFile(path: string): Promise; - getFileStub(path: string): Promise; - readStubContent(stub: UXFileInfoStub): Promise; - getStub(path: string): Promise; - getFiles(): Promise; - getFileNames(): Promise; - getFilesIncludeHidden(basePath: string, includeFilter?: CustomRegExp[], excludeFilter?: CustomRegExp[], skipFolder?: string[]): Promise; - touched(file: UXFileInfoStub | FilePathWithPrefix): Promise; - recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): Promise; - clearTouched(): void; - delete(file: FilePathWithPrefix | UXFileInfoStub | string, force: boolean): Promise; - trash(file: FilePathWithPrefix | UXFileInfoStub | string, system: boolean): Promise; - __deleteVaultItem(file: ExtractFile | ExtractFolder): Promise; - deleteVaultItem(fileSrc: FilePathWithPrefix | UXFileInfoStub | UXFolderInfo): Promise; -} diff --git a/_types/src/lib/src/serviceModules/ServiceFileHandlerBase.d.ts b/_types/src/lib/src/serviceModules/ServiceFileHandlerBase.d.ts deleted file mode 100644 index f534285e..00000000 --- a/_types/src/lib/src/serviceModules/ServiceFileHandlerBase.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { AnyEntry, FilePath, FilePathWithPrefix, MetaEntry, UXFileInfo, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/types"; -import type { IFileHandler } from "@lib/interfaces/FileHandler.ts"; -import { ServiceModuleBase } from "@lib/serviceModules/ServiceModuleBase"; -import type { APIService } from "@lib/services/base/APIService.ts"; -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess.ts"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess.ts"; -import type { FileProcessingService } from "@lib/services/base/FileProcessingService.ts"; -import type { ReplicationService } from "@lib/services/base/ReplicationService.ts"; -import type { ConflictService } from "@lib/services/base/ConflictService.ts"; -import type { PathService } from "@lib/services/base/PathService.ts"; -import type { SettingService } from "@lib/services/base/SettingService.ts"; -import type { VaultService } from "@lib/services/base/VaultService.ts"; -export interface ServiceFileHandlerDependencies { - API: APIService; - databaseFileAccess: DatabaseFileAccess; - storageAccess: StorageAccess; - fileProcessing: FileProcessingService; - replication: ReplicationService; - conflict: ConflictService; - path: PathService; - setting: SettingService; - vault: VaultService; -} -export declare abstract class ServiceFileHandlerBase extends ServiceModuleBase implements IFileHandler { - private databaseFileAccess; - private storageAccess; - private conflict; - private path; - private setting; - private vault; - constructor(services: ServiceFileHandlerDependencies); - get db(): DatabaseFileAccess; - get storage(): StorageAccess; - getPath(entry: AnyEntry): FilePathWithPrefix; - getPathWithoutPrefix(entry: AnyEntry): FilePathWithPrefix; - readFileFromStub(file: UXFileInfoStub | UXFileInfo): Promise; - private infoToStub; - storeFileToDB(info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, force?: boolean, onlyChunks?: boolean): Promise; - deleteFileFromDB(info: UXFileInfoStub | UXInternalFileInfoStub | FilePath): Promise; - renameFileInDB(info: UXFileInfoStub | UXFileInfo, oldPath: FilePath | FilePathWithPrefix): Promise; - deleteRevisionFromDB(info: UXFileInfoStub | FilePath | FilePathWithPrefix, rev: string): Promise; - resolveConflictedByDeletingRevision(info: UXFileInfoStub | FilePath, rev: string): Promise; - dbToStorageWithSpecificRev(info: UXFileInfoStub | UXFileInfo | FilePath | null, rev: string, force?: boolean): Promise; - dbToStorage(entryInfo: MetaEntry | FilePathWithPrefix, info: UXFileInfoStub | UXFileInfo | FilePath | null, force?: boolean): Promise; - private preserveUnsyncedStorageAsConflict; - private _anyHandlerProcessesFileEvent; - _anyProcessReplicatedDoc(entry: MetaEntry): Promise; - createAllChunks(showingNotice?: boolean): Promise; -} diff --git a/_types/src/lib/src/serviceModules/ServiceModuleBase.d.ts b/_types/src/lib/src/serviceModules/ServiceModuleBase.d.ts deleted file mode 100644 index 751a754b..00000000 --- a/_types/src/lib/src/serviceModules/ServiceModuleBase.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { APIService } from "@lib/services/base/APIService"; -import { createInstanceLogFunction } from "@lib/services/lib/logUtils"; -export interface ServiceModuleBaseDependencies { - API: APIService; -} -export declare abstract class ServiceModuleBase { - _log: ReturnType; - get name(): string; - constructor(services: T); -} diff --git a/_types/src/lib/src/serviceModules/adapters/IConversionAdapter.d.ts b/_types/src/lib/src/serviceModules/adapters/IConversionAdapter.d.ts deleted file mode 100644 index bd30f936..00000000 --- a/_types/src/lib/src/serviceModules/adapters/IConversionAdapter.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types.ts"; -/** - * Conversion adapter interface - * Converts between native file system types and universal types - */ -export interface IConversionAdapter { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Convert a native file object to a universal file info stub - */ - nativeFileToUXFileInfoStub(file: TNativeFile): UXFileInfoStub; - /** - * Convert a native folder object to a universal folder info - */ - nativeFolderToUXFolder(folder: TNativeFolder): UXFolderInfo; -} diff --git a/_types/src/lib/src/serviceModules/adapters/IFileSystemAdapter.d.ts b/_types/src/lib/src/serviceModules/adapters/IFileSystemAdapter.d.ts deleted file mode 100644 index cba85358..00000000 --- a/_types/src/lib/src/serviceModules/adapters/IFileSystemAdapter.d.ts +++ /dev/null @@ -1,49 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, UXStat } from "@lib/common/types.ts"; -import type { IPathAdapter } from "./IPathAdapter.ts"; -import type { ITypeGuardAdapter } from "./ITypeGuardAdapter.ts"; -import type { IConversionAdapter } from "./IConversionAdapter.ts"; -import type { IStorageAdapter } from "./IStorageAdapter.ts"; -import type { IVaultAdapter } from "./IVaultAdapter.ts"; -/** - * Main file system adapter interface - * Composes all other adapters and provides platform-specific operations - */ -export interface IFileSystemAdapter { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** Path operations */ - readonly path: IPathAdapter; - /** Type guard operations */ - readonly typeGuard: ITypeGuardAdapter; - /** Conversion operations */ - readonly conversion: IConversionAdapter; - /** Storage operations */ - readonly storage: IStorageAdapter; - /** Vault operations */ - readonly vault: IVaultAdapter; - /** - * Get a file or folder by path (case-sensitive) - */ - getAbstractFileByPath(path: FilePath | string): Promise; - /** - * Get a file or folder by path (case-insensitive) - */ - getAbstractFileByPathInsensitive(path: FilePath | string): Promise; - /** - * Get all files in the vault - */ - getFiles(): Promise; - /** - * Rename a file and refresh any platform-specific caches - */ - renameFile(file: TNativeFile, newPath: string): Promise; - /** - * Get file statistics from a native file object - */ - statFromNative(file: TNativeFile): Promise; - /** - * Reconcile internal file state - * Platform-specific operation for syncing internal metadata - */ - reconcileInternalFile(path: string): Promise; -} diff --git a/_types/src/lib/src/serviceModules/adapters/IPathAdapter.d.ts b/_types/src/lib/src/serviceModules/adapters/IPathAdapter.d.ts deleted file mode 100644 index edfcb133..00000000 --- a/_types/src/lib/src/serviceModules/adapters/IPathAdapter.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath } from "@lib/common/types.ts"; -/** - * Path operations adapter interface - * Handles path normalization and extraction - */ -export interface IPathAdapter { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Get the path from a file object or return the path string as-is - */ - getPath(file: TNativeAbstractFile | string): FilePath; - /** - * Normalize a path according to the platform's conventions - */ - normalisePath(path: string): string; -} diff --git a/_types/src/lib/src/serviceModules/adapters/IStorageAdapter.d.ts b/_types/src/lib/src/serviceModules/adapters/IStorageAdapter.d.ts deleted file mode 100644 index 22f68af3..00000000 --- a/_types/src/lib/src/serviceModules/adapters/IStorageAdapter.d.ts +++ /dev/null @@ -1,58 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** - * Focused compatibility views of the existing storage adapter. - * - * These interfaces allow internal consumers to depend on fewer operations while - * retaining the existing `UXStat`, `UXDataWriteOptions`, and method semantics. - * They are not a neutral or extraction-ready storage API. Their names, shared - * types, and behavioural contracts may change when the cross-platform contract - * is designed and stabilised. - * - * @packageDocumentation - */ -import type { UXDataWriteOptions, UXStat } from "@lib/common/types.ts"; -/** File and directory existence and metadata operations. */ -export interface IStorageMetadataAccess { - exists(path: string): Promise; - trystat(path: string): Promise; - stat(path: string): Promise; -} -/** Text-file read operation. */ -export interface IStorageTextReadAccess { - read(path: string): Promise; -} -/** Binary-file read operation. */ -export interface IStorageBinaryReadAccess { - readBinary(path: string): Promise; -} -/** Text-file write operation. */ -export interface IStorageTextWriteAccess { - write(path: string, data: string, options?: UXDataWriteOptions): Promise; -} -/** Binary-file write operation. */ -export interface IStorageBinaryWriteAccess { - writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise; -} -/** Text-file append operation. */ -export interface IStorageTextAppendAccess { - append(path: string, data: string, options?: UXDataWriteOptions): Promise; -} -/** Directory creation and direct-child listing operations. */ -export interface IStorageDirectoryAccess { - mkdir(path: string): Promise; - list(basePath: string): Promise<{ - files: string[]; - folders: string[]; - }>; -} -/** File or directory removal operation. */ -export interface IStorageRemoveAccess { - remove(path: string): Promise; -} -/** - * Storage adapter interface - * Backwards-compatible aggregate of the focused storage capability views. - */ -export interface IStorageAdapter extends IStorageMetadataAccess, IStorageTextReadAccess, IStorageBinaryReadAccess, IStorageTextWriteAccess, IStorageBinaryWriteAccess, IStorageTextAppendAccess, IStorageDirectoryAccess, IStorageRemoveAccess { // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface -} diff --git a/_types/src/lib/src/serviceModules/adapters/ITypeGuardAdapter.d.ts b/_types/src/lib/src/serviceModules/adapters/ITypeGuardAdapter.d.ts deleted file mode 100644 index 2195823f..00000000 --- a/_types/src/lib/src/serviceModules/adapters/ITypeGuardAdapter.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** - * Type guard adapter interface - * Provides runtime type checking for native file system objects - */ -export interface ITypeGuardAdapter { - /** - * Check if the given object is a file - */ - isFile(file: unknown): file is TNativeFile; - /** - * Check if the given object is a folder - */ - isFolder(item: unknown): item is TNativeFolder; -} diff --git a/_types/src/lib/src/serviceModules/adapters/IVaultAdapter.d.ts b/_types/src/lib/src/serviceModules/adapters/IVaultAdapter.d.ts deleted file mode 100644 index c6fd22b3..00000000 --- a/_types/src/lib/src/serviceModules/adapters/IVaultAdapter.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UXDataWriteOptions } from "@lib/common/types.ts"; -/** - * Vault adapter interface - * High-level file operations that interact with the vault layer - */ -export interface IVaultAdapter { - /** - * Read a file as text - */ - read(file: TNativeFile): Promise; - /** - * Read a file using cached content if available - */ - cachedRead(file: TNativeFile): Promise; - /** - * Read a file as binary - */ - readBinary(file: TNativeFile): Promise; - /** - * Modify an existing file with text content - */ - modify(file: TNativeFile, data: string, options?: UXDataWriteOptions): Promise; - /** - * Modify an existing file with binary content - */ - modifyBinary(file: TNativeFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise; - /** - * Create a new file with text content - */ - create(path: string, data: string, options?: UXDataWriteOptions): Promise; - /** - * Create a new file with binary content - */ - createBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise; - /** - * Rename or move an existing file - */ - rename(file: TNativeFile, newPath: string): Promise; - /** - * Delete a file or folder - */ - delete(file: TNativeFile | TNativeFolder, force?: boolean): Promise; - /** - * Move a file or folder to trash - */ - trash(file: TNativeFile | TNativeFolder, force?: boolean): Promise; - /** - * Trigger an event in the vault - */ - trigger(name: string, ...data: unknown[]): void; -} diff --git a/_types/src/lib/src/serviceModules/adapters/index.d.ts b/_types/src/lib/src/serviceModules/adapters/index.d.ts deleted file mode 100644 index 62d40d69..00000000 --- a/_types/src/lib/src/serviceModules/adapters/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type { IPathAdapter } from "./IPathAdapter.ts"; -export type { ITypeGuardAdapter } from "./ITypeGuardAdapter.ts"; -export type { IConversionAdapter } from "./IConversionAdapter.ts"; -export type { IStorageAdapter, IStorageBinaryReadAccess, IStorageBinaryWriteAccess, IStorageDirectoryAccess, IStorageMetadataAccess, IStorageRemoveAccess, IStorageTextAppendAccess, IStorageTextReadAccess, IStorageTextWriteAccess, } from "./IStorageAdapter.ts"; -export type { IVaultAdapter } from "./IVaultAdapter.ts"; -export type { IFileSystemAdapter } from "./IFileSystemAdapter.ts"; diff --git a/_types/src/lib/src/services/BrowserServices.d.ts b/_types/src/lib/src/services/BrowserServices.d.ts deleted file mode 100644 index ed077cff..00000000 --- a/_types/src/lib/src/services/BrowserServices.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { InjectableVaultServiceCompat } from "@lib/services/implements/injectable/InjectableVaultService"; -import { ServiceContext } from "@lib/services/base/ServiceBase"; -import { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub"; -export declare class BrowserServiceHub extends InjectableServiceHub { - get vault(): InjectableVaultServiceCompat; - constructor(); -} diff --git a/_types/src/lib/src/services/HeadlessServices.d.ts b/_types/src/lib/src/services/HeadlessServices.d.ts deleted file mode 100644 index 56f1f6dd..00000000 --- a/_types/src/lib/src/services/HeadlessServices.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ServiceContext } from "@lib/services/base/ServiceBase"; -import { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub"; -import type { DatabaseService } from "@lib/services/base/DatabaseService.ts"; -import type { Constructor } from "@lib/common/utils.type"; -export declare class HeadlessServiceHub extends InjectableServiceHub { - constructor(_context?: T, overrideServiceConstructor?: { - database?: Constructor>; - }); -} diff --git a/_types/src/lib/src/services/InjectableServices.d.ts b/_types/src/lib/src/services/InjectableServices.d.ts deleted file mode 100644 index 917671eb..00000000 --- a/_types/src/lib/src/services/InjectableServices.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub.ts"; diff --git a/_types/src/lib/src/services/ServiceHub.d.ts b/_types/src/lib/src/services/ServiceHub.d.ts deleted file mode 100644 index 0f3dd195..00000000 --- a/_types/src/lib/src/services/ServiceHub.d.ts +++ /dev/null @@ -1,83 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UIService } from "./implements/base/UIService.ts"; -import type { ConfigService } from "@lib/services/base/ConfigService.ts"; -import type { TestService } from "@lib/services/base/TestService.ts"; -import type { VaultService } from "@lib/services/base/VaultService.ts"; -import type { TweakValueService } from "@lib/services/base/TweakValueService.ts"; -import type { SettingService } from "@lib/services/base/SettingService.ts"; -import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService.ts"; -import type { ConflictService } from "@lib/services/base/ConflictService.ts"; -import type { RemoteService } from "@lib/services/base/RemoteService.ts"; -import type { ReplicationService } from "@lib/services/base/ReplicationService.ts"; -import type { ReplicatorService } from "@lib/services/base/ReplicatorService.ts"; -import type { FileProcessingService } from "@lib/services/base/FileProcessingService.ts"; -import type { DatabaseEventService } from "@lib/services/base/DatabaseEventService.ts"; -import type { DatabaseService } from "@lib/services/base/DatabaseService.ts"; -import type { PathService } from "@lib/services/base/PathService.ts"; -import type { APIService } from "@lib/services/base/APIService.ts"; -import type { ServiceContext } from "./base/ServiceBase.ts"; -import type { IServiceHub } from "./base/IService.ts"; -import type { KeyValueDBService } from "./base/KeyValueDBService.ts"; -import type { ControlService } from "./base/ControlService.ts"; -export type ServiceInstances = { - API?: APIService; - path?: PathService; - database?: DatabaseService; - databaseEvents?: DatabaseEventService; - replicator?: ReplicatorService; - fileProcessing?: FileProcessingService; - replication?: ReplicationService; - remote?: RemoteService; - conflict?: ConflictService; - appLifecycle?: AppLifecycleService; - setting?: SettingService; - tweakValue?: TweakValueService; - vault?: VaultService; - test?: TestService; - ui?: UIService; - config?: ConfigService; - keyValueDB?: KeyValueDBService; - control?: ControlService; -}; -export declare abstract class ServiceHub implements IServiceHub { - protected context: T; - protected abstract _api: APIService; - protected abstract _path: PathService; - protected abstract _database: DatabaseService; - protected abstract _databaseEvents: DatabaseEventService; - protected abstract _replicator: ReplicatorService; - protected abstract _fileProcessing: FileProcessingService; - protected abstract _replication: ReplicationService; - protected abstract _remote: RemoteService; - protected abstract _conflict: ConflictService; - protected abstract _appLifecycle: AppLifecycleService; - protected abstract _setting: SettingService; - protected abstract _tweakValue: TweakValueService; - protected abstract _vault: VaultService; - protected abstract _test: TestService; - protected abstract _ui: UIService; - protected abstract _config: ConfigService; - protected abstract _keyValueDB: KeyValueDBService; - protected abstract _control: ControlService; - protected _injected: ServiceInstances; - constructor(context: T, services?: ServiceInstances); - get API(): APIService; - get path(): PathService; - get database(): DatabaseService; - get databaseEvents(): DatabaseEventService; - get replicator(): ReplicatorService; - get fileProcessing(): FileProcessingService; - get replication(): ReplicationService; - get remote(): RemoteService; - get conflict(): ConflictService; - get appLifecycle(): AppLifecycleService; - get setting(): SettingService; - get tweakValue(): TweakValueService; - get vault(): VaultService; - get test(): TestService; - get UI(): UIService; - get config(): ConfigService; - get keyValueDB(): KeyValueDBService; - get control(): ControlService; -} diff --git a/_types/src/lib/src/services/base/APIService.d.ts b/_types/src/lib/src/services/base/APIService.d.ts deleted file mode 100644 index da01ac7c..00000000 --- a/_types/src/lib/src/services/base/APIService.d.ts +++ /dev/null @@ -1,95 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FetchHttpHandler } from "@smithy/fetch-http-handler"; -import type { LOG_LEVEL } from "@lib/common/logger"; -import type { IAPIService, ICommandCompat } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import type { Confirm } from "@lib/interfaces/Confirm"; -/** - * The APIService provides methods for interacting with the plug-in's API, - */ -export declare abstract class APIService extends ServiceBase implements IAPIService { - /** - * Get a custom fetch handler for making HTTP requests (e.g., S3 without CORS issues). - */ - abstract getCustomFetchHandler(): FetchHttpHandler; - /** - * Add a log entry to the log (Now not used). - * @param message The log message. - * @param level The log level. - * @param key The log key. - */ - abstract addLog(message: unknown, level: LOG_LEVEL, key: string): void; - /** - * Check if the app is running on a mobile device. - * @returns true if running on mobile, false otherwise. - */ - abstract isMobile(): boolean; - /** - * Show a window (or in Obsidian, a leaf). - * @param type The type of window to show. - */ - abstract showWindow(type: string): Promise; - /** - * Show a window on the right sidebar when supported. - * Platforms that do not support sidebars can fall back to showWindow. - */ - showWindowOnRight(type: string): Promise; - /** - * returns App ID. In Obsidian, it is vault ID. - */ - abstract getAppID(): string; - /** - * Returns the vaultName which system has identified, without any additional suffix. - */ - abstract getSystemVaultName(): string; - /** - * Check if the last POST request failed due to payload size. - */ - abstract getPlatform(): string; - abstract getAppVersion(): string; - abstract getPluginVersion(): string; - abstract getCrypto(): Crypto; - /** - * Register a command to the runtime. - * @param command - */ - abstract addCommand(command: TCommand): TCommand; - /** - * Register a window (or leaf) type to the runtime. - * @param type - * @param factory - */ - abstract registerWindow(type: string, factory: (leaf: T) => unknown): void; - /** - * Add a ribbon icon to the UI. - * @param icon - * @param title - * @param callback - */ - abstract addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => unknown): HTMLElement; - /** - * Register a protocol handler. - * @param action The action string for the protocol. - * @param handler The handler function for the protocol. - */ - abstract registerProtocolHandler(action: string, handler: (params: Record) => unknown): void; - /** - * Get the basic UI component for showing a confirmation dialog to the user. - */ - abstract get confirm(): Confirm; - requestCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - responseCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - get isOnline(): boolean; - webCompatFetch(req: string | Request, opts?: RequestInit): Promise; - nativeFetch(req: string | Request, opts?: RequestInit): Promise; - abstract addStatusBarItem(): HTMLElement | undefined; - setInterval(handler: () => void, timeout: number): number; - clearInterval(timerId: number): void; - /** - * Get the system configuration directory. - * This is used for storing configuration files in a consistent location across platforms. - * @returns - */ - getSystemConfigDir(): string; -} diff --git a/_types/src/lib/src/services/base/AppLifecycleService.d.ts b/_types/src/lib/src/services/base/AppLifecycleService.d.ts deleted file mode 100644 index 96f85bf7..00000000 --- a/_types/src/lib/src/services/base/AppLifecycleService.d.ts +++ /dev/null @@ -1,134 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { IAppLifecycleService, ISettingService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -export interface AppLifecycleServiceDependencies { - settingService: ISettingService; -} -/** - * The AppLifecycleService provides methods for managing the plug-in's lifecycle events. - */ -export declare abstract class AppLifecycleService extends ServiceBase implements IAppLifecycleService { - protected readonly settingService: ISettingService; - constructor(context: T, dependencies: AppLifecycleServiceDependencies); - /** - * Event triggered when the plug-in's layout is ready. - * In Obsidian, it is after the workspace is ready. - */ - readonly onLayoutReady: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in is being initialized for the first time. - * This is only called once per plug-in lifecycle. - */ - readonly onFirstInitialise: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in is fully ready. - * This is called after all initialisation processes are complete. - */ - readonly onReady: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered to wire up necessary event listeners. - * This is typically called during the initialisation phase. - */ - readonly onWireUpEvents: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in is being initialised. - */ - readonly onInitialise: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in is loading. - * This is typically called during the quite early initialisation phase, before everything. - * In Obsidian, it is in the onload() method of the plugin. - */ - readonly onLoad: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in's settings have been loaded and applied. - */ - readonly onSettingLoaded: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in has fully loaded. - * This is typically called after all initialisation and loading processes are complete. - */ - readonly onLoaded: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Scan for any startup issues that may affect the plug-in's operation. - */ - readonly onScanningStartupIssues: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in is unloading (e.g., during app shutdown or plug-in disable). - * This is typically called during the unload() method of the plugin. - * Entry point to unload everything. - */ - readonly onAppUnload: import("@lib/services/lib/HandlerUtils").CollectiveHandlerFunction<() => Promise, unknown>; - /** - * Event triggered before the plug-in is unloaded. - * This is typically used to perform any necessary cleanup or save state before the plug-in is unloaded. - */ - readonly onBeforeUnload: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in is being unloaded. - */ - readonly onUnload: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Perform an immediate restart of the application. - * Note that this is not graceful, and not only the plug-in. APPLICATION (means Obsidian) will be restarted. - */ - abstract performRestart(): void; - /** - * Ask the user for a restart. - * @param message Optional message to display to the user when asking for a restart. - */ - abstract askRestart(message?: string): void; - /** - * Schedule a restart of the application. - * After the current operation is done, the application will be restarted. - * Note that this is not graceful, and not only the plug-in. APPLICATION (means Obsidian) will be restarted. - */ - abstract scheduleRestart(): void; - /** - * Event triggered when the application is being suspended (e.g., system sleep). - */ - readonly onSuspending: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the application is resuming from a suspended state. - */ - readonly onResuming: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered after the application has resumed from a suspended state. - */ - readonly onResumed: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - private _isSuspended; - /** - * Check if the plug-in is currently suspended. - * Also consider the plug-in as suspended if it is not configured, to prevent any issues before configuration. - */ - isSuspended(): boolean; - /** - * Set the suspension state of the plug-in. - * @param suspend Set to true to suspend the plug-in, false to resume. - */ - setSuspended(suspend: boolean): void; - private _isReady; - /** - * Check if the plug-in is ready. - * A ready plug-in means it has been fully initialised and is operational. - * If not ready, most operations will be blocked. - */ - isReady(): boolean; - /** - * Mark the plug-in as ready. - */ - markIsReady(): void; - /** - * Reset the ready state of the plug-in. - */ - resetIsReady(): void; - /** - * Check if a restart has been scheduled. - */ - abstract isReloadingScheduled(): boolean; - /** - * Get unresolved error messages. - */ - readonly getUnresolvedMessages: import("@lib/services/lib/HandlerUtils").CollectiveHandlerFunction<() => Promise<(string | Error)[][]>, unknown>; -} diff --git a/_types/src/lib/src/services/base/ConfigService.d.ts b/_types/src/lib/src/services/base/ConfigService.d.ts deleted file mode 100644 index 258b2a85..00000000 --- a/_types/src/lib/src/services/base/ConfigService.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { IConfigService } from "@lib/services/base/IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -export declare abstract class ConfigService extends ServiceBase implements IConfigService { - abstract getSmallConfig(key: string): string | null; - abstract setSmallConfig(key: string, value: string): void; - abstract deleteSmallConfig(key: string): void; -} diff --git a/_types/src/lib/src/services/base/ConflictService.d.ts b/_types/src/lib/src/services/base/ConflictService.d.ts deleted file mode 100644 index fae199ca..00000000 --- a/_types/src/lib/src/services/base/ConflictService.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePathWithPrefix, MISSING_OR_ERROR, AUTO_MERGED } from "@lib/common/types"; -import type { IConflictService } from "@lib/services/base/IService"; -import { ServiceBase } from "@lib/services/base/ServiceBase"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -/** - * The ConflictService provides methods for handling file conflicts. - */ -export declare abstract class ConflictService extends ServiceBase implements IConflictService { - /** - * Get an optional conflict check method for a given file (virtual) path. - */ - readonly getOptionalConflictCheckMethod: import("@lib/services/lib/HandlerUtils").MultipleHandlerFunction<(path: FilePathWithPrefix) => Promise, unknown>; - /** - * Queue a check for conflicts if the file is currently open in the editor. - * @param path The file (virtual) path to check for conflicts. - */ - abstract queueCheckForIfOpen(path: FilePathWithPrefix): Promise; - /** - * Queue a check for conflicts for a given file (virtual) path. - * @param path The file (virtual) path to check for conflicts. - */ - abstract queueCheckFor(path: FilePathWithPrefix): Promise; - /** - * Ensure all queued file conflict checks are processed. - */ - abstract ensureAllProcessed(): Promise; - /** - * Resolve a conflict by user interaction (e.g., showing a modal dialog). - * @param filename The file (virtual) path with conflict. - * @param conflictCheckResult The result of the conflict check. - * @returns A promise that resolves to true if the conflict was resolved, false if not, or undefined if no action was taken. - */ - readonly resolveByUserInteraction: import("@lib/services/lib/HandlerUtils").MultipleHandlerFunction<(filename: FilePathWithPrefix, conflictCheckResult: import("@lib/common/types").diff_result) => Promise, unknown>; - /** - * Resolve a conflict by deleting a specific revision. - * @param path The file (virtual) path with conflict. - * @param deleteRevision The revision to delete. - * @param title The title of the conflict (for user display). - */ - abstract resolveByDeletingRevision(path: FilePathWithPrefix, deleteRevision: string, title: string): Promise; - /** - * Resolve a conflict as several possible strategies. - * It may involve user interaction (means raising resolveByUserInteraction). - * @param filename The file (virtual) path to resolve. - */ - abstract resolve(filename: FilePathWithPrefix): Promise; - /** - * Resolve a conflict by choosing the newest version. - * @param filename The file (virtual) path to resolve. - */ - abstract resolveByNewest(filename: FilePathWithPrefix): Promise; - abstract resolveAllConflictedFilesByNewerOnes(): Promise; - conflictProcessQueueCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; -} diff --git a/_types/src/lib/src/services/base/ControlService.d.ts b/_types/src/lib/src/services/base/ControlService.d.ts deleted file mode 100644 index 8db82b31..00000000 --- a/_types/src/lib/src/services/base/ControlService.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { createInstanceLogFunction } from "@lib/services/lib/logUtils"; -import type { APIService } from "./APIService"; -import type { DatabaseService } from "./DatabaseService"; -import type { IControlService, IFileProcessingService, IReplicatorService, ISettingService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import { type PromiseWithResolvers } from "octagonal-wheels/promises"; -import type { AppLifecycleService } from "./AppLifecycleService"; -export interface ControlServiceDependencies { - appLifecycleService: AppLifecycleService; - replicatorService: IReplicatorService; - settingService: ISettingService; - databaseService: DatabaseService; - fileProcessingService: IFileProcessingService; - APIService: APIService; -} -/** - * The ControlService provides methods for controlling the overall behaviour of the plugin, such as applying settings or handling lifecycle events. - */ -export declare class ControlService extends ServiceBase implements IControlService { - protected services: ControlServiceDependencies; - protected _log: ReturnType; - protected _unloaded: boolean; - protected _activated: PromiseWithResolvers; - /** - * Check if the plug-in has been unloaded. - */ - hasUnloaded(): boolean; - constructor(context: T, dependencies: ControlServiceDependencies); - get activated(): Promise; - private onActivated; - /** - * Apply current settings to reflect the changes immediately. - * @returns - */ - applySettings(): Promise; - private _onLiveSyncUnload; - /** - * Called when the plugin is loaded. It will trigger the app lifecycle event onLoad. - * Main process should be called in onReady. - * @returns - */ - onLoad(): Promise; - /** - * Main entry point of the plugin. It will trigger the app lifecycle event onReady. - * Usually it should be called on `app.workspace.onLayoutReady` - * @returns - */ - onReady(): Promise; - /** - * On unload event of the plugin. It will trigger the app lifecycle event onUnload. - * @returns - */ - onUnload(): Promise; -} diff --git a/_types/src/lib/src/services/base/DatabaseEventService.d.ts b/_types/src/lib/src/services/base/DatabaseEventService.d.ts deleted file mode 100644 index 2141c0b0..00000000 --- a/_types/src/lib/src/services/base/DatabaseEventService.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { IDatabaseEventService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -/** - * The DatabaseEventService provides methods for handling database lifecycle events. - */ -export declare abstract class DatabaseEventService extends ServiceBase implements IDatabaseEventService { - /** - * Event triggered when the database is about to be unloaded. - */ - readonly onUnloadDatabase: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(db: import("../../pouchdb/LiveSyncLocalDB").LiveSyncLocalDB) => Promise>; - /** - * Event triggered when the database is about to be closed. - */ - readonly onCloseDatabase: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(db: import("../../pouchdb/LiveSyncLocalDB").LiveSyncLocalDB) => Promise>; - /** - * Event triggered when the database is being initialized. - */ - readonly onDatabaseInitialisation: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(db: import("../../pouchdb/LiveSyncLocalDB").LiveSyncLocalDB) => Promise>; - /** - * Event triggered when the database has been initialized. - */ - readonly onDatabaseInitialised: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(showNotice: boolean) => Promise>; - /** - * Event triggered when the database is being reset. - */ - readonly onResetDatabase: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(db: import("../../pouchdb/LiveSyncLocalDB").LiveSyncLocalDB) => Promise>; - /** - * Event triggered when the database is ready for use. - */ - readonly onDatabaseHasReady: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Initialize the database. - * @param showingNotice Whether to show a notice to the user. - * @param reopenDatabase Whether to reopen the database if it is already open. - * @param ignoreSuspending Whether to ignore any suspending state. - */ - readonly initialiseDatabase: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean) => Promise>; -} diff --git a/_types/src/lib/src/services/base/DatabaseService.d.ts b/_types/src/lib/src/services/base/DatabaseService.d.ts deleted file mode 100644 index dee2b621..00000000 --- a/_types/src/lib/src/services/base/DatabaseService.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { IDatabaseService, IPathService, IVaultService, openDatabaseParameters } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import { LiveSyncLocalDB } from "@lib/pouchdb/LiveSyncLocalDB"; -import type { SettingService } from "./SettingService"; -import type { APIService } from "./APIService"; -import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type"; -export type DatabaseServiceDependencies = { - path: IPathService; - vault: IVaultService; - setting: SettingService; - API: APIService; -}; -/** - * The DatabaseService provides methods for managing the local database. - * Please note that each event of database lifecycle is handled in DatabaseEventService. - */ -export declare abstract class DatabaseService extends ServiceBase implements IDatabaseService { - _log: (msg: unknown, level?: import("octagonal-wheels/common/logger").LOG_LEVEL, key?: string) => void; - constructor(context: T, dependencies: DatabaseServiceDependencies); - protected _localDatabase: LiveSyncLocalDB | null; - protected services: DatabaseServiceDependencies; - onOpenDatabase: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(vaultName: string) => Promise>; - /** - * Called after the local database has been reset. - */ - onDatabaseReset: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - get localDatabase(): LiveSyncLocalDB; - get localDatabaseDirect(): LiveSyncLocalDB | null; - protected modifyDatabaseOptions(settings: ObsidianLiveSyncSettings, name: string, options: PouchDB.Configuration.DatabaseConfiguration): { - name: string; - options: PouchDB.Configuration.DatabaseConfiguration; - }; - createPouchDBInstance(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database; - openDatabase(params: openDatabaseParameters): Promise; - isDatabaseReady(): boolean; - resetDatabase(): Promise; -} diff --git a/_types/src/lib/src/services/base/FileProcessingService.d.ts b/_types/src/lib/src/services/base/FileProcessingService.d.ts deleted file mode 100644 index c25b445e..00000000 --- a/_types/src/lib/src/services/base/FileProcessingService.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { IFileProcessingService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -/** - * File processing service handles file events and processes them accordingly. - */ -export declare class FileProcessingService extends ServiceBase implements IFileProcessingService { - /** - * Process a file event item by the registered handlers. - */ - readonly processFileEvent: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(item: import("../../common/types").FileEventItem) => Promise>; - /** - * Process a file event item optionally, if any handler is registered. - * i.e., hidden files synchronisation or customisation sync. - */ - readonly processOptionalFileEvent: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(path: import("../../common/types").FilePath) => Promise>; - /** - * Commit any pending file events that have been queued for processing. - */ - readonly commitPendingFileEvents: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - batched: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - totalQueued: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - processing: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - totalStorageFileEventCount: number; - onStorageFileEvent(): void; -} diff --git a/_types/src/lib/src/services/base/IService.d.ts b/_types/src/lib/src/services/base/IService.d.ts deleted file mode 100644 index 3745feb1..00000000 --- a/_types/src/lib/src/services/base/IService.d.ts +++ /dev/null @@ -1,286 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FetchHttpHandler } from "@smithy/fetch-http-handler"; -import { type LOG_LEVEL } from "octagonal-wheels/common/logger"; -import type { AnyEntry, AUTO_MERGED, CouchDBCredentials, diff_result, DocumentID, EntryDoc, EntryHasPath, FileEventItem, FilePath, FilePathWithPrefix, LoadedEntry, MetaEntry, MISSING_OR_ERROR, ObsidianLiveSyncSettings, RemoteDBSettings, TweakValues, UXFileInfo, UXFileInfoStub } from "@lib/common/types"; -import type { LiveSyncLocalDB } from "@lib/pouchdb/LiveSyncLocalDB"; -import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator"; -import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive"; -import type { ReplicationStatics } from "@lib/common/models/shared.definition"; -import type { ReplicatorService } from "./ReplicatorService"; -import type { DatabaseEventService } from "./DatabaseEventService"; -import type { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols"; -import type { AsyncActivityOptions } from "@lib/interfaces/AsyncActivityRunner.ts"; -declare global { - interface OPTIONAL_SYNC_FEATURES { - DISABLE: "DISABLE"; - } -} -export interface ICommandCompat { - id: string; - name: string; - icon?: string; - callback?: () => any; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - checkCallback?: (checking: boolean) => boolean | void; - editorCallback?: (editor: any, ctx: any) => any; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - editorCheckCallback?: (checking: any, editor: any, ctx: any) => boolean | void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -} -export interface IAPIService { - getCustomFetchHandler(): FetchHttpHandler; - addStatusBarItem(): HTMLElement | undefined; - addLog(message: unknown, level: LOG_LEVEL, key?: string): void; - isMobile(): boolean; - showWindow(type: string): Promise; - showWindowOnRight?(type: string): Promise; - getAppID(): string; - getSystemVaultName(): string; - getPlatform(): string; - getAppVersion(): string; - getPluginVersion(): string; - addCommand(command: TCommand): TCommand; - registerWindow(type: string, factory: (leaf: T) => unknown): void; - addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => unknown): HTMLElement; - registerProtocolHandler(action: string, handler: (params: Record) => unknown): void; - confirm: Confirm; - responseCount: ReactiveSource; - requestCount: ReactiveSource; - isOnline: boolean; - webCompatFetch(url: string | Request, opts?: RequestInit): Promise; - nativeFetch(url: string | Request, opts?: RequestInit): Promise; - setInterval(handler: () => void, timeout: number): number; - clearInterval(timerId: number): void; - getSystemConfigDir(): string; -} -export interface IPathService { - id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix; - path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise; - getPath(entry: AnyEntry): FilePathWithPrefix; - markChangesAreSame(old: UXFileInfo | AnyEntry | FilePathWithPrefix, newMtime: number, oldMtime: number): boolean | undefined; - unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void; - compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN; - isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | FilePathWithPrefix, mtimes: number[]): undefined | typeof EVEN; -} -export interface openDatabaseParameters { - replicator: ReplicatorService; - databaseEvents: DatabaseEventService; -} -export interface IDatabaseService { - localDatabase: LiveSyncLocalDB; - createPouchDBInstance(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database; - openDatabase(params: openDatabaseParameters): Promise; - resetDatabase(): Promise; - onDatabaseReset: () => Promise; - onOpenDatabase: (vaultName: string) => Promise; - isDatabaseReady(): boolean; -} -export interface IDatabaseEventService { - onUnloadDatabase(db: LiveSyncLocalDB): Promise; - onCloseDatabase(db: LiveSyncLocalDB): Promise; - onDatabaseInitialisation(db: LiveSyncLocalDB): Promise; - onDatabaseInitialised(showNotice: boolean): Promise; - onDatabaseHasReady(): Promise; - onResetDatabase(db: LiveSyncLocalDB): Promise; - initialiseDatabase(showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean): Promise; -} -export interface IKeyValueDBService { - openSimpleStore(kind: string): SimpleStore; - simpleStore: SimpleStore; -} -export interface IFileProcessingService { - processFileEvent(item: FileEventItem): Promise; - processOptionalFileEvent(path: FilePath): Promise; - commitPendingFileEvents(): Promise; - batched: ReactiveSource; - processing: ReactiveSource; - totalQueued: ReactiveSource; - totalStorageFileEventCount: number; - onStorageFileEvent(): void; -} -export interface IReplicatorService { - onCloseActiveReplication(): Promise; - onReplicatorInitialised(): Promise; - getNewReplicator(settingOverride?: Partial): Promise; - getActiveReplicator(): LiveSyncAbstractReplicator | undefined; - replicationStatics: ReactiveSource; - /** Number of finite remote operations currently in progress. */ - boundedRemoteActivityCount: ReactiveSource; - /** Number of finite replication operations which can still deliver database documents. */ - finiteReplicationActivityCount: ReactiveSource; - /** Runs a finite remote operation within the host activity policy. */ - runBoundedRemoteActivity(task: () => T | PromiseLike, options?: AsyncActivityOptions): Promise; - /** Runs finite replication which may place documents in the local database. */ - runFiniteReplicationActivity(task: () => T | PromiseLike, options?: AsyncActivityOptions): Promise; -} -export interface IReplicationService { - processSynchroniseResult(doc: MetaEntry): Promise; - processOptionalSynchroniseResult(doc: LoadedEntry): Promise; - processVirtualDocument(docs: PouchDB.Core.ExistingDocument): Promise; - onBeforeReplicate(showMessage: boolean): Promise; - checkConnectionFailure(): Promise; - onCheckReplicationReady(showMessage: boolean): Promise; - isReplicationReady(showMessage: boolean): Promise; - performReplication(showMessage?: boolean): Promise; - replicate(showMessage?: boolean): Promise; - replicateByEvent(showMessage?: boolean): Promise; - onReplicationFailed(showMessage?: boolean): Promise; - parseSynchroniseResult(docs: Array>): Promise; - databaseQueueCount: ReactiveSource; - storageApplyingCount: ReactiveSource; - replicationResultCount: ReactiveSource; - replicateAllToRemote(showingNotice?: boolean, sendChunksInBulkDisabled?: boolean): Promise; - replicateAllFromRemote(showingNotice?: boolean): Promise; - markLocked(lockByClean?: boolean): Promise; - markUnlocked(): Promise; - markResolved(): Promise; -} -export interface IRemoteService { - connect(uri: string, auth: CouchDBCredentials, disableRequestURI: boolean, passphrase: string | false, useDynamicIterationCount: boolean, performSetup: boolean, skipInfo: boolean, compression: boolean, customHeaders: Record, useRequestAPI: boolean, getPBKDF2Salt: () => Promise): Promise; - info: PouchDB.Core.DatabaseInfo; - }>; - /** - * State if the last POST request failed due to payload size. - */ - get hadLastPostFailedBySize(): boolean; -} -export interface IConflictService { - getOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise; - resolveByUserInteraction: (filename: FilePathWithPrefix, conflictCheckResult: diff_result) => Promise; - queueCheckForIfOpen(path: FilePathWithPrefix): Promise; - queueCheckFor(path: FilePathWithPrefix): Promise; - ensureAllProcessed(): Promise; - resolveByDeletingRevision(path: FilePathWithPrefix, deleteRevision: string, title: string): Promise; - resolve(filename: FilePathWithPrefix): Promise; - resolveByNewest(filename: FilePathWithPrefix): Promise; - resolveAllConflictedFilesByNewerOnes(): Promise; - conflictProcessQueueCount: ReactiveSource; -} -export interface IAppLifecycleService { - onLayoutReady(): Promise; - onFirstInitialise(): Promise; - onReady(): Promise; - onWireUpEvents(): Promise; - onInitialise(): Promise; - onLoad(): Promise; - onSettingLoaded(): Promise; - onLoaded(): Promise; - onScanningStartupIssues(): Promise; - onAppUnload(): Promise; - onBeforeUnload(): Promise; - onUnload(): Promise; - onSuspending(): Promise; - onResuming(): Promise; - onResumed(): Promise; - getUnresolvedMessages: () => Promise<(string | Error)[][]>; - performRestart(): void; - askRestart(message?: string): void; - scheduleRestart(): void; - isSuspended(): boolean; - setSuspended(suspend: boolean): void; - isReady(): boolean; - markIsReady(): void; - resetIsReady(): void; - isReloadingScheduled(): boolean; -} -export interface ISettingService { - onBeforeRealiseSetting(): Promise; - onSettingRealised(): Promise; - onRealiseSetting(): Promise; - suspendAllSync(): Promise; - suspendExtraSync(): Promise; - suggestOptionalFeatures(opt: { - enableFetch?: boolean; - enableOverwrite?: boolean; - }): Promise; - enableOptionalFeature(mode: keyof OPTIONAL_SYNC_FEATURES): Promise; - clearUsedPassphrase(): void; - decryptSettings(settings: ObsidianLiveSyncSettings): Promise; - adjustSettings(settings: ObsidianLiveSyncSettings): Promise; - loadSettings(): Promise; - getDeviceAndVaultName(): string; - setDeviceAndVaultName(name: string): void; - saveDeviceAndVaultName(): void; - onBeforeSaveSettingData(nextSettings: ObsidianLiveSyncSettings, previousSettings: ObsidianLiveSyncSettings): Promise<(Partial | void)[]>; - saveSettingData(): Promise; - currentSettings(): ObsidianLiveSyncSettings; - updateSettings(updateFn: (current: ObsidianLiveSyncSettings) => ObsidianLiveSyncSettings, saveImmediately?: boolean): Promise; - applyExternalSettings(partial: Partial, saveImmediately?: boolean): Promise; - applyPartial(partial: Partial, saveImmediately?: boolean): Promise; - onSettingLoaded(settings: ObsidianLiveSyncSettings): Promise; - onSettingChanged(settings: ObsidianLiveSyncSettings): Promise; - onSettingSaved(settings: ObsidianLiveSyncSettings): Promise; - getSmallConfig(key: string): string | null; - setSmallConfig(key: string, value: string): void; - deleteSmallConfig(key: string): void; -} -export interface ITweakValueService { - fetchRemotePreferred(trialSetting: RemoteDBSettings): Promise; - checkAndAskResolvingMismatched(preferred: Partial): Promise<[TweakValues | boolean, boolean]>; - askResolvingMismatched(preferredSource: TweakValues): Promise<"OK" | "CHECKAGAIN" | "IGNORE">; - checkAndAskUseRemoteConfiguration(settings: RemoteDBSettings): Promise<{ - result: false | TweakValues; - requireFetch: boolean; - }>; - askUseRemoteConfiguration(trialSetting: RemoteDBSettings, preferred: TweakValues): Promise<{ - result: false | TweakValues; - requireFetch: boolean; - }>; -} -export interface IVaultService { - vaultName(): string; - getVaultName(): string; - scanVault(showingNotice?: boolean, ignoreSuspending?: boolean): Promise; - isIgnoredByIgnoreFile(file: string | UXFileInfoStub): Promise; - isTargetFile(file: string | UXFileInfoStub): Promise; - isTargetFileInExtra(file: string | UXFileInfoStub): Promise; - isFileSizeTooLarge(size: number): boolean; - getActiveFilePath(): FilePath | undefined; - isStorageInsensitive(): boolean; - shouldCheckCaseInsensitively(): boolean; - isValidPath(path: string): boolean; -} -export interface ITestService { - test(): Promise; - testMultiDevice(): Promise; - addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void; -} -export interface IUIService { - promptCopyToClipboard(title: string, value: string): Promise; - showMarkdownDialog(title: string, contentMD: string, buttons: T): Promise<(typeof buttons)[number] | false>; - get confirm(): Confirm; -} -export interface IConfigService { - getSmallConfig(key: string): string | null; - setSmallConfig(key: string, value: string): void; - deleteSmallConfig(key: string): void; -} -export interface IServiceHub { - API: IAPIService; - path: IPathService; - database: IDatabaseService; - databaseEvents: IDatabaseEventService; - replicator: IReplicatorService; - fileProcessing: IFileProcessingService; - replication: IReplicationService; - remote: IRemoteService; - conflict: IConflictService; - appLifecycle: IAppLifecycleService; - setting: ISettingService; - tweakValue: ITweakValueService; - vault: IVaultService; - test: ITestService; - UI: IUIService; - config: IConfigService; - keyValueDB: IKeyValueDBService; - control: IControlService; -} -export interface IControlService { - applySettings(): Promise; - onLoad(): Promise; - onReady(): Promise; - onUnload(): Promise; - hasUnloaded(): boolean; - activated: Promise; -} diff --git a/_types/src/lib/src/services/base/KeyValueDBService.d.ts b/_types/src/lib/src/services/base/KeyValueDBService.d.ts deleted file mode 100644 index 49d814fe..00000000 --- a/_types/src/lib/src/services/base/KeyValueDBService.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { SimpleStore } from "@lib/common/utils"; -import type { IKeyValueDBService, IVaultService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase"; -import type { InjectableDatabaseEventService } from "@lib/services/implements/injectable/InjectableDatabaseEventService"; -import type { AppLifecycleServiceBase } from "@lib/services/implements/injectable/InjectableAppLifecycleService"; -export interface KeyValueDBDependencies { - databaseEvents: InjectableDatabaseEventService; - vault: IVaultService; - appLifecycle: AppLifecycleServiceBase; -} -/** - * The KeyValueDBService provides methods for managing the local key-value database. - * Please note that each event of database lifecycle is handled in DatabaseEventService. - */ -export declare abstract class KeyValueDBService extends ServiceBase implements IKeyValueDBService { - private _kvDB; - private _simpleStore; - get simpleStore(): SimpleStore; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - get kvDB(): KeyValueDatabase; - private databaseEvents; - private vault; - private appLifecycle; - private _log; - private _everyOnResetDatabase; - private tryCloseKvDB; - private openKeyValueDB; - private _onOtherDatabaseUnload; - private _onOtherDatabaseClose; - private _everyOnInitializeDatabase; - private _everyOnloadAfterLoadSettings; - constructor(context: T, dependencies: KeyValueDBDependencies); - openSimpleStore(kind: string): { - get: (key: string) => Promise; - set: (key: string, value: unknown) => Promise; - delete: (key: string) => Promise; - keys: (from: string | undefined, to: string | undefined, count?: number) => Promise; - db: Promise; - }; -} diff --git a/_types/src/lib/src/services/base/PathService.d.ts b/_types/src/lib/src/services/base/PathService.d.ts deleted file mode 100644 index 275c2d3f..00000000 --- a/_types/src/lib/src/services/base/PathService.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DocumentID, EntryHasPath, FilePathWithPrefix, FilePath, AnyEntry, UXFileInfo, UXFileInfoStub } from "@lib/common/types"; -import type { IPathService, ISettingService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import type { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols"; -export interface PathServiceDependencies { - settingService: ISettingService; -} -/** - * The PathService provides methods for converting between file paths and document IDs. - * This class would be migrated to the new logic later. - */ -export declare abstract class PathService extends ServiceBase implements IPathService { - protected settingService: ISettingService; - protected abstract normalizePath(path: string): string; - get settings(): import("@lib/common/types").ObsidianLiveSyncSettings; - constructor(context: T, dependencies: PathServiceDependencies); - private _id2path; - private _path2id; - /** - * Convert a document ID or entry to a virtual file path. - * @param id A document ID. Nowadays, it is mostly not the same as the file path. - * If the document has `_` prefixed, saved as `/_`. - * @param entry An entry object. If provided, it can be used to get the path directly. - * @param stripPrefix Whether to strip the prefix from the path. - */ - id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix; - /** - * Convert a virtual file path to a document ID (with prefix if any). - * @param filename A file path with or without prefix. - * @param prefix The prefix to use for the document ID. - */ - path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise; - getPath(entry: AnyEntry): FilePathWithPrefix; - abstract markChangesAreSame(old: UXFileInfo | AnyEntry | FilePathWithPrefix, newMtime: number, oldMtime: number): boolean | undefined; - abstract unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void; - abstract compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN; - abstract isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | FilePathWithPrefix, mtimes: number[]): undefined | typeof EVEN; -} diff --git a/_types/src/lib/src/services/base/RemoteService.d.ts b/_types/src/lib/src/services/base/RemoteService.d.ts deleted file mode 100644 index 37b791b8..00000000 --- a/_types/src/lib/src/services/base/RemoteService.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { CouchDBCredentials, EntryDoc } from "@lib/common/types"; -import type { IRemoteService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import { type LOG_LEVEL } from "@lib/common/types"; -import { AuthorizationHeaderGenerator } from "@lib/replication/httplib"; -import type { APIService } from "@lib/services/base/APIService"; -import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; -import type { SettingService } from "@lib/services/base/SettingService"; -import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -export interface RemoteServiceDependencies { - APIService: APIService; - appLifecycle: AppLifecycleService; - setting: SettingService; -} -declare const FetchMethod: { - readonly webCompat: 0; - readonly native: 1; -}; -type FetchMethod = (typeof FetchMethod)[keyof typeof FetchMethod]; -/** - * The RemoteService provides methods for interacting with the remote database. - */ -export declare abstract class RemoteService extends ServiceBase implements IRemoteService { - /** - * Connect to the remote database with the provided settings. - * @param uri The URI of the remote database. - * @param auth The authentication credentials for the remote database. - * @param disableRequestURI Whether to disable the request URI. - * @param passphrase The passphrase for the remote database. - * @param useDynamicIterationCount Whether to use dynamic iteration count. - * @param performSetup Whether to perform setup. - * @param skipInfo Whether to skip information retrieval. - * @param compression Whether to enable compression. - * @param customHeaders Custom headers to include in the request. - * @param useRequestAPI Whether to use the request API. - * @param getPBKDF2Salt Function to retrieve the PBKDF2 salt. - * Note that this function is used for CouchDB and compatible only. - */ - protected _log: LogFunction; - protected _authHeader: AuthorizationHeaderGenerator; - protected _APIService: APIService; - protected _appLifecycleService: AppLifecycleService; - protected _settingService: SettingService; - protected _unresolvedErrors: UnresolvedErrorManager; - protected last_successful_post: boolean; - get hadLastPostFailedBySize(): boolean; - constructor(context: T, dependencies: RemoteServiceDependencies); - showError(msg: string, max_log_level?: LOG_LEVEL): void; - clearErrors(): void; - performFetch(req: string | Request, opts?: RequestInit, fetchMethod?: FetchMethod): Promise; - connect(uri: string, auth: CouchDBCredentials, disableRequestURI: boolean, passphrase: string | false, useDynamicIterationCount: boolean, performSetup: boolean, skipInfo: boolean, compression: boolean, customHeaders: Record, useRequestAPI: boolean, getPBKDF2Salt: () => Promise): Promise; - info: PouchDB.Core.DatabaseInfo; - }>; -} -export {}; diff --git a/_types/src/lib/src/services/base/ReplicationService.d.ts b/_types/src/lib/src/services/base/ReplicationService.d.ts deleted file mode 100644 index 1c7f70fb..00000000 --- a/_types/src/lib/src/services/base/ReplicationService.d.ts +++ /dev/null @@ -1,94 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type LOG_LEVEL } from "@lib/common/types"; -import type { IAPIService, IDatabaseService, IFileProcessingService, IReplicationService, IReplicatorService, ISettingService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator"; -import type { AppLifecycleService } from "./AppLifecycleService"; -export interface ReplicationServiceDependencies { - APIService: IAPIService; - settingService: ISettingService; - appLifecycleService: AppLifecycleService; - databaseService: IDatabaseService; - replicatorService: IReplicatorService; - fileProcessingService: IFileProcessingService; -} -/** - * The ReplicationService provides methods for managing replication processes. - */ -export declare abstract class ReplicationService extends ServiceBase implements IReplicationService { - private _unresolvedErrorManager; - showError(msg: string, max_log_level?: LOG_LEVEL): void; - clearErrors(): void; - _log: LogFunction; - settingService: ISettingService; - appLifecycleService: AppLifecycleService; - replicatorService: IReplicatorService; - APIService: IAPIService; - fileProcessing: IFileProcessingService; - databaseService: IDatabaseService; - constructor(context: T, dependencies: ReplicationServiceDependencies); - /** - * Process a synchronisation result document. - */ - readonly processSynchroniseResult: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(doc: import("@lib/common/types").MetaEntry) => Promise>; - /** - * Process a synchronisation result document for optional entries i.e., hidden files. - */ - readonly processOptionalSynchroniseResult: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(doc: import("@lib/common/types").LoadedEntry) => Promise>; - /** - * Process an array of synchronisation result documents. - * @param docs An array of documents to parse and handle. - */ - readonly parseSynchroniseResult: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(docs: Array>) => Promise>; - /** - * Process a virtual document (e.g., for customisation sync). - */ - readonly processVirtualDocument: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(docs: PouchDB.Core.ExistingDocument) => Promise>; - /** - * An event triggered before starting replication. - */ - readonly onBeforeReplicate: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(showMessage: boolean) => Promise>; - /** - * - */ - readonly onCheckReplicationReady: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(showMessage: boolean) => Promise>; - /** - * Check if the replication is ready to start. - * @param showMessage Whether to show messages to the user. - */ - isReplicationReady(showMessage?: boolean): Promise; - onReplicationFailed: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(showMessage?: boolean) => Promise>; - private performReplicationRequest; - /** - * Perform replication and handle a failed result. - * @param showMessage Whether to show replication progress messages. - */ - performReplication(showMessage?: boolean): Promise; - /** - * Start the replication process. - * @param showMessage Whether to show messages to the user. - */ - replicate(showMessage?: boolean): Promise; - previousReplicated: number; - /** - * Start the replication process triggered by an event (e.g., file change). - * @param showMessage Whether to show messages to the user. - */ - replicateByEvent(showMessage?: boolean): Promise; - /** - * Check if there is a connection failure with the remote database. - */ - readonly checkConnectionFailure: import("@lib/services/lib/HandlerUtils").MultipleHandlerFunction<() => Promise, unknown>; - databaseQueueCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - storageApplyingCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - replicationResultCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - getActiveReplicatorFor(usage: string): false | LiveSyncAbstractReplicator; - replicateAllToRemote(showingNotice?: boolean, sendChunksInBulkDisabled?: boolean): Promise; - replicateAllFromRemote(showingNotice?: boolean): Promise; - private _getReplicatorAndPerform; - markLocked(lockByClean?: boolean): Promise; - markUnlocked(): Promise; - markResolved(): Promise; -} diff --git a/_types/src/lib/src/services/base/ReplicatorService.d.ts b/_types/src/lib/src/services/base/ReplicatorService.d.ts deleted file mode 100644 index afe6dac9..00000000 --- a/_types/src/lib/src/services/base/ReplicatorService.d.ts +++ /dev/null @@ -1,68 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator"; -import type { IReplicatorService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import type { SettingService } from "./SettingService"; -import type { AppLifecycleService } from "./AppLifecycleService"; -import { UnresolvedErrorManager } from "./UnresolvedErrorManager"; -import type { DatabaseEventService } from "./DatabaseEventService"; -import { type AsyncActivityOptions, type AsyncActivityRunner } from "@lib/interfaces/AsyncActivityRunner.ts"; -export interface ReplicatorServiceDependencies { - settingService: SettingService; - appLifecycleService: AppLifecycleService; - databaseEventService: DatabaseEventService; - activityRunner?: AsyncActivityRunner; -} -/** - * The ReplicatorService provides methods for managing replication. - */ -export declare abstract class ReplicatorService extends ServiceBase implements IReplicatorService { - protected dependencies: ReplicatorServiceDependencies; - readonly boundedRemoteActivityCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - readonly finiteReplicationActivityCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - _log: (msg: unknown, level?: import("octagonal-wheels/common/logger").LOG_LEVEL, key?: string) => void; - private settingService; - private databaseEventService; - private _activeReplicator; - private _replicatorType; - private appLifecycleService; - _unresolvedErrorManager: UnresolvedErrorManager; - constructor(context: T, dependencies: ReplicatorServiceDependencies); - /** - * Runs one finite remote operation while exposing its lifetime to host policy and status UI. - * Continuous replication must not use this boundary. - */ - runBoundedRemoteActivity(task: () => TValue | PromiseLike, options?: AsyncActivityOptions): Promise; - /** Runs one finite replication and exposes its document-delivery lifetime. */ - runFiniteReplicationActivity(task: () => TValue | PromiseLike, options?: AsyncActivityOptions): Promise; - private runRemoteActivity; - private suspendReplication; - private reinitialiseReplicator; - private disposeReplicator; - private _initialiseReplicator; - /** - * Close the active replication if any. - * Not used currently. - */ - readonly onCloseActiveReplication: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Get a new replicator instance based on the provided settings. - */ - readonly getNewReplicator: import("@lib/services/lib/HandlerUtils").MultipleHandlerFunction<(settingOverride?: Partial) => Promise, unknown>; - readonly onReplicatorInitialised: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Get the currently active replicator instance. - * If no active replicator, return undefined but that is the fatal situation (on Obsidian). - */ - getActiveReplicator(): LiveSyncAbstractReplicator | undefined; - replicationStatics: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource<{ - sent: number; - arrived: number; - maxPullSeq: number; - maxPushSeq: number; - lastSyncPullSeq: number; - lastSyncPushSeq: number; - syncStatus: import("@lib/common/types").DatabaseConnectingStatus; - }>; -} diff --git a/_types/src/lib/src/services/base/ServiceBase.d.ts b/_types/src/lib/src/services/base/ServiceBase.d.ts deleted file mode 100644 index d47f290e..00000000 --- a/_types/src/lib/src/services/base/ServiceBase.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare class ServiceContext { -} -export declare abstract class ServiceBase { - protected context: T; - constructor(context: T); -} diff --git a/_types/src/lib/src/services/base/SettingService.d.ts b/_types/src/lib/src/services/base/SettingService.d.ts deleted file mode 100644 index 017d3c94..00000000 --- a/_types/src/lib/src/services/base/SettingService.d.ts +++ /dev/null @@ -1,114 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ObsidianLiveSyncSettings } from "@lib/common/types"; -import type { IAPIService, ISettingService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import { createInstanceLogFunction } from "@lib/services/lib/logUtils"; -export interface SettingServiceDependencies { - APIService: IAPIService; -} -export declare abstract class SettingService extends ServiceBase implements ISettingService { - deviceAndVaultName: string; - protected APIService: IAPIService; - protected abstract setItem(key: string, value: string): void; - protected abstract getItem(key: string): string; - protected abstract deleteItem(key: string): void; - _settings: ObsidianLiveSyncSettings; - get settings(): ObsidianLiveSyncSettings; - set settings(value: ObsidianLiveSyncSettings); - protected abstract saveData(setting: ObsidianLiveSyncSettings): Promise; - protected abstract loadData(): Promise; - private _lastPersistedSettings?; - _log: ReturnType; - constructor(context: T, dependencies: SettingServiceDependencies); - /** - * Adjust the given settings, e.g., migrate old settings to new format. - * @param settings The settings to adjust. - */ - adjustSettings(settings: ObsidianLiveSyncSettings): Promise; - /** - * Get the unique name for identify the device. - */ - getDeviceAndVaultName(): string; - /** - * Set the unique name for identify the device. - * @param name The unique name to set. - */ - setDeviceAndVaultName(name: string): void; - /** - * Save the current device and vault name to settings, aside from the main settings. - */ - saveDeviceAndVaultName(): void; - private additionalSuffixOfDatabaseName; - private getKey; - setSmallConfig(key: string, value: string): void; - getSmallConfig(key: string): string; - deleteSmallConfig(key: string): void; - /** - * Save the current settings to storage. - */ - saveSettingData(): Promise; - private encryptRemoteConfigurationUris; - private decryptRemoteConfigurationUris; - /** - * Event triggered before realising the settings. - * Handlers can return false to abort the realisation process. - */ - readonly onBeforeRealiseSetting: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered after the settings have been realised. - */ - readonly onSettingRealised: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered to realise the settings. - */ - readonly onRealiseSetting: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Suspend all synchronisation activities and save to the settings. - */ - readonly suspendAllSync: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Suspend extra synchronisation activities, e.g., hidden files sync. - */ - readonly suspendExtraSync: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Suggest enabling optional features to the user. - */ - readonly suggestOptionalFeatures: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(opt: { - enableFetch?: boolean; - enableOverwrite?: boolean; - }) => Promise>; - /** - * Enable an optional feature and save to the settings. - * It may also raised from `handleSuggestOptionalFeatures` if the user agrees. - * @param mode The optional feature to enable. - */ - readonly enableOptionalFeature: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(mode: keyof OPTIONAL_SYNC_FEATURES) => Promise>; - readonly onSettingLoaded: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(settings: ObsidianLiveSyncSettings) => Promise>; - readonly onSettingChanged: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(settings: ObsidianLiveSyncSettings) => Promise>; - readonly onSettingSaved: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(settings: ObsidianLiveSyncSettings) => Promise>; - readonly onBeforeSaveSettingData: import("@lib/services/lib/HandlerUtils").CollectiveHandlerFunction<(nextSettings: ObsidianLiveSyncSettings, previousSettings: ObsidianLiveSyncSettings) => Promise<(Partial | void)[]>, unknown>; - /** - * Get the current settings. - */ - currentSettings(): ObsidianLiveSyncSettings; - updateSettings(updateFn: (settings: ObsidianLiveSyncSettings) => ObsidianLiveSyncSettings, saveImmediately?: boolean): Promise; - applyExternalSettings(partial: Partial, saveImmediately?: boolean): Promise; - applyPartial(partial: Partial, saveImmediately?: boolean): Promise; - getPassphrase(settings: ObsidianLiveSyncSettings): Promise; - private usedPassphrase; - /** - * Clear any used passphrase from memory. - */ - clearUsedPassphrase(): void; - decryptConfigurationItem(encrypted: string, passphrase: string): Promise; - encryptConfigurationItem(src: string, settings: ObsidianLiveSyncSettings): Promise; - /** - * Decrypt the given settings. - * @param settings The settings to decrypt. - */ - decryptSettings(settings: ObsidianLiveSyncSettings): Promise; - loadSettings(): Promise; - private tryDecodeJson; - private cloneSettings; -} diff --git a/_types/src/lib/src/services/base/TestService.d.ts b/_types/src/lib/src/services/base/TestService.d.ts deleted file mode 100644 index 73655f04..00000000 --- a/_types/src/lib/src/services/base/TestService.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ITestService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -/** - * The TestService provides methods for adding and handling test results. - */ -export declare abstract class TestService extends ServiceBase implements ITestService { - /** - * Run the test suite to verify the plug-in's functionality. - * This is typically used for development and debugging purposes. - * It may involve user interaction (means raising resolveByUserInteraction). - */ - readonly test: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Run the multi-device test suite to verify the plug-in's functionality across multiple devices. - * This is typically used for development and debugging purposes. - * It may involve user interaction (means raising resolveByUserInteraction). - */ - readonly testMultiDevice: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Add a test result to the test suite. - * @param name The name of the test case. - * @param key The key of the test result. - * @param result The result of the test (true for success, false for failure). - * @param summary A brief summary of the test result. - * @param message A detailed message about the test result. - */ - abstract addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void; -} diff --git a/_types/src/lib/src/services/base/TweakValueService.d.ts b/_types/src/lib/src/services/base/TweakValueService.d.ts deleted file mode 100644 index e446b038..00000000 --- a/_types/src/lib/src/services/base/TweakValueService.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { RemoteDBSettings, TweakValues } from "@lib/common/types"; -import type { ITweakValueService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -/** - * The TweakValueService provides methods for managing tweak values and resolving mismatches. - */ -export declare abstract class TweakValueService extends ServiceBase implements ITweakValueService { - /** - * Fetch and trial the remote database settings to determine if they are preferred. - * @param trialSetting The remote database settings to connect. - */ - abstract fetchRemotePreferred(trialSetting: RemoteDBSettings): Promise; - /** - * Check and ask the user to resolve any mismatched tweak values. - * @param preferred The preferred tweak values to check against. - */ - abstract checkAndAskResolvingMismatched(preferred: Partial): Promise<[TweakValues | boolean, boolean]>; - /** - * Ask the user to resolve any mismatched tweak values. - * @param preferredSource The preferred tweak values to resolve against. - */ - abstract askResolvingMismatched(preferredSource: TweakValues): Promise<"OK" | "CHECKAGAIN" | "IGNORE">; - /** - * Check and ask the user to use the remote configuration. - * @param settings The remote database settings to connect. - */ - abstract checkAndAskUseRemoteConfiguration(settings: RemoteDBSettings): Promise<{ - result: false | TweakValues; - requireFetch: boolean; - }>; - /** - * Ask the user to use the remote configuration. - * @param trialSetting The remote database settings to connect. - * @param preferred The preferred tweak values to use. - */ - abstract askUseRemoteConfiguration(trialSetting: RemoteDBSettings, preferred: TweakValues): Promise<{ - result: false | TweakValues; - requireFetch: boolean; - }>; -} diff --git a/_types/src/lib/src/services/base/UnresolvedErrorManager.d.ts b/_types/src/lib/src/services/base/UnresolvedErrorManager.d.ts deleted file mode 100644 index e792034b..00000000 --- a/_types/src/lib/src/services/base/UnresolvedErrorManager.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type LOG_LEVEL } from "octagonal-wheels/common/logger"; -import type { AppLifecycleService } from "./AppLifecycleService"; -export declare class UnresolvedErrorManager { - private _log; - private appLifecycleService; - private _occurredErrors; - showError(msg: string, max_log_level?: LOG_LEVEL): void; - clearError(msg: string): void; - clearErrors(): void; - countErrors(needle: string): number; - private _reportUnresolvedMessages; - constructor(appLifecycleService: AppLifecycleService); -} diff --git a/_types/src/lib/src/services/base/VaultService.d.ts b/_types/src/lib/src/services/base/VaultService.d.ts deleted file mode 100644 index 3be8c816..00000000 --- a/_types/src/lib/src/services/base/VaultService.d.ts +++ /dev/null @@ -1,71 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath } from "@lib/common/types"; -import type { IAPIService, ISettingService, IVaultService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -export interface VaultServiceDependencies { - settingService: ISettingService; - APIService: IAPIService; -} -/** - * The VaultService provides methods for interacting with the vault (local file system). - */ -export declare abstract class VaultService extends ServiceBase implements IVaultService { - protected settingService: ISettingService; - protected APIService: IAPIService; - get settings(): import("@lib/common/types").ObsidianLiveSyncSettings; - constructor(context: T, dependencies: VaultServiceDependencies); - /** - * Get the vault name only. - */ - vaultName(): string; - /** - * Get the vault name with additional suffixes. - */ - getVaultName(): string; - /** - * Scan the vault for changes (especially for changes during the plug-in were not running). - * @param showingNotice Whether to show a notice to the user. - * @param ignoreSuspending Whether to ignore any suspending state. - */ - readonly scanVault: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(showingNotice?: boolean, ignoreSuspending?: boolean) => Promise>; - /** - * Check if a file is ignored by the ignore file (e.g., .gitignore, .obsidianignore). - * @param file The file path or file info stub to check. - */ - readonly isIgnoredByIgnoreFile: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(file: string | import("@lib/common/types").UXFileInfoStub) => Promise>; - /** - * Check if a file is a target file for synchronisation. - * @param file The file path or file info stub to check. - * @param keepFileCheckList Whether to keep the file in the check list. - */ - readonly isTargetFile: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(file: string | import("@lib/common/types").UXFileInfoStub) => Promise>; - /** - * Check if a file is a target file for some extra feature - */ - readonly isTargetFileInExtra: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(file: string | import("@lib/common/types").UXFileInfoStub) => Promise>; - /** - * Check if a filesize is too large against the current settings. - * @param size The file size to check. - */ - isFileSizeTooLarge(size: number): boolean; - /** - * Get the currently active file path in the editor, if any. - */ - abstract getActiveFilePath(): FilePath | undefined; - /** - * Check if the vault is on a case-insensitive file system. - * This is important for certain operating systems like Windows and macOS. - */ - abstract isStorageInsensitive(): boolean; - /** - * Check if the file system should be treated case-insensitively. - * This is important for certain operating systems like Windows and macOS. - */ - shouldCheckCaseInsensitively(): boolean; - /** - * Check if a given path is valid in the vault. - * @param path The file path to check. - */ - abstract isValidPath(path: string): boolean; -} diff --git a/_types/src/lib/src/services/implements/base/UIService.d.ts b/_types/src/lib/src/services/implements/base/UIService.d.ts deleted file mode 100644 index a3d273fc..00000000 --- a/_types/src/lib/src/services/implements/base/UIService.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { ComponentHasResult, SvelteDialogManagerBase } from "@lib/UI/svelteDialog"; -import type { IAPIService, IUIService } from "@lib/services/base/IService"; -import { type AppLifecycleService } from "@lib/services/base/AppLifecycleService.ts"; -import { ServiceBase, type ServiceContext } from "@lib/services/base/ServiceBase"; -export type UIServiceDependencies = { - appLifecycle: AppLifecycleService; - dialogManager: SvelteDialogManagerBase; - APIService: IAPIService; -}; -type DialogResult = "ok" | "cancel"; -type DialogParams = { - title: string; - dataToCopy: string; -}; -export declare abstract class UIService extends ServiceBase implements IUIService { - private _dialogManager; - protected _APIService: IAPIService; - abstract get dialogToCopy(): ComponentHasResult; - constructor(context: T, dependents: UIServiceDependencies); - get dialogManager(): SvelteDialogManagerBase; - promptCopyToClipboard(title: string, value: string): Promise; - showMarkdownDialog(title: string, contentMD: string, buttons: T, defaultAction?: (typeof buttons)[number]): Promise<(typeof buttons)[number] | false>; - get confirm(): Confirm; -} -export {}; diff --git a/_types/src/lib/src/services/implements/browser/BrowserAPIService.d.ts b/_types/src/lib/src/services/implements/browser/BrowserAPIService.d.ts deleted file mode 100644 index c9e00df0..00000000 --- a/_types/src/lib/src/services/implements/browser/BrowserAPIService.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService"; -import type { FetchHttpHandler } from "@smithy/fetch-http-handler"; -import type { ICommandCompat } from "@lib/services/base/IService"; -import type { Confirm } from "@lib/interfaces/Confirm"; -export declare const PACKAGE_VERSION: string; -export declare const MANIFEST_VERSION: string; -export declare class BrowserAPIService extends InjectableAPIService { - _confirmInstance: Confirm; - private commandBar; - private commandButtons; - private logPanel; - private logViewport; - private readonly maxLogLines; - private windowFactories; - private windowInstances; - private windowRoot; - private windowTabs; - private windowBody; - private windowPanels; - private activeWindowType; - constructor(context: T); - get confirm(): Confirm; - showWindow(type: string): Promise; - getCustomFetchHandler(): FetchHttpHandler; - isMobile(): boolean; - getAppID(): string; - getSystemVaultName: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => string, unknown>; - getAppVersion(): string; - getPluginVersion(): string; - getPlatform(): string; - getCrypto(): Crypto; - nativeFetch(req: string | Request, opts?: RequestInit): Promise; - private ensureLogPanel; - private formatLogLine; - private appendLog; - private ensureCommandBar; - private ensureWindowHost; - private ensureWindowTab; - private ensureWindowPanel; - private activateWindow; - private createLeafShim; - private evaluateEnabled; - private executeCommand; - private refreshCommandStates; - addCommand(command: TCommand): TCommand; - addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => any): HTMLElement; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - registerWindow(type: string, factory: (leaf: any) => any): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - registerProtocolHandler(action: string, handler: (params: Record) => any): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - addStatusBarItem(): HTMLElement | undefined; -} diff --git a/_types/src/lib/src/services/implements/browser/BrowserConfirm.d.ts b/_types/src/lib/src/services/implements/browser/BrowserConfirm.d.ts deleted file mode 100644 index 262d7eaa..00000000 --- a/_types/src/lib/src/services/implements/browser/BrowserConfirm.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare class BrowserConfirm implements Confirm { - _context: T; - constructor(context: T); - askYesNo(message: string): Promise<"yes" | "no">; - askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise; - askYesNoDialog(message: string, opt: { - title?: string; - defaultOption?: "Yes" | "No"; - timeout?: number; - }): Promise<"yes" | "no">; - askSelectString(message: string, items: string[]): Promise; - askSelectStringDialogue(message: string, buttons: T, opt: { - title?: string; - defaultAction: T[number]; - timeout?: number; - }): Promise; - askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void; - confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false>; -} diff --git a/_types/src/lib/src/services/implements/browser/BrowserDatabaseService.d.ts b/_types/src/lib/src/services/implements/browser/BrowserDatabaseService.d.ts deleted file mode 100644 index 4502b721..00000000 --- a/_types/src/lib/src/services/implements/browser/BrowserDatabaseService.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { KeyValueDBService } from "@lib/services/base/KeyValueDBService"; -import { DatabaseService } from "@lib/services/base/DatabaseService.ts"; -export declare class BrowserDatabaseService extends DatabaseService { -} -export declare class BrowserKeyValueDBService extends KeyValueDBService { -} diff --git a/_types/src/lib/src/services/implements/browser/BrowserUIService.d.ts b/_types/src/lib/src/services/implements/browser/BrowserUIService.d.ts deleted file mode 100644 index 71e48c61..00000000 --- a/_types/src/lib/src/services/implements/browser/BrowserUIService.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { UIService } from "@lib/services/implements/base/UIService"; -import type { ConfigService } from "@lib/services/base/ConfigService"; -import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; -import type { ReplicatorService } from "@lib/services/base/ReplicatorService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import type { IAPIService, IControlService } from "@lib/services/base/IService"; -export type BrowserUIServiceDependencies = { - appLifecycle: AppLifecycleService; - config: ConfigService; - replicator: ReplicatorService; - APIService: IAPIService; - control: IControlService; -}; -export declare class BrowserUIService extends UIService { - get dialogToCopy(): import("svelte/legacy").LegacyComponentType; - constructor(context: T, dependents: BrowserUIServiceDependencies); -} diff --git a/_types/src/lib/src/services/implements/browser/ConfigServiceBrowserCompat.d.ts b/_types/src/lib/src/services/implements/browser/ConfigServiceBrowserCompat.d.ts deleted file mode 100644 index 57c4a763..00000000 --- a/_types/src/lib/src/services/implements/browser/ConfigServiceBrowserCompat.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ConfigService } from "@lib/services/base/ConfigService"; -import type { IAPIService, ISettingService } from "@lib/services/base/IService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { createInstanceLogFunction } from "@lib/services/lib/logUtils"; -export interface ConfigServiceBrowserCompatDependencies { - settingService: ISettingService; - APIService: IAPIService; -} -export declare class ConfigServiceBrowserCompat extends ConfigService { - private _settingService; - _log: ReturnType; - constructor(context: T, dependencies: ConfigServiceBrowserCompatDependencies); - getSmallConfig(key: string): string | null; - setSmallConfig(key: string, value: string): void; - deleteSmallConfig(key: string): void; -} diff --git a/_types/src/lib/src/services/implements/browser/Menu.d.ts b/_types/src/lib/src/services/implements/browser/Menu.d.ts deleted file mode 100644 index 1cc11aa5..00000000 --- a/_types/src/lib/src/services/implements/browser/Menu.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type PromiseWithResolvers } from "octagonal-wheels/promises"; -export declare class MenuItem { - type: string; - title: string; - handler?: () => void | Promise; - icon: string; - setTitle(title: string): this; - onClick(callback: () => void | Promise): this; - setIcon(icon: string | null): this; -} -export declare class MenuSeparator { - type: string; -} -export declare class Menu { - type: string; - items: (MenuItem | MenuSeparator)[]; - constructor(); - addItem(callback: (item: MenuItem) => void): this; - addSeparator(): this; - waitingForClose?: PromiseWithResolvers; - showAtPosition(pos: { - x: number; - y: number; - }): Promise; - hide(): void; -} diff --git a/_types/src/lib/src/services/implements/browser/ui/renderMessageMarkdown.d.ts b/_types/src/lib/src/services/implements/browser/ui/renderMessageMarkdown.d.ts deleted file mode 100644 index c9bd82bb..00000000 --- a/_types/src/lib/src/services/implements/browser/ui/renderMessageMarkdown.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function renderMessageMarkdown(message: string): string; diff --git a/_types/src/lib/src/services/implements/headless/HeadlessAPIService.d.ts b/_types/src/lib/src/services/implements/headless/HeadlessAPIService.d.ts deleted file mode 100644 index 5bbb6d2a..00000000 --- a/_types/src/lib/src/services/implements/headless/HeadlessAPIService.d.ts +++ /dev/null @@ -1,54 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService"; -import type { FetchHttpHandler } from "@smithy/fetch-http-handler"; -import type { ICommandCompat } from "@lib/services/base/IService"; -import type { Confirm } from "@lib/interfaces/Confirm"; -/** - * Headless implementation of Confirm that returns sensible defaults instead - * of throwing. Dialogs are logged to stderr so the prompts are visible in - * service logs, and the default/conservative action is taken automatically. - */ -export declare class HeadlessConfirm implements Confirm { - askYesNo(message: string): Promise<"yes" | "no">; - askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise; - askYesNoDialog(message: string, opt: { - title?: string; - defaultOption?: "Yes" | "No"; - timeout?: number; - }): Promise<"yes" | "no">; - askSelectString(message: string, items: string[]): Promise; - askSelectStringDialogue(message: string, buttons: T, opt: { - title?: string; - defaultAction: T[number]; - timeout?: number; - }): Promise; - askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void; - confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false>; -} -export declare class HeadlessAPIService extends InjectableAPIService { - private _confirmInstance; - private _systemVaultName; - constructor(context: T); - get confirm(): Confirm; - showWindow(type: string): Promise; - getCustomFetchHandler(): FetchHttpHandler; - isMobile(): boolean; - getAppID(): string; - getAppVersion(): string; - getPluginVersion(): string; - getPlatform(): string; - getCrypto(): Crypto; - addCommand(command: TCommand): TCommand; - addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => any): HTMLElement; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - registerWindow(type: string, factory: (leaf: any) => any): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - registerProtocolHandler(action: string, handler: (params: Record) => any): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - addStatusBarItem(): HTMLElement | undefined; - private toSafeKeyPart; - private hash32; - private deriveSystemVaultName; - getSystemVaultName(): string; - get isOnline(): boolean; - nativeFetch(req: string | Request, opts?: RequestInit): Promise; -} diff --git a/_types/src/lib/src/services/implements/headless/HeadlessDatabaseService.d.ts b/_types/src/lib/src/services/implements/headless/HeadlessDatabaseService.d.ts deleted file mode 100644 index 0eb9c035..00000000 --- a/_types/src/lib/src/services/implements/headless/HeadlessDatabaseService.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { KeyValueDBService } from "@lib/services/base/KeyValueDBService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { DatabaseService } from "@lib/services/base/DatabaseService.ts"; -export declare class HeadlessDatabaseService extends DatabaseService { -} -export declare class HeadlessKeyValueDBService extends KeyValueDBService { -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableAPIService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableAPIService.d.ts deleted file mode 100644 index 72c7c24f..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableAPIService.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { APIService } from "@lib/services/base/APIService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare abstract class InjectableAPIService extends APIService { - addLog: import("@lib/services/lib/HandlerUtils").HandlerFunction<(message: unknown, level: import("octagonal-wheels/common/logger").LOG_LEVEL, key?: string) => void, unknown>; - getPlatform(): string; - getCrypto(): Crypto; -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableAppLifecycleService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableAppLifecycleService.d.ts deleted file mode 100644 index 26a0bca1..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableAppLifecycleService.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; -import type { IAppLifecycleService } from "@lib/services/base/IService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare abstract class AppLifecycleServiceBase extends AppLifecycleService { - askRestart: import("@lib/services/lib/HandlerUtils").HandlerFunction<(message?: string) => void, unknown>; - scheduleRestart: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => void, unknown>; - isReloadingScheduled: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => boolean, unknown>; -} -export declare abstract class InjectableAppLifecycleService extends AppLifecycleServiceBase implements IAppLifecycleService { - performRestart: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => void, unknown>; -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableConflictService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableConflictService.d.ts deleted file mode 100644 index f9c6ad03..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableConflictService.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ConflictService } from "@lib/services/base/ConflictService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare class InjectableConflictService extends ConflictService { - queueCheckForIfOpen: import("@lib/services/lib/HandlerUtils").HandlerFunction<(path: import("../../../common/types").FilePathWithPrefix) => Promise, unknown>; - queueCheckFor: import("@lib/services/lib/HandlerUtils").HandlerFunction<(path: import("../../../common/types").FilePathWithPrefix) => Promise, unknown>; - ensureAllProcessed: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => Promise, unknown>; - resolveByDeletingRevision: import("@lib/services/lib/HandlerUtils").HandlerFunction<(path: import("../../../common/types").FilePathWithPrefix, deleteRevision: string, title: string) => Promise, unknown>; - resolve: import("@lib/services/lib/HandlerUtils").HandlerFunction<(filename: import("../../../common/types").FilePathWithPrefix) => Promise, unknown>; - resolveByNewest: import("@lib/services/lib/HandlerUtils").HandlerFunction<(filename: import("../../../common/types").FilePathWithPrefix) => Promise, unknown>; - resolveAllConflictedFilesByNewerOnes: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => Promise, unknown>; -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableDatabaseEventService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableDatabaseEventService.d.ts deleted file mode 100644 index a1162a55..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableDatabaseEventService.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { DatabaseEventService } from "@lib/services/base/DatabaseEventService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare class InjectableDatabaseEventService extends DatabaseEventService { -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableFileProcessingService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableFileProcessingService.d.ts deleted file mode 100644 index 2fdf909e..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableFileProcessingService.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { FileProcessingService } from "@lib/services/base/FileProcessingService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare class InjectableFileProcessingService extends FileProcessingService { -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectablePathService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectablePathService.d.ts deleted file mode 100644 index d16e609d..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectablePathService.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UXFileInfo, AnyEntry, UXFileInfoStub, FilePathWithPrefix } from "@lib/common/types"; -import { PathService } from "@lib/services/base/PathService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols"; -export declare function compareFileFreshnessGeneric(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN; -export declare class PathServiceCompat extends PathService { - markChangesAreSame(old: UXFileInfo | AnyEntry | FilePathWithPrefix, newMtime: number, oldMtime: number): boolean | undefined; - unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void; - compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN; - isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | FilePathWithPrefix, mtimes: number[]): undefined | typeof EVEN; - normalizePath(path: string): string; -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableRemoteService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableRemoteService.d.ts deleted file mode 100644 index b906494e..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableRemoteService.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { RemoteService } from "@lib/services/base/RemoteService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare class InjectableRemoteService extends RemoteService { -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableReplicationService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableReplicationService.d.ts deleted file mode 100644 index 5118a276..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableReplicationService.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ReplicationService } from "@lib/services/base/ReplicationService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare class InjectableReplicationService extends ReplicationService { -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableReplicatorService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableReplicatorService.d.ts deleted file mode 100644 index 267a973c..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableReplicatorService.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ReplicatorService } from "@lib/services/base/ReplicatorService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare class InjectableReplicatorService extends ReplicatorService { -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableServiceHub.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableServiceHub.d.ts deleted file mode 100644 index f5f7a38c..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableServiceHub.d.ts +++ /dev/null @@ -1,73 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ConfigService } from "@lib/services/base/ConfigService"; -import { ControlService } from "@lib/services/base/ControlService"; -import type { KeyValueDBService } from "@lib/services/base/KeyValueDBService"; -import { PathService } from "@lib/services/base/PathService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import type { SettingService } from "@lib/services/base/SettingService"; -import { ServiceHub } from "@lib/services/ServiceHub"; -import type { UIService } from "@lib/services/implements/base/UIService"; -import { InjectableAPIService } from "./InjectableAPIService"; -import { type AppLifecycleServiceBase } from "./InjectableAppLifecycleService"; -import { InjectableConflictService } from "./InjectableConflictService"; -import { InjectableDatabaseEventService } from "./InjectableDatabaseEventService"; -import { InjectableFileProcessingService } from "./InjectableFileProcessingService"; -import { InjectableRemoteService } from "./InjectableRemoteService"; -import { InjectableReplicationService } from "./InjectableReplicationService"; -import { InjectableReplicatorService } from "./InjectableReplicatorService"; -import type { InjectableServiceInstances } from "./InjectableServices"; -import { InjectableTestService } from "./InjectableTestService"; -import { InjectableTweakValueService } from "./InjectableTweakValueService"; -import { InjectableVaultService } from "./InjectableVaultService"; -import type { DatabaseService } from "@lib/services/base/DatabaseService.ts"; -export declare class InjectableServiceHub extends ServiceHub { - protected readonly _api: InjectableAPIService; - protected readonly _path: PathService; - protected readonly _database: DatabaseService; - protected readonly _databaseEvents: InjectableDatabaseEventService; - protected readonly _replicator: InjectableReplicatorService; - protected readonly _fileProcessing: InjectableFileProcessingService; - protected readonly _replication: InjectableReplicationService; - protected readonly _remote: InjectableRemoteService; - protected readonly _conflict: InjectableConflictService; - protected readonly _appLifecycle: AppLifecycleServiceBase; - protected readonly _setting: SettingService; - protected readonly _tweakValue: InjectableTweakValueService; - protected readonly _vault: InjectableVaultService; - protected readonly _test: InjectableTestService; - protected readonly _ui: UIService; - protected readonly _config: ConfigService; - protected readonly _keyValueDB: KeyValueDBService; - protected readonly _control: ControlService; - get API(): InjectableAPIService; - get path(): PathService; - get database(): DatabaseService; - get databaseEvents(): InjectableDatabaseEventService; - get replicator(): InjectableReplicatorService; - get fileProcessing(): InjectableFileProcessingService; - get replication(): InjectableReplicationService; - get remote(): InjectableRemoteService; - get conflict(): InjectableConflictService; - get appLifecycle(): AppLifecycleServiceBase; - get setting(): SettingService; - get tweakValue(): InjectableTweakValueService; - get vault(): InjectableVaultService; - get test(): InjectableTestService; - get control(): ControlService; - get keyValueDB(): KeyValueDBService; - get UI(): UIService; - get config(): ConfigService; - constructor(context: T, services: InjectableServiceInstances & { - setting: SettingService; - appLifecycle: AppLifecycleServiceBase; - path: PathService; - API: InjectableAPIService; - ui: UIService; - config: ConfigService; - database: DatabaseService; - vault: InjectableVaultService; - keyValueDB: KeyValueDBService; - replicator: InjectableReplicatorService; - }); -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableServices.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableServices.d.ts deleted file mode 100644 index 9fc7fd77..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableServices.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ServiceInstances } from "@lib/services/ServiceHub.ts"; -import type { UIService } from "@lib/services/implements/base/UIService.ts"; -import type { ConfigService } from "@lib/services/base/ConfigService.ts"; -import type { ServiceContext } from "@lib/services/base/ServiceBase.ts"; -import type { InjectableAPIService } from "./InjectableAPIService"; -import type { InjectableDatabaseEventService } from "./InjectableDatabaseEventService"; -import type { InjectableReplicatorService } from "./InjectableReplicatorService"; -import type { InjectableFileProcessingService } from "./InjectableFileProcessingService"; -import type { InjectableReplicationService } from "./InjectableReplicationService"; -import type { InjectableRemoteService } from "./InjectableRemoteService"; -import type { InjectableConflictService } from "./InjectableConflictService"; -import type { AppLifecycleServiceBase } from "./InjectableAppLifecycleService"; -import type { InjectableTweakValueService } from "./InjectableTweakValueService"; -import type { InjectableVaultService } from "./InjectableVaultService"; -import type { InjectableTestService } from "./InjectableTestService"; -import type { PathService } from "@lib/services/base/PathService"; -import type { DatabaseService } from "@lib/services/base/DatabaseService.ts"; -import type { SettingService } from "@lib/services/base/SettingService"; -export type InjectableServiceInstances = ServiceInstances & { - API?: InjectableAPIService; - path?: PathService; - database?: DatabaseService; - databaseEvents?: InjectableDatabaseEventService; - replicator?: InjectableReplicatorService; - fileProcessing?: InjectableFileProcessingService; - replication?: InjectableReplicationService; - remote?: InjectableRemoteService; - conflict?: InjectableConflictService; - appLifecycle?: AppLifecycleServiceBase; - setting?: SettingService; - tweakValue?: InjectableTweakValueService; - vault?: InjectableVaultService; - test?: InjectableTestService; - ui?: UIService; - config?: ConfigService; -}; diff --git a/_types/src/lib/src/services/implements/injectable/InjectableSettingService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableSettingService.d.ts deleted file mode 100644 index 9890e2e4..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableSettingService.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { SettingService, type SettingServiceDependencies } from "@lib/services/base/SettingService"; -import type { ObsidianLiveSyncSettings } from "@lib/common/types"; -export declare class InjectableSettingService extends SettingService { - constructor(context: T, dependencies: SettingServiceDependencies); - protected setItem(key: string, value: string): void; - protected getItem(key: string): string; - protected deleteItem(key: string): void; - saveData: import("@lib/services/lib/HandlerUtils").HandlerFunction<(data: ObsidianLiveSyncSettings) => Promise, unknown>; - loadData: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => Promise, unknown>; -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableTestService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableTestService.d.ts deleted file mode 100644 index e44929af..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableTestService.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { TestService } from "@lib/services/base/TestService"; -export declare class InjectableTestService extends TestService { - addTestResult: import("@lib/services/lib/HandlerUtils").HandlerFunction<(name: string, key: string, result: boolean, summary?: string, message?: string) => void, unknown>; -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableTweakValueService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableTweakValueService.d.ts deleted file mode 100644 index e53214c7..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableTweakValueService.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { TweakValueService } from "@lib/services/base/TweakValueService"; -export declare class InjectableTweakValueService extends TweakValueService { - fetchRemotePreferred: import("@lib/services/lib/HandlerUtils").HandlerFunction<(trialSetting: import("../../../common/types").RemoteDBSettings) => Promise, unknown>; - checkAndAskResolvingMismatched: import("@lib/services/lib/HandlerUtils").HandlerFunction<(preferred: Partial) => Promise<[import("../../../common/types").TweakValues | boolean, boolean]>, unknown>; - askResolvingMismatched: import("@lib/services/lib/HandlerUtils").HandlerFunction<(preferredSource: import("../../../common/types").TweakValues) => Promise<"OK" | "CHECKAGAIN" | "IGNORE">, unknown>; - checkAndAskUseRemoteConfiguration: import("@lib/services/lib/HandlerUtils").HandlerFunction<(settings: import("../../../common/types").RemoteDBSettings) => Promise<{ - result: false | import("../../../common/types").TweakValues; - requireFetch: boolean; - }>, unknown>; - askUseRemoteConfiguration: import("@lib/services/lib/HandlerUtils").HandlerFunction<(trialSetting: import("../../../common/types").RemoteDBSettings, preferred: import("../../../common/types").TweakValues) => Promise<{ - result: false | import("../../../common/types").TweakValues; - requireFetch: boolean; - }>, unknown>; -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableVaultService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableVaultService.d.ts deleted file mode 100644 index bac5c061..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableVaultService.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { VaultService } from "@lib/services/base/VaultService"; -export declare abstract class InjectableVaultService extends VaultService { -} -export declare class InjectableVaultServiceCompat extends InjectableVaultService { - isStorageInsensitive: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => boolean, unknown>; - getActiveFilePath: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => import("../../../common/types").FilePath | undefined, unknown>; - isValidPath(path: string): boolean; -} diff --git a/_types/src/lib/src/services/implements/obsidian/ObsidianServiceContext.d.ts b/_types/src/lib/src/services/implements/obsidian/ObsidianServiceContext.d.ts deleted file mode 100644 index a52aaf9b..00000000 --- a/_types/src/lib/src/services/implements/obsidian/ObsidianServiceContext.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ServiceContext } from "@lib/services/base/ServiceBase"; -import type ObsidianLiveSyncPlugin from "@/main"; -import type { App, Plugin } from "@/deps"; -export declare class ObsidianServiceContext extends ServiceContext { - app: App; - plugin: Plugin; - liveSyncPlugin: ObsidianLiveSyncPlugin; - constructor(app: App, plugin: Plugin, liveSyncPlugin: ObsidianLiveSyncPlugin); -} diff --git a/_types/src/lib/src/services/lib/HandlerUtils.d.ts b/_types/src/lib/src/services/lib/HandlerUtils.d.ts deleted file mode 100644 index 28e53314..00000000 --- a/_types/src/lib/src/services/lib/HandlerUtils.d.ts +++ /dev/null @@ -1,376 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** - * A function type that can be used as a handler. - */ -type HandlerFunc = (...args: TArg) => TResult | Promise; -/** - * A function type that returns a boolean or a Promise of boolean. - */ -type BooleanHandlerFunc = (...args: TArg) => U | Promise; -/** - * An interface for invokable handlers that can add and remove handler functions. - */ -export interface InvokableHandler { - /** - * Invokes the handler with the provided arguments. - * @param args The arguments to pass to the handler. - * @returns A Promise that resolves to the result of the handler. - */ - invoke(...args: T): Promise; -} -/** - * An interface for invokable boolean handlers that can add and remove handler functions. - */ -type InvokableBooleanHandler = InvokableHandler; -/** - * A function type that can be used to unregister a handler. - */ -export type UnregisterFunction = () => void; -/** - * An interface for binder handlers that can assign a single handler function. - */ -export interface BinderHandler { - assign(callback: HandlerFunc, override?: boolean): UnregisterFunction; -} -/** - * An interface for multi-binder handlers that can add and remove handler functions. - */ -export interface MultiRegisterHandler { - /** - * Adds a handler function. - * Note: The same function only added once. - * If you want to prevent duplication, please remove the existing handler before adding it again. - * @param callback The handler function to add. - * @returns A function to remove the added handler. - */ - addHandler(callback: BooleanHandlerFunc): UnregisterFunction; - /** - * Removes a handler function. - * @param callback The handler function to remove. - */ - removeHandler(callback: BooleanHandlerFunc): void; - use(callback: BooleanHandlerFunc): UnregisterFunction; -} -/** - * An interface for dispatch handlers that can dispatch events to multiple handlers. - */ -export interface DispatcherHandler { - dispatch(...args: T): Promise<(Awaited | Error)[]>; -} -/** - * An interface for dispatch handlers that can add and remove handler functions. - */ -export interface DispatchHandler extends DispatcherHandler, MultiRegisterHandler { // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface -} -/** - * A binder that allows assigning and invoking a single handler function. - */ -export declare class Binder any> implements BinderHandler, ReturnType>, InvokableHandler, ReturnType> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - private _name; - /** - * Creates a new Binder instance. - * @param name The name of the handler. - * @param initialCallback An optional initial callback function to assign. - */ - constructor(name: string, initialCallback?: T); - private _callback; - /** - * Assigns a new handler function. - * @param callback The new handler function to assign. - */ - assign(callback: T, override?: boolean): () => void; - /** - * Invokes the assigned handler function with the provided arguments. - * @param args The arguments to pass to the handler function. - * @returns The result of the handler function. - */ - invoke(...args: Parameters): ReturnType; -} -/** - * A binder that allows assigning and invoking a single handler function asynchronously. - * The invocation will wait until a handler is assigned. - */ -export declare class LazyBinder any> implements BinderHandler, ReturnType>, InvokableHandler, Promise>>> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - private _name; - private _callbackPromise; - private _callback; - /** - * Creates a new LazyBinder instance. - * @param name The name of the handler. - * @param initialCallback An optional initial callback function to assign. - */ - constructor(name: string, initialCallback?: T); - assign(callback: T, override?: boolean): () => void; - /** - * Invokes the assigned handler function with the provided arguments. - * @param args The arguments to pass to the handler function. - * @returns The result of the handler function. - */ - invoke(...args: Parameters): Promise>>; -} -/** - * A multi-binder that allows adding and removing multiple handler functions. - */ -export declare class MultiBinder any> implements MultiRegisterHandler, ReturnType> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - protected _name: string; - /** - * Creates a new MultiBinder instance. - * @param name The name of the handler. - */ - constructor(name: string); - protected _callbackMap: Map; - protected _isCallbackDirty: boolean; - protected _maxUsedPriority: number; - /** - * Adds a handler function. - * Note: The same function is only added once. - * @param callback The handler function to add. - * @param priority The priority of the handler, Do not use floating numbers to prevent confusion. - * @returns A function to unregister the added handler. - * - */ - addHandler(callback: T, priority?: number, allowSwap?: boolean): UnregisterFunction; - /** - * Removes a handler function. - * @param callback The handler function to remove. - */ - removeHandler(callback: T): void; - /** - * Adds a handler function (alias of addHandler, but more semantic). - * @param callback - * @returns - */ - use(callback: T, priority?: number): UnregisterFunction; - _sortedCallbacks: T[]; - protected get _callbacks(): T[]; -} -/** - * A dispatcher that invokes all added handler functions sequentially and collects their results. - * */ -export declare class Dispatch extends MultiBinder> implements DispatcherHandler { - /** - * Dispatches the event to all registered handlers sequentially. - * @param args The arguments to pass to the handlers. - * @returns An array of results or errors from each handler. - */ - dispatch(...args: T): Promise<(Awaited | Error)[]>; -} -/** - * A dispatcher that invokes all added handler functions in parallel and collects their results. - */ -export declare class DispatchParallel extends MultiBinder> implements DispatcherHandler { - /** - * Dispatches the event to all registered handlers in parallel. - * @param args The arguments to pass to the handlers. - * @returns An array of results or errors from each handler. - */ - dispatch(...args: T): Promise<(Awaited | Error)[]>; -} -/** - * A base class for boolean handlers that can add and remove handler functions. - */ -export declare abstract class BooleanHandlerBase extends MultiBinder> implements InvokableBooleanHandler { - abstract invoke(...args: T): Promise; -} -/** - * A handler that invokes all added handler functions sequentially until one returns false. - */ -export declare class AllHandler extends BooleanHandlerBase { - /** - * Invoke all handlers sequentially until one returns false. - * @param args The arguments to pass to the handlers. - * @returns A Promise that resolves to true if all handlers return true, otherwise false. - */ - invoke(...args: T): Promise; -} -/** - * A handler that invokes all added handler functions in parallel and returns true only if all return true. - */ -export declare class ParallelAllHandler extends BooleanHandlerBase { - /** - * Invoke all handlers in parallel - * @param args The arguments to pass to the handlers. - * @returns True if all handlers return true, otherwise false. - */ - invoke(...args: T): Promise; -} -/** - * A handler that invokes all added handler functions sequentially until one returns true. - */ -export declare class AnySuccessHandler extends BooleanHandlerBase { - /** - * Invokes handlers sequentially until one returns true. - * @param args The arguments to pass to the handlers. - * @returns True if any handler returns true, otherwise false. - */ - invoke(...args: T): Promise; -} -/** - * A handler that invokes all added handler functions sequentially until one returns a non-falsy value. - */ -export declare class FirstResultHandler extends MultiBinder> { - /** - * Invokes handlers sequentially until one returns a non-falsy value. - * @param args The arguments to pass to the handlers. - * @returns The first non-falsy result from the handlers, or false if none found. - */ - invoke(...args: T): Promise; -} -/** - * A function type that can be used as a handler with assignable functionality. - */ -export interface HandlerFunction U | Promise, U = unknown> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Invokes the handler function with the provided arguments. - */ - (...args: Parameters): ReturnType; - /** - * Assigns a new handler function. - * @param callback The new handler function to assign. - * @param override Whether to override the existing handler if one is already assigned. - * @returns A function to unregister the assigned handler. - */ - setHandler: (callback: TFunc, override?: boolean) => void; -} -/** - * A function type that can be used as a handler with assignable functionality. - */ -export interface LazyHandlerFunction U | Promise, U = unknown> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Invokes the handler function with the provided arguments. - */ - (...args: Parameters): Promise>>; - /** - * Assigns a new handler function. - * @param callback The new handler function to assign. - * @param override Whether to override the existing handler if one is already assigned. - * @returns A function to unregister the assigned handler. - */ - setHandler: (callback: TFunc, override?: boolean) => void; -} -/** - * A function type that can be used as a multiple handler with add/remove functionality. - */ -export interface MultipleHandlerFunction U | Promise, U = unknown> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Invokes the handler function with the provided arguments. - */ - (...args: Parameters): ReturnType; - /** - * Adds a handler function. - * @param callback The handler function to add. - * @returns A function to remove the added handler. - */ - addHandler: (callback: TFunc) => () => void; - /** - * Removes a handler function. - * @param callback The handler function to remove. - * @returns - */ - removeHandler: (callback: TFunc) => void; -} -/** - * A function type that can be used as a value-collecting handler with add/remove functionality. - */ -export type CollectorFunction U | Promise, U = unknown> = (...args: Parameters) => Promise>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -/** - * A Handler function type that can have multiple handlers added or removed, and collects their results into an array. - */ -export interface CollectiveHandlerFunction U[] | Promise, U = unknown> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Invokes the handler function with the provided arguments. - */ - (...args: Parameters): ReturnType; - /** - * Adds a handler function. - * @param callback The handler function to add. - * @returns A function to remove the added handler. - */ - addHandler: (callback: CollectorFunction) => () => void; - /** - * Removes a handler function. - * @param callback The handler function to remove. - * @returns - */ - removeHandler: (callback: CollectorFunction) => void; -} -export interface BooleanMultipleHandlerFunction boolean | Promise> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Invokes the handler function with the provided arguments. - */ - (...args: Parameters): ReturnType; - /** - * Adds a handler function. - * @param callback The handler function to add. - * @returns A function to remove the added handler. - */ - addHandler: (callback: TFunc, priority?: number) => () => void; - /** - * Removes a handler function. - * @param callback The handler function to remove. - * @returns - */ - removeHandler: (callback: TFunc) => void; -} -export interface MultiBinderInstance extends InvokableHandler, MultiRegisterHandler { // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface -} -export interface BooleanMultiBinderInstance extends InvokableBooleanHandler, MultiRegisterHandler { // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface -} -export declare function allFunction Promise>(name?: string): BooleanMultipleHandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export declare function bailFirstFailureFunction Promise>(name?: string): BooleanMultipleHandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export declare function allParallelFunction Promise>(name?: string): BooleanMultipleHandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export declare function anySuccessFunction Promise>(name?: string): BooleanMultipleHandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export declare function firstResultFunction Promise>(name?: string): MultipleHandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export declare function dispatchParallelFunction Promise>(name?: string): CollectiveHandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export declare function bindableFunction any>(name?: string): HandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export declare function lazyBindableFunction any>(name?: string): LazyHandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -type FunctionKeys = Extract<{ - [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -}[keyof T], string>; -export declare function handlers(): { - /** - * Create a handler that invokes all added handler functions sequentially until one returns false. - * @param name - * @returns - */ - all>(name: K): BooleanMultipleHandlerFunction Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Create a handler that invokes all added handler functions in parallel and returns true only if all return true. - * @param name - * @returns - */ - allParallel>(name: K): BooleanMultipleHandlerFunction Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Create a handler that invokes all added handler functions sequentially until one returns false. - * @param name - * @returns - */ - bailFirstFailure>(name: K): BooleanMultipleHandlerFunction Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Create a handler that invokes all added handler functions sequentially until one returns true. - * @param name - * @returns - */ - anySuccess>(name: K): BooleanMultipleHandlerFunction Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Create a handler that invokes all added handler functions sequentially until one returns a non-falsy value. - * @param name - * @returns - */ - firstResult>(name: K): MultipleHandlerFunction Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Create a handler that invokes all added handler functions in parallel. - * @param name - * @returns - */ - dispatchParallel>(name: K): CollectiveHandlerFunction Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Create a binder handler that can assign a single handler function. - * @param name - * @returns - */ - binder>(name: K): HandlerFunction any>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - lazyBinder>(name: K): LazyHandlerFunction any>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -}; -export {}; diff --git a/_types/src/lib/src/services/lib/logUtils.d.ts b/_types/src/lib/src/services/lib/logUtils.d.ts deleted file mode 100644 index 1e8a40f0..00000000 --- a/_types/src/lib/src/services/lib/logUtils.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type LOG_LEVEL } from "octagonal-wheels/common/logger"; -import type { IAPIService } from "@lib/services/base/IService"; -export declare const MARK_LOG_SEPARATOR = "\u200A"; -export declare const MARK_LOG_NETWORK_ERROR = "\u200B"; -/** - * Creates a log function that prefixes messages with the service name and uses the provided APIService's addLog method if available. - * If APIService is not provided, it falls back to using the global Logger function. - * @param serviceName The name of the service to prefix log messages with. - * @param APIService An optional APIService instance to use for logging. - * @returns A log function that can be used to log messages with the specified service name and APIService. - */ -export declare function createInstanceLogFunction(serviceName: string, APIService?: IAPIService): (msg: unknown, level?: LOG_LEVEL, key?: string) => void; -export type LogFunction = ReturnType; diff --git a/_types/src/lib/src/services/lib/remoteActivity.d.ts b/_types/src/lib/src/services/lib/remoteActivity.d.ts deleted file mode 100644 index d6c69410..00000000 --- a/_types/src/lib/src/services/lib/remoteActivity.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive"; -export type TrackedPhysicalRequestCounters = { - requestCount: ReactiveSource; - responseCount: ReactiveSource; -}; -/** - * Tracks one physical-request unit for status reporting. - * - * A unit may be an exact transport attempt or a higher-level SDK command. The - * resulting in-flight count is deliberately approximate and must not be used - * for protocol correctness, throttling, or completion decisions. - */ -export declare function runWithTrackedPhysicalRequest(counters: TrackedPhysicalRequestCounters, task: () => T | PromiseLike): Promise; diff --git a/_types/src/lib/src/string_and_binary/chunks.d.ts b/_types/src/lib/src/string_and_binary/chunks.d.ts deleted file mode 100644 index 6e9dcc3f..00000000 --- a/_types/src/lib/src/string_and_binary/chunks.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function splitPiecesTextV2(dataSrc: string | string[], pieceSize: number, minimumChunkSize: number): () => Generator; -export declare function binaryTextSplit(data: string, pieceSize: number, minimumChunkSize: number): () => Generator; -export declare function splitPiecesText(dataSrc: string | string[], pieceSize: number, plainSplit: boolean, minimumChunkSize: number, useSegmenter: boolean): () => Generator; -export declare function splitPiecesTextV1(dataSrc: string | string[], pieceSize: number, plainSplit: boolean, minimumChunkSize: number): () => Generator; -export declare function collectGenAll(strGen: AsyncGenerator | Generator): Promise; -export declare function concatGeneratedAll(strGen: AsyncGenerator | Generator): Promise; -export declare function splitPieces2V2(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): Promise<(() => Generator) | (() => AsyncGenerator)>; -export declare function splitPieces2(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): Promise<(() => Generator) | (() => AsyncGenerator)>; -export declare function splitPiecesRabinKarp(dataSrc: Blob, absoluteMaxPieceSize: number, doPlainSplit: boolean, minimumChunkSize: number, _filename?: string, _useSegmenter?: boolean): Promise<() => AsyncGenerator>; -export declare function splitPiecesRabinKarpOld(dataSrc: Blob, absoluteMaxPieceSize: number, doPlainSplit: boolean, minimumChunkSize: number, _filename?: string, _useSegmenter?: boolean): Promise<() => AsyncGenerator>; diff --git a/_types/src/lib/src/string_and_binary/convert.d.ts b/_types/src/lib/src/string_and_binary/convert.d.ts deleted file mode 100644 index 34f8b9be..00000000 --- a/_types/src/lib/src/string_and_binary/convert.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { arrayBufferToBase64, base64ToArrayBuffer, base64ToArrayBufferInternalBrowser, readString, writeString, tryConvertBase64ToArrayBuffer } from "octagonal-wheels/binary"; -export { arrayBufferToBase64, base64ToArrayBuffer, base64ToArrayBufferInternalBrowser, readString, writeString, tryConvertBase64ToArrayBuffer, }; -export declare function arrayBufferToBase64Single(buffer: Uint8Array | ArrayBuffer): Promise; -export { uint8ArrayToHexString, hexStringToUint8Array } from "octagonal-wheels/binary/hex"; -export { encodeBinaryEach, decodeToArrayBuffer } from "octagonal-wheels/binary/encodedUTF16"; -export { decodeBinary, encodeBinary } from "octagonal-wheels/binary"; -export { escapeStringToHTML } from "octagonal-wheels/string"; -export declare function versionNumberString2Number(version: string): number; diff --git a/_types/src/lib/src/string_and_binary/hash.d.ts b/_types/src/lib/src/string_and_binary/hash.d.ts deleted file mode 100644 index 1fd9da95..00000000 --- a/_types/src/lib/src/string_and_binary/hash.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export * from "octagonal-wheels/hash/xxhash.js"; -export type * from "octagonal-wheels/hash/xxhash.js"; diff --git a/_types/src/lib/src/string_and_binary/path.d.ts b/_types/src/lib/src/string_and_binary/path.d.ts deleted file mode 100644 index fc15a7a9..00000000 --- a/_types/src/lib/src/string_and_binary/path.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "@lib/common/types.ts"; -export declare function isValidFilenameInWidows(filename: string): boolean; -export declare function isValidFilenameInDarwin(filename: string): boolean; -export declare function isValidFilenameInLinux(filename: string): boolean; -export declare function isValidFilenameInAndroid(filename: string): boolean; -export declare function isFilePath(path: FilePath | FilePathWithPrefix): path is FilePath; -export declare function stripAllPrefixes(prefixedPath: FilePathWithPrefix): FilePath; -export declare function addPrefix(path: FilePath | FilePathWithPrefix, prefix: string): FilePathWithPrefix; -export declare function expandFilePathPrefix(path: FilePathWithPrefix | FilePath): [string, FilePathWithPrefix]; -export declare function expandDocumentIDPrefix(id: DocumentID): [string, FilePathWithPrefix]; -export declare function path2id_base(filenameSrc: FilePathWithPrefix | FilePath, obfuscatePassphrase: string | false, caseInsensitive: boolean): Promise; -export declare function id2path_base(id: DocumentID, entry?: EntryHasPath): FilePathWithPrefix; -export declare function getPath(entry: AnyEntry): FilePathWithPrefix; -export declare function getPathWithoutPrefix(entry: AnyEntry): FilePath; -export declare function stripPrefix(prefixedPath: FilePathWithPrefix): FilePath; -export declare function shouldBeIgnored(filename: string): boolean; -export declare function isPlainText(filename: string): boolean; -export declare function shouldSplitAsPlainText(filename: string): boolean; -/** - * returns whether the given path is accepted (not ignored) by the `.gitignore`. - * @param path path of the file which is relative from `.gitignore` file - * @param ignore lines of `.gitignore` - * @returns true when accepted. - * false when not accepted. - * undefined when the path is not mentioned in the `.gitignore` file. - */ -export declare function isAccepted(path: string, ignore: string[]): boolean | undefined; -/** - * Checks whether the path is accepted by all ignored files. - * @param path path of target file - * @param ignoreFiles list of ignore files. i.e. [".gitignore", ".dockerignore"] - * @param getList function to retrieve the file. - * @returns true when accepted. false when should be ignored. - */ -export declare function isAcceptedAll(path: string, ignoreFiles: string[], getList: (path: string) => Promise): Promise; diff --git a/_types/src/lib/src/worker/bg.common.d.ts b/_types/src/lib/src/worker/bg.common.d.ts deleted file mode 100644 index f2aa078d..00000000 --- a/_types/src/lib/src/worker/bg.common.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { END_OF_DATA } from "./universalTypes.ts"; -export declare function postBack(key: number, seq: number, data: string | END_OF_DATA): void; diff --git a/_types/src/lib/src/worker/bg.worker.d.ts b/_types/src/lib/src/worker/bg.worker.d.ts deleted file mode 100644 index 467855c2..00000000 --- a/_types/src/lib/src/worker/bg.worker.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export {}; diff --git a/_types/src/lib/src/worker/bg.worker.encryption.d.ts b/_types/src/lib/src/worker/bg.worker.encryption.d.ts deleted file mode 100644 index ce827c47..00000000 --- a/_types/src/lib/src/worker/bg.worker.encryption.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EncryptHKDFArguments } from "./universalTypes.ts"; -import type { EncryptArguments } from "./universalTypes.ts"; -/** - * Processes the encryption of data. - * @param data The data to be encrypted or decrypted. - */ -export declare function processEncryption(data: EncryptArguments | EncryptHKDFArguments): Promise; diff --git a/_types/src/lib/src/worker/bg.worker.splitting.d.ts b/_types/src/lib/src/worker/bg.worker.splitting.d.ts deleted file mode 100644 index be153611..00000000 --- a/_types/src/lib/src/worker/bg.worker.splitting.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { SplitArguments } from "./universalTypes.ts"; -/** - * Processes the splitting of data into chunks. - * @param data The data to be split. - */ -export declare function processSplit(data: SplitArguments): Promise; diff --git a/_types/src/lib/src/worker/bgWorker.d.ts b/_types/src/lib/src/worker/bgWorker.d.ts deleted file mode 100644 index 26c02125..00000000 --- a/_types/src/lib/src/worker/bgWorker.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EncryptArguments, EncryptHKDFArguments, EncryptHKDFProcessItem, EncryptProcessItem, ProcessItem, SplitArguments, SplitProcessItem } from "./universalTypes.ts"; -export type WorkerInstance = { - worker: Worker; - processing: number; - /** Keys of tasks currently dispatched to this worker instance. */ - taskKeys: Set; -}; -export declare function splitPieces2Worker(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): () => AsyncGenerator; -export declare function splitPieces2WorkerV2(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): () => AsyncGenerator; -export declare function splitPieces2WorkerRabinKarp(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): () => AsyncGenerator; -export declare function encryptWorker(input: string, passphrase: string, autoCalculateIterations: boolean): Promise; -export declare function decryptWorker(input: string, passphrase: string, autoCalculateIterations: boolean): Promise; -export declare function encryptHKDFWorker(input: string, passphrase: string, pbkdf2Salt: Uint8Array): Promise; -export declare function decryptHKDFWorker(input: string, passphrase: string, pbkdf2Salt: Uint8Array): Promise; -export declare const tasks: Map; -/** - * Remove a completed (or aborted) task from both the tasks map and its worker's taskKeys set. - */ -export declare function removeTask(key: number): void; -export declare function initialiseWorkerModule(): void; -export declare function startWorker(data: Omit): EncryptHKDFProcessItem; -export declare function startWorker(data: Omit): EncryptProcessItem; -export declare function startWorker(data: Omit): SplitProcessItem; -export declare function terminateWorker(): void; diff --git a/_types/src/lib/src/worker/bgWorker.encryption.d.ts b/_types/src/lib/src/worker/bgWorker.encryption.d.ts deleted file mode 100644 index 30afe2b1..00000000 --- a/_types/src/lib/src/worker/bgWorker.encryption.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EncryptHKDFProcessItem, type ResultPayload } from "./universalTypes.ts"; -import { type EncryptProcessItem } from "./universalTypes.ts"; -import { type EncryptHKDFArguments } from "./universalTypes.ts"; -import { type EncryptArguments } from "./universalTypes.ts"; -/** - * Offloads encryption to a web worker. - * @param data The data to be encrypted. - * @returns A promise that resolves with the encryption result. - */ -export declare function encryptionOnWorker(data: Omit): Promise; -/** - * Offloads HKDF encryption to a web worker. - * @param data The data to be encrypted. - * @returns A promise that resolves with the encryption result. - */ -export declare function encryptionHKDFOnWorker(data: Omit): Promise; -/** - * Handles the encryption callbacks - * @param process The process item associated with the task. - * @param data The data to be processed. - */ -export declare function handleTaskEncrypt(process: EncryptProcessItem | EncryptHKDFProcessItem, data: ResultPayload): void; diff --git a/_types/src/lib/src/worker/bgWorker.mock.d.ts b/_types/src/lib/src/worker/bgWorker.mock.d.ts deleted file mode 100644 index 9aa57050..00000000 --- a/_types/src/lib/src/worker/bgWorker.mock.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EncryptHKDFProcessItem, EncryptProcessItem, SplitProcessItem, ProcessItem } from "./universalTypes.ts"; -export type SplitArguments = { - key: number; - type: "split"; - dataSrc: Blob; - pieceSize: number; - plainSplit: boolean; - minimumChunkSize: number; - filename?: string; - useV2: boolean; - useSegmenter: boolean; -}; -export type EncryptArguments = { - key: number; - type: "encrypt" | "decrypt"; - input: string; - passphrase: string; - autoCalculateIterations: boolean; -}; -export type EncryptHKDFArguments = { - key: number; - type: "encryptHKDF" | "decryptHKDF"; - input: string; - passphrase: string; - pbkdf2Salt: Uint8Array; -}; -export declare function terminateWorker(): void; -export declare function splitPieces2Worker(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): Promise<(() => Generator) | (() => AsyncGenerator)>; -export declare function splitPieces2WorkerV2(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): Promise<(() => Generator) | (() => AsyncGenerator)>; -export declare function splitPieces2WorkerRabinKarp(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): () => AsyncGenerator; -export declare function encryptWorker(input: string, passphrase: string, autoCalculateIterations: boolean): Promise; -export declare function decryptWorker(input: string, passphrase: string, autoCalculateIterations: boolean): Promise; -export declare function encryptHKDFWorker(input: string, passphrase: string, pbkdf2Salt: Uint8Array): Promise; -export declare function decryptHKDFWorker(input: string, passphrase: string, pbkdf2Salt: Uint8Array): Promise; -export declare function startWorker(data: Omit): EncryptHKDFProcessItem; -export declare function startWorker(data: Omit): EncryptProcessItem; -export declare function startWorker(data: Omit): SplitProcessItem; -export declare const tasks: Map; -export declare function initialiseWorkerModule(): void; diff --git a/_types/src/lib/src/worker/bgWorker.splitting.d.ts b/_types/src/lib/src/worker/bgWorker.splitting.d.ts deleted file mode 100644 index b519756c..00000000 --- a/_types/src/lib/src/worker/bgWorker.splitting.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ResultPayloadWithSeq, type SplitProcessItem } from "./universalTypes"; -/** - * Splits data into pieces using a worker. - * @param dataSrc The source data to be split. - * @param pieceSize The size of each piece. - * @param plainSplit Whether to use plain splitting. - * @param minimumChunkSize The minimum size of each chunk. - * @param filename The name of the file being processed. - * @param splitVersion The version of the splitting algorithm to use. - * @param useSegmenter Whether to use a segmenter (only works on splitVersion:2) - * @returns A generator that yields the split pieces. - */ -export declare function _splitPieces2Worker(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename: string | undefined, splitVersion: 1 | 2 | 3, useSegmenter: boolean): () => AsyncGenerator; -/** - * Aborts all in-flight split tasks identified by the given keys. - * Called when the background worker that owned these tasks has crashed, so the streams - * will never receive any more data and must be torn down to unblock callers. - * @param keys The task keys to abort. - * @param error The error to report to each stream. - */ -export declare function abortSplitTasks(keys: number[], error: Error): void; -/** - * Handles the splitting callback from the worker. - * @param process the splitting process item - * @param data the data received from the worker - */ -export declare function handleTaskSplit(process: SplitProcessItem, data: ResultPayloadWithSeq): void; diff --git a/_types/src/lib/src/worker/universalTypes.d.ts b/_types/src/lib/src/worker/universalTypes.d.ts deleted file mode 100644 index 6421b5a6..00000000 --- a/_types/src/lib/src/worker/universalTypes.d.ts +++ /dev/null @@ -1,65 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { PromiseWithResolvers } from "octagonal-wheels/promises"; -export type EncryptArguments = { - key: number; - type: "encrypt" | "decrypt"; - input: string; - passphrase: string; - autoCalculateIterations: boolean; -}; -export type EncryptHKDFArguments = { - key: number; - type: "encryptHKDF" | "decryptHKDF"; - input: string; - passphrase: string; - pbkdf2Salt: Uint8Array; -}; -export type SplitArguments = { - key: number; - type: "split"; - dataSrc: Blob; - pieceSize: number; - plainSplit: boolean; - minimumChunkSize: number; - filename?: string; - useSegmenter: boolean; - splitVersion: 1 | 2 | 3; -}; -export type SplitProcessItem = { - key: number; - type: SplitArguments["type"]; - finalize: () => void; -}; -export type EncryptProcessItem = { - key: number; - task: PromiseWithResolvers; - type: EncryptArguments["type"]; - finalize: () => void; -}; -export type EncryptHKDFProcessItem = { - key: number; - task: PromiseWithResolvers; - type: EncryptHKDFArguments["type"]; - finalize: () => void; -}; -export type ProcessItem = SplitProcessItem | EncryptProcessItem | EncryptHKDFProcessItem; -export declare const END_OF_DATA: null; -export type END_OF_DATA = typeof END_OF_DATA; -export type ResultPayloadBase = { - key: number; -}; -export type ResultPayloadWithResult = ResultPayloadBase & { - result: string; -}; -export type ResultPayloadWithError = ResultPayloadBase & { - error: unknown; -}; -export type ResultPayload = ResultPayloadWithResult | ResultPayloadWithError; -export type ResultPayloadWithSeqBase = ResultPayload & { - seq: number; -}; -export type ResultPayloadPiece = ResultPayloadWithSeqBase & { - result: string | END_OF_DATA; -}; -export type ResultPayloadWithSeq = ResultPayloadPiece | ResultPayloadWithError; diff --git a/_types/src/main.d.ts b/_types/src/main.d.ts deleted file mode 100644 index 0f2836f3..00000000 --- a/_types/src/main.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { Plugin, type App, type PluginManifest } from "./deps"; -import { LiveSyncCommands } from "./features/LiveSyncCommands.ts"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext.ts"; -import { LiveSyncBaseCore } from "./LiveSyncBaseCore.ts"; -export type LiveSyncCore = LiveSyncBaseCore; -export default class ObsidianLiveSyncPlugin extends Plugin { - core: LiveSyncCore; - /** - * Initialise service modules. - */ - private initialiseServiceModules; - /** - * @obsolete Use services.setting.saveSettingData instead. Save the settings to the disk. This is usually called after changing the settings in the code, to persist the changes. - */ - saveSettings(): Promise; - constructor(app: App, manifest: PluginManifest); - private _startUp; - onload(): void; - onunload(): undefined; -} diff --git a/_types/src/managers/ObsidianStorageEventManagerAdapter.d.ts b/_types/src/managers/ObsidianStorageEventManagerAdapter.d.ts deleted file mode 100644 index 1a81fb03..00000000 --- a/_types/src/managers/ObsidianStorageEventManagerAdapter.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { TFile, TFolder } from "@/deps"; -import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/types"; -import type { FileEventItem } from "@lib/common/types"; -import type { IStorageEventManagerAdapter } from "@lib/managers/adapters"; -import type { IStorageEventTypeGuardAdapter, IStorageEventPersistenceAdapter, IStorageEventWatchAdapter, IStorageEventStatusAdapter, IStorageEventConverterAdapter, IStorageEventWatchHandlers } from "@lib/managers/adapters"; -import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager"; -import type ObsidianLiveSyncPlugin from "@/main"; -import type { LiveSyncCore } from "@/main"; -import type { FileProcessingService } from "@lib/services/base/FileProcessingService"; -/** - * Obsidian-specific type guard adapter - */ -declare class ObsidianTypeGuardAdapter implements IStorageEventTypeGuardAdapter { - isFile(file: unknown): file is TFile; - isFolder(item: unknown): item is TFolder; -} -/** - * Obsidian-specific persistence adapter - */ -declare class ObsidianPersistenceAdapter implements IStorageEventPersistenceAdapter { - private core; - constructor(core: LiveSyncCore); - saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]): Promise; - loadSnapshot(): Promise<(FileEventItem | FileEventItemSentinel)[] | null>; -} -/** - * Obsidian-specific status adapter - */ -declare class ObsidianStatusAdapter implements IStorageEventStatusAdapter { - private fileProcessing; - constructor(fileProcessing: FileProcessingService); - updateStatus(status: { - batched: number; - processing: number; - totalQueued: number; - }): void; -} -/** - * Obsidian-specific converter adapter - */ -declare class ObsidianConverterAdapter implements IStorageEventConverterAdapter { - toFileInfo(file: TFile, deleted?: boolean): UXFileInfoStub; - toInternalFileInfo(path: FilePath): UXInternalFileInfoStub; -} -/** - * Obsidian-specific watch adapter - */ -declare class ObsidianWatchAdapter implements IStorageEventWatchAdapter { - private plugin; - constructor(plugin: ObsidianLiveSyncPlugin); - beginWatch(handlers: IStorageEventWatchHandlers): Promise; -} -/** - * Composite adapter for Obsidian StorageEventManager - */ -export declare class ObsidianStorageEventManagerAdapter implements IStorageEventManagerAdapter { - readonly typeGuard: ObsidianTypeGuardAdapter; - readonly persistence: ObsidianPersistenceAdapter; - readonly watch: ObsidianWatchAdapter; - readonly status: ObsidianStatusAdapter; - readonly converter: ObsidianConverterAdapter; - constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, fileProcessing: FileProcessingService); -} -export {}; diff --git a/_types/src/managers/StorageEventManagerObsidian.d.ts b/_types/src/managers/StorageEventManagerObsidian.d.ts deleted file mode 100644 index 6b0a6b18..00000000 --- a/_types/src/managers/StorageEventManagerObsidian.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath } from "@lib/common/types"; -import type ObsidianLiveSyncPlugin from "@/main"; -import type { LiveSyncCore } from "@/main"; -import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@lib/managers/StorageEventManager"; -import { ObsidianStorageEventManagerAdapter } from "./ObsidianStorageEventManagerAdapter"; -export declare class StorageEventManagerObsidian extends StorageEventManagerBase { - core: LiveSyncCore; - constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, dependencies: StorageEventManagerBaseDependencies); - /** - * Override _watchVaultRawEvents to add Obsidian-specific logic - */ - protected _watchVaultRawEvents(path: FilePath): Promise; -} diff --git a/_types/src/modules/AbstractModule.d.ts b/_types/src/modules/AbstractModule.d.ts deleted file mode 100644 index 7243de91..00000000 --- a/_types/src/modules/AbstractModule.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { AnyEntry, FilePathWithPrefix } from "@lib/common/types"; -import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare abstract class AbstractModule = LiveSyncBaseCore> { - core: T; - _log: (msg: unknown, level?: import("octagonal-wheels/common/logger").LOG_LEVEL, key?: string) => void; - get services(): import("../lib/src/services/InjectableServices").InjectableServiceHub; - addCommand: (command: TCommand) => TCommand; - registerView: (type: string, factory: (leaf: T_1) => unknown) => void; - addRibbonIcon: (icon: string, title: string, callback: (evt: MouseEvent) => unknown) => HTMLElement; - registerObsidianProtocolHandler: (action: string, handler: (params: Record) => unknown) => void; - get localDatabase(): import("../lib/src/pouchdb/LiveSyncLocalDB").LiveSyncLocalDB; - get settings(): import("@lib/common/types").ObsidianLiveSyncSettings; - set settings(value: import("@lib/common/types").ObsidianLiveSyncSettings); - getPath(entry: AnyEntry): FilePathWithPrefix; - getPathWithoutPrefix(entry: AnyEntry): FilePathWithPrefix; - onBindFunction(core: T, services: typeof core.services): void; - constructor(core: T); - saveSettings: () => Promise; - addTestResult(key: string, value: boolean, summary?: string, message?: string): void; - testDone(result?: boolean): Promise; - testFail(message: string): Promise; - isMainReady(): boolean; - isMainSuspended(): boolean; - isDatabaseReady(): boolean; -} diff --git a/_types/src/modules/AbstractObsidianModule.d.ts b/_types/src/modules/AbstractObsidianModule.d.ts deleted file mode 100644 index 7ff21c6a..00000000 --- a/_types/src/modules/AbstractObsidianModule.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LiveSyncCore } from "@/main"; -import type ObsidianLiveSyncPlugin from "@/main"; -import { AbstractModule } from "./AbstractModule.ts"; -export declare abstract class AbstractObsidianModule extends AbstractModule { - plugin: ObsidianLiveSyncPlugin; - get app(): import("obsidian").App; - constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore); - isThisModuleEnabled(): boolean; -} diff --git a/_types/src/modules/core/ModulePeriodicProcess.d.ts b/_types/src/modules/core/ModulePeriodicProcess.d.ts deleted file mode 100644 index 29ae3d91..00000000 --- a/_types/src/modules/core/ModulePeriodicProcess.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { PeriodicProcessor } from "@/common/PeriodicProcessor"; -import type { LiveSyncCore } from "@/main"; -import { AbstractModule } from "@/modules/AbstractModule"; -export declare class ModulePeriodicProcess extends AbstractModule { - periodicSyncProcessor: PeriodicProcessor; - disablePeriodic(): Promise; - resumePeriodic(): Promise; - private _allOnUnload; - private _everyBeforeRealizeSetting; - private _everyBeforeSuspendProcess; - private _everyAfterResumeProcess; - private _everyAfterRealizeSetting; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/core/ModuleReplicator.d.ts b/_types/src/modules/core/ModuleReplicator.d.ts deleted file mode 100644 index aad4e833..00000000 --- a/_types/src/modules/core/ModuleReplicator.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AbstractModule } from "@/modules/AbstractModule"; -import { type EntryDoc, type RemoteType } from "@lib/common/types"; -import type { LiveSyncCore } from "@/main"; -import { ReplicateResultProcessor } from "./ReplicateResultProcessor"; -export declare class ModuleReplicator extends AbstractModule { - _replicatorType?: RemoteType; - processor: ReplicateResultProcessor; - private _unresolvedErrorManager; - clearErrors(): void; - private _everyOnloadAfterLoadSettings; - _onReplicatorInitialised(): Promise; - _everyOnDatabaseInitialized(showNotice: boolean): Promise; - _everyBeforeReplicate(showMessage: boolean): Promise; - /** - * obsolete method. No longer maintained and will be removed in the future. - * @deprecated v0.24.17 - * @param showMessage If true, show message to the user. - */ - cleaned(showMessage: boolean): Promise; - private onReplicationFailed; - _parseReplicationResult(docs: Array>): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/core/ModuleReplicatorCouchDB.d.ts b/_types/src/modules/core/ModuleReplicatorCouchDB.d.ts deleted file mode 100644 index 285810f4..00000000 --- a/_types/src/modules/core/ModuleReplicatorCouchDB.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type RemoteDBSettings } from "@lib/common/types"; -import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator"; -import { AbstractModule } from "@/modules/AbstractModule"; -import type { LiveSyncCore } from "@/main"; -export declare class ModuleReplicatorCouchDB extends AbstractModule { - _anyNewReplicator(settingOverride?: Partial): Promise; - _everyAfterResumeProcess(): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/core/ModuleReplicatorMinIO.d.ts b/_types/src/modules/core/ModuleReplicatorMinIO.d.ts deleted file mode 100644 index 46012eba..00000000 --- a/_types/src/modules/core/ModuleReplicatorMinIO.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type RemoteDBSettings } from "@lib/common/types"; -import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator"; -import type { LiveSyncCore } from "@/main"; -import { AbstractModule } from "@/modules/AbstractModule"; -export declare class ModuleReplicatorMinIO extends AbstractModule { - _anyNewReplicator(settingOverride?: Partial): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/core/ReplicateResultProcessor.d.ts b/_types/src/modules/core/ReplicateResultProcessor.d.ts deleted file mode 100644 index e4592b9e..00000000 --- a/_types/src/modules/core/ReplicateResultProcessor.d.ts +++ /dev/null @@ -1,116 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type AnyEntry, type EntryDoc, type LoadedEntry, type MetaEntry } from "@lib/common/types"; -import type { ModuleReplicator } from "./ModuleReplicator"; -import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2"; -import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore"; -export declare class ReplicateResultProcessor { - private log; - private logError; - private replicator; - constructor(replicator: ModuleReplicator); - get localDatabase(): import("../../lib/src/pouchdb/LiveSyncLocalDB").LiveSyncLocalDB; - get services(): import("../../lib/src/services/InjectableServices").InjectableServiceHub; - get core(): LiveSyncBaseCore; - getPath(entry: AnyEntry): string; - suspend(): void; - resume(): void; - private _suspended; - get isSuspended(): boolean; - /** - * Take a snapshot of the current processing state. - * This snapshot is stored in the KV database for recovery on restart. - */ - protected _takeSnapshot(): Promise; - /** - * Trigger taking a snapshot. - */ - protected _triggerTakeSnapshot(): void; - /** - * Throttled version of triggerTakeSnapshot. - */ - protected triggerTakeSnapshot: import("octagonal-wheels/function").ThrottledFunction<() => void>; - /** - * Restore from snapshot. - */ - restoreFromSnapshot(): Promise; - private _restoreFromSnapshot; - /** - * Restore from snapshot only once. - * @returns Promise that resolves when restoration is complete. - */ - restoreFromSnapshotOnce(): Promise; - /** - * Perform the given procedure while counting the concurrency. - * @param proc async procedure to perform - * @param countValue reactive source to count concurrency - * @returns result of the procedure - */ - withCounting(proc: () => Promise, countValue: ReactiveSource): Promise; - /** - * Report the current status. - */ - protected reportStatus(): void; - /** - * Enqueue all the given changes for processing. - * @param changes Changes to enqueue - */ - enqueueAll(changes: PouchDB.Core.ExistingDocument[]): void; - /** - * Process the change if it is not a document change. - * @param change Change to process - * @returns True if the change was processed; false otherwise - */ - protected processIfNonDocumentChange(change: PouchDB.Core.ExistingDocument): boolean; - /** - * Queue of changes to be processed. - */ - private _queuedChanges; - /** - * List of changes being processed. - */ - private _processingChanges; - /** - * Enqueue the given document change for processing. - * @param doc Document change to enqueue - * @returns - */ - protected enqueueChange(doc: PouchDB.Core.ExistingDocument): void; - /** - * Trigger processing of the queued changes. - */ - protected triggerProcessQueue(): void; - /** - * Semaphore to limit concurrent processing. - * This is the per-id semaphore + concurrency-control (max 10 concurrent = 10 documents being processed at the same time). - */ - private _semaphore; - /** - * Flag indicating whether the process queue is currently running. - */ - private _isRunningProcessQueue; - /** - * Process the queued changes. - */ - private runProcessQueue; - /** - * Parse the given document change. - * @param change - * @returns - */ - parseDocumentChange(change: PouchDB.Core.ExistingDocument): Promise; - protected applyToDatabase(doc: PouchDB.Core.ExistingDocument): Promise; - private _applyToDatabase; - /** - * Phase 3: Apply the given entry to storage. - * @param entry - * @returns - */ - protected applyToStorage(entry: MetaEntry): Promise; - /** - * Check whether processing is required for the given document. - * @param dbDoc Document to check - * @returns True if processing is required; false otherwise - */ - protected checkIsChangeRequiredForDatabaseProcessing(dbDoc: LoadedEntry): Promise; -} diff --git a/_types/src/modules/coreFeatures/ModuleConflictChecker.d.ts b/_types/src/modules/coreFeatures/ModuleConflictChecker.d.ts deleted file mode 100644 index ba36ebe7..00000000 --- a/_types/src/modules/coreFeatures/ModuleConflictChecker.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AbstractModule } from "@/modules/AbstractModule.ts"; -import { type FilePathWithPrefix } from "@lib/common/types"; -import { QueueProcessor } from "octagonal-wheels/concurrency/processor"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleConflictChecker extends AbstractModule { - _queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise; - _queueConflictCheck(file: FilePathWithPrefix): Promise; - _waitForAllConflictProcessed(): Promise; - conflictResolveQueue: QueueProcessor; - conflictCheckQueue: QueueProcessor; - onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void; -} diff --git a/_types/src/modules/coreFeatures/ModuleConflictResolver.d.ts b/_types/src/modules/coreFeatures/ModuleConflictResolver.d.ts deleted file mode 100644 index 235e2c01..00000000 --- a/_types/src/modules/coreFeatures/ModuleConflictResolver.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AbstractModule } from "@/modules/AbstractModule.ts"; -import { type diff_check_result, type FilePathWithPrefix } from "@lib/common/types"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts"; -import type { LiveSyncCore } from "@/main.ts"; -declare global { - interface LSEvents { - "conflict-cancelled": FilePathWithPrefix; - } -} -export declare class ModuleConflictResolver extends AbstractModule { - private _resolveConflictByDeletingRev; - checkConflictAndPerformAutoMerge(path: FilePathWithPrefix): Promise; - private _resolveConflict; - private _anyResolveConflictByNewest; - private _resolveAllConflictedFilesByNewerOnes; - onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void; -} diff --git a/_types/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.d.ts b/_types/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.d.ts deleted file mode 100644 index 7f147277..00000000 --- a/_types/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type TweakValues, type ObsidianLiveSyncSettings, type RemoteDBSettings } from "@lib/common/types.ts"; -import { AbstractModule } from "@/modules/AbstractModule.ts"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleResolvingMismatchedTweaks extends AbstractModule { - private _hasNotifiedAutoAcceptCompatibleUndefined; - private _collectMismatchedTweakKeys; - private _selectNewerTweakSide; - private _shouldAutoAcceptCompatibleLossy; - /** - * Hook before saving settings, to check if there are changes in tweak values, and if so, - * update the tweakModified timestamp to current time. - * This allows other devices to know that the tweak values have been changed and decide whether to accept the new values based on the modification time. - * @param next - * @param previous - * @returns - */ - _onBeforeSaveSettingData(next: ObsidianLiveSyncSettings, previous: ObsidianLiveSyncSettings): Promise<{ - tweakModified: number; - } | undefined>; - _anyAfterConnectCheckFailed(): Promise; - _checkAndAskResolvingMismatchedTweaks(preferred: TweakValues): Promise<[TweakValues | boolean, boolean]>; - _askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE">; - _fetchRemotePreferredTweakValues(trialSetting: RemoteDBSettings): Promise; - _checkAndAskUseRemoteConfiguration(trialSetting: RemoteDBSettings): Promise<{ - result: false | TweakValues; - requireFetch: boolean; - }>; - _askUseRemoteConfiguration(trialSetting: RemoteDBSettings, preferred: TweakValues): Promise<{ - result: false | TweakValues; - requireFetch: boolean; - }>; - onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void; -} diff --git a/_types/src/modules/coreObsidian/UILib/dialogs.d.ts b/_types/src/modules/coreObsidian/UILib/dialogs.d.ts deleted file mode 100644 index 59e5fc7e..00000000 --- a/_types/src/modules/coreObsidian/UILib/dialogs.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ButtonComponent } from "@/deps.ts"; -import { App, FuzzySuggestModal, Modal, Plugin, Component } from "@/deps.ts"; -import { type CompatIntervalHandle } from "@lib/common/coreEnvFunctions.ts"; -declare class AutoClosableModal extends Modal { - _closeByUnload(): void; - constructor(app: App); - onClose(): void; -} -export declare class InputStringDialog extends AutoClosableModal { - result: string | false; - onSubmit: (result: string | false) => void; - title: string; - key: string; - placeholder: string; - isManuallyClosed: boolean; - isPassword: boolean; - constructor(app: App, title: string, key: string, placeholder: string, isPassword: boolean, onSubmit: (result: string | false) => void); - onOpen(): void; - onClose(): void; -} -export declare class PopoverSelectString extends FuzzySuggestModal { - _app: App; - callback: ((e: string) => void) | undefined; - getItemsFun: () => string[]; - constructor(app: App, note: string, placeholder: string | undefined, getItemsFun: (() => string[]) | undefined, callback: (e: string) => void); - getItems(): string[]; - getItemText(item: string): string; - onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void; - onClose(): void; -} -export declare class MessageBox extends AutoClosableModal { - plugin: Plugin; - title: string; - contentMd: string; - buttons: T; - result: string | false; - isManuallyClosed: boolean; - defaultAction: string | undefined; - timeout: number | undefined; - timer: CompatIntervalHandle | undefined; - defaultButtonComponent: ButtonComponent | undefined; - wideButton: boolean; - onSubmit: (result: string | false) => void; - component: Component; - constructor(plugin: Plugin, title: string, contentMd: string, buttons: T, defaultAction: T[number], timeout: number | undefined, wideButton: boolean, onSubmit: (result: T[number] | false) => void); - onOpen(): void; - onClose(): void; -} -export declare function confirmWithMessage(plugin: Plugin, title: string, contentMd: string, buttons: T, defaultAction: T[number], timeout?: number): Promise; -export declare function confirmWithMessageWithWideButton(plugin: Plugin, title: string, contentMd: string, buttons: T, defaultAction: T[number], timeout?: number): Promise; -export declare const askYesNo: (app: App, message: string) => Promise<"yes" | "no">; -export declare const askSelectString: (app: App, message: string, items: string[]) => Promise; -export declare const askString: (app: App, title: string, key: string, placeholder: string, isPassword?: boolean) => Promise; -export {}; diff --git a/_types/src/modules/coreObsidian/storageLib/utilObsidian.d.ts b/_types/src/modules/coreObsidian/storageLib/utilObsidian.d.ts deleted file mode 100644 index d072ad83..00000000 --- a/_types/src/modules/coreObsidian/storageLib/utilObsidian.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { TFile, type TAbstractFile, type TFolder } from "@/deps.ts"; -import type { FilePathWithPrefix, UXFileInfo, UXFileInfoStub, UXFolderInfo, UXInternalFileInfoStub } from "@lib/common/types.ts"; -import type { LiveSyncCore } from "@/main.ts"; -import type { FileAccessObsidian } from "@/serviceModules/FileAccessObsidian.ts"; -export declare function TFileToUXFileInfo(core: LiveSyncCore, file: TFile, prefix?: string, deleted?: boolean): Promise; -export declare function InternalFileToUXFileInfo(fullPath: string, vaultAccess: FileAccessObsidian, prefix?: string): Promise; -export declare function TFileToUXFileInfoStub(file: TFile | TAbstractFile, deleted?: boolean): UXFileInfoStub; -export declare function InternalFileToUXFileInfoStub(filename: FilePathWithPrefix, deleted?: boolean): UXInternalFileInfoStub; -export declare function TFolderToUXFileInfoStub(file: TFolder): UXFolderInfo; diff --git a/_types/src/modules/essential/ModuleBasicMenu.d.ts b/_types/src/modules/essential/ModuleBasicMenu.d.ts deleted file mode 100644 index b56b684a..00000000 --- a/_types/src/modules/essential/ModuleBasicMenu.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LiveSyncCore } from "@/main"; -import { AbstractModule } from "@/modules/AbstractModule"; -export declare class ModuleBasicMenu extends AbstractModule { - _everyOnloadStart(): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/essential/ModuleMigration.d.ts b/_types/src/modules/essential/ModuleMigration.d.ts deleted file mode 100644 index 0820ab61..00000000 --- a/_types/src/modules/essential/ModuleMigration.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AbstractModule } from "@/modules/AbstractModule.ts"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleMigration extends AbstractModule { - migrateUsingDoctor(skipRebuild?: boolean, activateReason?: string, forceRescan?: boolean): Promise; - migrateDisableBulkSend(): Promise; - initialMessage(): Promise; - askAgainForSetupURI(): Promise; - hasIncompleteDocs(force?: boolean): Promise; - hasCompromisedChunks(): Promise; - _everyOnFirstInitialize(): Promise; - _everyOnLayoutReady(): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/essentialObsidian/APILib/ObsHttpHandler.d.ts b/_types/src/modules/essentialObsidian/APILib/ObsHttpHandler.d.ts deleted file mode 100644 index 2fed2e5d..00000000 --- a/_types/src/modules/essentialObsidian/APILib/ObsHttpHandler.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { FetchHttpHandler, type FetchHttpHandlerOptions } from "@smithy/fetch-http-handler"; -import { HttpRequest, HttpResponse, type HttpHandlerOptions } from "@smithy/protocol-http"; -/** - * This is close to origin implementation of FetchHttpHandler - * https://github.com/aws/aws-sdk-js-v3/blob/main/packages/fetch-http-handler/src/fetch-http-handler.ts - * that is released under Apache 2 License. - * But this uses Obsidian requestUrl instead. - */ -export declare class ObsHttpHandler extends FetchHttpHandler { - requestTimeoutInMs: number | undefined; - reverseProxyNoSignUrl: string | undefined; - constructor(options?: FetchHttpHandlerOptions, reverseProxyNoSignUrl?: string); - handle(request: HttpRequest, { abortSignal }?: HttpHandlerOptions): Promise<{ - response: HttpResponse; - }>; -} diff --git a/_types/src/modules/essentialObsidian/ModuleObsidianEvents.d.ts b/_types/src/modules/essentialObsidian/ModuleObsidianEvents.d.ts deleted file mode 100644 index d35de4b0..00000000 --- a/_types/src/modules/essentialObsidian/ModuleObsidianEvents.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts"; -import type { TFile } from "@/deps.ts"; -import { type ReactiveSource } from "octagonal-wheels/dataobject/reactive"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleObsidianEvents extends AbstractObsidianModule { - _everyOnloadStart(): Promise; - __performAppReload(): void; - initialCallback: (() => void) | undefined; - swapSaveCommand(): void; - registerWatchEvents(): void; - hasFocus: boolean; - isLastHidden: boolean; - private boundedRemoteActivityEndHandler?; - private deferredBoundedLifecycle?; - private keepReplicationActiveInBackground; - private applyDeferredBoundedActivityLifecycle; - private deferLifecycleUntilBoundedRemoteActivityEnds; - setHasFocus(hasFocus: boolean): void; - watchWindowVisibility(): void; - watchOnline(): void; - watchOnlineAsync(): Promise; - watchWindowVisibilityAsync(): Promise; - watchWorkspaceOpen(file: TFile | null): void; - watchWorkspaceOpenAsync(file: TFile): Promise; - _everyOnLayoutReady(): Promise; - private _askReload; - _totalProcessingCount?: ReactiveSource; - private _scheduleAppReload; - _isReloadingScheduled(): boolean; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/essentialObsidian/ModuleObsidianMenu.d.ts b/_types/src/modules/essentialObsidian/ModuleObsidianMenu.d.ts deleted file mode 100644 index c0a90cc8..00000000 --- a/_types/src/modules/essentialObsidian/ModuleObsidianMenu.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LiveSyncCore } from "@/main.ts"; -import { AbstractModule } from "@/modules/AbstractModule.ts"; -export declare class ModuleObsidianMenu extends AbstractModule { - _everyOnloadStart(): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/features/DocumentHistory/DocumentHistoryModal.d.ts b/_types/src/modules/features/DocumentHistory/DocumentHistoryModal.d.ts deleted file mode 100644 index 782090b9..00000000 --- a/_types/src/modules/features/DocumentHistory/DocumentHistoryModal.d.ts +++ /dev/null @@ -1,71 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { TFile, Modal, App } from "@/deps.ts"; -import ObsidianLiveSyncPlugin from "@/main.ts"; -import { type DocumentID, type FilePathWithPrefix, type LoadedEntry } from "@lib/common/types.ts"; -import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts"; -export declare class DocumentHistoryModal extends Modal { - plugin: ObsidianLiveSyncPlugin; - core: LiveSyncBaseCore; - get services(): import("../../../lib/src/services/InjectableServices").InjectableServiceHub; - range: HTMLInputElement; - contentView: HTMLDivElement; - info: HTMLDivElement; - fileInfo: HTMLDivElement; - showDiff: boolean; - diffOnly: boolean; - id?: DocumentID; - file: FilePathWithPrefix; - revs_info: PouchDB.Core.RevisionInfo[]; - currentDoc?: LoadedEntry; - currentText: string; - currentDeleted: boolean; - initialRev?: string; - currentDiffIndex: number; - diffNavContainer: HTMLDivElement; - diffNavIndicator: HTMLSpanElement; - diffOnlyLabel: HTMLLabelElement; - searchKeyword: string; - searchResults: { - rev: string; - index: number; - matchType: "Content" | "Diff"; - }[]; - currentSearchIndex: number; - searchResultIndicator: HTMLSpanElement; - searchProgressIndicator: HTMLSpanElement; - searchTimeout: number | null; - constructor(app: App, core: LiveSyncBaseCore, plugin: ObsidianLiveSyncPlugin, file: TFile | FilePathWithPrefix, id?: DocumentID, revision?: string); - loadFile(initialRev?: string): Promise; - loadRevs(initialRev?: string): Promise; - BlobURLs: Map; - revokeURL(key: string): void; - generateBlobURL(key: string, data: Uint8Array): string; - prepareContentView(usePreformatted?: boolean): void; - appendTextDiff(diff: [number, string][]): void; - appendSearchHighlightedText(container: HTMLElement, text: string): void; - appendImageDiff(baseSrc: string, overlaySrc?: string): void; - appendDeletedNotice(usePreformatted?: boolean): void; - showExactRev(rev: string): Promise; - /** - * Navigate to the previous or next diff block in the content view. - * Only effective when diff highlighting is enabled. - */ - navigateDiff(direction: "prev" | "next"): void; - /** - * Reset the diff navigation index and update the indicator. - */ - resetDiffNavigation(): void; - /** - * Show or hide the diff navigation buttons based on the showDiff state. - */ - updateDiffNavVisibility(): void; - /** - * Search through the last 100 revisions for the given keyword. - */ - performSearch(keyword: string): Promise; - updateSearchUI(): void; - navigateSearch(direction: "prev" | "next"): void; - onOpen(): void; - onClose(): void; -} diff --git a/_types/src/modules/features/GlobalHistory/GlobalHistoryView.d.ts b/_types/src/modules/features/GlobalHistory/GlobalHistoryView.d.ts deleted file mode 100644 index cb01653f..00000000 --- a/_types/src/modules/features/GlobalHistory/GlobalHistoryView.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { WorkspaceLeaf } from "@/deps.ts"; -import type ObsidianLiveSyncPlugin from "@/main.ts"; -import { SvelteItemView } from "@/common/SvelteItemView.ts"; -export declare const VIEW_TYPE_GLOBAL_HISTORY = "global-history"; -export declare class GlobalHistoryView extends SvelteItemView { - instantiateComponent(target: HTMLElement): { - $on?(type: string, callback: (e: any) => void): () => void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - $set?(props: Partial>): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - } & Record; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - plugin: ObsidianLiveSyncPlugin; - icon: string; - title: string; - navigation: boolean; - getIcon(): string; - constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin); - getViewType(): string; - getDisplayText(): string; -} diff --git a/_types/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.d.ts b/_types/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.d.ts deleted file mode 100644 index 1938be88..00000000 --- a/_types/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { App, Modal } from "@/deps.ts"; -import { CANCELLED, LEAVE_TO_SUBSEQUENT, type diff_result } from "@lib/common/types.ts"; -import { eventHub } from "@/common/events.ts"; -export type MergeDialogResult = typeof CANCELLED | typeof LEAVE_TO_SUBSEQUENT | string; -declare global { - interface Slips extends LSSlips { - "conflict-resolved": typeof CANCELLED | MergeDialogResult; - } -} -export declare class ConflictResolveModal extends Modal { - result: diff_result; - filename: string; - response: MergeDialogResult; - isClosed: boolean; - consumed: boolean; - title: string; - pluginPickMode: boolean; - localName: string; - remoteName: string; - offEvent?: ReturnType; - currentDiffIndex: number; - diffView: HTMLDivElement; - diffNavIndicator: HTMLSpanElement; - constructor(app: App, filename: string, diff: diff_result, pluginPickMode?: boolean, remoteName?: string); - appendDiffFragment(container: HTMLDivElement, text: string, cls: string): void; - appendVersionInfo(container: HTMLDivElement, cls: string, name: string, date: string): void; - navigateDiff(direction: "prev" | "next"): void; - resetDiffNavigation(): void; - onOpen(): void; - sendResponse(result: MergeDialogResult): void; - onClose(): void; - waitForResult(): Promise; -} diff --git a/_types/src/modules/features/Log/LogPaneView.d.ts b/_types/src/modules/features/Log/LogPaneView.d.ts deleted file mode 100644 index 62a47d39..00000000 --- a/_types/src/modules/features/Log/LogPaneView.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { WorkspaceLeaf } from "@/deps.ts"; -import type ObsidianLiveSyncPlugin from "@/main.ts"; -import { SvelteItemView } from "@/common/SvelteItemView.ts"; -export declare const VIEW_TYPE_LOG = "log-log"; -export declare class LogPaneView extends SvelteItemView { - instantiateComponent(target: HTMLElement): { - $on?(type: string, callback: (e: any) => void): () => void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - $set?(props: Partial>): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - } & Record; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - plugin: ObsidianLiveSyncPlugin; - icon: string; - title: string; - navigation: boolean; - getIcon(): string; - constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin); - getViewType(): string; - getDisplayText(): import("octagonal-wheels/common/types").TaggedType; -} diff --git a/_types/src/modules/features/ModuleGlobalHistory.d.ts b/_types/src/modules/features/ModuleGlobalHistory.d.ts deleted file mode 100644 index 96949683..00000000 --- a/_types/src/modules/features/ModuleGlobalHistory.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts"; -export declare class ModuleObsidianGlobalHistory extends AbstractObsidianModule { - _everyOnloadStart(): Promise; - showGlobalHistory(): void; - onBindFunction(core: typeof this.core, services: typeof core.services): void; -} diff --git a/_types/src/modules/features/ModuleInteractiveConflictResolver.d.ts b/_types/src/modules/features/ModuleInteractiveConflictResolver.d.ts deleted file mode 100644 index cd5259a2..00000000 --- a/_types/src/modules/features/ModuleInteractiveConflictResolver.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type FilePathWithPrefix, type diff_result } from "@lib/common/types.ts"; -import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleInteractiveConflictResolver extends AbstractObsidianModule { - _everyOnloadStart(): Promise; - _anyResolveConflictByUI(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise; - allConflictCheck(): Promise; - pickFileForResolve(): Promise; - _allScanStat(): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/features/ModuleLog.d.ts b/_types/src/modules/features/ModuleLog.d.ts deleted file mode 100644 index d1e7a7ef..00000000 --- a/_types/src/modules/features/ModuleLog.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ReactiveValue } from "octagonal-wheels/dataobject/reactive"; -import { type LOG_LEVEL } from "@lib/common/types.ts"; -import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts"; -import { Notice } from "@/deps.ts"; -import { P2PLogCollector } from "@lib/replication/trystero/P2PLogCollector.ts"; -import type { LiveSyncCore } from "@/main.ts"; -import { compatGlobal } from "@lib/common/coreEnvFunctions.ts"; -export declare const MARK_DONE = "\u2009\u2009"; -export declare class ModuleLog extends AbstractObsidianModule { - statusBar?: HTMLElement; - statusDiv?: HTMLElement; - statusLine?: HTMLDivElement; - logMessage?: HTMLDivElement; - logHistory?: HTMLDivElement; - messageArea?: HTMLDivElement; - statusBarLabels: ReactiveValue<{ - message: string; - status: string; - }>; - statusLog: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - activeFileStatus: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - notifies: { - [key: string]: { - notice: Notice; - count: number; - }; - }; - p2pLogCollector: P2PLogCollector; - observeForLogs(): void; - private _everyOnload; - adjustStatusDivPosition(): void; - getActiveFileStatus(): Promise; - setFileStatus(): Promise; - updateMessageArea(): Promise; - onActiveLeafChange(): void; - nextFrameQueue: ReturnType | undefined; - logLines: { - ttl: number; - message: string; - }[]; - applyStatusBarText(): void; - private _allStartOnUnload; - _everyOnloadStart(): Promise; - private _everyOnloadAfterLoadSettings; - writeLogToTheFile(now: Date, vaultName: string, newMessage: string): void; - __addLog(message: unknown, level?: LOG_LEVEL, key?: string): void; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/features/ModuleObsidianDocumentHistory.d.ts b/_types/src/modules/features/ModuleObsidianDocumentHistory.d.ts deleted file mode 100644 index 20a847ab..00000000 --- a/_types/src/modules/features/ModuleObsidianDocumentHistory.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type TFile } from "@/deps.ts"; -import type { FilePathWithPrefix, DocumentID } from "@lib/common/types.ts"; -import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts"; -export declare class ModuleObsidianDocumentHistory extends AbstractObsidianModule { - _everyOnloadStart(): Promise; - showHistory(file: TFile | FilePathWithPrefix, id?: DocumentID): void; - fileHistory(): Promise; - onBindFunction(core: typeof this.core, services: typeof core.services): void; -} diff --git a/_types/src/modules/features/ModuleObsidianSettingAsMarkdown.d.ts b/_types/src/modules/features/ModuleObsidianSettingAsMarkdown.d.ts deleted file mode 100644 index 173cd987..00000000 --- a/_types/src/modules/features/ModuleObsidianSettingAsMarkdown.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ObsidianLiveSyncSettings } from "@lib/common/types"; -import { AbstractModule } from "@/modules/AbstractModule.ts"; -import type { ServiceContext } from "@lib/services/base/ServiceBase.ts"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleObsidianSettingsAsMarkdown extends AbstractModule { - _everyOnloadStart(): Promise; - extractSettingFromWholeText(data: string): { - preamble: string; - body: string; - postscript: string; - }; - parseSettingFromMarkdown(filename: string, data?: string): Promise<{ - preamble: string; - body: string; - postscript: string; - }>; - checkAndApplySettingFromMarkdown(filename: string, automated?: boolean): Promise; - generateSettingForMarkdown(settings?: ObsidianLiveSyncSettings, keepCredential?: boolean): Partial; - saveSettingToMarkdown(filename: string): Promise; - onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void; -} diff --git a/_types/src/modules/features/ModuleObsidianSettingTab.d.ts b/_types/src/modules/features/ModuleObsidianSettingTab.d.ts deleted file mode 100644 index 3e8f4b2c..00000000 --- a/_types/src/modules/features/ModuleObsidianSettingTab.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ObsidianLiveSyncSettingTab } from "./SettingDialogue/ObsidianLiveSyncSettingTab.ts"; -import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleObsidianSettingDialogue extends AbstractObsidianModule { - settingTab: ObsidianLiveSyncSettingTab; - _everyOnloadStart(): Promise; - openSetting(): void; - get appId(): string; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/features/RemoteActivityStatus.d.ts b/_types/src/modules/features/RemoteActivityStatus.d.ts deleted file mode 100644 index a1b47074..00000000 --- a/_types/src/modules/features/RemoteActivityStatus.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** Status icon for a finite remote operation whose lifetime is known. */ -export declare const REMOTE_OPERATION_ACTIVITY_ICON = "\uD83D\uDCF2"; -/** Status icon for approximate physical remote-request activity. */ -export declare const REMOTE_REQUEST_ACTIVITY_ICON = "\uD83C\uDF10"; -/** Avoids hiding very short remote requests before the status bar can render them. */ -export declare const REMOTE_REQUEST_ACTIVITY_MINIMUM_VISIBLE_MS = 150; -export type RemoteActivityStatus = { - remoteOperationCount: number; - trackedRequestCount: number; -}; -/** Returns the non-negative difference between tracked request starts and completions. */ -export declare function getTrackedRequestCount(requestCount: number, responseCount: number): number; -/** Formats the compact prefix shown before the replication status. */ -export declare function formatRemoteActivityStatusLabel(status: RemoteActivityStatus): string; diff --git a/_types/src/modules/features/SettingDialogue/LiveSyncSetting.d.ts b/_types/src/modules/features/SettingDialogue/LiveSyncSetting.d.ts deleted file mode 100644 index 855c980c..00000000 --- a/_types/src/modules/features/SettingDialogue/LiveSyncSetting.d.ts +++ /dev/null @@ -1,55 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { Setting, TextComponent, type ToggleComponent, type DropdownComponent, ButtonComponent, type TextAreaComponent, type ValueComponent } from "@/deps.ts"; -import { type ConfigurationItem } from "@lib/common/types.ts"; -import { type ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import { type AllSettingItemKey, type AllSettings, type AllStringItemKey, type AllNumericItemKey, type AllBooleanItemKey } from "./settingConstants.ts"; -import { type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts"; -export declare class LiveSyncSetting extends Setting { - autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent; - applyButtonComponent?: ButtonComponent; - selfKey?: AllSettingItemKey; - watchDirtyKeys: AllSettingItemKey[]; - holdValue: boolean; - static env: ObsidianLiveSyncSettingTab; - descBuf: string | DocumentFragment; - nameBuf: string | DocumentFragment; - placeHolderBuf: string; - hasPassword: boolean; - invalidateValue?: () => void; - setValue?: (value: unknown) => void; - constructor(containerEl: HTMLElement); - setDesc(desc: string | DocumentFragment): this; - setName(name: string | DocumentFragment): this; - setAuto(key: AllSettingItemKey, opt?: AutoWireOption): this; - autoWireSetting(key: AllSettingItemKey, opt?: AutoWireOption): { - name: string; - desc?: string; - placeHolder?: string; - status?: "BETA" | "ALPHA" | "EXPERIMENTAL"; - obsolete?: boolean; - level?: import("@lib/common/types.ts").ConfigLevel; - isHidden?: boolean; - isAdvanced?: boolean; - } | undefined; - autoWireComponent(component: ValueComponent, conf?: ConfigurationItem, opt?: AutoWireOption): void; - commitValue(value: AllSettings[T]): Promise; - autoWireText(key: AllStringItemKey, opt?: AutoWireOption): this; - autoWireTextArea(key: AllStringItemKey, opt?: AutoWireOption): this; - autoWireNumeric(key: AllNumericItemKey, opt: AutoWireOption & { - clampMin?: number; - clampMax?: number; - acceptZero?: boolean; - }): this; - autoWireToggle(key: AllBooleanItemKey, opt?: AutoWireOption): this; - autoWireDropDown(key: AllStringItemKey, opt: AutoWireOption & { - options: Record; - }): this; - addApplyButton(keys: AllSettingItemKey[], text?: string): this; - addOnUpdate(func: () => OnUpdateResult): this; - updateHandlers: Set<() => OnUpdateResult>; - prevStatus: OnUpdateResult; - _getComputedStatus(): OnUpdateResult; - _applyOnUpdateHandlers(): void; - _onUpdate(): void; -} diff --git a/_types/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.d.ts b/_types/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.d.ts deleted file mode 100644 index a5ec9518..00000000 --- a/_types/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.d.ts +++ /dev/null @@ -1,104 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { App, Component, PluginSettingTab } from "@/deps.ts"; -import { type ObsidianLiveSyncSettings } from "@lib/common/types.ts"; -import ObsidianLiveSyncPlugin from "@/main.ts"; -import { type AllSettingItemKey, type AllStringItemKey, type AllNumericItemKey, type AllBooleanItemKey, type AllSettings, OnDialogSettingsDefault, type OnDialogSettings } from "./settingConstants.ts"; -import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; -import { type OnSavedHandler, type OnSavedHandlerFunc, type OnUpdateFunc, type OnUpdateResult, type UpdateFunction } from "./SettingPane.ts"; -import { JournalSyncCore } from "@lib/replication/journal/JournalSyncCore.js"; -export declare class ObsidianLiveSyncSettingTab extends PluginSettingTab { - plugin: ObsidianLiveSyncPlugin; - private _lifetimeComponent; - get lifetimeComponent(): Component; - get core(): import("@/main.ts").LiveSyncCore; - get services(): import("../../../lib/src/services/InjectableServices.ts").InjectableServiceHub; - selectedScreen: string; - _editingSettings?: AllSettings; - get editingSettings(): AllSettings; - set editingSettings(v: AllSettings); - initialSettings?: typeof this.editingSettings; - /** - * Apply editing setting to the plug-in. - * @param keys setting keys for applying - */ - applySetting(keys: AllSettingItemKey[]): void; - applyAllSettings(): void; - saveLocalSetting(key: keyof typeof OnDialogSettingsDefault): Promise; - /** - * Apply and save setting to the plug-in. - * @param keys setting keys for applying - */ - saveSettings(keys: AllSettingItemKey[]): Promise; - /** - * Apply all editing setting to the plug-in. - * @param keys setting keys for applying - */ - saveAllDirtySettings(): Promise; - /** - * Invalidate buffered value and fetch the latest. - */ - requestUpdate(): void; - reloadAllLocalSettings(): { - configPassphrase: string; - preset: "" | "PERIODIC" | "LIVESYNC" | "DISABLE"; - syncMode: "ONEVENTS" | "PERIODIC" | "LIVESYNC"; - dummy: number; - deviceAndVaultName: string; - }; - computeAllLocalSettings(): Partial; - /** - * Reread all settings and request invalidate - */ - reloadAllSettings(skipUpdate?: boolean): void; - /** - * Reread each setting and request invalidate - */ - refreshSetting(key: AllSettingItemKey): void; - isDirty(key: AllSettingItemKey): boolean; - isSomeDirty(keys: AllSettingItemKey[]): boolean; - isConfiguredAs(key: AllStringItemKey, value: string): boolean; - isConfiguredAs(key: AllNumericItemKey, value: number): boolean; - isConfiguredAs(key: AllBooleanItemKey, value: boolean): boolean; - settingComponents: Setting[]; - controlledElementFunc: UpdateFunction[]; - onSavedHandlers: OnSavedHandler[]; - inWizard: boolean; - constructor(app: App, plugin: ObsidianLiveSyncPlugin); - testConnection(settingOverride?: Partial): Promise; - closeSetting(): void; - handleElement(element: HTMLElement, func: OnUpdateFunc): void; - createEl(el: HTMLElement, tag: T, o?: string | DomElementInfo, callback?: (el: HTMLElementTagNameMap[T]) => void, func?: OnUpdateFunc): HTMLElementTagNameMap[T]; - addEl(el: HTMLElement, tag: T, o?: string | DomElementInfo, callback?: (el: HTMLElementTagNameMap[T]) => void, func?: OnUpdateFunc): Promise>; - addOnSaved(key: T, func: OnSavedHandlerFunc): void; - resetEditingSettings(): void; - hide(): void; - isShown: boolean; - requestReload(): void; - manifestVersion: string; - lastVersion: number; - screenElements: { - [key: string]: HTMLElement[]; - }; - changeDisplay(screen: string): void; - enableMinimalSetup(): Promise; - menuEl?: HTMLElement; - addScreenElement(key: string, element: HTMLElement): void; - selectPane(event: Event): void; - isNeedRebuildLocal(): boolean; - isNeedRebuildRemote(): boolean; - isAnySyncEnabled(): boolean; - enableOnlySyncDisabled: OnUpdateFunc; - onlyOnP2POrCouchDB: () => OnUpdateResult; - onlyOnCouchDB: () => OnUpdateResult; - onlyOnMinIO: () => OnUpdateResult; - onlyOnOnlyP2P: () => OnUpdateResult; - onlyOnCouchDBOrMinIO: () => OnUpdateResult; - checkWorkingPassphrase: () => Promise; - isPassphraseValid: () => Promise; - rebuildDB: (method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks") => Promise; - confirmRebuild(): Promise; - display(): void; - getMinioJournalSyncClient(): JournalSyncCore; - resetRemoteBucket(): Promise; -} diff --git a/_types/src/modules/features/SettingDialogue/PaneAdvanced.d.ts b/_types/src/modules/features/SettingDialogue/PaneAdvanced.d.ts deleted file mode 100644 index 144d32df..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneAdvanced.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneAdvanced(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneChangeLog.d.ts b/_types/src/modules/features/SettingDialogue/PaneChangeLog.d.ts deleted file mode 100644 index 81c747e9..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneChangeLog.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -export declare function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneCustomisationSync.d.ts b/_types/src/modules/features/SettingDialogue/PaneCustomisationSync.d.ts deleted file mode 100644 index 309f516f..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneCustomisationSync.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneCustomisationSync(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneGeneral.d.ts b/_types/src/modules/features/SettingDialogue/PaneGeneral.d.ts deleted file mode 100644 index 0b1f4a06..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneGeneral.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneGeneral(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel, addPane }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneHatch.d.ts b/_types/src/modules/features/SettingDialogue/PaneHatch.d.ts deleted file mode 100644 index dc90c38b..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneHatch.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneMaintenance.d.ts b/_types/src/modules/features/SettingDialogue/PaneMaintenance.d.ts deleted file mode 100644 index c431319b..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneMaintenance.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab"; -import { type PageFunctions } from "./SettingPane"; -export declare function paneMaintenance(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PanePatches.d.ts b/_types/src/modules/features/SettingDialogue/PanePatches.d.ts deleted file mode 100644 index 89b5973b..00000000 --- a/_types/src/modules/features/SettingDialogue/PanePatches.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PanePowerUsers.d.ts b/_types/src/modules/features/SettingDialogue/PanePowerUsers.d.ts deleted file mode 100644 index e2c047ad..00000000 --- a/_types/src/modules/features/SettingDialogue/PanePowerUsers.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function panePowerUsers(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneRemoteConfig.d.ts b/_types/src/modules/features/SettingDialogue/PaneRemoteConfig.d.ts deleted file mode 100644 index bee6bcc3..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneRemoteConfig.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneRemoteConfig(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel, addPane }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneSelector.d.ts b/_types/src/modules/features/SettingDialogue/PaneSelector.d.ts deleted file mode 100644 index a33e6312..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneSelector.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneSetup.d.ts b/_types/src/modules/features/SettingDialogue/PaneSetup.d.ts deleted file mode 100644 index 206126da..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneSetup.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneSetup(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel, addPane }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneSyncSettings.d.ts b/_types/src/modules/features/SettingDialogue/PaneSyncSettings.d.ts deleted file mode 100644 index c710f270..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneSyncSettings.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneSyncSettings(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel, addPane }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/SettingPane.d.ts b/_types/src/modules/features/SettingDialogue/SettingPane.d.ts deleted file mode 100644 index 0cf9dcef..00000000 --- a/_types/src/modules/features/SettingDialogue/SettingPane.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ConfigLevel } from "@lib/common/types"; -import type { AllSettingItemKey, AllSettings } from "./settingConstants"; -export declare const combineOnUpdate: (func1: OnUpdateFunc, func2: OnUpdateFunc) => OnUpdateFunc; -export declare const setLevelClass: (el: HTMLElement, level?: ConfigLevel) => void; -export declare function setStyle(el: HTMLElement, styleHead: string, condition: () => boolean): void; -export declare function visibleOnly(cond: () => boolean): OnUpdateFunc; -export declare function enableOnly(cond: () => boolean): OnUpdateFunc; -export type OnUpdateResult = { - visibility?: boolean; - disabled?: boolean; - classes?: string[]; - isCta?: boolean; - isWarning?: boolean; -}; -export type OnUpdateFunc = () => OnUpdateResult; -export type UpdateFunction = () => void; -export type OnSavedHandlerFunc = (value: AllSettings[T]) => Promise | void; -export type OnSavedHandler = { - key: T; - handler: OnSavedHandlerFunc; -}; -export declare function getLevelStr(level: ConfigLevel): "" | import("octagonal-wheels/common/types").TaggedType | import("octagonal-wheels/common/types").TaggedType | import("octagonal-wheels/common/types").TaggedType; -export type AutoWireOption = { - placeHolder?: string; - holdValue?: boolean; - isPassword?: boolean; - invert?: boolean; - onUpdate?: OnUpdateFunc; - obsolete?: boolean; -}; -export declare function findAttrFromParent(el: HTMLElement, attr: string): string; -export declare function wrapMemo(func: (arg: T) => void): (arg: T) => void; -export type PageFunctions = { - addPane: (parentEl: HTMLElement, title: string, icon: string, order: number, wizardHidden: boolean, level?: ConfigLevel) => Promise; - addPanel: (parentEl: HTMLElement, title: string, callback?: (el: HTMLDivElement) => void, func?: OnUpdateFunc, level?: ConfigLevel) => Promise; -}; diff --git a/_types/src/modules/features/SettingDialogue/SveltePanel.d.ts b/_types/src/modules/features/SettingDialogue/SveltePanel.d.ts deleted file mode 100644 index 9a581d41..00000000 --- a/_types/src/modules/features/SettingDialogue/SveltePanel.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type Component } from "svelte"; -import { type Writable } from "svelte/store"; -/** - * Props passed to Svelte panels, containing a writable port - * to communicate with the panel - */ -export type SveltePanelProps = { - port: Writable; -}; -/** - * A class to manage a Svelte panel within Obsidian - * Especially useful for settings panels - */ -export declare class SveltePanel { - private _mountedComponent; - private _componentValue; - /** - * Creates a Svelte panel instance - * @param component Component to mount - * @param mountTo HTMLElement to mount the component to - * @param valueStore Optional writable store to bind to the component's port, if not provided a new one will be created - * @returns The SveltePanel instance - */ - constructor(component: Component>, mountTo: HTMLElement, valueStore?: Writable); - /** - * Destroys the Svelte panel instance by unmounting the component - */ - destroy(): void; - /** - * Gets or sets the current value of the component's port - */ - get componentValue(): T | undefined; - set componentValue(value: T | undefined); -} diff --git a/_types/src/modules/features/SettingDialogue/remoteConfigBuffer.d.ts b/_types/src/modules/features/SettingDialogue/remoteConfigBuffer.d.ts deleted file mode 100644 index 16dbbf6f..00000000 --- a/_types/src/modules/features/SettingDialogue/remoteConfigBuffer.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettings } from "@lib/common/types.ts"; -export declare function syncActivatedRemoteSettings(target: Partial, source: ObsidianLiveSyncSettings): void; diff --git a/_types/src/modules/features/SettingDialogue/settingConstants.d.ts b/_types/src/modules/features/SettingDialogue/settingConstants.d.ts deleted file mode 100644 index edc2ff40..00000000 --- a/_types/src/modules/features/SettingDialogue/settingConstants.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export * from "@lib/common/settingConstants.ts"; diff --git a/_types/src/modules/features/SettingDialogue/settingUtils.d.ts b/_types/src/modules/features/SettingDialogue/settingUtils.d.ts deleted file mode 100644 index 99d52117..00000000 --- a/_types/src/modules/features/SettingDialogue/settingUtils.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ObsidianLiveSyncSettings } from "@lib/common/types"; -/** - * Generates a summary of P2P configuration settings - * @param setting Settings object - * @param additional Additional summary information to include - * @param showAdvanced Whether to include advanced settings - * @returns Summary object - */ -export declare function getP2PConfigSummary(setting: ObsidianLiveSyncSettings, additional?: Record, showAdvanced?: boolean): { - [x: string]: string; -}; -/** - * Generates a summary of Object Storage configuration settings - * @param setting Settings object - * @param showAdvanced Whether to include advanced settings - * @returns Summary object - */ -export declare function getBucketConfigSummary(setting: ObsidianLiveSyncSettings, showAdvanced?: boolean): Record; -/** - * Generates a summary of CouchDB configuration settings - * @param setting Settings object - * @param showAdvanced Whether to include advanced settings - * @returns Summary object - */ -export declare function getCouchDBConfigSummary(setting: ObsidianLiveSyncSettings, showAdvanced?: boolean): Record; -/** - * Generates a summary of E2EE configuration settings - * @param setting Settings object - * @param showAdvanced Whether to include advanced settings - * @returns Summary object - */ -export declare function getE2EEConfigSummary(setting: ObsidianLiveSyncSettings, showAdvanced?: boolean): Record; -/** - * Converts partial settings into a summary object - * @param setting Partial settings object - * @param showAdvanced Whether to include advanced settings - * @returns Summary object - */ -export declare function getSummaryFromPartialSettings(setting: Partial, showAdvanced?: boolean): Record; -/** - * Copy document from one database to another for migration purposes - * @param docName document ID - * @param dbFrom source database - * @param dbTo destination database - * @returns - */ -export declare function copyMigrationDocs(docName: string, dbFrom: PouchDB.Database, dbTo: PouchDB.Database): Promise; -type PouchDBOpenFunction = () => Promise | PouchDB.Database; -/** - * Migrate databases from one to another - * @param operationName Name of the migration operation - * @param from source database - * @param openTo function to open destination database - * @returns True if migration succeeded - */ -export declare function migrateDatabases(operationName: string, from: PouchDB.Database, openTo: PouchDBOpenFunction): Promise; -export {}; diff --git a/_types/src/modules/features/SetupManager.d.ts b/_types/src/modules/features/SetupManager.d.ts deleted file mode 100644 index 711717a2..00000000 --- a/_types/src/modules/features/SetupManager.d.ts +++ /dev/null @@ -1,121 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ObsidianLiveSyncSettings } from "@lib/common/types.ts"; -import { AbstractModule } from "@/modules/AbstractModule.ts"; -/** - * User modes for onboarding and setup - */ -export declare const enum UserMode { - /** - * New User Mode - for users who are new to the plugin - */ - NewUser = "new-user", - /** - * Existing User Mode - for users who have used the plugin before, or just configuring again - */ - ExistingUser = "existing-user", - /** - * Unknown User Mode - for cases where the user mode is not determined - */ - Unknown = "unknown", - /** - * Update User Mode - for users who are updating configuration. May be `existing-user` as well, but possibly they want to treat it differently. - */ - Update = "unknown" // eslint-disable-line @typescript-eslint/no-duplicate-enum-values -- Duplicate enum value -} -/** - * Setup Manager to handle onboarding and configuration setup - */ -export declare class SetupManager extends AbstractModule { - get dialogManager(): import("../../lib/src/UI/svelteDialog.ts").SvelteDialogManagerBase; - /** - * Starts the onboarding process - * @returns Promise that resolves to true if onboarding completed successfully, false otherwise - */ - startOnBoarding(): Promise; - /** - * Handles the onboarding process based on user mode - * @param userMode - * @returns Promise that resolves to true if onboarding completed successfully, false otherwise - */ - onOnboard(userMode: UserMode): Promise; - /** - * Handles setup using a setup URI - * @param userMode - * @param setupURI - * @returns Promise that resolves to true if onboarding completed successfully, false otherwise - */ - onUseSetupURI(userMode: UserMode, setupURI?: string): Promise; - /** - * Handles manual setup for CouchDB - * @param userMode - * @param currentSetting - * @param activate Whether to activate the CouchDB as remote type - * @returns Promise that resolves to true if setup completed successfully, false otherwise - */ - onCouchDBManualSetup(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings, activate?: boolean): Promise; - /** - * Handles manual setup for S3-compatible bucket - * @param userMode - * @param currentSetting - * @param activate Whether to activate the Bucket as remote type - * @returns Promise that resolves to true if setup completed successfully, false otherwise - */ - onBucketManualSetup(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings, activate?: boolean): Promise; - /** - * Handles manual setup for P2P - * @param userMode - * @param currentSetting - * @param activate Whether to activate the P2P as remote type (as P2P Only setup) - * @returns Promise that resolves to true if setup completed successfully, false otherwise - */ - onP2PManualSetup(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings, activate?: boolean): Promise; - /** - * Handles only E2EE configuration - * @param userMode - * @param currentSetting - * @returns - */ - onlyE2EEConfiguration(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings): Promise; - /** - * Handles manual configuration flow (E2EE + select server) - * @param originalSetting - * @param userMode - * @returns - */ - onConfigureManually(originalSetting: ObsidianLiveSyncSettings, userMode: UserMode): Promise; - /** - * Handles server selection during manual configuration - * @param currentSetting - * @param userMode - * @returns - */ - onSelectServer(currentSetting: ObsidianLiveSyncSettings, userMode: UserMode): Promise; - /** - * Confirms and applies settings obtained from the wizard - * @param newConf - * @param _userMode - * @param activate Whether to activate the remote type in the new settings - * @param extra Extra function to run before applying settings - * @returns Promise that resolves to true if settings applied successfully, false otherwise - */ - onConfirmApplySettingsFromWizard(newConf: ObsidianLiveSyncSettings, _userMode: UserMode, activate?: boolean, extra?: () => void): Promise; - /** - * Prompts the user with QR code scanning instructions - * @returns Promise that resolves to false as QR code instruction dialog does not yield settings directly - */ - onPromptQRCodeInstruction(): Promise; - /** - * Decodes settings from a QR code string and applies them - * @param qr QR code string containing encoded settings - * @returns Promise that resolves to true if settings applied successfully, false otherwise - */ - decodeQR(qr: string): Promise; - /** - * Applies the new settings to the core settings and saves them - * @param newConf - * @param userMode - * @returns Promise that resolves to true if settings applied successfully, false otherwise - */ - applySetting(newConf: ObsidianLiveSyncSettings, userMode: UserMode): Promise; -} diff --git a/_types/src/modules/features/SetupWizard/dialogs/setupDialogTypes.d.ts b/_types/src/modules/features/SetupWizard/dialogs/setupDialogTypes.d.ts deleted file mode 100644 index 995c6bf9..00000000 --- a/_types/src/modules/features/SetupWizard/dialogs/setupDialogTypes.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { BucketSyncSetting, CouchDBConnection, EncryptionSettings, ObsidianLiveSyncSettings, P2PConnectionInfo } from "@lib/common/models/setting.type"; -export declare const TYPE_IDENTICAL = "identical"; -export declare const TYPE_INDEPENDENT = "independent"; -export declare const TYPE_UNBALANCED = "unbalanced"; -export declare const TYPE_CANCEL = "cancelled"; -export declare const TYPE_BACKUP_DONE = "backup_done"; -export declare const TYPE_BACKUP_SKIPPED = "backup_skipped"; -export declare const TYPE_UNABLE_TO_BACKUP = "unable_to_backup"; -export declare const TYPE_NEW_USER = "new-user"; -export declare const TYPE_EXISTING_USER = "existing-user"; -export declare const TYPE_CANCELLED = "cancelled"; -export declare const TYPE_EXISTING = "existing-user"; -export declare const TYPE_NEW = "new-user"; -export declare const TYPE_COMPATIBLE_EXISTING = "compatible-existing-user"; -export declare const TYPE_APPLY = "apply"; -export declare const TYPE_USE_SETUP_URI = "use-setup-uri"; -export declare const TYPE_SCAN_QR_CODE = "scan-qr-code"; -export declare const TYPE_CONFIGURE_MANUALLY = "configure-manually"; -export declare const TYPE_CLOSE = "close"; -export declare const TYPE_COUCHDB = "couchdb"; -export declare const TYPE_BUCKET = "bucket"; -export declare const TYPE_P2P = "p2p"; -export type ResultTypeVault = typeof TYPE_IDENTICAL | typeof TYPE_INDEPENDENT | typeof TYPE_UNBALANCED | typeof TYPE_CANCEL; -export type ResultTypeBackup = typeof TYPE_BACKUP_DONE | typeof TYPE_BACKUP_SKIPPED | typeof TYPE_UNABLE_TO_BACKUP | typeof TYPE_CANCEL; -export type ResultTypeExtra = { - preventFetchingConfig: boolean; -}; -export type FetchEverythingResult = { - vault: ResultTypeVault; - backup: ResultTypeBackup; - extra: ResultTypeExtra; -} | typeof TYPE_CANCEL; -export type RebuildEverythingResult = { - backup: ResultTypeBackup; - extra: ResultTypeExtra; -} | typeof TYPE_CANCEL; -export type IntroResultType = typeof TYPE_NEW_USER | typeof TYPE_EXISTING_USER | typeof TYPE_CANCELLED; -export type OutroAskUserModeResultType = typeof TYPE_EXISTING | typeof TYPE_NEW | typeof TYPE_COMPATIBLE_EXISTING | typeof TYPE_CANCELLED; -export type OutroExistingUserResultType = typeof TYPE_APPLY | typeof TYPE_CANCELLED; -export type OutroNewUserResultType = typeof TYPE_APPLY | typeof TYPE_CANCELLED; -export type SelectMethodNewUserResultType = typeof TYPE_USE_SETUP_URI | typeof TYPE_CONFIGURE_MANUALLY | typeof TYPE_CANCELLED; -export type SelectMethodExistingResultType = typeof TYPE_USE_SETUP_URI | typeof TYPE_SCAN_QR_CODE | typeof TYPE_CONFIGURE_MANUALLY | typeof TYPE_CANCELLED; -export type SetupRemoteResultType = typeof TYPE_COUCHDB | typeof TYPE_BUCKET | typeof TYPE_P2P | typeof TYPE_CANCELLED; -export type UseSetupURIResultType = typeof TYPE_CANCELLED | ObsidianLiveSyncSettings; -export type SetupRemoteE2EEResultType = typeof TYPE_CANCELLED | EncryptionSettings; -export type SetupRemoteBucketResultType = typeof TYPE_CANCELLED | BucketSyncSetting; -export type SetupRemoteCouchDBResultType = typeof TYPE_CANCELLED | CouchDBConnection; -export type SetupRemoteP2PResultType = typeof TYPE_CANCELLED | P2PConnectionInfo; -export type ScanQRCodeResultType = typeof TYPE_CLOSE; diff --git a/_types/src/modules/features/StatusBarDisplay.d.ts b/_types/src/modules/features/StatusBarDisplay.d.ts deleted file mode 100644 index 4a0714bb..00000000 --- a/_types/src/modules/features/StatusBarDisplay.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ReactiveValue } from "octagonal-wheels/dataobject/reactive"; -export declare const STATUS_COUNTER_INACTIVE_LINGER_MS = 3000; -export type DisposableReactiveValue = ReactiveValue & { - dispose(): void; -}; -/** - * Mirrors an activity count while keeping each visible period on screen for a - * minimum total lifetime. The delay applies only when the source becomes zero. - */ -export declare function createMinimumVisibleActivityCount(source: ReactiveValue, minimumVisibleMs: number): DisposableReactiveValue; -/** - * Formats a counter with a stable width and briefly retains its zero value so - * that the completion of queued work remains visible. - */ -export declare function createPaddedCounterLabel(source: ReactiveValue, mark: string, inactiveLingerMs?: number): DisposableReactiveValue; diff --git a/_types/src/modules/main/ModuleLiveSyncMain.d.ts b/_types/src/modules/main/ModuleLiveSyncMain.d.ts deleted file mode 100644 index e49f2958..00000000 --- a/_types/src/modules/main/ModuleLiveSyncMain.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AbstractModule } from "@/modules/AbstractModule.ts"; -import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub.ts"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleLiveSyncMain extends AbstractModule { - _onLiveSyncReady(): Promise; - _wireUpEvents(): Promise; - _onLiveSyncLoad(): Promise; - onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void; -} diff --git a/_types/src/modules/services/ObsidianAPIService.d.ts b/_types/src/modules/services/ObsidianAPIService.d.ts deleted file mode 100644 index 8de3744d..00000000 --- a/_types/src/modules/services/ObsidianAPIService.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import { type Command } from "@/deps.ts"; -import { ObsHttpHandler } from "@/modules/essentialObsidian/APILib/ObsHttpHandler"; -import type { Confirm } from "@lib/interfaces/Confirm"; -declare module "obsidian" { - interface App { - appId?: string; - isMobile?: boolean; - } -} -export declare class ObsidianAPIService extends InjectableAPIService { - _customHandler: ObsHttpHandler | undefined; - _confirmInstance: Confirm; - constructor(context: ObsidianServiceContext); - getCustomFetchHandler(): ObsHttpHandler; - showWindow(viewType: string): Promise; - showWindowOnRight(viewType: string): Promise; - private get app(); - getPlatform(): string; - isMobile(): boolean; - getAppID(): string; - getSystemVaultName(): string; - getAppVersion(): string; - getPluginVersion(): string; - get confirm(): Confirm; - addCommand(command: TCommand): TCommand; - registerWindow(type: string, factory: (leaf: T) => unknown): void; - addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => unknown): HTMLElement; - registerProtocolHandler(action: string, handler: (params: Record) => unknown): void; - /** - * In Obsidian, we will use the native `requestUrl` function as the default fetch handler, - * to address unavoidable CORS issues. - */ - nativeFetch(req: string | Request, opts?: RequestInit): Promise; - addStatusBarItem(): HTMLElement | undefined; - setInterval(handler: () => void, timeout: number): number; - getSystemConfigDir(): string; -} diff --git a/_types/src/modules/services/ObsidianAppLifecycleService.d.ts b/_types/src/modules/services/ObsidianAppLifecycleService.d.ts deleted file mode 100644 index 362af798..00000000 --- a/_types/src/modules/services/ObsidianAppLifecycleService.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AppLifecycleServiceBase } from "@lib/services/implements/injectable/InjectableAppLifecycleService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -declare module "obsidian" { - interface App { - commands: { - executeCommandById: (id: string) => Promise; - }; - } -} -export declare class ObsidianAppLifecycleService extends AppLifecycleServiceBase { - performRestart(): void; -} diff --git a/_types/src/modules/services/ObsidianConfirm.d.ts b/_types/src/modules/services/ObsidianConfirm.d.ts deleted file mode 100644 index 01c28cd1..00000000 --- a/_types/src/modules/services/ObsidianConfirm.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type App, type Plugin } from "@/deps"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -export declare class ObsidianConfirm implements Confirm { - private _context; - get _app(): App; - get _plugin(): Plugin; - constructor(context: T); - askYesNo(message: string): Promise<"yes" | "no">; - askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise; - askYesNoDialog(message: string, opt?: { - title?: string; - defaultOption?: "Yes" | "No"; - timeout?: number; - }): Promise<"yes" | "no">; - askSelectString(message: string, items: string[]): Promise; - askSelectStringDialogue(message: string, buttons: T, opt: { - title?: string; - defaultAction: T[number]; - timeout?: number; - }): Promise; - askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void; - confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false>; -} diff --git a/_types/src/modules/services/ObsidianDatabaseService.d.ts b/_types/src/modules/services/ObsidianDatabaseService.d.ts deleted file mode 100644 index ef6d3bf9..00000000 --- a/_types/src/modules/services/ObsidianDatabaseService.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import { DatabaseService, type DatabaseServiceDependencies } from "@lib/services/base/DatabaseService.ts"; -export declare class ObsidianDatabaseService extends DatabaseService { - private __onOpenDatabase; - constructor(context: T, dependencies: DatabaseServiceDependencies); -} diff --git a/_types/src/modules/services/ObsidianPathService.d.ts b/_types/src/modules/services/ObsidianPathService.d.ts deleted file mode 100644 index ee27eaee..00000000 --- a/_types/src/modules/services/ObsidianPathService.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import { PathService } from "@lib/services/base/PathService"; -import { type BASE_IS_NEW, type TARGET_IS_NEW, type EVEN } from "@/common/utils"; -import type { UXFileInfo, AnyEntry, UXFileInfoStub, FilePathWithPrefix } from "@lib/common/types"; -export declare class ObsidianPathService extends PathService { - markChangesAreSame(old: UXFileInfo | AnyEntry | FilePathWithPrefix, newMtime: number, oldMtime: number): boolean | undefined; - unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void; - compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN; - isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | FilePathWithPrefix, mtimes: number[]): undefined | typeof EVEN; - protected normalizePath(path: string): string; -} diff --git a/_types/src/modules/services/ObsidianServiceHub.d.ts b/_types/src/modules/services/ObsidianServiceHub.d.ts deleted file mode 100644 index 1c8cf0bc..00000000 --- a/_types/src/modules/services/ObsidianServiceHub.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub"; -import { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import type ObsidianLiveSyncPlugin from "@/main"; -export declare class ObsidianServiceHub extends InjectableServiceHub { - constructor(plugin: ObsidianLiveSyncPlugin); -} diff --git a/_types/src/modules/services/ObsidianServices.d.ts b/_types/src/modules/services/ObsidianServices.d.ts deleted file mode 100644 index 3b333228..00000000 --- a/_types/src/modules/services/ObsidianServices.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { InjectableConflictService } from "@lib/services/implements/injectable/InjectableConflictService"; -import { InjectableDatabaseEventService } from "@lib/services/implements/injectable/InjectableDatabaseEventService"; -import { InjectableFileProcessingService } from "@lib/services/implements/injectable/InjectableFileProcessingService"; -import { InjectableRemoteService } from "@lib/services/implements/injectable/InjectableRemoteService"; -import { InjectableReplicationService } from "@lib/services/implements/injectable/InjectableReplicationService"; -import { InjectableReplicatorService } from "@lib/services/implements/injectable/InjectableReplicatorService"; -import { InjectableTestService } from "@lib/services/implements/injectable/InjectableTestService"; -import { InjectableTweakValueService } from "@lib/services/implements/injectable/InjectableTweakValueService"; -import { ConfigServiceBrowserCompat } from "@lib/services/implements/browser/ConfigServiceBrowserCompat"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext.ts"; -import { KeyValueDBService } from "@lib/services/base/KeyValueDBService"; -import { ControlService } from "@lib/services/base/ControlService"; -export declare class ObsidianDatabaseEventService extends InjectableDatabaseEventService { -} -export declare class ObsidianReplicatorService extends InjectableReplicatorService { -} -export declare class ObsidianFileProcessingService extends InjectableFileProcessingService { -} -export declare class ObsidianReplicationService extends InjectableReplicationService { -} -export declare class ObsidianRemoteService extends InjectableRemoteService { -} -export declare class ObsidianConflictService extends InjectableConflictService { -} -export declare class ObsidianTweakValueService extends InjectableTweakValueService { -} -export declare class ObsidianTestService extends InjectableTestService { -} -export declare class ObsidianConfigService extends ConfigServiceBrowserCompat { -} -export declare class ObsidianKeyValueDBService extends KeyValueDBService { -} -export declare class ObsidianControlService extends ControlService { -} diff --git a/_types/src/modules/services/ObsidianSettingService.d.ts b/_types/src/modules/services/ObsidianSettingService.d.ts deleted file mode 100644 index bd143f22..00000000 --- a/_types/src/modules/services/ObsidianSettingService.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ObsidianLiveSyncSettings } from "@lib/common/types"; -import { SettingService, type SettingServiceDependencies } from "@lib/services/base/SettingService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -export declare class ObsidianSettingService extends SettingService { - constructor(context: T, dependencies: SettingServiceDependencies); - protected setItem(key: string, value: string): void; - protected getItem(key: string): string; - protected deleteItem(key: string): void; - protected saveData(data: ObsidianLiveSyncSettings): Promise; - protected loadData(): Promise; -} diff --git a/_types/src/modules/services/ObsidianUIService.d.ts b/_types/src/modules/services/ObsidianUIService.d.ts deleted file mode 100644 index 383c840a..00000000 --- a/_types/src/modules/services/ObsidianUIService.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ConfigService } from "@lib/services/base/ConfigService"; -import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; -import type { ReplicatorService } from "@lib/services/base/ReplicatorService"; -import { UIService } from "@lib/services/implements/base/UIService"; -import { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import type { IAPIService, IControlService } from "@lib/services/base/IService"; -export type ObsidianUIServiceDependencies = { - appLifecycle: AppLifecycleService; - config: ConfigService; - replicator: ReplicatorService; - APIService: IAPIService; - control: IControlService; -}; -export declare class ObsidianUIService extends UIService { - get dialogToCopy(): import("svelte/legacy").LegacyComponentType; - constructor(context: ObsidianServiceContext, dependents: ObsidianUIServiceDependencies); -} diff --git a/_types/src/modules/services/ObsidianVaultService.d.ts b/_types/src/modules/services/ObsidianVaultService.d.ts deleted file mode 100644 index 5a732866..00000000 --- a/_types/src/modules/services/ObsidianVaultService.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { InjectableVaultService } from "@lib/services/implements/injectable/InjectableVaultService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import type { FilePath } from "@lib/common/types"; -declare module "obsidian" { - interface DataAdapter { - insensitive?: boolean; - } -} -export declare class ObsidianVaultService extends InjectableVaultService { - vaultName(): string; - getActiveFilePath(): FilePath | undefined; - isStorageInsensitive(): boolean; - shouldCheckCaseInsensitively(): boolean; - isValidPath(path: string): boolean; -} diff --git a/_types/src/serviceFeatures/onLayoutReady/enablei18n.d.ts b/_types/src/serviceFeatures/onLayoutReady/enablei18n.d.ts deleted file mode 100644 index b45ae5a3..00000000 --- a/_types/src/serviceFeatures/onLayoutReady/enablei18n.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const enableI18nFeature: import("@lib/interfaces/ServiceModule").ServiceFeatureFunction, keyof import("@lib/interfaces/ServiceModule").ServiceModules, Promise>; diff --git a/_types/src/serviceFeatures/redFlag.d.ts b/_types/src/serviceFeatures/redFlag.d.ts deleted file mode 100644 index 1543900a..00000000 --- a/_types/src/serviceFeatures/redFlag.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type"; -/** - * Flag file handler interface, similar to target filter pattern. - */ -interface FlagFileHandler { - priority: number; - check: () => Promise; - handle: () => Promise; -} -export declare function isFlagFileExist(host: NecessaryServices, path: string): Promise; -export declare function deleteFlagFile(host: NecessaryServices, log: LogFunction, path: string): Promise; -/** - * Factory function to create a fetch all flag handler. - * All logic related to fetch all flag is encapsulated here. - */ -export declare function createFetchAllFlagHandler(host: NecessaryServices<"vault" | "fileProcessing" | "tweakValue" | "UI" | "setting" | "appLifecycle" | "path" | "keyValueDB" | "database", "storageAccess" | "rebuilder" | "fileHandler">, log: LogFunction): FlagFileHandler; -/** - * Adjust setting to remote configuration. - * @param config current configuration to retrieve remote preferred config - * @returns updated configuration if applied, otherwise null. - */ -export declare function adjustSettingToRemote(host: NecessaryServices<"tweakValue" | "UI" | "setting", never>, log: LogFunction, config: ObsidianLiveSyncSettings): Promise; -/** - * Adjust setting to remote if needed. - * @param extra result of dialogues that may contain preventFetchingConfig flag (e.g, from FetchEverything or RebuildEverything) - * @param config current configuration to retrieve remote preferred config - */ -export declare function adjustSettingToRemoteIfNeeded(host: NecessaryServices<"tweakValue" | "UI" | "setting", never>, log: LogFunction, extra: { - preventFetchingConfig: boolean; -}, config: ObsidianLiveSyncSettings): Promise; -/** - * Process vault initialisation with suspending file watching and sync. - * @param proc process to be executed during initialisation, should return true if can be continued, false if app is unable to continue the process. - * @param keepSuspending whether to keep suspending file watching after the process. - * @returns result of the process, or false if error occurs. - */ -export declare function processVaultInitialisation(host: NecessaryServices<"setting", never>, log: LogFunction, proc: () => Promise, keepSuspending?: boolean): Promise; -export declare function verifyAndUnlockSuspension(host: NecessaryServices<"setting" | "appLifecycle" | "UI", never>, log: LogFunction): Promise; -/** - * Factory function to create a rebuild flag handler. - * All logic related to rebuild flag is encapsulated here. - */ -export declare function createRebuildFlagHandler(host: NecessaryServices<"setting" | "appLifecycle" | "UI" | "tweakValue", "storageAccess" | "rebuilder">, log: LogFunction): { - priority: number; - check: () => Promise; - handle: () => Promise; -}; -/** - * Factory function to create a suspend all flag handler. - * All logic related to suspend flag is encapsulated here. - */ -export declare function createSuspendFlagHandler(host: NecessaryServices<"setting", "storageAccess">, log: LogFunction): FlagFileHandler; -export declare function flagHandlerToEventHandler(flagHandler: FlagFileHandler): () => Promise; -export declare function useRedFlagFeatures(host: NecessaryServices<"API" | "appLifecycle" | "UI" | "setting" | "tweakValue" | "fileProcessing" | "vault" | "path" | "keyValueDB" | "database", "storageAccess" | "rebuilder" | "fileHandler">): void; -export {}; diff --git a/_types/src/serviceFeatures/redFlag.simpleFetch.d.ts b/_types/src/serviceFeatures/redFlag.simpleFetch.d.ts deleted file mode 100644 index 339ebdc1..00000000 --- a/_types/src/serviceFeatures/redFlag.simpleFetch.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -import { type FullScanOptions } from "@lib/serviceFeatures/offlineScanner"; -export declare const SIMPLE_FETCH_STAGE1_REMOTE_WINS = "Overwrite all with remote files"; -export declare const SIMPLE_FETCH_STAGE1_NEWER_WINS = "Compare time and take newer"; -export declare const SIMPLE_FETCH_STAGE1_LEGACY = "Use the detailed flow"; -export declare const SIMPLE_FETCH_STAGE1_CANCEL = "Cancel"; -export declare const SIMPLE_FETCH_STAGE2_REMOTE_DELETE_NONE = "Keep local files even if not on remote"; -export declare const SIMPLE_FETCH_STAGE2_REMOTE_DELETE_ALL = "Delete local files if not on remote"; -export declare const SIMPLE_FETCH_STAGE2_NEWER_CLEANUP = "Delete local files if deleted on remote"; -export declare const SIMPLE_FETCH_STAGE2_NEWER_SYNC_ALL = "Keep local files even if deleted on remote"; -export declare const STAGE2_ABORT = "Cancel all and reboot"; -export declare function askSimpleFetchMode(host: NecessaryServices<"UI" | "setting", never>): Promise<{ - mode: string; - options: Partial; -} | "cancelled" | "aborted">; -export declare function askAndPerformFastSetupOnScheduledFetchAll(host: NecessaryServices<"vault" | "fileProcessing" | "tweakValue" | "UI" | "setting" | "appLifecycle" | "path" | "keyValueDB" | "database", "storageAccess" | "rebuilder" | "fileHandler">, log: LogFunction, cleanupFlag: () => Promise): Promise; diff --git a/_types/src/serviceFeatures/setupObsidian/setupManagerHandlers.d.ts b/_types/src/serviceFeatures/setupObsidian/setupManagerHandlers.d.ts deleted file mode 100644 index ea8d7c7b..00000000 --- a/_types/src/serviceFeatures/setupObsidian/setupManagerHandlers.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type SetupManager } from "@/modules/features/SetupManager"; -import type { SetupFeatureHost } from "@lib/serviceFeatures/setupObsidian/types"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -export declare function openSetupURI(setupManager: SetupManager): Promise; -export declare function openP2PSettings(host: SetupFeatureHost, setupManager: SetupManager): Promise; -export declare function useSetupManagerHandlersFeature(host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>, setupManager: SetupManager): void; diff --git a/_types/src/serviceFeatures/setupObsidian/setupProtocol.d.ts b/_types/src/serviceFeatures/setupObsidian/setupProtocol.d.ts deleted file mode 100644 index f597168c..00000000 --- a/_types/src/serviceFeatures/setupObsidian/setupProtocol.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LogFunction } from "@lib/services/lib/logUtils"; -import type { SetupFeatureHost } from "@lib/serviceFeatures/setupObsidian/types"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { type SetupManager } from "@/modules/features/SetupManager"; -export declare function registerSetupProtocolHandler(host: SetupFeatureHost, log: LogFunction, setupManager: SetupManager): void; -export declare function useSetupProtocolFeature(host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>, setupManager: SetupManager): void; diff --git a/_types/src/serviceFeatures/useP2PReplicatorUI.d.ts b/_types/src/serviceFeatures/useP2PReplicatorUI.d.ts deleted file mode 100644 index 4e838b3e..00000000 --- a/_types/src/serviceFeatures/useP2PReplicatorUI.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { type UseP2PReplicatorResult } from "@lib/replication/trystero/UseP2PReplicatorResult"; -import { P2PLogCollector } from "@lib/replication/trystero/P2PLogCollector"; -import type { LiveSyncCore } from "@/main"; -/** - * ServiceFeature: P2P Replicator lifecycle management. - * Binds a LiveSyncTrysteroReplicator to the host's lifecycle events, - * following the same middleware style as useOfflineScanner. - * - * @param viewTypeAndFactory Optional [viewType, factory] pair for registering the P2P pane view. - * When provided, also registers commands and ribbon icon via services.API. - */ -export declare function useP2PReplicatorUI(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "database" | "databaseEvents" | "keyValueDB" | "replication" | "config" | "UI" | "replicator", never>, core: LiveSyncCore, replicator: UseP2PReplicatorResult): { - replicator: import("../lib/src/replication/trystero/LiveSyncTrysteroReplicator").LiveSyncTrysteroReplicator; - p2pLogCollector: P2PLogCollector; - storeP2PStatusLine: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; -}; diff --git a/_types/src/serviceModules/DatabaseFileAccess.d.ts b/_types/src/serviceModules/DatabaseFileAccess.d.ts deleted file mode 100644 index 2cfd564f..00000000 --- a/_types/src/serviceModules/DatabaseFileAccess.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess.ts"; -import { ServiceDatabaseFileAccessBase } from "@lib/serviceModules/ServiceDatabaseFileAccessBase"; -export declare class ServiceDatabaseFileAccess extends ServiceDatabaseFileAccessBase implements DatabaseFileAccess { -} diff --git a/_types/src/serviceModules/FileAccessObsidian.d.ts b/_types/src/serviceModules/FileAccessObsidian.d.ts deleted file mode 100644 index a7a12a86..00000000 --- a/_types/src/serviceModules/FileAccessObsidian.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type App } from "@/deps"; -import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase.ts"; -import { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter"; -/** - * Obsidian-specific implementation of FileAccessBase - * Uses ObsidianFileSystemAdapter for platform-specific operations - */ -export declare class FileAccessObsidian extends FileAccessBase { - constructor(app: App, dependencies: FileAccessBaseDependencies); -} diff --git a/_types/src/serviceModules/FileHandler.d.ts b/_types/src/serviceModules/FileHandler.d.ts deleted file mode 100644 index 843572c7..00000000 --- a/_types/src/serviceModules/FileHandler.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ServiceFileHandlerBase } from "@lib/serviceModules/ServiceFileHandlerBase"; -export declare class ServiceFileHandler extends ServiceFileHandlerBase { -} diff --git a/_types/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.d.ts b/_types/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.d.ts deleted file mode 100644 index 6896bd23..00000000 --- a/_types/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types"; -import type { IConversionAdapter } from "@lib/serviceModules/adapters"; -import type { TFile, TFolder } from "obsidian"; -/** - * Conversion adapter implementation for Obsidian - */ -export declare class ObsidianConversionAdapter implements IConversionAdapter { - nativeFileToUXFileInfoStub(file: TFile): UXFileInfoStub; - nativeFolderToUXFolder(folder: TFolder): UXFolderInfo; -} diff --git a/_types/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.d.ts b/_types/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.d.ts deleted file mode 100644 index c0dfe204..00000000 --- a/_types/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, UXStat } from "@lib/common/types"; -import type { IFileSystemAdapter, IPathAdapter, ITypeGuardAdapter, IConversionAdapter, IStorageAdapter, IVaultAdapter } from "@lib/serviceModules/adapters"; -import type { TAbstractFile, TFile, TFolder, Stat, App } from "obsidian"; -declare module "obsidian" { - interface Vault { - getAbstractFileByPathInsensitive(path: string): TAbstractFile | null; - } - interface DataAdapter { - reconcileInternalFile?(path: string): Promise; - } -} -/** - * Complete file system adapter implementation for Obsidian - */ -export declare class ObsidianFileSystemAdapter implements IFileSystemAdapter { - private app; - readonly path: IPathAdapter; - readonly typeGuard: ITypeGuardAdapter; - readonly conversion: IConversionAdapter; - readonly storage: IStorageAdapter; - readonly vault: IVaultAdapter; - constructor(app: App); - getAbstractFileByPath(path: FilePath | string): Promise; - getAbstractFileByPathInsensitive(path: FilePath | string): Promise; - getFiles(): Promise; - renameFile(file: TFile, newPath: string): Promise; - statFromNative(file: TFile): Promise; - reconcileInternalFile(path: string): Promise; -} diff --git a/_types/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.d.ts b/_types/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.d.ts deleted file mode 100644 index 43421db9..00000000 --- a/_types/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type TAbstractFile } from "@/deps"; -import type { FilePath } from "@lib/common/types"; -import type { IPathAdapter } from "@lib/serviceModules/adapters"; -/** - * Path adapter implementation for Obsidian - */ -export declare class ObsidianPathAdapter implements IPathAdapter { - getPath(file: string | TAbstractFile): FilePath; - normalisePath(path: string): string; -} diff --git a/_types/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.d.ts b/_types/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.d.ts deleted file mode 100644 index ef874d13..00000000 --- a/_types/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UXDataWriteOptions } from "@lib/common/types"; -import type { IStorageAdapter } from "@lib/serviceModules/adapters"; -import type { Stat, App } from "obsidian"; -/** - * Storage adapter implementation for Obsidian - */ -export declare class ObsidianStorageAdapter implements IStorageAdapter { - private app; - constructor(app: App); - exists(path: string): Promise; - trystat(path: string): Promise; - stat(path: string): Promise; - mkdir(path: string): Promise; - remove(path: string): Promise; - read(path: string): Promise; - readBinary(path: string): Promise; - write(path: string, data: string, options?: UXDataWriteOptions): Promise; - writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise; - append(path: string, data: string, options?: UXDataWriteOptions): Promise; - list(basePath: string): Promise<{ - files: string[]; - folders: string[]; - }>; -} diff --git a/_types/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.d.ts b/_types/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.d.ts deleted file mode 100644 index 768a5721..00000000 --- a/_types/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ITypeGuardAdapter } from "@lib/serviceModules/adapters"; -import { TFile, TFolder } from "obsidian"; -/** - * Type guard adapter implementation for Obsidian - */ -export declare class ObsidianTypeGuardAdapter implements ITypeGuardAdapter { - isFile(file: unknown): file is TFile; - isFolder(item: unknown): item is TFolder; -} diff --git a/_types/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.d.ts b/_types/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.d.ts deleted file mode 100644 index 6a2eccea..00000000 --- a/_types/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UXDataWriteOptions } from "@lib/common/types"; -import type { IVaultAdapter } from "@lib/serviceModules/adapters"; -import type { TFile, App, TFolder } from "obsidian"; -/** - * Vault adapter implementation for Obsidian - */ -export declare class ObsidianVaultAdapter implements IVaultAdapter { - private app; - constructor(app: App); - read(file: TFile): Promise; - cachedRead(file: TFile): Promise; - readBinary(file: TFile): Promise; - modify(file: TFile, data: string, options?: UXDataWriteOptions): Promise; - modifyBinary(file: TFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise; - create(path: string, data: string, options?: UXDataWriteOptions): Promise; - createBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise; - rename(file: TFile, newPath: string): Promise; - delete(file: TFile | TFolder, force?: boolean): Promise; - trash(file: TFile | TFolder, force?: boolean): Promise; - trigger(name: string, ...data: unknown[]): void; -} diff --git a/_types/src/serviceModules/ServiceFileAccessImpl.d.ts b/_types/src/serviceModules/ServiceFileAccessImpl.d.ts deleted file mode 100644 index dd046dd2..00000000 --- a/_types/src/serviceModules/ServiceFileAccessImpl.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ServiceFileAccessBase } from "@lib/serviceModules/ServiceFileAccessBase"; -import type { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter"; -export declare class ServiceFileAccessObsidian extends ServiceFileAccessBase { -} diff --git a/_types/src/types.d.ts b/_types/src/types.d.ts deleted file mode 100644 index e80355b5..00000000 --- a/_types/src/types.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess"; -import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder"; -import type { IFileHandler } from "@lib/interfaces/FileHandler"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess"; -import type { IServiceHub } from "@lib/services/base/IService"; -export interface ServiceModules { - storageAccess: StorageAccess; - /** - * Database File Accessor for handling file operations related to the database, such as exporting the database, importing from a file, etc. - */ - databaseFileAccess: DatabaseFileAccess; - /** - * File Handler for handling file operations related to replication, such as resolving conflicts, applying changes from replication, etc. - */ - fileHandler: IFileHandler; - /** - * Rebuilder for handling database rebuilding operations. - */ - rebuilder: Rebuilder; -} -export interface LiveSyncHost { - services: IServiceHub; - serviceModules: ServiceModules; -} diff --git a/devs.md b/devs.md index 38432cf7..8136aac0 100644 --- a/devs.md +++ b/devs.md @@ -10,22 +10,19 @@ Self-hosted LiveSync is an Obsidian plugin for synchronising vaults across devic #### First-time Setup -This repository uses submodules by convention. Therefore, you must use the `--recursive` flag when cloning it. - ```bash -git clone --recursive https://github.com/vrtmrz/obsidian-livesync +git clone https://github.com/vrtmrz/obsidian-livesync +cd obsidian-livesync npm ci npm run build ``` -Note: if you already cloned without submodules, run: `git submodule update --init --recursive` - #### Branch switching -When switching branches, please make sure to update submodules as well, since they may be updated in the new branch. +When switching branches, reinstall dependencies when the lockfile changes. ```bash -git checkout --recurse-submodules 0.25.70-patch1 # tag or branch name +git checkout 0.25.70-patch1 # tag or branch name npm ci npm run build ``` @@ -38,7 +35,6 @@ npm run check # TypeScript and svelte type checking npm run dev # Development build with auto-rebuild (uses .env for test vault paths) npm run build # Production build npm run buildDev # Development build (one-time) -npm run bakei18n # Pre-build step: compile i18n resources (YAML → JSON → TS) npm run test:unit # Run unit tests only (no Docker services required) npm test # Run Harness based vitest tests (requires Docker services), not recommended, unstable. ``` @@ -70,7 +66,7 @@ To facilitate development and testing, the build process can automatically copy - If you add a feature that interacts with the remote database (e.g., replication changes, custom changes feed parameters, or custom HTTP queries), you strongly expected to write an integration test to verify the behaviour against a real CouchDB server. - **Browser Harness Tests** (`vitest.config.ts`): Transitional browser-based harness tests using Playwright/Chromium. Executed via `npm run test`. This layer is no longer the preferred destination for new broad E2E coverage because `test/harness` can drift from real Obsidian behaviour. - **P2P Tests** (`vitest.config.p2p.ts`): Browser-based Peer-to-Peer replication tests. Executed via `npm run test:p2p`. - - **RPC Unit Tests** (`vitest.config.rpc-unit.ts`): RPC-specific unit tests with coverage thresholds. + - **Commonlib Tests**: Commonlib owns unit and package tests for shared RPC, storage, replication, and platform contracts. LiveSync CI verifies the exact packed dependency as a downstream consumer. - **Real Obsidian E2E** (`test/e2e-obsidian/`): Local-first scripts that launch real Obsidian with temporary vaults and the built Self-hosted LiveSync plug-in. Use these for boot-up sequence, vault reflection, RedFlag flows, Fast Setup (Simple Fetch), settings dialogues, restart-sensitive workflows, Object Storage regressions, and other behaviour that depends on Obsidian itself. Run focused scripts such as `npm run test:e2e:obsidian:two-vault-sync`, or use `npm run test:e2e:obsidian:local-suite:services` to run the broader local suite with CouchDB and MinIO fixtures managed by the wrapper. - **Docker Services**: Tests require CouchDB, MinIO (S3), and P2P services: @@ -92,7 +88,7 @@ To facilitate development and testing, the build process can automatically copy ### Import Path Normalisation -The codebase uses `@/` and `@lib/` path aliases to keep import structures clean. To normalise imports and exports across files, use the following utility script: +The codebase uses the `@/` alias for source owned by this repository. Commonlib imports use explicit `@vrtmrz/livesync-commonlib` package subpaths. To normalise LiveSync-owned imports and exports, use the following utility script: ```bash npm run pretty:importpath @@ -103,18 +99,11 @@ Under the hood, this runs Deno with the script [utilsdeno/normalise-imports.ts]( - `--run`: Applies the changes (the script runs in dry-run mode by default). - `--all-alias`: Normalises sibling/child relative imports starting with `./` to use aliases. -### Type Generation +### Commonlib dependency -To generate fallback type definitions for the shared library and add appropriate Deno ignore comments (which suppresses Deno compilation warnings and linting warnings inside the `_types` directory), run: +Shared synchronisation code is compiled and typed by the `@vrtmrz/livesync-commonlib` package. `npm ci` installs the exact artefact recorded by the lockfile; this repository does not compile Commonlib source or commit fallback declarations. -```bash -npm run build:lib:types -``` - -This script executes: - -1. TypeScript compilation (`tsconfig.types.json`) to output definitions to the `_types` directory. -2. The Deno script [utilsdeno/types-add-ignore.ts](file:///p:/plant25/obsidian/projects/obsidian-livesync/utilsdeno/types-add-ignore.ts) to prepend Deno ignore comments to the generated type files. +Changes spanning both repositories must first produce a packed Commonlib artefact which passes its standalone package checks. Install that exact artefact in LiveSync, then run the LiveSync type checks, unit tests, application builds, CLI E2E, and any focused real-Obsidian E2E required by the changed boundary. Replace the temporary artefact reference with the reviewed immutable package version before release. ## Architecture @@ -149,10 +138,10 @@ Hence, the new feature should be implemented as follows: ### Key Architectural Components -- **LiveSyncLocalDB** (`src/lib/src/pouchdb/`): Local PouchDB database wrapper -- **Replicators** (`src/lib/src/replication/`): CouchDB, Journal, and MinIO sync engines +- **LiveSyncLocalDB** (`@vrtmrz/livesync-commonlib/compat/pouchdb/LiveSyncLocalDB`): Local PouchDB database wrapper +- **Replicators** (`@vrtmrz/livesync-commonlib/compat/replication/*`): CouchDB, Journal, and P2P synchronisation engines - **Service Hub** (`src/modules/services/`): Central service registry using dependency injection -- **Common Library** (`src/lib/`): Platform-independent sync logic, shared with other tools +- **Common Library** (`@vrtmrz/livesync-commonlib`): Platform-independent synchronisation logic, shared with the CLI, Webapp, WebPeer, and external tools ### Conflict Merge Policy @@ -172,15 +161,15 @@ This policy is intentionally aligned with the conflict checkboxes and compatibil - **Platform-specific code**: Use `.platform.ts` suffix (replaced with `.obsidian.ts` in production builds via esbuild) - **Development code**: Use `.dev.ts` suffix (replaced with `.prod.ts` in production) -- **Path aliases**: `@/*` maps to `src/*`, `@lib/*` maps to `src/lib/src/*` +- **Path aliases**: `@/*` maps to `src/*`; Commonlib uses package exports rather than a source alias ## Code Conventions ### Internationalisation (i18n) - **Translation Workflow**: - 1. Edit YAML files in `src/lib/src/common/messagesYAML/` (human-editable) - 2. Run `npm run bakei18n` to compile: YAML → JSON → TypeScript constants + 1. Edit the human-readable YAML files in `src/common/messagesYAML/` in the `livesync-commonlib` repository + 2. Run `npm run i18n:bake` in that repository to compile YAML → JSON → TypeScript constants 3. Use `$t()`, `$msg()` functions for translations You can also use `$f` for formatted messages with Tagged Template Literals. - **Usage**: @@ -195,7 +184,7 @@ This policy is intentionally aligned with the conflict checkboxes and compatibil - Use tagged types from `types.ts`: `FilePath`, `FilePathWithPrefix`, `DocumentID` - Prefix constants: `CHeader` (chunks), `ICHeader`/`ICHeaderEnd` (internal data) -- Path utilities in `src/lib/src/string_and_binary/path.ts`: `addPrefix()`, `stripAllPrefixes()`, `shouldBeIgnored()` +- Path utilities are supplied by the focused Commonlib compatibility path `@vrtmrz/livesync-commonlib/compat/string_and_binary/path` ### Logging & Debugging @@ -224,8 +213,8 @@ export class ModuleExample extends AbstractObsidianModule { ### Settings Management -- Settings defined in `src/lib/src/common/types.ts` (`ObsidianLiveSyncSettings`) -- Configuration metadata in `src/lib/src/common/settingConstants.ts` +- Settings are defined by Commonlib (`ObsidianLiveSyncSettings`) +- Configuration metadata is supplied by the Commonlib settings exports - Use `this.services.setting.saveSettingData()` instead of using plugin methods directly ### Database Operations @@ -272,7 +261,7 @@ In short, the situation remains unchanged for me, but it means you all become a This workflow is for maintainers. Contributors should update `## Unreleased` for user-facing feature or fix PRs, but do not need to run the release workflows. The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `release` GitHub Environment. Configure Environment protection in the repository settings so tag creation and release publication require maintainer approval. -- Run the `Prepare Release PR` workflow with the target version. It creates the release branch, updates versions, regenerates the `_types` fallback definitions used by the community plug-in scan, moves the `## Unreleased` notes to the target version, commits the release preparation, pushes the branch, and opens a draft release PR. +- Run the `Prepare Release PR` workflow with the target version. It creates the release branch, updates versions, confirms that Commonlib is locked to an immutable package version, moves the `## Unreleased` notes to the target version, commits the release preparation, pushes the branch, and opens a draft release PR. - Do not tag the release branch when the PR is first created. Polish the release PR first, especially `updates.md`. - Once the release PR head is fixed, run the `Finalise Release Tags` workflow with its full head commit SHA. It validates the release branch, ensures that both the plug-in tag (for example, `0.25.81`) and the CLI tag (for example, `0.25.81-cli`) point to that commit, and explicitly dispatches the plug-in and CLI publishing workflows. The workflow can be retried when existing tags already point to the reviewed commit, but stops if either tag points elsewhere. - The plug-in publishing workflow is intentionally dispatch-only. Pushing a plug-in tag directly does not publish a GitHub Release; use `Finalise Release Tags`, or dispatch `Release Obsidian Plugin` explicitly for recovery or a pre-release. The CLI Docker workflow retains its documented branch, tag, and manual triggers. @@ -294,7 +283,7 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel - `allow_empty_updates`: leave disabled unless the release intentionally has no user-facing notes. 3. Review the generated draft PR. - Polish `updates.md`. - - Confirm `package.json`, `manifest.json`, `versions.json`, workspace package versions, and the generated `_types` definitions. + - Confirm `package.json`, `manifest.json`, `versions.json`, workspace package versions, and the locked Commonlib package version. - Confirm that `manifest.json` has the intended `minAppVersion`. - Wait for the necessary CI checks. 4. When the PR head is fixed, run `Finalise Release Tags`. diff --git a/docs/adding_translations.md b/docs/adding_translations.md index 4aa595e2..7340607e 100644 --- a/docs/adding_translations.md +++ b/docs/adding_translations.md @@ -2,9 +2,9 @@ ## Getting ready -1. Clone this repository recursively. +1. Clone the Commonlib repository, which owns the translation catalogue. ```sh -git clone --recursive https://github.com/vrtmrz/obsidian-livesync +git clone https://github.com/vrtmrz/livesync-commonlib ``` 2. Make `ls-debug` folder under your vault's `.obsidian` folder (as like `.../dev/.obsidian/ls-debug`). @@ -12,15 +12,15 @@ git clone --recursive https://github.com/vrtmrz/obsidian-livesync 1. Install dependencies, and build the plug-in as dev. build. ```sh -cd obsidian-livesync -npm i -D -npm run buildDev +cd livesync-commonlib +npm ci +npm run i18n:bake ``` -2. Copy the `main.js` to `.obsidian/plugins/obsidian-livesync` folder of your vault, and run Obsidian-Self-hosted LiveSync. +2. Install the resulting packed Commonlib artefact in a Self-hosted LiveSync development checkout, build the plug-in, copy `main.js` to `.obsidian/plugins/obsidian-livesync` in your test Vault, and run Self-hosted LiveSync. 3. You will get the `missing-translation-yyyy-mm-dd.jsonl`, please fill in new translations. 4. Build the plug-in again, and confirm that displayed things were expected. -5. Merge them into `rosetta.ts`, and make the PR to `https://github.com/vrtmrz/livesync-commonlib`. +5. Add the translations to the Commonlib catalogue, run `npm run i18n:bake`, and make the pull request to `https://github.com/vrtmrz/livesync-commonlib`. ## Make messages to be translated @@ -31,4 +31,4 @@ npm run buildDev + Logger($tf('someKeyForPassphraseError'), LOG_LEVEL_URGENT); ``` 3. Make the PR to `https://github.com/vrtmrz/obsidian-livesync`. -4. Follow the steps of "Add translations for already defined terms" to add the translations. \ No newline at end of file +4. Follow the steps of "Add translations for already defined terms" to add the translations. diff --git a/docs/adr/2026_07_common_library_package_boundary.md b/docs/adr/2026_07_common_library_package_boundary.md index 26bc0f80..bcdddb36 100644 --- a/docs/adr/2026_07_common_library_package_boundary.md +++ b/docs/adr/2026_07_common_library_package_boundary.md @@ -2,7 +2,7 @@ ## Status -Proposed +Proposed — the implementation proof is complete locally; publication and the community-scanner preview remain pending. ## Context @@ -144,6 +144,28 @@ The common-library package publishes compiled ESM and generated declaration file Svelte source, worker query imports, AWS SDK adapters, PouchDB adapters, and Node-only crypto must not leak through the root entry point. Optional feature subpaths may carry their own heavier dependency surface. +### Platform host entries + +Platform-specific host access is explicit rather than inferred. `@vrtmrz/livesync-commonlib/node` supplies Node-only capabilities and `createNodeStorage({ rootPath })`. `@vrtmrz/livesync-commonlib/browser` supplies `createFileSystemAccessStorage({ rootHandle })` for the browser File System Access API. Browser bundles cannot resolve the Node implementation through the browser entry. + +Both storage factories receive an existing root from their host. Commonlib does not choose a process directory, present a browser directory picker, request browser permission, or persist a `FileSystemDirectoryHandle`. The CLI owns path selection and configuration. The Webapp owns user activation, permission, handle persistence, and re-authorisation. The adapters own only path containment and storage operations below the injected root. + +Here, the browser capability means the browser File System Access API, not Node's `fs` API. The package proof deliberately moves the rooted `IStorageAdapter` implementation first. The Webapp retains its LiveSync-specific `IFileSystemAdapter` composition, file-object cache, Vault semantics, picker flow, and storage-event policy. Those responsibilities can move later only with their own documented contracts; they are not implied by the low-level browser entry point. + +A later composition may place the constructed storage contract on a host-specific `ServiceContext` subtype, so services depend only on the injected capability while each platform owns context initialisation. This proof does not add storage or raw platform objects to the base `ServiceContext`: doing so would make an optional application capability mandatory for Obsidian, CLI, Webapp, WebPeer, and test contexts at the same time. A future change should inject the narrow `IStorageAdapter` contract rather than expose `FileSystemDirectoryHandle`, Node `fs`, or environment detection through the shared context. + +The paired adapters run against the same contract suite for metadata, text and binary access, append, listing, removal, missing paths, parent creation, empty-root handling, and traversal rejection. Timestamp fidelity remains platform-specific because the File System Access API does not provide the same creation-time and timestamp-setting facilities as Node. + +The Node entry also centralises direct Node built-in access needed by trusted headless application code. This is a package and scanner boundary, not an assertion that Node and browser APIs are interchangeable. Cross-platform behaviour belongs in a shared contract with separate implementations, as demonstrated by rooted storage. + +### Context and result compatibility + +`ServiceContextContract` is the minimum host-neutral composition contract. It supplies an event channel and message translator selected by the host. Obsidian and CLI contexts extend the default implementation with their own capabilities; the Webapp currently uses the default implementation. Every Service Hub and every service in one composition must retain the exact Context object supplied by that host. + +Compatibility is established through observable results, not only structural TypeScript compatibility. A shared probe verifies event delivery, translation results, and Context identity across the public Service Hub surface. Commonlib separately verifies that default contexts isolate their event channels and translators. Real Obsidian runs the same invariants against the loaded plug-in through `obsidian-cli eval`; the CLI runs its composition contract in unit tests and then exercises the built Node artefact through Deno E2E; and the Webapp composition runs the shared contract directly without depending on its currently stale Playwright workflow. + +The same rule applies as more APIs move: define the shared result set first, run the same cases against each implementation, and document platform-specific behaviour outside that result set. Matching method names or return types alone does not establish behavioural compatibility. The contract runner remains test support during this proof rather than becoming a new public package API. + ### Barrels and export surfaces The migration does not prohibit every barrel. A small root entry point or task-oriented subpath entry point is an intentional package contract when it has one documented responsibility, uses explicit named exports, and does not load unrelated implementations or optional dependencies. The root client entry point, a focused RPC entry point, and type-only storage-adapter entry points are examples which may remain after review. @@ -211,9 +233,23 @@ Every retained barrel must correspond to an explicit `exports` entry, list named - Adapt `DirectFileManipulator` behind that façade or deprecate it; do not treat its current constructor and deep dependencies as the final API. - Narrow or remove compatibility-only export paths as Self-hosted LiveSync imports migrate to documented package modules. +## Implementation Proof + +The local proof builds Commonlib `0.1.0-package-proof.8` as one compiled ESM package with a small root, `context`, `browser`, `node`, and `rpc` entries, plus 118 explicit compatibility exports required by the current downstream migration. It publishes neither raw TypeScript nor Svelte source. The reviewed tarball has integrity `sha512-XiS2BJGYQtBgTdLf9G577q+65wXKjdjFIzsLA1yWhSKgg+nyh/zREEjdAE0y07QyA0Sq8PiIruZB7cYTpqKjvQ==`. It can be installed into a clean consumer, imported in Node, type-checked from declarations, and bundled independently for browser context, browser storage, browser services, and workers. + +The proof found and fixed three boundary defects which source-alias consumption had hidden: compiled JSON imports required explicit output extensions, precompiled Svelte output could not safely be treated as source by the downstream Svelte pipeline, and Vite's default client conditions selected Commonlib's browser worker while building the Node CLI. Packed-consumer regressions cover the first two. The CLI now uses Vite's server conditions and treats every Node built-in reported by Commonlib's Node entry as external; the built CLI is exercised through Deno E2E. Importing root or context also no longer patches DOM prototypes, and translator injection prevents the context entry from loading the complete language catalogue. + +Self-hosted LiveSync, its CLI, Webapp, WebPeer, plug-in source, and tests compile against that exact local tarball without `@lib/*`. Focused downstream storage tests pass against the package-owned Node and File System Access API implementations. The old `src/lib` Git submodule, generated `_types` fallback, type-generation scripts, and source aliases are removed by the proof branch. + +The Commonlib contract suite passes 16 tests covering Context results and both platform storage implementations; its complete suite passes 1,087 tests across 56 files. Self-hosted LiveSync's three host-composition contract tests and complete 333-test unit suite pass against the tarball. The plug-in, CLI, Webapp, and WebPeer production builds also pass. The CLI contract command completes its nine-step Deno workflow against the built Node artefact. + +The local real-Obsidian suite verifies the actual loaded `ObsidianServiceContext`, all 18 services, Vault reflection, CouchDB and Object Storage transfer, remote-activity accounting, CLI-to-Obsidian encrypted synchronisation, startup scanning, two-Vault create, update, delete, ordinary rename, case-only rename, target mismatch, Hidden File Sync, Customisation Sync, and setting Markdown export. These checks establish observable results and host composition rather than relying on declaration compatibility alone. + +The current browser Harness has a pre-existing settings-migration initialisation timeout which reproduces on the untouched baseline. The Webapp Playwright workflow also currently reuses an unrelated process on its configured port and reaches a `Not Found` page. Neither failure is evidence for or against the package boundary. The Webapp production build and direct composition contract pass, while Webapp browser E2E remains supplementary until its runner is repaired. Current acceptance therefore relies primarily on Commonlib owner and packed-consumer tests, LiveSync unit and integration tests, Deno CLI E2E, application production builds, and the focused local real-Obsidian suite. The unrelated Harness and Playwright defects should be tracked independently. + ## Scanner and Repository Consequences -Consuming the npm package is expected to remove the common-library source and generated `_types` from the plug-in repository's community scan, because package dependencies are treated as dependencies rather than maintained plug-in source. This expectation must be verified with the scanner preview before removing the old integration. +Consuming the npm package is expected to remove the common-library source and generated `_types` from the plug-in repository's community scan, because package dependencies are treated as dependencies rather than maintained plug-in source. The proof removes the old integration, but this scanner expectation must still be verified before the decision is accepted and released. The change does not hide or resolve warnings in Self-hosted LiveSync-owned source. The scanner will continue to inspect the plug-in, CLI, Webapp, and WebPeer while those applications remain in the repository. Moving those applications to a LiveSync-family application repository may be considered after the package boundary is stable, but it is not part of this decision because the CLI and Webapp still import shared plug-in composition code. @@ -248,8 +284,8 @@ The migration is complete only when: - a headless client does not bundle Svelte or the full language catalogue; - retained entry-point barrels do not load unrelated optional implementations, and no removed barrel is replaced by unrestricted deep exports; - two clients with different database, encryption, language, and lifecycle settings can coexist without sharing mutable client state; -- the Fancy Kit Modal host proves one-shot settlement, cancellation, disposer execution, focus, viewport containment, safe-area containment, and touch-target layout in its own unit and real-Obsidian Harness tests; -- Self-hosted LiveSync verifies its Svelte adapter and workflow policy through injected tests, with a focused composition smoke test when the adapter is first adopted rather than duplicating every Kit-owned device test; +- Self-hosted LiveSync verifies its local Svelte adapter and workflow policy through injected tests and a focused composition smoke test; +- if the framework-neutral Modal host is later promoted to Fancy Kit, Kit-owned lifecycle, viewport, safe-area, and touch-target guarantees replace duplicated device tests in LiveSync; - Self-hosted LiveSync no longer has `src/lib`, `_types`, or `@lib/*` source aliases; - the CLI, Webapp, WebPeer, plug-in build, unit tests, integration tests, and focused real-Obsidian E2E pass against the reviewed package artefact; - common-library downstream CI records and tests the exact Self-hosted LiveSync ref; diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 23b8fe9b..e2e16e8e 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -73,8 +73,12 @@ const moduleAliasPlugin = { const removePragmaCommentsPlugin = { name: "remove-pragma-comments", setup(build) { + const sourceRoot = `${path.resolve("src")}${path.sep}`; // Filter target extensions (e.g., JavaScript and TypeScript) build.onLoad({ filter: /\.[jt]s?$/ }, async (args) => { + // Dependencies are already compiled and may contain comment-like text inside strings. + // Only maintained plug-in source needs its local suppression directives removed. + if (!args.path.startsWith(sourceRoot)) return; const source = await fs.promises.readFile(args.path, "utf8"); // Regex targeting both single-line and multi-line comments diff --git a/eslint.config.common.mjs b/eslint.config.common.mjs index e9e36021..546afcec 100644 --- a/eslint.config.common.mjs +++ b/eslint.config.common.mjs @@ -112,7 +112,6 @@ export const ImportAliasRules = (base) => ({ aliasForSubpaths: true, alias: { "@": `${base}/src`, - "@lib": `${base}/src/lib/src`, }, }, ], diff --git a/eslint.config.mjs b/eslint.config.mjs index 69384d2e..9bf2d358 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -26,17 +26,6 @@ export default defineConfig([ "src/apps", "utils", - // Specific exclusions from common library (src/lib) - "src/lib/coverage", - "src/lib/browsertest", - "src/lib/test", - "src/lib/_tools", - "src/lib/src/patches/pouchdb-utils", - "src/lib/src/cli", - "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", @@ -59,7 +48,6 @@ export default defineConfig([ importAlias.configs.recommended, { files: ["**/*.ts"], - // ignores:["src/lib/**/*.ts"], // Exclude library files from root linting (they have different environments and rules). languageOptions: { globals: { ...globals.browser, PouchDB: "readonly" }, parser: tsParser, diff --git a/generate-types.mjs b/generate-types.mjs deleted file mode 100644 index 868e6fae..00000000 --- a/generate-types.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import { execSync } from "node:child_process"; -import fs from "node:fs"; -try { - fs.rmSync("./_types", { recursive: true, force: true }); - fs.mkdirSync("./_types"); - console.log("[Postbuild] Generating type definitions for fallback..."); - execSync("npx tsc -p tsconfig.types.json", { stdio: "inherit" }); - - console.log("[Postbuild] Type definitions generated successfully."); -} catch (error) { - // Ignore compiler errors from tsc so that pre-existing type errors in the submodule - // do not block the build from succeeding. - console.warn("[Postbuild] Type definitions generated with some compilation warnings."); - // process.exit(-1); -} - -try { - console.log("[Postbuild] Type definitions generated successfully. Adding ignore comments..."); - execSync("deno run -A ./utilsdeno/types-add-ignore.ts", { stdio: "inherit" }); - console.log("[Postbuild] Ignore comments added successfully."); -} catch (error) { - console.warn("[Postbuild] Failed to add ignore comments to type definitions."); - process.exit(-1); -} diff --git a/package-lock.json b/package-lock.json index 56640f4f..40be13dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@trystero-p2p/nostr": "^0.24.0", + "@vrtmrz/livesync-commonlib": "file:../livesync-commonlib-package-boundary/artifacts/vrtmrz-livesync-commonlib-0.1.0-package-proof.8.tgz", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", "idb": "^8.0.3", @@ -2300,7 +2301,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2311,7 +2312,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2322,7 +2323,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2343,14 +2344,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3877,7 +3878,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -4413,7 +4414,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/which": { @@ -4946,6 +4947,53 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vrtmrz/livesync-commonlib": { + "version": "0.1.0-package-proof.8", + "resolved": "file:../livesync-commonlib-package-boundary/artifacts/vrtmrz-livesync-commonlib-0.1.0-package-proof.8.tgz", + "integrity": "sha512-XiS2BJGYQtBgTdLf9G577q+65wXKjdjFIzsLA1yWhSKgg+nyh/zREEjdAE0y07QyA0Sq8PiIruZB7cYTpqKjvQ==", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-s3": "^3.808.0", + "@smithy/fetch-http-handler": "^5.3.10", + "@smithy/md5-js": "^4.2.9", + "@smithy/middleware-apply-body-checksum": "^4.3.9", + "@smithy/types": "^4.14.3", + "@smithy/util-retry": "^4.4.5", + "@trystero-p2p/nostr": "^0.24.0", + "diff-match-patch": "^1.0.5", + "events": "^3.3.0", + "fflate": "^0.8.2", + "idb": "^8.0.3", + "markdown-it": "^14.2.0", + "minimatch": "^10.2.5", + "octagonal-wheels": "^0.1.51", + "pouchdb-adapter-http": "^9.0.0", + "pouchdb-adapter-idb": "^9.0.0", + "pouchdb-adapter-indexeddb": "^9.0.0", + "pouchdb-adapter-memory": "^9.0.0", + "pouchdb-core": "^9.0.0", + "pouchdb-errors": "^9.0.0", + "pouchdb-find": "^9.0.0", + "pouchdb-mapreduce": "^9.0.0", + "pouchdb-merge": "^9.0.0", + "pouchdb-replication": "^9.0.0", + "pouchdb-utils": "^9.0.0", + "qrcode-generator": "^1.4.4", + "transform-pouch": "^2.0.0", + "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "svelte": "5.56.3" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, "node_modules/@vrtmrz/obsidian-test-session": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-test-session/-/obsidian-test-session-0.1.0.tgz", @@ -5227,7 +5275,7 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "bin": { @@ -5520,7 +5568,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -5782,7 +5830,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -6404,7 +6452,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -6892,7 +6940,7 @@ "version": "5.8.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/diff-match-patch": { @@ -8355,7 +8403,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/espree": { @@ -8420,7 +8468,7 @@ "version": "2.2.11", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.11.tgz", "integrity": "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -8491,7 +8539,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.x" @@ -8895,7 +8942,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true, "license": "MIT" }, "node_modules/functions-have-names": { @@ -9884,7 +9930,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.6" @@ -11051,7 +11097,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/locate-path": { @@ -11165,7 +11211,7 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -11257,7 +11303,6 @@ "resolved": "https://registry.npmjs.org/memdown/-/memdown-1.4.1.tgz", "integrity": "sha512-iVrGHZB8i4OQfM155xx8akvG9FIj+ht14DX5CQkCTG4EHzZ3d3sgckIf/Lm9ivZalEsFuEVnWv2B2WZvbrro2w==", "deprecated": "Superseded by memory-level (https://github.com/Level/community#faq)", - "dev": true, "license": "MIT", "dependencies": { "abstract-leveldown": "~2.7.1", @@ -11273,7 +11318,6 @@ "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.7.2.tgz", "integrity": "sha512-+OVvxH2rHVEhWLdbudP6p0+dNMXu8JA1CbhP19T8paTYAcX7oJ4OVjT+ZUVpv7mITxXHqDMej+GdqXBmXkw09w==", "deprecated": "Superseded by abstract-level (https://github.com/Level/community#faq)", - "dev": true, "license": "MIT", "dependencies": { "xtend": "~4.0.0" @@ -12362,7 +12406,6 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/pouchdb-adapter-idb/-/pouchdb-adapter-idb-9.0.0.tgz", "integrity": "sha512-2oLlgwMyOQwdKuzrEmOv8T7jFVgX7JgT4Cr81zX3eiiRClp7xXGgjv41ZRdVCAbM530sIN8BudafaQRVFKRVmA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "pouchdb-adapter-utils": "9.0.0", @@ -12377,7 +12420,6 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/pouchdb-adapter-indexeddb/-/pouchdb-adapter-indexeddb-9.0.0.tgz", "integrity": "sha512-/mcCbnVR0VKwtVZWKf8lVSdADLD0yApjFudu4d+0jeLWAeBSGZBRKYlogz2PGs4uTA7GVc2TXjVCNGUdkCM9ZQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "pouchdb-adapter-utils": "9.0.0", @@ -12426,7 +12468,6 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/pouchdb-adapter-memory/-/pouchdb-adapter-memory-9.0.0.tgz", "integrity": "sha512-XbCwJ5f5U9dGdkiDikzYjTebdPHuA6Ghylx1Pq0lDe4y6l8R9xhjDSUy56pJ8G2F4Z+8QdB5FBY9EQoFlFSXWQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "memdown": "1.4.1", @@ -13208,7 +13249,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/safe-push-apply": { @@ -14040,7 +14080,7 @@ "version": "5.56.3", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.3.tgz", "integrity": "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -16164,7 +16204,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/zip-stream": { @@ -16244,7 +16284,6 @@ "werift": "^0.23.0" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^7.1.2", "typescript": "5.9.3", "vite": "^8.0.16", "vitest": "^4.1.8" diff --git a/package.json b/package.json index f738ae5c..f7fef73a 100644 --- a/package.json +++ b/package.json @@ -5,18 +5,8 @@ "main": "main.js", "type": "module", "scripts": { - "bakei18n": "npm run i18n:yaml2json && npm run i18n:bakejson", - "i18n:bakejson": "npx tsx ./src/lib/_tools/bakei18n.ts", - "i18n:yaml2json": "npx tsx ./src/lib/_tools/yaml2json.ts", - "i18n:json2yaml": "npx tsx ./src/lib/_tools/json2yaml.ts", - "prettyjson": "prettier --config ./.prettierrc.mjs ./src/lib/src/common/messagesJson/*.json --write --log-level error", - "postbakei18n": "prettier --config ./.prettierrc.mjs ./src/lib/src/common/messages/*.ts --write --log-level error", - "posti18n:yaml2json": "npm run prettyjson", - "predev": "npm run bakei18n", "dev": "node --env-file=.env esbuild.config.mjs", - "prebuild": "npm run bakei18n", "build": "node esbuild.config.mjs production", - "build:lib:types": "node generate-types.mjs", "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", @@ -29,11 +19,14 @@ "prettyCheck": "npm run prettyNoWrite -- --check", "prettyNoWrite": "prettier --config ./.prettierrc.mjs \"**/*.js\" \"**/*.ts\" \"**/*.json\" ", "check:compatibility": "node utils/check-compatibility.js --file main.js --ios 15", - "precheck": "npm run build:lib:types", "check": "npm run tsc-check && npm run lint && npm run svelte-check && npm run check:compatibility", "unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/", "test": "vitest run", "test:unit": "vitest run --config vitest.config.unit.ts", + "test:contract:contexts": "vitest run --config vitest.config.unit.ts src/modules/services/ObsidianServiceContext.unit.spec.ts src/apps/cli/services/NodeServiceContext.unit.spec.ts src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts", + "test:contract:context:webapp": "vitest run --config vitest.config.unit.ts src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts", + "test:contract:context:cli": "npm run build --workspace self-hosted-livesync-cli && deno task --cwd src/apps/cli/testdeno test:setup-put-cat", + "test:contract:context:obsidian": "npm run build && npm run test:e2e:obsidian:smoke", "test:integration": "npx dotenv-cli -e .env -e .test.env -- vitest run --config vitest.config.integration.ts", "test:unit:coverage": "vitest run --config vitest.config.unit.ts --coverage", "test:install-playwright": "npx playwright install chromium", @@ -85,6 +78,8 @@ "license": "MIT", "devDependencies": { "@dword-design/eslint-plugin-import-alias": "^8.1.8", + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", "@eslint/js": "^9.39.3", "@playwright/test": "^1.58.2", "@sveltejs/vite-plugin-svelte": "^7.1.2", @@ -144,9 +139,7 @@ "vite": "^8.0.16", "vitest": "^4.1.8", "webdriverio": "^9.27.0", - "yaml": "^2.8.2", - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1" + "yaml": "^2.8.2" }, "dependencies": { "@aws-sdk/client-s3": "^3.808.0", @@ -158,6 +151,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@trystero-p2p/nostr": "^0.24.0", + "@vrtmrz/livesync-commonlib": "file:../livesync-commonlib-package-boundary/artifacts/vrtmrz-livesync-commonlib-0.1.0-package-proof.8.tgz", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", "idb": "^8.0.3", diff --git a/src/LiveSyncBaseCore.ts b/src/LiveSyncBaseCore.ts index ca4f6a2a..c4ad902d 100644 --- a/src/LiveSyncBaseCore.ts +++ b/src/LiveSyncBaseCore.ts @@ -1,22 +1,22 @@ 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 { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "@lib/common/types"; -import { __$checkInstanceBinding } from "@lib/dev/checks"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess"; -import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder"; -import type { IFileHandler } from "@lib/interfaces/FileHandler"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess"; -import type { LiveSyncLocalDBEnv } from "@lib/pouchdb/LiveSyncLocalDB"; -import type { LiveSyncCouchDBReplicatorEnv } from "@lib/replication/couchdb/LiveSyncReplicator"; -import type { CheckPointInfo } from "@lib/replication/journal/JournalSyncTypes"; -import type { LiveSyncJournalReplicatorEnv } from "@lib/replication/journal/LiveSyncJournalReplicatorEnv"; -import type { LiveSyncReplicatorEnv } from "@lib/replication/LiveSyncAbstractReplicator"; -import { useTargetFilters } from "@lib/serviceFeatures/targetFilter"; -import { useRemoteConfigurationMigration } from "@lib/serviceFeatures/remoteConfig"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices"; +import type { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { __$checkInstanceBinding } from "@vrtmrz/livesync-commonlib/compat/dev/checks"; +import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm"; +import type { DatabaseFileAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseFileAccess"; +import type { Rebuilder } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseRebuilder"; +import type { IFileHandler } from "@vrtmrz/livesync-commonlib/compat/interfaces/FileHandler"; +import type { StorageAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/StorageAccess"; +import type { LiveSyncLocalDBEnv } from "@vrtmrz/livesync-commonlib/compat/pouchdb/LiveSyncLocalDB"; +import type { LiveSyncCouchDBReplicatorEnv } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator"; +import type { CheckPointInfo } from "@vrtmrz/livesync-commonlib/compat/replication/journal/JournalSyncTypes"; +import type { LiveSyncJournalReplicatorEnv } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicatorEnv"; +import type { LiveSyncReplicatorEnv } from "@vrtmrz/livesync-commonlib/compat/replication/LiveSyncAbstractReplicator"; +import { useTargetFilters } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/targetFilter"; +import { useRemoteConfigurationMigration } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig"; +import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase"; +import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub"; import { AbstractModule } from "./modules/AbstractModule"; import { ModulePeriodicProcess } from "./modules/core/ModulePeriodicProcess"; import { ModuleReplicator } from "./modules/core/ModuleReplicator"; @@ -26,10 +26,10 @@ import { ModuleConflictChecker } from "./modules/coreFeatures/ModuleConflictChec import { ModuleConflictResolver } from "./modules/coreFeatures/ModuleConflictResolver"; import { ModuleResolvingMismatchedTweaks } from "./modules/coreFeatures/ModuleResolveMismatchedTweaks"; import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain"; -import type { ServiceModules } from "@lib/interfaces/ServiceModule"; +import type { ServiceModules } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; import { ModuleBasicMenu } from "./modules/essential/ModuleBasicMenu"; -import { usePrepareDatabaseForUse } from "@lib/serviceFeatures/prepareDatabaseForUse"; -import type { Constructor } from "@lib/common/utils.type"; +import { usePrepareDatabaseForUse } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/prepareDatabaseForUse"; +import type { Constructor } from "@vrtmrz/livesync-commonlib/compat/common/utils.type"; export class LiveSyncBaseCore< T extends ServiceContext = ServiceContext, diff --git a/src/apps/_test/storageAdapterContract.ts b/src/apps/_test/storageAdapterContract.ts index 5693e198..0d325c7e 100644 --- a/src/apps/_test/storageAdapterContract.ts +++ b/src/apps/_test/storageAdapterContract.ts @@ -1,4 +1,4 @@ -import type { IStorageAdapter } from "@lib/serviceModules/adapters"; +import type { IStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters"; /** One platform-neutral storage adapter contract case. */ export interface StorageAdapterContractCase { diff --git a/src/apps/browser/BrowserConfirm.ts b/src/apps/browser/BrowserConfirm.ts new file mode 100644 index 00000000..d4d178dc --- /dev/null +++ b/src/apps/browser/BrowserConfirm.ts @@ -0,0 +1,107 @@ +import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm"; + +import MessageBox from "./ui/MessageBox.svelte"; +import TextInputBox from "./ui/TextInputBox.svelte"; + +import { mount } from "svelte"; +import { promiseWithResolvers } from "octagonal-wheels/promises"; +import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase"; +import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; + +function displayMessageBox( + message: string, + buttons: U, + title: string, + commit: (ret: U[number]) => T +): Promise { + const el = _activeDocument.createElement("div"); + const p = promiseWithResolvers(); + mount(MessageBox, { + target: el, + props: { + message, + buttons: buttons as string[], + title: title, + commit: (action: U[number]) => { + const ret = commit(action); + p.resolve(ret); + }, + }, + }); + _activeDocument.body.appendChild(el); + void p.promise.finally(() => { + el.remove(); + }); + return p.promise; +} +function promptForInput( + title: string, + key: string, + placeholder: string, + isPassword?: boolean +): Promise { + const el = _activeDocument.createElement("div"); + const p = promiseWithResolvers(); + mount(TextInputBox, { + target: el, + props: { + title, + message: key, + placeholder, + isPassword, + commit: (text: string | false) => { + p.resolve(text); + }, + }, + }); + _activeDocument.body.appendChild(el); + void p.promise.finally(() => { + el.remove(); + }); + return p.promise; +} + +export class BrowserConfirm implements Confirm { + _context: T; + constructor(context: T) { + this._context = context; + } + askYesNo(message: string): Promise<"yes" | "no"> { + return displayMessageBox(message, ["Yes", "No"] as const, "Confirm", (action) => + action == "Yes" ? "yes" : "no" + ); + } + askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise { + return promptForInput(title, key, placeholder, isPassword); + } + askYesNoDialog( + message: string, + opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } + ): Promise<"yes" | "no"> { + return displayMessageBox(message, ["Yes", "No"] as const, opt.title ?? "Confirm", (action) => + action == "Yes" ? "yes" : "no" + ); + } + askSelectString(message: string, items: string[]): Promise { + return displayMessageBox(message, [...items] as const, "Confirm", (action) => action); + } + askSelectStringDialogue( + message: string, + buttons: T, + opt: { title?: string; defaultAction: T[number]; timeout?: number } + ): Promise { + return displayMessageBox(message, [...buttons] as const, opt.title ?? "Confirm", (action) => action); + } + askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void { + throw new Error("Method not implemented."); + } + confirmWithMessage( + title: string, + contentMd: string, + buttons: string[], + defaultAction: (typeof buttons)[number], + timeout?: number + ): Promise<(typeof buttons)[number] | false> { + return displayMessageBox(contentMd, [...buttons] as const, title ?? "Confirm", (action) => action); + } +} diff --git a/src/apps/browser/BrowserMenu.ts b/src/apps/browser/BrowserMenu.ts new file mode 100644 index 00000000..efbdf892 --- /dev/null +++ b/src/apps/browser/BrowserMenu.ts @@ -0,0 +1,71 @@ +import { promiseWithResolver, type PromiseWithResolvers } from "octagonal-wheels/promises"; +import { mount } from "svelte"; +import MenuView from "./ui/MenuView.svelte"; +import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; + +export class MenuItem { + type = "item"; + title = ""; + handler?: () => void | Promise; + icon: string = ""; + setTitle(title: string) { + this.title = title; + return this; + } + onClick(callback: () => void | Promise) { + this.handler = callback; + return this; + } + setIcon(icon: string | null) { + this.icon = icon || ""; + return this; + } +} +export class MenuSeparator { + type = "separator"; +} +export class Menu { + type = "menu"; + items: (MenuItem | MenuSeparator)[] = []; + + constructor() {} + addItem(callback: (item: MenuItem) => void) { + const item = new MenuItem(); + callback(item); + this.items.push(item); + return this; + } + addSeparator() { + this.items.push(new MenuSeparator()); + return this; + } + waitingForClose?: PromiseWithResolvers; + showAtPosition(pos: { x: number; y: number }) { + const el = _activeDocument.createElement("div"); + if (this.waitingForClose) { + this.waitingForClose.resolve(); + } + this.waitingForClose = promiseWithResolver(); + mount(MenuView, { + target: el, + props: { + items: this.items, + closeMenu: () => { + this.waitingForClose?.resolve(); + this.waitingForClose = undefined; + }, + x: pos.x, + y: pos.y, + }, + }); + _activeDocument.body.appendChild(el); + void this.waitingForClose.promise.finally(() => { + el.remove(); + }); + return this.waitingForClose.promise; + } + hide() { + this.waitingForClose?.resolve(); + this.waitingForClose = undefined; + } +} diff --git a/src/apps/browser/BrowserSvelteDialogManager.ts b/src/apps/browser/BrowserSvelteDialogManager.ts new file mode 100644 index 00000000..f14a98c7 --- /dev/null +++ b/src/apps/browser/BrowserSvelteDialogManager.ts @@ -0,0 +1,79 @@ +import { + type ComponentHasResult, + SvelteDialogManagerBase, + SvelteDialogMixIn, +} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog"; +import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase"; +import type { SvelteDialogManagerDependencies } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog"; +import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; +import DialogHost from "@/modules/services/LiveSyncUI/DialogHost.svelte"; + +export class ShimModal { + contentEl: HTMLElement; + titleEl: HTMLElement; + modalEl: HTMLElement; + isOpen: boolean = false; + baseEl: HTMLElement; + constructor() { + const baseEl = _activeDocument.createElement("popup"); + this.baseEl = baseEl; + this.contentEl = _activeDocument.createElement("div"); + this.contentEl.className = "modal-content"; + this.titleEl = _activeDocument.createElement("div"); + this.titleEl.className = "modal-title"; + this.modalEl = _activeDocument.createElement("div"); + this.modalEl.className = "modal"; + this.modalEl.style.display = "none"; + this.modalEl.appendChild(this.titleEl); + this.modalEl.appendChild(this.contentEl); + this.baseEl.appendChild(this.modalEl); + } + open() { + this.isOpen = true; + this.modalEl.style.display = "block"; + if (!this.baseEl.parentElement) { + _activeDocument.body.appendChild(this.baseEl); + } + this.onOpen(); + } + close() { + this.isOpen = false; + this.baseEl.style.display = "none"; + this.baseEl.remove(); + this.onClose(); + } + onOpen() {} + onClose() {} + setPlaceholder(p: string) {} + setTitle(t: string) { + this.titleEl.textContent = t; + } +} + +const BrowserSvelteDialogBase = SvelteDialogMixIn(ShimModal, DialogHost); + +export class LiveSyncBrowserDialog extends BrowserSvelteDialogBase< + T, + U, + C +> { + constructor( + context: C, + dependents: SvelteDialogManagerDependencies, + component: ComponentHasResult, + initialData?: U + ) { + super(); + this.initDialog(context, dependents, component, initialData); + } +} +export class BrowserSvelteDialogManager extends SvelteDialogManagerBase { + override async openSvelteDialog( + component: ComponentHasResult, + initialData?: TU + ): Promise { + const dialog = new LiveSyncBrowserDialog(this.context, this.dependents, component, initialData); + dialog.open(); + return await dialog.waitForClose(); + } +} diff --git a/src/apps/browser/LiveSyncBrowserUIService.ts b/src/apps/browser/LiveSyncBrowserUIService.ts new file mode 100644 index 00000000..406c3095 --- /dev/null +++ b/src/apps/browser/LiveSyncBrowserUIService.ts @@ -0,0 +1,25 @@ +import type { BrowserServiceHostDependencies } from "@vrtmrz/livesync-commonlib/compat/services/BrowserServices"; +import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase"; +import { UIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/UIService"; +import DialogToCopy from "@/modules/services/LiveSyncUI/dialogues/DialogueToCopy.svelte"; +import { BrowserSvelteDialogManager } from "./BrowserSvelteDialogManager"; + +export class LiveSyncBrowserUIService extends UIService { + override get dialogToCopy() { + return DialogToCopy; + } + constructor(context: T, dependents: BrowserServiceHostDependencies) { + const browserConfirm = dependents.API.confirm; + const obsidianSvelteDialogManager = new BrowserSvelteDialogManager(context, { + appLifecycle: dependents.appLifecycle, + config: dependents.config, + replicator: dependents.replicator, + confirm: browserConfirm, + control: dependents.control, + }); + super(context, { + dialogManager: obsidianSvelteDialogManager, + APIService: dependents.API, + }); + } +} diff --git a/src/apps/browser/createLiveSyncBrowserServiceHub.ts b/src/apps/browser/createLiveSyncBrowserServiceHub.ts new file mode 100644 index 00000000..0a796d72 --- /dev/null +++ b/src/apps/browser/createLiveSyncBrowserServiceHub.ts @@ -0,0 +1,33 @@ +import { BrowserServiceHub, type BrowserServiceHost } from "@vrtmrz/livesync-commonlib/compat/services/BrowserServices"; +import type { KeyValueDatabaseFactory } from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase"; +import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase"; +import { BrowserAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/browser/BrowserAPIService"; +import { BrowserConfirm } from "./BrowserConfirm"; +import { LiveSyncBrowserUIService } from "./LiveSyncBrowserUIService"; + +export type LiveSyncBrowserServiceHubOptions = { + context?: T; + openKeyValueDatabase?: KeyValueDatabaseFactory; +}; + +function createLiveSyncBrowserHost(): BrowserServiceHost { + return { + createAPI(context) { + return new BrowserAPIService(context, { + confirm: new BrowserConfirm(context), + }); + }, + createUI(context, dependencies) { + return new LiveSyncBrowserUIService(context, dependencies); + }, + }; +} + +export function createLiveSyncBrowserServiceHub( + options: LiveSyncBrowserServiceHubOptions = {} +): BrowserServiceHub { + return new BrowserServiceHub({ + ...options, + host: createLiveSyncBrowserHost(), + }); +} diff --git a/src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts b/src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts new file mode 100644 index 00000000..bae3ac92 --- /dev/null +++ b/src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts @@ -0,0 +1,28 @@ +import { createServiceContext } from "@vrtmrz/livesync-commonlib/context"; +import { describe, expect, it } from "vitest"; + +import { + observeServiceComposition, + observeServiceContext, + SERVICE_CONTEXT_MEMBERS, +} from "../../../test/contracts/serviceContext"; +import { createLiveSyncBrowserServiceHub } from "./createLiveSyncBrowserServiceHub"; + +describe("LiveSync browser service context contract", () => { + it("preserves one injected context and its API results throughout the Webapp composition", () => { + const context = createServiceContext({ + translate: (key) => `webapp:${key}`, + }); + const hub = createLiveSyncBrowserServiceHub({ context }); + + expect(observeServiceContext(context, "message")).toEqual({ + translation: "webapp:message", + receivedEvents: ["context-contract-event"], + }); + const composition = observeServiceComposition(hub, context); + expect(composition.hubUsesExpectedContext).toBe(true); + expect(SERVICE_CONTEXT_MEMBERS.filter((member) => !composition.servicesUsingExpectedContext[member])).toEqual( + [] + ); + }); +}); diff --git a/src/apps/browser/ui/MenuItemView.svelte b/src/apps/browser/ui/MenuItemView.svelte new file mode 100644 index 00000000..d7d74e48 --- /dev/null +++ b/src/apps/browser/ui/MenuItemView.svelte @@ -0,0 +1,51 @@ + + +
  • + {renderIcon(item)} + +
  • + + diff --git a/src/apps/browser/ui/MenuSeparatorView.svelte b/src/apps/browser/ui/MenuSeparatorView.svelte new file mode 100644 index 00000000..071a14d4 --- /dev/null +++ b/src/apps/browser/ui/MenuSeparatorView.svelte @@ -0,0 +1,10 @@ + + +
    diff --git a/src/apps/browser/ui/MenuView.svelte b/src/apps/browser/ui/MenuView.svelte new file mode 100644 index 00000000..53b70b19 --- /dev/null +++ b/src/apps/browser/ui/MenuView.svelte @@ -0,0 +1,89 @@ + + + + + +
    closeMenu()} onkeydown={handleKey} role="none">
    + + diff --git a/src/apps/browser/ui/MessageBox.svelte b/src/apps/browser/ui/MessageBox.svelte new file mode 100644 index 00000000..bcd308ad --- /dev/null +++ b/src/apps/browser/ui/MessageBox.svelte @@ -0,0 +1,127 @@ + + + +
    {title}
    +
    {@html renderedMessage}
    +
    + {#each buttons as button} + + {/each} +
    +
    +
    commit("")} onkeydown={handleEsc} role="none">
    + + diff --git a/src/apps/browser/ui/TextInputBox.svelte b/src/apps/browser/ui/TextInputBox.svelte new file mode 100644 index 00000000..96a53415 --- /dev/null +++ b/src/apps/browser/ui/TextInputBox.svelte @@ -0,0 +1,122 @@ + + + +
    {title}
    +
    +
    {message}
    +
    + +
    +
    + +
    + + +
    +
    +
    + + diff --git a/src/apps/browser/ui/renderMessageMarkdown.ts b/src/apps/browser/ui/renderMessageMarkdown.ts new file mode 100644 index 00000000..8e937561 --- /dev/null +++ b/src/apps/browser/ui/renderMessageMarkdown.ts @@ -0,0 +1,21 @@ +import MarkdownIt from "markdown-it"; + +const markdownRenderer = new MarkdownIt({ + html: false, + breaks: true, + linkify: true, +}); + +const defaultLinkOpenRenderer = + markdownRenderer.renderer.rules.link_open ?? + ((tokens, idx, options, _env, self) => self.renderToken(tokens, idx, options)); + +markdownRenderer.renderer.rules.link_open = (tokens, idx, options, env, self) => { + tokens[idx].attrSet("target", "_blank"); + tokens[idx].attrSet("rel", "noopener noreferrer"); + return defaultLinkOpenRenderer(tokens, idx, options, env, self); +}; + +export function renderMessageMarkdown(message: string): string { + return markdownRenderer.render(message); +} diff --git a/src/apps/browser/ui/renderMessageMarkdown.unit.spec.ts b/src/apps/browser/ui/renderMessageMarkdown.unit.spec.ts new file mode 100644 index 00000000..cb5a53a6 --- /dev/null +++ b/src/apps/browser/ui/renderMessageMarkdown.unit.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; + +import { renderMessageMarkdown } from "./renderMessageMarkdown"; + +describe("renderMessageMarkdown", () => { + it("renders basic markdown features used by browser dialogues", () => { + const html = renderMessageMarkdown("# Title\n\n| left | right |\n| --- | --- |\n| a | b |\n"); + + expect(html).toContain("

    Title

    "); + expect(html).toContain(""); + expect(html).toContain(""); + }); + + it("escapes inline HTML instead of rendering it", () => { + const html = renderMessageMarkdown("BeforeAfter"); + + expect(html).not.toContain(" + +
    + +
    + + diff --git a/src/modules/services/LiveSyncUI/components/Check.svelte b/src/modules/services/LiveSyncUI/components/Check.svelte new file mode 100644 index 00000000..aa5e8862 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Check.svelte @@ -0,0 +1,53 @@ + + + +
    + + {#if value && noteOnSelected} + {@render noteOnSelected()} + {:else if !value && noteOnUnselected} + {@render noteOnUnselected()} + {/if} + {@render children?.()} +
    + + diff --git a/src/modules/services/LiveSyncUI/components/Decision.svelte b/src/modules/services/LiveSyncUI/components/Decision.svelte new file mode 100644 index 00000000..ee4cdaa0 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Decision.svelte @@ -0,0 +1,24 @@ + + + diff --git a/src/modules/services/LiveSyncUI/components/DialogHeader.svelte b/src/modules/services/LiveSyncUI/components/DialogHeader.svelte new file mode 100644 index 00000000..c717d30a --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/DialogHeader.svelte @@ -0,0 +1,41 @@ + + +
    +

    {translatedTitle}

    + {#if translatedSubtitle} +

    {translatedSubtitle}

    + {/if} +
    + + diff --git a/src/modules/services/LiveSyncUI/components/ExtraItems.svelte b/src/modules/services/LiveSyncUI/components/ExtraItems.svelte new file mode 100644 index 00000000..80a8eda5 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/ExtraItems.svelte @@ -0,0 +1,17 @@ + + +
    + {translatedTitle} +
    + {@render children?.()} +
    +
    diff --git a/src/modules/services/LiveSyncUI/components/Guidance.svelte b/src/modules/services/LiveSyncUI/components/Guidance.svelte new file mode 100644 index 00000000..a2ab5e76 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Guidance.svelte @@ -0,0 +1,21 @@ + + +
    + {#if translatedTitle} +

    {translatedTitle}

    + {/if} + {@render children?.()} +
    diff --git a/src/modules/services/LiveSyncUI/components/InfoNote.svelte b/src/modules/services/LiveSyncUI/components/InfoNote.svelte new file mode 100644 index 00000000..3d1fac4c --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/InfoNote.svelte @@ -0,0 +1,87 @@ + + +{#if visible === undefined || visible === true} +
    + {#if signalWordText} +
    {signalWordText}
    + {/if} + {#if translatedTitle}

    {translatedTitle}

    {/if} + {#if translatedMessage}

    {translatedMessage}

    {/if} + {@render children?.()} +
    +{/if} diff --git a/src/modules/services/LiveSyncUI/components/InfoTable.svelte b/src/modules/services/LiveSyncUI/components/InfoTable.svelte new file mode 100644 index 00000000..e779e624 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/InfoTable.svelte @@ -0,0 +1,74 @@ + + +
    +
    + {#each infoEntries as [key, value]} +
    +
    {key}
    +
    +
    +
    {value}
    +
    + {/each} +
    +
    + + diff --git a/src/modules/services/LiveSyncUI/components/InputRow.svelte b/src/modules/services/LiveSyncUI/components/InputRow.svelte new file mode 100644 index 00000000..ae42ad90 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/InputRow.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/modules/services/LiveSyncUI/components/Instruction.svelte b/src/modules/services/LiveSyncUI/components/Instruction.svelte new file mode 100644 index 00000000..fde612f5 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Instruction.svelte @@ -0,0 +1,10 @@ + + +
    + {@render children?.()} +
    diff --git a/src/modules/services/LiveSyncUI/components/Option.svelte b/src/modules/services/LiveSyncUI/components/Option.svelte new file mode 100644 index 00000000..0edf9768 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Option.svelte @@ -0,0 +1,81 @@ + + +
    + +
    + + diff --git a/src/modules/services/LiveSyncUI/components/Options.svelte b/src/modules/services/LiveSyncUI/components/Options.svelte new file mode 100644 index 00000000..678a37fd --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Options.svelte @@ -0,0 +1,14 @@ + + +
    + {@render children?.()} +
    diff --git a/src/modules/services/LiveSyncUI/components/Password.svelte b/src/modules/services/LiveSyncUI/components/Password.svelte new file mode 100644 index 00000000..8b635051 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Password.svelte @@ -0,0 +1,34 @@ + + + + diff --git a/src/modules/services/LiveSyncUI/components/Question.svelte b/src/modules/services/LiveSyncUI/components/Question.svelte new file mode 100644 index 00000000..3956bf11 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Question.svelte @@ -0,0 +1,26 @@ + + +
    + {#if question}

    {@render question?.()}

    {/if} +
    + {@render children?.()} +
    +
    + + diff --git a/src/modules/services/LiveSyncUI/components/UserDecisions.svelte b/src/modules/services/LiveSyncUI/components/UserDecisions.svelte new file mode 100644 index 00000000..7aadf81c --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/UserDecisions.svelte @@ -0,0 +1,12 @@ + + +
    + {#if children} + {@render children()} + {/if} +
    diff --git a/src/modules/services/LiveSyncUI/dialogues/DialogueToCopy.svelte b/src/modules/services/LiveSyncUI/dialogues/DialogueToCopy.svelte new file mode 100644 index 00000000..cdd112cb --- /dev/null +++ b/src/modules/services/LiveSyncUI/dialogues/DialogueToCopy.svelte @@ -0,0 +1,59 @@ + + + + + + + + + + + Your {title || "data"} has been copied to the clipboard. + + + + + + diff --git a/src/modules/services/LiveSyncUI/svelteDialog.ts b/src/modules/services/LiveSyncUI/svelteDialog.ts new file mode 100644 index 00000000..648e4057 --- /dev/null +++ b/src/modules/services/LiveSyncUI/svelteDialog.ts @@ -0,0 +1,14 @@ +export type { + HasSetResult, + HasGetInitialData, + ComponentHasResult, + GuestDialogProps, + DialogSvelteComponentBaseProps, + DialogControlBase, +} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog"; +export { + CONTEXT_DIALOG_CONTROLS, + setupDialogContext, + getDialogContext, + SvelteDialogManagerBase, +} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog"; diff --git a/src/modules/services/ObsidianAPIService.ts b/src/modules/services/ObsidianAPIService.ts index daa0a895..c455d172 100644 --- a/src/modules/services/ObsidianAPIService.ts +++ b/src/modules/services/ObsidianAPIService.ts @@ -1,11 +1,11 @@ -import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; +import { InjectableAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAPIService"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; import { Platform, type Command, type ViewCreator } from "@/deps.ts"; import { ObsHttpHandler } from "@/modules/essentialObsidian/APILib/ObsHttpHandler"; import { ObsidianConfirm } from "./ObsidianConfirm"; -import type { Confirm } from "@lib/interfaces/Confirm"; +import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm"; import { requestUrl, type RequestUrlParam } from "@/deps"; -import { compatGlobal } from "@lib/common/coreEnvFunctions"; +import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; // All Services will be migrated to be based on Plain Services, not Injectable Services. // This is a migration step. diff --git a/src/modules/services/ObsidianAppLifecycleService.ts b/src/modules/services/ObsidianAppLifecycleService.ts index 6c730e62..e2888a7a 100644 --- a/src/modules/services/ObsidianAppLifecycleService.ts +++ b/src/modules/services/ObsidianAppLifecycleService.ts @@ -1,5 +1,5 @@ -import { AppLifecycleServiceBase } from "@lib/services/implements/injectable/InjectableAppLifecycleService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; +import { AppLifecycleServiceBase } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAppLifecycleService"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; declare module "obsidian" { interface App { commands: { diff --git a/src/modules/services/ObsidianConfirm.ts b/src/modules/services/ObsidianConfirm.ts index ca4f9c1a..5941c6d8 100644 --- a/src/modules/services/ObsidianConfirm.ts +++ b/src/modules/services/ObsidianConfirm.ts @@ -1,8 +1,8 @@ import { type App, type Plugin, Notice } from "@/deps"; import { scheduleTask, memoIfNotExist, memoObject, retrieveMemoObject, disposeMemoObject } from "@/common/utils"; -import { $msg } from "@lib/common/i18n"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; +import { $msg } from "@vrtmrz/livesync-commonlib/compat/common/i18n"; +import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; import { askYesNo, askString, diff --git a/src/modules/services/ObsidianDatabaseService.ts b/src/modules/services/ObsidianDatabaseService.ts index 39759f9d..2768a601 100644 --- a/src/modules/services/ObsidianDatabaseService.ts +++ b/src/modules/services/ObsidianDatabaseService.ts @@ -1,8 +1,8 @@ import { initializeStores } from "@/common/stores"; // import { InjectableDatabaseService } from "@/lib/src/services/implements/injectable/InjectableDatabaseService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import { DatabaseService, type DatabaseServiceDependencies } from "@lib/services/base/DatabaseService.ts"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; +import { DatabaseService, type DatabaseServiceDependencies } from "@vrtmrz/livesync-commonlib/compat/services/base/DatabaseService"; export class ObsidianDatabaseService extends DatabaseService { private __onOpenDatabase(vaultName: string) { diff --git a/src/modules/services/ObsidianPathService.ts b/src/modules/services/ObsidianPathService.ts index ed479e03..12dee2b0 100644 --- a/src/modules/services/ObsidianPathService.ts +++ b/src/modules/services/ObsidianPathService.ts @@ -1,6 +1,6 @@ -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; import { normalizePath } from "@/deps"; -import { PathService } from "@lib/services/base/PathService"; +import { PathService } from "@vrtmrz/livesync-commonlib/compat/services/base/PathService"; import { type BASE_IS_NEW, @@ -11,7 +11,7 @@ import { compareFileFreshness, isMarkedAsSameChanges, } from "@/common/utils"; -import type { UXFileInfo, AnyEntry, UXFileInfoStub, FilePathWithPrefix } from "@lib/common/types"; +import type { UXFileInfo, AnyEntry, UXFileInfoStub, FilePathWithPrefix } from "@vrtmrz/livesync-commonlib/compat/common/types"; export class ObsidianPathService extends PathService { override markChangesAreSame( old: UXFileInfo | AnyEntry | FilePathWithPrefix, diff --git a/src/modules/services/ObsidianServiceContext.ts b/src/modules/services/ObsidianServiceContext.ts new file mode 100644 index 00000000..fe2d1dd1 --- /dev/null +++ b/src/modules/services/ObsidianServiceContext.ts @@ -0,0 +1,19 @@ +import type ObsidianLiveSyncPlugin from "@/main"; +import type { App, Plugin } from "@/deps"; +import { ServiceContext } from "@vrtmrz/livesync-commonlib/context"; +import { eventHub } from "@/common/events"; +import { translateLiveSyncMessage } from "@/common/translation"; + +/** Host capabilities owned by one Self-hosted LiveSync plug-in instance. */ +export class ObsidianServiceContext extends ServiceContext { + app: App; + plugin: Plugin; + liveSyncPlugin: ObsidianLiveSyncPlugin; + + constructor(app: App, plugin: Plugin, liveSyncPlugin: ObsidianLiveSyncPlugin) { + super({ events: eventHub, translate: translateLiveSyncMessage }); + this.app = app; + this.plugin = plugin; + this.liveSyncPlugin = liveSyncPlugin; + } +} diff --git a/src/modules/services/ObsidianServiceContext.unit.spec.ts b/src/modules/services/ObsidianServiceContext.unit.spec.ts new file mode 100644 index 00000000..ffefbec9 --- /dev/null +++ b/src/modules/services/ObsidianServiceContext.unit.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; + +import { eventHub } from "@/common/events"; +import { translateLiveSyncMessage } from "@/common/translation"; +import { observeServiceContext } from "../../../test/contracts/serviceContext"; +import { ObsidianServiceContext } from "./ObsidianServiceContext"; + +const TRANSLATION_KEY = "Replicator.Message.InitialiseFatalError"; + +describe("ObsidianServiceContext contract", () => { + it("preserves the plug-in capabilities and host-neutral API results", () => { + type Parameters = ConstructorParameters; + const app = {} as Parameters[0]; + const plugin = {} as Parameters[1]; + const liveSyncPlugin = {} as Parameters[2]; + const context = new ObsidianServiceContext(app, plugin, liveSyncPlugin); + + expect(observeServiceContext(context, TRANSLATION_KEY)).toEqual({ + translation: translateLiveSyncMessage(TRANSLATION_KEY), + receivedEvents: ["context-contract-event"], + }); + expect(context.events).toBe(eventHub); + expect(context.app).toBe(app); + expect(context.plugin).toBe(plugin); + expect(context.liveSyncPlugin).toBe(liveSyncPlugin); + }); +}); diff --git a/src/modules/services/ObsidianServiceHub.ts b/src/modules/services/ObsidianServiceHub.ts index 917637a2..bca05290 100644 --- a/src/modules/services/ObsidianServiceHub.ts +++ b/src/modules/services/ObsidianServiceHub.ts @@ -1,6 +1,6 @@ -import { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub"; -import { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import type { ServiceInstances } from "@lib/services/ServiceHub"; +import { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub"; +import { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; +import type { ServiceInstances } from "@vrtmrz/livesync-commonlib/compat/services/ServiceHub"; import type ObsidianLiveSyncPlugin from "@/main"; import { ObsidianConflictService, @@ -23,6 +23,8 @@ import { ObsidianPathService } from "./ObsidianPathService"; import { ObsidianVaultService } from "./ObsidianVaultService"; import { ObsidianUIService } from "./ObsidianUIService"; import { createScreenWakeLockManager } from "octagonal-wheels/browser/wakeLock"; +import { PouchDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/pouchdb-browser"; +import { OpenKeyValueDatabase } from "@/common/KeyValueDB"; // InjectableServiceHub @@ -43,6 +45,7 @@ export class ObsidianServiceHub extends InjectableServiceHub {} diff --git a/src/modules/services/ObsidianSettingService.ts b/src/modules/services/ObsidianSettingService.ts index f3a32eef..4bfc8cbb 100644 --- a/src/modules/services/ObsidianSettingService.ts +++ b/src/modules/services/ObsidianSettingService.ts @@ -1,19 +1,18 @@ -import { compatGlobal } from "@lib/common/coreEnvFunctions"; -import { type ObsidianLiveSyncSettings } from "@lib/common/types"; -import { EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED } from "@lib/events/coreEvents"; -import { eventHub } from "@lib/hub/hub"; -import { SettingService, type SettingServiceDependencies } from "@lib/services/base/SettingService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; +import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; +import { type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; +import { SettingService, type SettingServiceDependencies } from "@vrtmrz/livesync-commonlib/compat/services/base/SettingService"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; export class ObsidianSettingService extends SettingService { constructor(context: T, dependencies: SettingServiceDependencies) { super(context, dependencies); this.onSettingSaved.addHandler((settings) => { - eventHub.emitEvent(EVENT_SETTING_SAVED, settings); + this.context.events.emitEvent(EVENT_SETTING_SAVED, settings); return Promise.resolve(true); }); this.onSettingLoaded.addHandler((settings) => { - eventHub.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB); + this.context.events.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB); return Promise.resolve(true); }); } diff --git a/src/modules/services/ObsidianUIService.ts b/src/modules/services/ObsidianUIService.ts index 45975af2..c5abf23f 100644 --- a/src/modules/services/ObsidianUIService.ts +++ b/src/modules/services/ObsidianUIService.ts @@ -1,11 +1,11 @@ -import type { ConfigService } from "@lib/services/base/ConfigService"; -import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; -import type { ReplicatorService } from "@lib/services/base/ReplicatorService"; -import { UIService } from "@lib/services/implements/base/UIService"; -import { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; +import type { ConfigService } from "@vrtmrz/livesync-commonlib/compat/services/base/ConfigService"; +import type { AppLifecycleService } from "@vrtmrz/livesync-commonlib/compat/services/base/AppLifecycleService"; +import type { ReplicatorService } from "@vrtmrz/livesync-commonlib/compat/services/base/ReplicatorService"; +import { UIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/UIService"; +import { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; import { ObsidianSvelteDialogManager } from "./SvelteDialogObsidian"; -import DialogToCopy from "@lib/UI/dialogues/DialogueToCopy.svelte"; -import type { IAPIService, IControlService } from "@lib/services/base/IService"; +import DialogToCopy from "@/modules/services/LiveSyncUI/dialogues/DialogueToCopy.svelte"; +import type { IAPIService, IControlService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService"; export type ObsidianUIServiceDependencies = { appLifecycle: AppLifecycleService; config: ConfigService; @@ -28,7 +28,6 @@ export class ObsidianUIService extends UIService { control: dependents.control, }); super(context, { - appLifecycle: dependents.appLifecycle, dialogManager: obsidianSvelteDialogManager, APIService: dependents.APIService, }); diff --git a/src/modules/services/ObsidianVaultService.ts b/src/modules/services/ObsidianVaultService.ts index d86c7b19..e3bd3a91 100644 --- a/src/modules/services/ObsidianVaultService.ts +++ b/src/modules/services/ObsidianVaultService.ts @@ -1,7 +1,7 @@ import { getPathFromTFile, isValidPath } from "@/common/utils"; -import { InjectableVaultService } from "@lib/services/implements/injectable/InjectableVaultService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import type { FilePath } from "@lib/common/types"; +import { InjectableVaultService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableVaultService"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; +import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types"; declare module "obsidian" { interface DataAdapter { diff --git a/src/modules/services/SvelteDialogObsidian.ts b/src/modules/services/SvelteDialogObsidian.ts index 95c9ba23..65cd3988 100644 --- a/src/modules/services/SvelteDialogObsidian.ts +++ b/src/modules/services/SvelteDialogObsidian.ts @@ -5,9 +5,9 @@ import { SvelteDialogMixIn, type ComponentHasResult, type SvelteDialogManagerDependencies, -} from "@lib/services/implements/base/SvelteDialog"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import DialogHost from "@lib/UI/DialogHost.svelte"; +} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; +import DialogHost from "@/modules/services/LiveSyncUI/DialogHost.svelte"; export const SvelteDialogBase = SvelteDialogMixIn(Modal, DialogHost); export class SvelteDialogObsidian< T, diff --git a/src/rabinKarpBom.unit.spec.ts b/src/rabinKarpBom.unit.spec.ts index 56596aa1..67e4781f 100644 --- a/src/rabinKarpBom.unit.spec.ts +++ b/src/rabinKarpBom.unit.spec.ts @@ -1,4 +1,4 @@ -import { splitPiecesRabinKarp } from "@lib/string_and_binary/chunks.ts"; +import { splitPiecesRabinKarp } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/chunks"; import { describe, expect, it } from "vitest"; describe("Rabin-Karp text splitting", () => { diff --git a/src/serviceFeatures/onLayoutReady/enablei18n.ts b/src/serviceFeatures/onLayoutReady/enablei18n.ts index fe8e98a8..dacb2d06 100644 --- a/src/serviceFeatures/onLayoutReady/enablei18n.ts +++ b/src/serviceFeatures/onLayoutReady/enablei18n.ts @@ -1,7 +1,7 @@ import { getLanguage } from "@/deps"; -import { createServiceFeature } from "@lib/interfaces/ServiceModule"; -import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@lib/common/rosetta"; -import { $msg, __onMissingTranslation, setLang } from "@lib/common/i18n"; +import { createServiceFeature } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; +import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@vrtmrz/livesync-commonlib/compat/common/rosetta"; +import { $msg, __onMissingTranslation, setLang } from "@vrtmrz/livesync-commonlib/compat/common/i18n"; function tryGetLanguage() { try { diff --git a/src/serviceFeatures/redFlag.simpleFetch.ts b/src/serviceFeatures/redFlag.simpleFetch.ts index 981556d6..95b0fa48 100644 --- a/src/serviceFeatures/redFlag.simpleFetch.ts +++ b/src/serviceFeatures/redFlag.simpleFetch.ts @@ -1,7 +1,7 @@ import { LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager"; +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; +import { type LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; +import { UnresolvedErrorManager } from "@vrtmrz/livesync-commonlib/compat/services/base/UnresolvedErrorManager"; import { ExtraOnLocal, ExtraOnRemote, @@ -9,7 +9,7 @@ import { normaliseFullScanOptions, synchroniseAllFilesBetweenDBandStorage, type FullScanOptions, -} from "@lib/serviceFeatures/offlineScanner"; +} from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner"; import { adjustSettingToRemoteIfNeeded, processVaultInitialisation } from "./redFlag"; export const SIMPLE_FETCH_STAGE1_REMOTE_WINS = "Overwrite all with remote files"; @@ -215,7 +215,7 @@ export async function askAndPerformFastSetupOnScheduledFetchAll( await host.serviceModules.rebuilder.$fetchLocalDBFast(false); // 2. Call the extended synchroniseAllFilesBetweenDBandStorage to reflect changes in storage - const errorManager = new UnresolvedErrorManager(host.services.appLifecycle); + const errorManager = new UnresolvedErrorManager(host.services.appLifecycle, host.services.context.events); const syncResult = await synchroniseAllFilesBetweenDBandStorage( host, log, diff --git a/src/serviceFeatures/redFlag.ts b/src/serviceFeatures/redFlag.ts index 4fe9d8d8..5b1576fd 100644 --- a/src/serviceFeatures/redFlag.ts +++ b/src/serviceFeatures/redFlag.ts @@ -1,20 +1,20 @@ import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { createInstanceLogFunction, type LogFunction } from "@lib/services/lib/logUtils"; -import { FlagFilesHumanReadable, FlagFilesOriginal } from "@lib/common/models/redflag.const"; +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; +import { createInstanceLogFunction, type LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; +import { FlagFilesHumanReadable, FlagFilesOriginal } from "@vrtmrz/livesync-commonlib/compat/common/models/redflag.const"; import FetchEverything from "@/modules/features/SetupWizard/dialogs/FetchEverything.svelte"; import RebuildEverything from "@/modules/features/SetupWizard/dialogs/RebuildEverything.svelte"; import { extractObject } from "octagonal-wheels/object"; -import { REMOTE_MINIO, REMOTE_P2P } from "@lib/common/models/setting.const"; -import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type"; -import { TweakValuesShouldMatchedTemplate } from "@lib/common/models/tweak.definition"; +import { REMOTE_MINIO, REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const"; +import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.type"; +import { TweakValuesShouldMatchedTemplate } from "@vrtmrz/livesync-commonlib/compat/common/models/tweak.definition"; import type { FetchEverythingResult, RebuildEverythingResult, } from "@/modules/features/SetupWizard/dialogs/setupDialogTypes"; import { askAndPerformFastSetupOnScheduledFetchAll } from "./redFlag.simpleFetch"; -import { ConnectionStringParser } from "@lib/common/ConnectionString"; -import { activateRemoteConfiguration } from "@lib/serviceFeatures/remoteConfig"; +import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString"; +import { activateRemoteConfiguration } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig"; /** * Flag file handler interface, similar to target filter pattern. diff --git a/src/serviceFeatures/redFlag.unit.spec.ts b/src/serviceFeatures/redFlag.unit.spec.ts index f94103e0..408e3e75 100644 --- a/src/serviceFeatures/redFlag.unit.spec.ts +++ b/src/serviceFeatures/redFlag.unit.spec.ts @@ -1,7 +1,8 @@ import { describe, it, expect, vi } from "vitest"; -import type { LogFunction } from "@lib/services/lib/logUtils"; -import { FlagFilesHumanReadable, FlagFilesOriginal } from "@lib/common/models/redflag.const"; -import { REMOTE_MINIO } from "@lib/common/models/setting.const"; +import { createServiceContext } from "@vrtmrz/livesync-commonlib/context"; +import type { LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; +import { FlagFilesHumanReadable, FlagFilesOriginal } from "@vrtmrz/livesync-commonlib/compat/common/models/redflag.const"; +import { REMOTE_MINIO } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const"; import { createFetchAllFlagHandler, createRebuildFlagHandler, @@ -18,12 +19,12 @@ import { TweakValuesRecommendedTemplate, TweakValuesShouldMatchedTemplate, TweakValuesTemplate, -} from "@lib/common/types"; +} from "@vrtmrz/livesync-commonlib/compat/common/types"; import { ExtraOnLocal, FullScanModes, synchroniseAllFilesBetweenDBandStorage, -} from "@lib/serviceFeatures/offlineScanner"; +} from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner"; import { SIMPLE_FETCH_STAGE1_LEGACY, SIMPLE_FETCH_STAGE1_NEWER_WINS, @@ -36,9 +37,9 @@ import { askAndPerformFastSetupOnScheduledFetchAll, askSimpleFetchMode, } from "./redFlag.simpleFetch"; -import { activateRemoteConfiguration } from "@lib/serviceFeatures/remoteConfig"; +import { activateRemoteConfiguration } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig"; //Mock synchroniseAllFilesBetweenDBandStorage -vi.mock("@/lib/src/serviceFeatures/offlineScanner", async (importOriginal) => { +vi.mock("@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner", async (importOriginal) => { const originalModule = (await importOriginal()) as any; return { ...originalModule, @@ -46,7 +47,7 @@ vi.mock("@/lib/src/serviceFeatures/offlineScanner", async (importOriginal) => { }; }); -vi.mock("@lib/serviceFeatures/remoteConfig", () => { +vi.mock("@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig", () => { return { activateRemoteConfiguration: vi.fn((settings: any, configurationId: string) => { if (!settings?.remoteConfigurations?.[configurationId]) return false; @@ -159,6 +160,7 @@ const createHostMock = () => { return { services: { + context: createServiceContext(), setting: settingMock, appLifecycle: appLifecycleMock, UI: uiMock, diff --git a/src/serviceFeatures/setupObsidian/qrCode.ts b/src/serviceFeatures/setupObsidian/qrCode.ts new file mode 100644 index 00000000..6498c1fb --- /dev/null +++ b/src/serviceFeatures/setupObsidian/qrCode.ts @@ -0,0 +1,75 @@ +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; +import { + encodeQR, + encodeSettingsToQRCodeData, + OutputFormat, +} from "@vrtmrz/livesync-commonlib/compat/API/processSetting"; +import { EVENT_REQUEST_SHOW_SETUP_QR } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; +import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import type { SetupFeatureHost } from "./types"; + +export async function encodeSetupSettingsAsQR(host: SetupFeatureHost) { + const settingString = encodeSettingsToQRCodeData(host.services.setting.currentSettings()); + const result = encodeQR(settingString, OutputFormat.SVG); + if (result === "") { + return ""; + } + + if (typeof result === "string") { + const msg = host.services.context.translate("Setup.QRCode", { qr_image: result }); + await host.services.UI.confirm.confirmWithMessage("Settings QR Code", msg, ["OK"], "OK"); + return result; + } else { + // Multi-page QR code + let currentIndex = 0; + while (currentIndex < result.total) { + const msg = `The setting is too large for a single QR code. +We are using the aggregator to combine multiple QR codes. +Your settings will not be sent to any server; they will be processed only on your device. +Please scan this QR code with your mobile's camera, and open the page in your browser. +After all parts are collected, the page will navigate you back to Obsidian with the aggregated settings. + +Progress: ${currentIndex + 1} / ${result.total} +${result.parts[currentIndex]}`; + + const buttons = []; + if (currentIndex > 0) buttons.push("Back"); + if (currentIndex < result.total - 1) { + buttons.push("Next"); + buttons.push("Cancel"); + } else { + buttons.push("Done"); + } + + const choice = await host.services.UI.confirm.confirmWithMessage( + "Settings QR Code (Aggregated)", + msg, + buttons, + buttons[buttons.indexOf("Next") !== -1 ? buttons.indexOf("Next") : buttons.indexOf("Done")] + ); + + if (choice === "Next") { + currentIndex++; + } else if (choice === "Back") { + currentIndex--; + } else { + break; + } + } + return result.parts[0]; // Return the first one for compatibility + } +} + +export function useSetupQRCodeFeature(host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>) { + host.services.appLifecycle.onLoaded.addHandler(() => { + host.services.API.addCommand({ + id: "livesync-setting-qr", + name: "Show settings as a QR code", + callback: () => fireAndForget(encodeSetupSettingsAsQR(host)), + }); + host.services.context.events.onEvent(EVENT_REQUEST_SHOW_SETUP_QR, () => + fireAndForget(() => encodeSetupSettingsAsQR(host)) + ); + return Promise.resolve(true); + }); +} diff --git a/src/serviceFeatures/setupObsidian/qrCode.unit.spec.ts b/src/serviceFeatures/setupObsidian/qrCode.unit.spec.ts new file mode 100644 index 00000000..766d8ec4 --- /dev/null +++ b/src/serviceFeatures/setupObsidian/qrCode.unit.spec.ts @@ -0,0 +1,117 @@ +import { describe, expect, it, vi, afterEach } from "vitest"; +import { EVENT_REQUEST_SHOW_SETUP_QR } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; +import { createServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase"; +import { encodeSetupSettingsAsQR, useSetupQRCodeFeature } from "./qrCode"; +import { encodeQR, encodeSettingsToQRCodeData } from "@vrtmrz/livesync-commonlib/compat/API/processSetting"; + +vi.mock("@vrtmrz/livesync-commonlib/compat/API/processSetting", () => { + return { + encodeQR: vi.fn(), + encodeSettingsToQRCodeData: vi.fn(), + OutputFormat: { + SVG: "svg", + }, + }; +}); + +describe("setupObsidian/qrCode", () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.clearAllMocks(); + }); + + it("encodeSetupSettingsAsQR should return empty string when QR generation fails", async () => { + const confirmWithMessage = vi.fn(); + const host = { + services: { + context: createServiceContext(), + setting: { + currentSettings: vi.fn(() => ({ any: "settings" })), + }, + UI: { + confirm: { + confirmWithMessage, + }, + }, + }, + } as any; + + vi.mocked(encodeSettingsToQRCodeData).mockReturnValue("encoded-settings"); + vi.mocked(encodeQR).mockReturnValue(""); + + const result = await encodeSetupSettingsAsQR(host); + + expect(result).toBe(""); + expect(confirmWithMessage).not.toHaveBeenCalled(); + }); + + it("encodeSetupSettingsAsQR should show confirm dialog when QR is generated", async () => { + const confirmWithMessage = vi.fn(() => true); + const translate = vi.fn(() => "qr-message"); + const host = { + services: { + context: createServiceContext({ translate }), + setting: { + currentSettings: vi.fn(() => ({ any: "settings" })), + }, + UI: { + confirm: { + confirmWithMessage, + }, + }, + }, + } as any; + + vi.mocked(encodeSettingsToQRCodeData).mockReturnValue("encoded-settings"); + vi.mocked(encodeQR).mockReturnValue(""); + + const result = await encodeSetupSettingsAsQR(host); + + expect(result).toBe(""); + expect(translate).toHaveBeenCalledWith("Setup.QRCode", { qr_image: "" }); + expect(confirmWithMessage).toHaveBeenCalledWith("Settings QR Code", "qr-message", ["OK"], "OK"); + }); + + it("useSetupQRCodeFeature should register onLoaded handler that wires command and event", async () => { + const addHandler = vi.fn(); + const addCommand = vi.fn(); + const context = createServiceContext(); + const onEventSpy = vi.spyOn(context.events, "onEvent"); + + const host = { + services: { + context, + API: { + addCommand, + }, + appLifecycle: { + onLoaded: { + addHandler, + }, + }, + setting: { + currentSettings: vi.fn(() => ({ any: "settings" })), + }, + UI: { + confirm: { + confirmWithMessage: vi.fn(), + }, + }, + }, + } as any; + + useSetupQRCodeFeature(host); + expect(addHandler).toHaveBeenCalledTimes(1); + + const loadedHandler = addHandler.mock.calls[0][0] as () => Promise; + await loadedHandler(); + + expect(addCommand).toHaveBeenCalledWith( + expect.objectContaining({ + id: "livesync-setting-qr", + name: "Show settings as a QR code", + }) + ); + expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_SHOW_SETUP_QR, expect.any(Function)); + }); +}); diff --git a/src/serviceFeatures/setupObsidian/setupManagerHandlers.ts b/src/serviceFeatures/setupObsidian/setupManagerHandlers.ts index 4eb9e425..0eae88c1 100644 --- a/src/serviceFeatures/setupObsidian/setupManagerHandlers.ts +++ b/src/serviceFeatures/setupObsidian/setupManagerHandlers.ts @@ -1,9 +1,8 @@ import { type SetupManager, UserMode } from "@/modules/features/SetupManager"; -import type { SetupFeatureHost } from "@lib/serviceFeatures/setupObsidian/types"; -import { EVENT_REQUEST_OPEN_P2P_SETTINGS, EVENT_REQUEST_OPEN_SETUP_URI } from "@lib/events/coreEvents"; -import { eventHub } from "@lib/hub/hub"; -import { fireAndForget } from "@lib/common/utils"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; +import type { SetupFeatureHost } from "@/serviceFeatures/setupObsidian/types"; +import { EVENT_REQUEST_OPEN_P2P_SETTINGS, EVENT_REQUEST_OPEN_SETUP_URI } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; +import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; export async function openSetupURI(setupManager: SetupManager) { await setupManager.onUseSetupURI(UserMode.Unknown); @@ -24,8 +23,10 @@ export function useSetupManagerHandlersFeature( callback: () => fireAndForget(openSetupURI(setupManager)), }); - eventHub.onEvent(EVENT_REQUEST_OPEN_SETUP_URI, () => fireAndForget(() => openSetupURI(setupManager))); - eventHub.onEvent(EVENT_REQUEST_OPEN_P2P_SETTINGS, () => + host.services.context.events.onEvent(EVENT_REQUEST_OPEN_SETUP_URI, () => + fireAndForget(() => openSetupURI(setupManager)) + ); + host.services.context.events.onEvent(EVENT_REQUEST_OPEN_P2P_SETTINGS, () => fireAndForget(() => openP2PSettings(host, setupManager)) ); diff --git a/src/serviceFeatures/setupObsidian/setupManagerHandlers.unit.spec.ts b/src/serviceFeatures/setupObsidian/setupManagerHandlers.unit.spec.ts index 067d860c..b45ad3ea 100644 --- a/src/serviceFeatures/setupObsidian/setupManagerHandlers.unit.spec.ts +++ b/src/serviceFeatures/setupObsidian/setupManagerHandlers.unit.spec.ts @@ -1,6 +1,5 @@ import { describe, expect, it, vi, afterEach } from "vitest"; -import { eventHub } from "@lib/hub/hub"; -import { EVENT_REQUEST_OPEN_P2P_SETTINGS, EVENT_REQUEST_OPEN_SETUP_URI } from "@lib/events/coreEvents"; +import { EVENT_REQUEST_OPEN_P2P_SETTINGS, EVENT_REQUEST_OPEN_SETUP_URI } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; import { openP2PSettings, openSetupURI, useSetupManagerHandlersFeature } from "./setupManagerHandlers"; vi.mock("@/modules/features/SetupManager", () => { @@ -47,10 +46,11 @@ describe("setupObsidian/setupManagerHandlers", () => { it("useSetupManagerHandlersFeature should register onLoaded handler that wires command and events", async () => { const addHandler = vi.fn(); const addCommand = vi.fn(); - const onEventSpy = vi.spyOn(eventHub, "onEvent"); + const events = { onEvent: vi.fn() }; const host = { services: { + context: { events }, API: { addCommand, }, @@ -81,7 +81,7 @@ describe("setupObsidian/setupManagerHandlers", () => { name: "Use the copied setup URI (Formerly Open setup URI)", }) ); - expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_OPEN_SETUP_URI, expect.any(Function)); - expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_OPEN_P2P_SETTINGS, expect.any(Function)); + expect(events.onEvent).toHaveBeenCalledWith(EVENT_REQUEST_OPEN_SETUP_URI, expect.any(Function)); + expect(events.onEvent).toHaveBeenCalledWith(EVENT_REQUEST_OPEN_P2P_SETTINGS, expect.any(Function)); }); }); diff --git a/src/serviceFeatures/setupObsidian/setupProtocol.ts b/src/serviceFeatures/setupObsidian/setupProtocol.ts index 5310fbf8..3c3566ff 100644 --- a/src/serviceFeatures/setupObsidian/setupProtocol.ts +++ b/src/serviceFeatures/setupObsidian/setupProtocol.ts @@ -1,9 +1,9 @@ -import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@lib/common/types"; -import type { LogFunction } from "@lib/services/lib/logUtils"; -import { createInstanceLogFunction } from "@lib/services/lib/logUtils"; -import type { SetupFeatureHost } from "@lib/serviceFeatures/setupObsidian/types"; +import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; +import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; +import type { SetupFeatureHost } from "@/serviceFeatures/setupObsidian/types"; import { configURIBase } from "@/common/types"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; import { type SetupManager, UserMode } from "@/modules/features/SetupManager"; async function handleSetupProtocol(setupManager: SetupManager, conf: Record) { diff --git a/src/serviceFeatures/setupObsidian/setupUri.ts b/src/serviceFeatures/setupObsidian/setupUri.ts new file mode 100644 index 00000000..9a8d8c65 --- /dev/null +++ b/src/serviceFeatures/setupObsidian/setupUri.ts @@ -0,0 +1,73 @@ +import { LOG_LEVEL_NOTICE, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; +import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; +import { encodeSettingsToSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting"; +import { EVENT_REQUEST_COPY_SETUP_URI } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; +import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; +import type { SetupFeatureHost } from "./types"; + +export async function askEncryptingPassphrase(host: SetupFeatureHost): Promise { + return await host.services.UI.confirm.askString( + "Encrypt your settings", + "The passphrase to encrypt the setup URI", + "", + true + ); +} + +export async function copySetupURI(host: SetupFeatureHost, log: LogFunction, stripExtra = true) { + const encryptingPassphrase = await askEncryptingPassphrase(host); + if (encryptingPassphrase === false) return; + const encryptedURI = await encodeSettingsToSetupURI( + host.services.setting.currentSettings(), + encryptingPassphrase, + [...((stripExtra ? ["pluginSyncExtendedSetting"] : []) as (keyof ObsidianLiveSyncSettings)[])], + true + ); + if (await host.services.UI.promptCopyToClipboard("Setup URI", encryptedURI)) { + log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); + } +} + +export async function copySetupURIFull(host: SetupFeatureHost, log: LogFunction) { + const encryptingPassphrase = await askEncryptingPassphrase(host); + if (encryptingPassphrase === false) return; + const encryptedURI = await encodeSettingsToSetupURI( + host.services.setting.currentSettings(), + encryptingPassphrase, + [], + false + ); + if (await host.services.UI.promptCopyToClipboard("Setup URI", encryptedURI)) { + log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); + } +} + +export function useSetupURIFeature(host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>) { + const log = createInstanceLogFunction("SF:SetupURI", host.services.API); + host.services.appLifecycle.onLoaded.addHandler(() => { + host.services.API.addCommand({ + id: "livesync-copysetupuri", + name: "Copy settings as a new setup URI", + callback: () => fireAndForget(copySetupURI(host, log)), + }); + + host.services.API.addCommand({ + id: "livesync-copysetupuri-short", + name: "Copy settings as a new setup URI (With customization sync)", + callback: () => fireAndForget(copySetupURI(host, log, false)), + }); + + host.services.API.addCommand({ + id: "livesync-copysetupurifull", + name: "Copy settings as a new setup URI (Full)", + callback: () => fireAndForget(copySetupURIFull(host, log)), + }); + + host.services.context.events.onEvent(EVENT_REQUEST_COPY_SETUP_URI, () => + fireAndForget(() => copySetupURI(host, log)) + ); + return Promise.resolve(true); + }); +} diff --git a/src/serviceFeatures/setupObsidian/setupUri.unit.spec.ts b/src/serviceFeatures/setupObsidian/setupUri.unit.spec.ts new file mode 100644 index 00000000..64d3b80b --- /dev/null +++ b/src/serviceFeatures/setupObsidian/setupUri.unit.spec.ts @@ -0,0 +1,159 @@ +import { describe, expect, it, vi, afterEach } from "vitest"; +import { EVENT_REQUEST_COPY_SETUP_URI } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; +import { createServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase"; +import { askEncryptingPassphrase, copySetupURI, copySetupURIFull, useSetupURIFeature } from "./setupUri"; +import { encodeSettingsToSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting"; + +vi.mock("@vrtmrz/livesync-commonlib/compat/API/processSetting", () => { + return { + encodeSettingsToSetupURI: vi.fn(), + }; +}); + +describe("setupObsidian/setupUri", () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.clearAllMocks(); + }); + + it("askEncryptingPassphrase should delegate to confirm.askString", async () => { + const askString = vi.fn(() => "secret"); + const host = { + services: { + UI: { + confirm: { + askString, + }, + }, + }, + } as any; + + const result = await askEncryptingPassphrase(host); + expect(result).toBe("secret"); + expect(askString).toHaveBeenCalled(); + }); + + it("copySetupURI should return early when user cancels passphrase", async () => { + const promptCopyToClipboard = vi.fn(); + const host = { + services: { + setting: { + currentSettings: vi.fn(() => ({ foo: "bar" })), + }, + UI: { + confirm: { + askString: vi.fn(() => false), + }, + promptCopyToClipboard, + }, + }, + } as any; + const log = vi.fn(); + + await copySetupURI(host, log); + + expect(encodeSettingsToSetupURI).not.toHaveBeenCalled(); + expect(promptCopyToClipboard).not.toHaveBeenCalled(); + expect(log).not.toHaveBeenCalled(); + }); + + it("copySetupURI should encode with short mode by default", async () => { + const promptCopyToClipboard = vi.fn(() => true); + const currentSettings = { pluginSyncExtendedSetting: true, x: 1 }; + const host = { + services: { + setting: { + currentSettings: vi.fn(() => currentSettings), + }, + UI: { + confirm: { + askString: vi.fn(() => "pass"), + }, + promptCopyToClipboard, + }, + }, + } as any; + const log = vi.fn(); + vi.mocked(encodeSettingsToSetupURI).mockResolvedValue("uri://value" as any); + + await copySetupURI(host, log); + + expect(encodeSettingsToSetupURI).toHaveBeenCalledWith( + currentSettings, + "pass", + ["pluginSyncExtendedSetting"], + true + ); + expect(promptCopyToClipboard).toHaveBeenCalledWith("Setup URI", "uri://value"); + expect(log).toHaveBeenCalled(); + }); + + it("copySetupURIFull should encode with full mode", async () => { + const promptCopyToClipboard = vi.fn(() => true); + const currentSettings = { pluginSyncExtendedSetting: true, x: 1 }; + const host = { + services: { + setting: { + currentSettings: vi.fn(() => currentSettings), + }, + UI: { + confirm: { + askString: vi.fn(() => "pass-full"), + }, + promptCopyToClipboard, + }, + }, + } as any; + const log = vi.fn(); + vi.mocked(encodeSettingsToSetupURI).mockResolvedValue("uri://full" as any); + + await copySetupURIFull(host, log); + + expect(encodeSettingsToSetupURI).toHaveBeenCalledWith(currentSettings, "pass-full", [], false); + expect(promptCopyToClipboard).toHaveBeenCalledWith("Setup URI", "uri://full"); + expect(log).toHaveBeenCalled(); + }); + + it("useSetupURIFeature should register onLoaded handler that wires commands and event", async () => { + const addHandler = vi.fn(); + const addCommand = vi.fn(); + const context = createServiceContext(); + const onEventSpy = vi.spyOn(context.events, "onEvent"); + + const host = { + services: { + context, + API: { + addCommand, + addLog: vi.fn(), + }, + appLifecycle: { + onLoaded: { + addHandler, + }, + }, + setting: { + currentSettings: vi.fn(() => ({ x: 1 })), + }, + UI: { + confirm: { + askString: vi.fn(() => "pass"), + }, + promptCopyToClipboard: vi.fn(() => true), + }, + }, + } as any; + + useSetupURIFeature(host); + expect(addHandler).toHaveBeenCalledTimes(1); + + const loadedHandler = addHandler.mock.calls[0][0] as () => Promise; + await loadedHandler(); + + expect(addCommand).toHaveBeenCalledTimes(3); + expect(addCommand).toHaveBeenCalledWith(expect.objectContaining({ id: "livesync-copysetupuri" })); + expect(addCommand).toHaveBeenCalledWith(expect.objectContaining({ id: "livesync-copysetupuri-short" })); + expect(addCommand).toHaveBeenCalledWith(expect.objectContaining({ id: "livesync-copysetupurifull" })); + expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_COPY_SETUP_URI, expect.any(Function)); + }); +}); diff --git a/src/serviceFeatures/setupObsidian/types.ts b/src/serviceFeatures/setupObsidian/types.ts new file mode 100644 index 00000000..0e15898e --- /dev/null +++ b/src/serviceFeatures/setupObsidian/types.ts @@ -0,0 +1,3 @@ +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; + +export type SetupFeatureHost = NecessaryServices<"API" | "UI" | "setting", never>; diff --git a/src/serviceFeatures/useP2PReplicatorUI.ts b/src/serviceFeatures/useP2PReplicatorUI.ts index 8dc5a86e..39cca2c0 100644 --- a/src/serviceFeatures/useP2PReplicatorUI.ts +++ b/src/serviceFeatures/useP2PReplicatorUI.ts @@ -1,8 +1,8 @@ import { eventHub, EVENT_REQUEST_OPEN_P2P } from "@/common/events"; import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { type UseP2PReplicatorResult } from "@lib/replication/trystero/UseP2PReplicatorResult"; -import { P2PLogCollector } from "@lib/replication/trystero/P2PLogCollector"; +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; +import { type UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult"; +import { P2PLogCollector } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PLogCollector"; import { P2PReplicatorPaneView, VIEW_TYPE_P2P } from "@/features/P2PSync/P2PReplicator/P2PReplicatorPaneView"; import { P2PServerStatusPaneView, @@ -10,7 +10,7 @@ import { } from "@/features/P2PSync/P2PReplicator/P2PServerStatusPaneView"; import type { LiveSyncCore } from "@/main"; import type { WorkspaceLeaf } from "@/deps"; -import { REMOTE_P2P } from "@lib/common/models/setting.const"; +import { REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const"; /** * ServiceFeature: P2P Replicator lifecycle management. @@ -54,7 +54,7 @@ export function useP2PReplicatorUI( // const env: LiveSyncTrysteroReplicatorEnv = { services: host.services as any }; const getReplicator = () => replicator.replicator; - const p2pLogCollector = new P2PLogCollector(); + const p2pLogCollector = new P2PLogCollector(host.services.context.events); const storeP2PStatusLine = reactiveSource(""); p2pLogCollector.p2pReplicationLine.onChanged((line) => { storeP2PStatusLine.value = line.value; diff --git a/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts b/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts index 5495d4d3..f860c6f4 100644 --- a/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts +++ b/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from "vitest"; +import { createServiceContext } from "@vrtmrz/livesync-commonlib/context"; vi.mock("@/features/P2PSync/P2PReplicator/P2PReplicatorPaneView", () => ({ P2PReplicatorPaneView: class {}, @@ -19,6 +20,7 @@ describe("useP2PReplicatorUI commands", () => { const runFiniteReplicationActivity = vi.fn(async (task: () => unknown) => await task()); const host = { services: { + context: createServiceContext(), API: { showWindow: vi.fn(async () => undefined), registerWindow: vi.fn(), diff --git a/src/serviceModules/DatabaseFileAccess.ts b/src/serviceModules/DatabaseFileAccess.ts index 645888ef..93bdb0a3 100644 --- a/src/serviceModules/DatabaseFileAccess.ts +++ b/src/serviceModules/DatabaseFileAccess.ts @@ -1,8 +1,8 @@ -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess.ts"; -import { ServiceDatabaseFileAccessBase } from "@lib/serviceModules/ServiceDatabaseFileAccessBase"; +import type { DatabaseFileAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseFileAccess"; +import { ServiceDatabaseFileAccessBase } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceDatabaseFileAccessBase"; // markChangesAreSame uses persistent data implicitly, we should refactor it too. // For now, to make the refactoring done once, we just use them directly. -// Hence it is not on /src/lib/src/serviceModules. (markChangesAreSame is using indexedDB). +// Hence it remains in the plug-in rather than Commonlib. (markChangesAreSame is using indexedDB). // Refactored, now migrating... export class ServiceDatabaseFileAccess extends ServiceDatabaseFileAccessBase implements DatabaseFileAccess {} diff --git a/src/serviceModules/FileAccessObsidian.ts b/src/serviceModules/FileAccessObsidian.ts index 5b318cc8..2d5cacb0 100644 --- a/src/serviceModules/FileAccessObsidian.ts +++ b/src/serviceModules/FileAccessObsidian.ts @@ -1,5 +1,5 @@ import { type App } from "@/deps"; -import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase.ts"; +import { FileAccessBase, type FileAccessBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/serviceModules/FileAccessBase"; import { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter"; /** diff --git a/src/serviceModules/FileHandler.ts b/src/serviceModules/FileHandler.ts index c47cc337..46febb4a 100644 --- a/src/serviceModules/FileHandler.ts +++ b/src/serviceModules/FileHandler.ts @@ -1,7 +1,7 @@ -import { ServiceFileHandlerBase } from "@lib/serviceModules/ServiceFileHandlerBase"; +import { ServiceFileHandlerBase } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceFileHandlerBase"; // markChangesAreSame uses persistent data implicitly, we should refactor it too. // also, compareFileFreshness depends on marked changes, so we should refactor it as well. For now, to make the refactoring done once, we just use them directly. -// Hence it is not on /src/lib/src/serviceModules. (markChangesAreSame is using indexedDB). +// Hence it remains in the plug-in rather than Commonlib. (markChangesAreSame is using indexedDB). // Refactored: markChangesAreSame, unmarkChanges, compareFileFreshness, isMarkedAsSameChanges are now moved to PathService export class ServiceFileHandler extends ServiceFileHandlerBase {} diff --git a/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.ts index 02957595..179d4d9d 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.ts @@ -1,5 +1,5 @@ -import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types"; -import type { IConversionAdapter } from "@lib/serviceModules/adapters"; +import type { UXFileInfoStub, UXFolderInfo } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { IConversionAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters"; import { TFileToUXFileInfoStub, TFolderToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian"; import type { TFile, TFolder } from "obsidian"; diff --git a/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts index 58f1f694..534d1063 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts @@ -1,4 +1,4 @@ -import type { FilePath, UXStat } from "@lib/common/types"; +import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types"; import type { IFileSystemAdapter, IPathAdapter, @@ -6,7 +6,7 @@ import type { IConversionAdapter, IStorageAdapter, IVaultAdapter, -} from "@lib/serviceModules/adapters"; +} from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters"; import type { TAbstractFile, TFile, TFolder, Stat, App } from "obsidian"; import { ObsidianConversionAdapter } from "./ObsidianConversionAdapter"; import { ObsidianPathAdapter } from "./ObsidianPathAdapter"; diff --git a/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts index a21ced14..6557c7b0 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts @@ -1,6 +1,6 @@ import { type TAbstractFile, normalizePath } from "@/deps"; -import type { FilePath } from "@lib/common/types"; -import type { IPathAdapter } from "@lib/serviceModules/adapters"; +import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { IPathAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters"; /** * Path adapter implementation for Obsidian diff --git a/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts index a9133018..2cec3048 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts @@ -1,6 +1,6 @@ -import type { UXDataWriteOptions } from "@lib/common/types"; -import type { IStorageAdapter } from "@lib/serviceModules/adapters"; -import { toArrayBuffer } from "@lib/serviceModules/FileAccessBase"; +import type { UXDataWriteOptions } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { IStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters"; +import { toArrayBuffer } from "@vrtmrz/livesync-commonlib/compat/serviceModules/FileAccessBase"; import type { Stat, App } from "obsidian"; /** diff --git a/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.ts index 74e05bd5..656fabc2 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.ts @@ -1,4 +1,4 @@ -import type { ITypeGuardAdapter } from "@lib/serviceModules/adapters"; +import type { ITypeGuardAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters"; import { TFile, TFolder } from "obsidian"; /** diff --git a/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts index 81d51e04..0cc05d71 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts @@ -1,6 +1,6 @@ -import type { UXDataWriteOptions } from "@lib/common/types"; -import type { IVaultAdapter } from "@lib/serviceModules/adapters"; -import { toArrayBuffer } from "@lib/serviceModules/FileAccessBase"; +import type { UXDataWriteOptions } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { IVaultAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters"; +import { toArrayBuffer } from "@vrtmrz/livesync-commonlib/compat/serviceModules/FileAccessBase"; import type { TFile, App, TFolder } from "obsidian"; /** diff --git a/src/serviceModules/ServiceFileAccessImpl.ts b/src/serviceModules/ServiceFileAccessImpl.ts index 204855ef..c2b40187 100644 --- a/src/serviceModules/ServiceFileAccessImpl.ts +++ b/src/serviceModules/ServiceFileAccessImpl.ts @@ -1,4 +1,4 @@ -import { ServiceFileAccessBase } from "@lib/serviceModules/ServiceFileAccessBase"; +import { ServiceFileAccessBase } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceFileAccessBase"; import type { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter"; // For now, this is just a re-export of ServiceFileAccess with the Obsidian-specific adapter type. diff --git a/src/types.ts b/src/types.ts index b4c40bfd..bb3bb5c5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,8 @@ -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess"; -import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder"; -import type { IFileHandler } from "@lib/interfaces/FileHandler"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess"; -import type { IServiceHub } from "@lib/services/base/IService"; +import type { DatabaseFileAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseFileAccess"; +import type { Rebuilder } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseRebuilder"; +import type { IFileHandler } from "@vrtmrz/livesync-commonlib/compat/interfaces/FileHandler"; +import type { StorageAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/StorageAccess"; +import type { IServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/base/IService"; export interface ServiceModules { storageAccess: StorageAccess; diff --git a/test/contracts/serviceContext.ts b/test/contracts/serviceContext.ts new file mode 100644 index 00000000..9779e195 --- /dev/null +++ b/test/contracts/serviceContext.ts @@ -0,0 +1,75 @@ +import type { ServiceContextContract } from "@vrtmrz/livesync-commonlib/context"; +import type { ServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/ServiceHub"; + +export const SERVICE_CONTEXT_MEMBERS = [ + "API", + "path", + "database", + "databaseEvents", + "replicator", + "fileProcessing", + "replication", + "remote", + "conflict", + "appLifecycle", + "setting", + "tweakValue", + "vault", + "test", + "UI", + "config", + "keyValueDB", + "control", +] as const satisfies readonly Exclude[]; + +export type ServiceContextMember = (typeof SERVICE_CONTEXT_MEMBERS)[number]; +type MissingServiceContextMember = Exclude, ServiceContextMember>; +const serviceContextMembersAreExhaustive: [MissingServiceContextMember] extends [never] ? true : never = true; +void serviceContextMembersAreExhaustive; + +export type ServiceContextResult = { + translation: string; + receivedEvents: string[]; +}; + +export type ServiceCompositionResult = { + hubUsesExpectedContext: boolean; + servicesUsingExpectedContext: Record; +}; + +/** + * Observe the host-neutral results promised by ServiceContextContract. + * + * The caller chooses the translation key because translated text is + * host-configured. Event delivery itself is shared behaviour. + */ +export function observeServiceContext(context: ServiceContextContract, translationKey: string): ServiceContextResult { + const receivedEvents: string[] = []; + const unsubscribe = context.events.onEvent("hello", (value) => receivedEvents.push(value)); + try { + context.events.emitEvent("hello", "context-contract-event"); + } finally { + unsubscribe(); + } + return { + translation: context.translate(translationKey), + receivedEvents, + }; +} + +/** + * Inspect whether a Service Hub and all public services preserve one exact + * context object instead of silently constructing or substituting another. + */ +export function observeServiceComposition( + hub: { readonly context: ServiceContextContract }, + expectedContext: ServiceContextContract +): ServiceCompositionResult { + const members = hub as unknown as Record; + return { + hubUsesExpectedContext: hub.context === expectedContext, + servicesUsingExpectedContext: Object.fromEntries( + SERVICE_CONTEXT_MEMBERS.map((member) => [member, members[member].context === expectedContext]) + ) as Record, + }; +} diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index 56008f2f..0da649e3 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -4,7 +4,7 @@ This directory contains the experimental real Obsidian end-to-end runner. The generic application discovery, isolated-vault, plug-in installation, process lifecycle, CLI, CDP, and readiness implementation comes from `@vrtmrz/obsidian-test-session`. The small modules under `runner/` preserve LiveSync's existing imports and supply its plug-in ID and artefact location. LiveSync-specific fixtures, services, settings, workflows, and assertions remain in this repository. -The current smoke runner verifies only the launch path: +The current smoke runner verifies the launch path and the loaded plug-in's Service Context composition: 1. create a temporary vault, 2. install the built Self-hosted LiveSync plug-in artifacts, @@ -13,8 +13,10 @@ The current smoke runner verifies only the launch path: 5. enable Obsidian community plug-ins for the temporary app profile, 6. reload Self-hosted LiveSync through `obsidian-cli`, 7. verify through `obsidian-cli eval` that the plug-in is loaded, -8. optionally drive a real vault or CouchDB workflow through Obsidian's own API, -9. terminate Obsidian and remove the temporary vault. +8. observe event and translation results from the actual `ObsidianServiceContext`, +9. verify that the Service Hub and every exposed service retain that exact Context, +10. optionally drive a real vault or CouchDB workflow through Obsidian's own API, and +11. terminate Obsidian and remove the temporary vault. The runner does not require Self-hosted LiveSync to expose an E2E-only bridge. Readiness is checked from outside the plug-in through Obsidian's own CLI. @@ -45,6 +47,10 @@ These tests are intended for local verification, not the default CI gate. Reuse ## Commands ```bash +npm run test:contract:contexts +npm run test:contract:context:webapp +npm run test:contract:context:cli +npm run test:contract:context:obsidian npm run test:e2e:obsidian:install-appimage npm run test:e2e:obsidian:discover npm run test:e2e:obsidian:cli-help -- vaults verbose @@ -62,6 +68,10 @@ npm run test:e2e:obsidian:local-suite npm run test:e2e:obsidian:local-suite:services ``` +`test:contract:contexts` runs the directly observable host contract against the Obsidian, CLI, and Webapp compositions. It verifies event and translation results, host-specific capabilities, and that the CLI and Webapp Service Hubs pass one exact Context to all exposed services. `test:contract:context:webapp` runs only the Webapp part. + +`test:contract:context:cli` builds the Node CLI and runs its existing Deno setup, put, read, list, information, remove, conflict-resolution, and revision workflow. `test:contract:context:obsidian` builds the plug-in and runs the real-Obsidian smoke test, including the Context inspection. These runtime scripts are local validation entry points and are not added to the default CI gate by this change. + `test:e2e:obsidian:local-suite` builds the plug-in and, unless `LIVESYNC_CLI_COMMAND` selects an external CLI, the local LiveSync CLI. It then runs discovery, smoke, vault reflection, CouchDB upload, CLI-to-Obsidian synchronisation, Object Storage upload, startup scan, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export in sequence. Start the local CouchDB and MinIO fixtures before running it, or use `test:e2e:obsidian:local-suite:services` to let the wrapper stop leftover fixtures, start fresh fixtures, and stop them again after the run. `test:e2e:obsidian:couchdb-upload` reuses the CouchDB variables from `.test.env` or the process environment. It expects a reachable CouchDB service, creates a unique database, configures Self-hosted LiveSync through `obsidian-cli eval`, creates a note in real Obsidian, commits the note into the local database, runs one-shot synchronisation, and verifies that the remote database contains both the metadata document and its chunk documents. diff --git a/test/e2e-obsidian/runner/liveSyncWorkflow.ts b/test/e2e-obsidian/runner/liveSyncWorkflow.ts index ef842c30..cd85854a 100644 --- a/test/e2e-obsidian/runner/liveSyncWorkflow.ts +++ b/test/e2e-obsidian/runner/liveSyncWorkflow.ts @@ -1,4 +1,5 @@ import { evalObsidianJson } from "./cli.ts"; +import { SERVICE_CONTEXT_MEMBERS } from "../../contracts/serviceContext.ts"; import type { CouchDbConfig } from "./couchdb.ts"; import type { ObjectStorageConfig } from "./objectStorage.ts"; @@ -20,6 +21,17 @@ export type CoreReadiness = { appReady: boolean; }; +export type ObsidianServiceContextContractResult = { + contextType: string; + eventResult: string[]; + translationResult: string; + hubUsesContext: boolean; + serviceContextMismatches: string[]; + appCapabilityMatches: boolean; + pluginCapabilityMatches: boolean; + liveSyncPluginCapabilityMatches: boolean; +}; + export type LocalDatabaseEntry = { id: string; rev: string; @@ -172,6 +184,68 @@ export async function waitForLiveSyncCoreReady( throw new Error(`Timed out waiting for Self-hosted LiveSync core readiness: ${JSON.stringify(lastReadiness)}`); } +/** + * Inspect the actual Obsidian composition through Obsidian's CLI. + * + * This observes public Context results and verifies that the Hub and every + * exposed service retain the exact Context created by the plug-in host. + */ +export async function inspectObsidianServiceContextContract( + cliBinary: string, + env: NodeJS.ProcessEnv +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const plugin=app.plugins.plugins['obsidian-livesync'];", + "const services=plugin.core.services;", + "const context=services.context;", + `const serviceNames=${JSON.stringify(SERVICE_CONTEXT_MEMBERS)};`, + "const eventResult=[];", + "const unsubscribe=context.events.onEvent('hello',(value)=>eventResult.push(value));", + "try{context.events.emitEvent('hello','context-contract-event');}finally{unsubscribe();}", + "return JSON.stringify({", + "contextType:context.constructor.name,", + "eventResult,", + "translationResult:context.translate('Replicator.Message.InitialiseFatalError'),", + "hubUsesContext:services.context===context,", + "serviceContextMismatches:serviceNames.filter((name)=>services[name].context!==context),", + "appCapabilityMatches:context.app===app,", + "pluginCapabilityMatches:context.plugin===plugin,", + "liveSyncPluginCapabilityMatches:context.liveSyncPlugin===plugin,", + "});", + "})()", + ].join(""), + env + ); +} + +export function assertObsidianServiceContextContract(result: ObsidianServiceContextContractResult): void { + assertEqual(result.contextType, "ObsidianServiceContext", "Unexpected Obsidian service Context type."); + assertEqual(result.hubUsesContext, true, "The Obsidian Service Hub substituted its host Context."); + assertEqual( + result.serviceContextMismatches.length, + 0, + `Services used a different Context: ${result.serviceContextMismatches.join(", ")}` + ); + assertEqual( + JSON.stringify(result.eventResult), + JSON.stringify(["context-contract-event"]), + "The Obsidian Context event API returned an unexpected result." + ); + if (result.translationResult.length === 0) { + throw new Error("The Obsidian Context translator returned an empty result."); + } + assertEqual(result.appCapabilityMatches, true, "The Obsidian Context lost its App capability."); + assertEqual(result.pluginCapabilityMatches, true, "The Obsidian Context lost its Plugin capability."); + assertEqual( + result.liveSyncPluginCapabilityMatches, + true, + "The Obsidian Context lost its Self-hosted LiveSync plug-in capability." + ); +} + export async function prepareRemote(cliBinary: string, env: NodeJS.ProcessEnv): Promise { await evalObsidianJson( cliBinary, diff --git a/test/e2e-obsidian/scripts/smoke.ts b/test/e2e-obsidian/scripts/smoke.ts index c00644f3..8dee624a 100644 --- a/test/e2e-obsidian/scripts/smoke.ts +++ b/test/e2e-obsidian/scripts/smoke.ts @@ -1,4 +1,8 @@ import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { + assertObsidianServiceContextContract, + inspectObsidianServiceContextContract, +} from "../runner/liveSyncWorkflow.ts"; import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; import { createTemporaryVault } from "../runner/vault.ts"; @@ -25,6 +29,11 @@ async function main(): Promise { console.log( `Obsidian plug-in ready: ${readiness.pluginId}@${readiness.pluginVersion} in ${readiness.vaultName}` ); + const contextContract = await inspectObsidianServiceContextContract(cli.binary, session.cliEnv); + assertObsidianServiceContextContract(contextContract); + console.log( + `Obsidian service Context contract passed: ${contextContract.contextType}, ${contextContract.serviceContextMismatches.length} mismatches.` + ); await new Promise((resolve) => setTimeout(resolve, Number(process.env.E2E_OBSIDIAN_SMOKE_TIMEOUT_MS ?? 1000))); console.log("Obsidian stayed alive after the plug-in readiness check."); } finally { diff --git a/test/harness/harness.ts b/test/harness/harness.ts index ef2d20eb..732493f3 100644 --- a/test/harness/harness.ts +++ b/test/harness/harness.ts @@ -1,10 +1,10 @@ import { App } from "@/deps.ts"; import ObsidianLiveSyncPlugin from "@/main"; -import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@/lib/src/common/types"; -import { LOG_LEVEL_VERBOSE, setGlobalLogFunction } from "@lib/common/logger"; +import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { LOG_LEVEL_VERBOSE, setGlobalLogFunction } from "@vrtmrz/livesync-commonlib/compat/common/logger"; import { SettingCache } from "./obsidian-mock"; import { delay, fireAndForget, promiseWithResolvers } from "octagonal-wheels/promises"; -import { EVENT_PLATFORM_UNLOADED } from "@lib/events/coreEvents"; +import { EVENT_PLATFORM_UNLOADED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; import { EVENT_LAYOUT_READY, eventHub } from "@/common/events"; import { env } from "../suite/variables"; diff --git a/test/lib/commands.ts b/test/lib/commands.ts index 762b5c0c..c8655380 100644 --- a/test/lib/commands.ts +++ b/test/lib/commands.ts @@ -1,4 +1,4 @@ -import type { P2PSyncSetting } from "@/lib/src/common/types"; +import type { P2PSyncSetting } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { delay } from "octagonal-wheels/promises"; import type { BrowserContext, Page } from "playwright"; import type { Plugin } from "vitest/config"; diff --git a/test/lib/ui.ts b/test/lib/ui.ts index 3d2381a6..b4071cf5 100644 --- a/test/lib/ui.ts +++ b/test/lib/ui.ts @@ -1,5 +1,5 @@ import { page } from "vitest/browser"; -import { delay } from "@/lib/src/common/utils"; +import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils"; export async function waitForDialogShown(dialogText: string, timeout = 500) { const ttl = Date.now() + timeout; diff --git a/test/lib/util.ts b/test/lib/util.ts index 502d0d2c..23a8ce65 100644 --- a/test/lib/util.ts +++ b/test/lib/util.ts @@ -1,4 +1,4 @@ -import { delay } from "@/lib/src/common/utils"; +import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils"; export async function waitTaskWithFollowups( task: Promise, diff --git a/test/suite/db_common.ts b/test/suite/db_common.ts index f81f084f..d6e82012 100644 --- a/test/suite/db_common.ts +++ b/test/suite/db_common.ts @@ -1,7 +1,7 @@ import { compareMTime, EVEN } from "@/common/utils"; import { TFile, type DataWriteOptions } from "@/deps"; -import type { FilePath } from "@/lib/src/common/types"; -import { isDocContentSame, readContent } from "@/lib/src/common/utils"; +import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { isDocContentSame, readContent } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import { waitForIdle, type LiveSyncHarness } from "../harness/harness"; import { expect } from "vitest"; diff --git a/test/suite/onlylocaldb.test.ts b/test/suite/onlylocaldb.test.ts index acfbb65b..a140dd58 100644 --- a/test/suite/onlylocaldb.test.ts +++ b/test/suite/onlylocaldb.test.ts @@ -1,8 +1,8 @@ import { beforeAll, describe, expect, it, test } from "vitest"; import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness"; import { TFile } from "@/deps.ts"; -import { DEFAULT_SETTINGS, type FilePath, type ObsidianLiveSyncSettings } from "@/lib/src/common/types"; -import { isDocContentSame, readContent } from "@/lib/src/common/utils"; +import { DEFAULT_SETTINGS, type FilePath, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { isDocContentSame, readContent } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import { DummyFileSourceInisialised, generateBinaryFile, generateFile, init } from "../utils/dummyfile"; const localdb_test_setting = { diff --git a/test/suite/sync.senario.basic.ts b/test/suite/sync.senario.basic.ts index 3eafe3cb..2c8b301f 100644 --- a/test/suite/sync.senario.basic.ts +++ b/test/suite/sync.senario.basic.ts @@ -3,7 +3,7 @@ // and edge, resolving conflicts, etc. will be covered in separate test suites. import { afterAll, beforeAll, describe, expect, it, test } from "vitest"; import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness"; -import { RemoteTypes, type FilePath, type ObsidianLiveSyncSettings } from "@/lib/src/common/types"; +import { RemoteTypes, type FilePath, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { DummyFileSourceInisialised, @@ -13,7 +13,7 @@ import { generateFile, } from "../utils/dummyfile"; import { checkStoredFileInDB, testFileRead, testFileWrite } from "./db_common"; -import { delay } from "@/lib/src/common/utils"; +import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import { commands } from "vitest/browser"; import { closeReplication, performReplication, prepareRemote } from "./sync_common"; import type { DataWriteOptions } from "@/deps.ts"; diff --git a/test/suite/sync.single.test.ts b/test/suite/sync.single.test.ts index 9be98b44..a03f3bb0 100644 --- a/test/suite/sync.single.test.ts +++ b/test/suite/sync.single.test.ts @@ -7,7 +7,7 @@ import { PREFERRED_SETTING_SELF_HOSTED, RemoteTypes, type ObsidianLiveSyncSettings, -} from "@/lib/src/common/types"; +} from "@vrtmrz/livesync-commonlib/compat/common/types"; import { defaultFileOption } from "./db_common"; import { syncBasicCase } from "./sync.senario.basic.ts"; diff --git a/test/suite/sync.test.ts b/test/suite/sync.test.ts index aa284c17..6873f121 100644 --- a/test/suite/sync.test.ts +++ b/test/suite/sync.test.ts @@ -7,7 +7,7 @@ import { PREFERRED_SETTING_SELF_HOSTED, RemoteTypes, type ObsidianLiveSyncSettings, -} from "@/lib/src/common/types"; +} from "@vrtmrz/livesync-commonlib/compat/common/types"; import { defaultFileOption } from "./db_common"; import { syncBasicCase } from "./sync.senario.basic.ts"; diff --git a/test/suite/sync_common.ts b/test/suite/sync_common.ts index 74da8664..82647d78 100644 --- a/test/suite/sync_common.ts +++ b/test/suite/sync_common.ts @@ -1,10 +1,10 @@ import { expect } from "vitest"; import { waitForIdle, type LiveSyncHarness } from "../harness/harness"; -import { RemoteTypes, type ObsidianLiveSyncSettings } from "@/lib/src/common/types"; +import { RemoteTypes, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; -import { delay, fireAndForget } from "@/lib/src/common/utils"; +import { delay, fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import { commands } from "vitest/browser"; -import { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator"; +import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator"; import { waitTaskWithFollowups } from "../lib/util"; async function waitForP2PPeers(harness: LiveSyncHarness) { if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { diff --git a/test/suite/variables.ts b/test/suite/variables.ts index f55cce26..6207c86a 100644 --- a/test/suite/variables.ts +++ b/test/suite/variables.ts @@ -1,10 +1,10 @@ -import { DoctorRegulation } from "@/lib/src/common/configForDoc"; +import { DoctorRegulation } from "@vrtmrz/livesync-commonlib/compat/common/configForDoc"; import { DEFAULT_SETTINGS, ChunkAlgorithms, AutoAccepting, type ObsidianLiveSyncSettings, -} from "@/lib/src/common/types"; +} from "@vrtmrz/livesync-commonlib/compat/common/types"; export const env = (import.meta as any).env; export const settingBase = { ...DEFAULT_SETTINGS, diff --git a/test/suitep2p/sync_common_p2p.ts b/test/suitep2p/sync_common_p2p.ts index 53009894..3b37f814 100644 --- a/test/suitep2p/sync_common_p2p.ts +++ b/test/suitep2p/sync_common_p2p.ts @@ -7,9 +7,9 @@ */ import { expect } from "vitest"; import { waitForIdle, type LiveSyncHarness } from "../harness/harness"; -import { RemoteTypes, type ObsidianLiveSyncSettings } from "@/lib/src/common/types"; -import { delay } from "@/lib/src/common/utils"; -import { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator"; +import { RemoteTypes, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator"; import { waitTaskWithFollowups } from "../lib/util"; const P2P_REPLICATION_TIMEOUT_MS = 180000; diff --git a/test/suitep2p/syncp2p.p2p-down.test.ts b/test/suitep2p/syncp2p.p2p-down.test.ts index 7f3b77f7..4d1c4e17 100644 --- a/test/suitep2p/syncp2p.p2p-down.test.ts +++ b/test/suitep2p/syncp2p.p2p-down.test.ts @@ -15,10 +15,10 @@ import { type FilePath, type ObsidianLiveSyncSettings, AutoAccepting, -} from "@/lib/src/common/types"; +} from "@vrtmrz/livesync-commonlib/compat/common/types"; import { DummyFileSourceInisialised, generateBinaryFile, generateFile } from "../utils/dummyfile"; import { defaultFileOption, testFileRead } from "../suite/db_common"; -import { delay } from "@/lib/src/common/utils"; +import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import { closeReplication, performReplication } from "./sync_common_p2p"; import { settingBase } from "../suite/variables"; diff --git a/test/suitep2p/syncp2p.p2p-up.test.ts b/test/suitep2p/syncp2p.p2p-up.test.ts index 7c463eb3..de5d5cef 100644 --- a/test/suitep2p/syncp2p.p2p-up.test.ts +++ b/test/suitep2p/syncp2p.p2p-up.test.ts @@ -17,7 +17,7 @@ import { RemoteTypes, type ObsidianLiveSyncSettings, AutoAccepting, -} from "@/lib/src/common/types"; +} from "@vrtmrz/livesync-commonlib/compat/common/types"; import { DummyFileSourceInisialised, FILE_SIZE_BINS, @@ -26,7 +26,7 @@ import { generateFile, } from "../utils/dummyfile"; import { checkStoredFileInDB, defaultFileOption, testFileWrite } from "../suite/db_common"; -import { delay } from "@/lib/src/common/utils"; +import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import { closeReplication, performReplication } from "./sync_common_p2p"; import { settingBase } from "../suite/variables"; diff --git a/test/suitep2p/syncp2p.test.ts b/test/suitep2p/syncp2p.test.ts index 08c2c101..416ab1f1 100644 --- a/test/suitep2p/syncp2p.test.ts +++ b/test/suitep2p/syncp2p.test.ts @@ -7,7 +7,7 @@ import { PREFERRED_SETTING_SELF_HOSTED, RemoteTypes, type ObsidianLiveSyncSettings, -} from "@/lib/src/common/types"; +} from "@vrtmrz/livesync-commonlib/compat/common/types"; import { settingBase } from "../suite/variables.ts"; import { defaultFileOption } from "../suite/db_common"; diff --git a/test/unit/dialog.test.ts b/test/unit/dialog.test.ts index 86c424cc..8ebbf12c 100644 --- a/test/unit/dialog.test.ts +++ b/test/unit/dialog.test.ts @@ -3,12 +3,12 @@ import { beforeAll, describe, expect, it } from "vitest"; import { commands } from "vitest/browser"; import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness"; -import { ChunkAlgorithms, DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@/lib/src/common/types"; +import { ChunkAlgorithms, DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { DummyFileSourceInisialised } from "../utils/dummyfile"; import { page } from "vitest/browser"; -import { DoctorRegulation } from "@/lib/src/common/configForDoc"; +import { DoctorRegulation } from "@vrtmrz/livesync-commonlib/compat/common/configForDoc"; import { waitForDialogHidden, waitForDialogShown } from "../lib/ui"; const env = (import.meta as any).env; const dialog_setting_base = { diff --git a/test/utils/dummyfile.ts b/test/utils/dummyfile.ts index ab4b8b3f..66cf6878 100644 --- a/test/utils/dummyfile.ts +++ b/test/utils/dummyfile.ts @@ -1,4 +1,4 @@ -import { DEFAULT_SETTINGS } from "@/lib/src/common/types.ts"; +import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { readFile } from "../utils/fileapi.vite.ts"; let charset = ""; export async function init() { diff --git a/tsconfig.json b/tsconfig.json index 80944670..e30457f9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,8 +19,7 @@ "strictBindCallApply": true, "strictFunctionTypes": true, "paths": { - "@/*": ["./src/*"], - "@lib/*": ["./src/lib/src/*", "./_types/src/lib/src/*"] + "@/*": ["./src/*"] } }, "include": ["**/*.ts", "test/**/*.test.ts", "**/*.unit.spec.ts", "**/*.svelte"], diff --git a/tsconfig.types.json b/tsconfig.types.json deleted file mode 100644 index 089dbfa5..00000000 --- a/tsconfig.types.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "noEmit": false, - "declaration": true, - "emitDeclarationOnly": true, - "outDir": "./_types", - "rootDir": "." - }, - "include": ["src/lib/**/*.ts"], - "exclude": [ - "_types", - "pouchdb-browser-webpack", - "utils", - "src/apps", - "src/**/*.test.ts", - "src/lib/_tools", - "src/lib/apps", - "src/lib/src/cli", - "**/_test/**", - "utilsdeno", - "node_modules", - "test/**/*.test.ts", - "**/*.unit.spec.ts" - ] -} diff --git a/updates.md b/updates.md index 36b28f43..e9d1f9cf 100644 --- a/updates.md +++ b/updates.md @@ -5,6 +5,15 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid ## Unreleased +### Miscellaneous + +- Replaced the embedded Commonlib source and generated fallback declarations with a locked compiled package, reducing duplicated release and repository-scanner inputs without changing synchronisation behaviour. + +### Testing + +- Added packed-package and downstream checks for Commonlib entry points, including isolated Node and browser File System Access API storage implementations. +- Added reusable Context result contracts for Obsidian, CLI, and Webapp compositions, including a real-Obsidian smoke assertion that every service retains the host-provided Context. + ## 0.25.83 16th July, 2026 diff --git a/utils/bench/splitPiecesRabinKarp.ts b/utils/bench/splitPiecesRabinKarp.ts index 1c4642c7..f529b448 100644 --- a/utils/bench/splitPiecesRabinKarp.ts +++ b/utils/bench/splitPiecesRabinKarp.ts @@ -2,15 +2,19 @@ import { glob } from "glob"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { promises as fs } from "node:fs"; -import { isPlainText, shouldSplitAsPlainText } from "../../src/lib/src/string_and_binary/path"; -import { splitPiecesRabinKarp } from "../../src/lib/src/string_and_binary/chunks"; +import { isPlainText, shouldSplitAsPlainText } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path"; +import { splitPiecesRabinKarp } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/chunks"; import { PREFERRED_BASE, PREFERRED_JOURNAL_SYNC, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED, -} from "../../src/lib/src/common/models/setting.const.preferred"; -import { type ObsidianLiveSyncSettings, DEFAULT_SETTINGS, MAX_DOC_SIZE_BIN } from "../../src/lib/src/common/types"; +} from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const.preferred"; +import { + type ObsidianLiveSyncSettings, + DEFAULT_SETTINGS, + MAX_DOC_SIZE_BIN, +} from "@vrtmrz/livesync-commonlib/compat/common/types"; async function blobFromString(content: string): Promise { return new Blob([content], { type: "text/plain" }); diff --git a/utils/release-process.unit.spec.ts b/utils/release-process.unit.spec.ts index 94c9c125..5c3194f0 100644 --- a/utils/release-process.unit.spec.ts +++ b/utils/release-process.unit.spec.ts @@ -130,20 +130,13 @@ describe("release notes", () => { }); describe("release workflow", () => { - it("regenerates and stages fallback type definitions", () => { + it("uses the locked Commonlib package instead of generated fallback declarations", () => { const workflow = readFileSync(prepareReleaseWorkflow, "utf8"); - expect(workflow).toContain("npm run build:lib:types"); - expect(workflow).toMatch(/git add[^\n]*_types/); - }); - - it("installs Deno before post-processing fallback type definitions", () => { - const workflow = readFileSync(prepareReleaseWorkflow, "utf8"); - const setupDeno = workflow.indexOf("denoland/setup-deno@v2"); - const buildTypes = workflow.indexOf("npm run build:lib:types"); - - expect(setupDeno).toBeGreaterThan(-1); - expect(setupDeno).toBeLessThan(buildTypes); + expect(workflow).not.toContain("npm run build:lib:types"); + expect(workflow).not.toMatch(/git add[^\n]*_types/); + expect(workflow).toMatch(/git add[^\n]*package-lock\.json/); + expect(workflow).toContain("locked Commonlib package version"); }); it("keeps the release PR in draft until BRAT validation", () => { diff --git a/utilsdeno/README.md b/utilsdeno/README.md index dc46fc49..515c2239 100644 --- a/utilsdeno/README.md +++ b/utilsdeno/README.md @@ -32,7 +32,7 @@ Converts standard global variable usages to compatibility wrappers to ensure saf * **Targets**: `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval`, `requestAnimationFrame`, `cancelAnimationFrame`, `localStorage`, `navigator`, `location`, `window`, `globalThis`, and `document`. * **Actions**: * Replaces global namespace references (like `window` and `globalThis`) with `compatGlobal`. - * Replaces `document` with `_activeDocument` (from `@lib/common/coreEnvFunctions.ts`). +* Replaces `document` with `_activeDocument` from the Commonlib compatibility entry. * Injects or updates the necessary imports in modified files. * **Command**: ```bash @@ -86,29 +86,15 @@ Scans the codebase and logs all occurrences of explicit `any` types. ``` ### 6. Import Normalisation (`normalise-imports.ts`) -Ensures that all import statements are standardised across the codebase, resolving paths to aliases such as `@lib/` and `@/` where applicable. +Ensures that internal plug-in import statements are standardised to the `@/` alias where applicable. Commonlib imports remain explicit package subpaths and are not rewritten. * **Command**: ```bash deno run --allow-read --allow-write --allow-env normalise-imports.ts ``` -### 7. CLI Node.js Import Redirection (`refactor-cli-node-imports.ts`) -Redirects direct Node.js built-in module imports (like `fs` and `path`) within the CLI codebase to use a single barrel file (`src/apps/cli/node-compat.ts`). - -* **Actions**: - * Finds imports of Node.js built-in APIs (`fs`, `fs/promises`, `path`, and `readline/promises`) in CLI source files. - * Replaces them with imports from the local `node-compat.ts` barrel file. - * This eliminates duplicate browser-targeted linter warnings on Node.js built-ins in the CLI workspace, keeping linter ignores consolidated. -* **Command**: - ```bash - deno run --allow-read --allow-write --allow-env refactor-cli-node-imports.ts - ``` - ---- - ## Safety and Exclusions * **Tests Excluded**: All scripts automatically skip files located in `_test/` or `testdeno/` folders, as well as files ending with `.spec.ts` or `.test.ts`. -* **Submodule Caution**: Some tools will run against the `src/lib/` submodule. Ensure you verify changes inside the submodule prior to committing. +* **Package Boundary**: These tools operate on this repository only. Changes to Commonlib belong in its own repository and must be validated with its package checks. * **Verification**: Always run `npm run check` and `npm run test:unit` after performing refactoring tasks to verify that type safety and tests remain intact. diff --git a/utilsdeno/normalise-imports.ts b/utilsdeno/normalise-imports.ts index 11c3d153..c23ee0eb 100644 --- a/utilsdeno/normalise-imports.ts +++ b/utilsdeno/normalise-imports.ts @@ -1,4 +1,4 @@ -// Normalise import and export paths in the codebase to use @lib/ and @/ aliases correctly. +// Normalise import and export paths in the codebase to use the @/ alias correctly. // Use this script by running `deno run --allow-read --allow-write normalise-imports.ts` from the utilsdeno directory. // Set the --run flag to apply changes: `deno run --allow-read --allow-write normalise-imports.ts --run` // Set the --all-alias flag to also normalise sibling/child imports (starting with ./): `deno run --allow-read --allow-write normalise-imports.ts --all-alias` @@ -39,12 +39,9 @@ function toPosixPath(filePath: string): string { const posixProjectRoot = toPosixPath(projectRoot); const posixSrc = `${posixProjectRoot}/src`; -const posixLibSrc = `${posixProjectRoot}/src/lib/src`; -const posixSubrepo = `${posixProjectRoot}/src/lib`; console.log(`Project Root: ${posixProjectRoot}`); console.log(`Source Directory: ${posixSrc}`); -console.log(`Library Source Directory: ${posixLibSrc}`); console.log(""); let modifiedFilesCount = 0; @@ -87,7 +84,7 @@ for (const sourceFile of project.getSourceFiles()) { // Determine if it is an internal import. const isRelative = moduleSpecifier.startsWith("."); - const isAlias = moduleSpecifier.startsWith("@/") || moduleSpecifier.startsWith("@lib/"); + const isAlias = moduleSpecifier.startsWith("@/"); if (!isRelative && !isAlias) { // Skip external packages/modules. @@ -96,9 +93,7 @@ for (const sourceFile of project.getSourceFiles()) { // Resolve path to an absolute POSIX path. let resolvedPath = ""; - if (moduleSpecifier.startsWith("@lib/")) { - resolvedPath = `${posixLibSrc}/${moduleSpecifier.slice(5)}`; - } else if (moduleSpecifier.startsWith("@/")) { + if (moduleSpecifier.startsWith("@/")) { resolvedPath = `${posixSrc}/${moduleSpecifier.slice(2)}`; } else { // Relative path. @@ -107,14 +102,9 @@ for (const sourceFile of project.getSourceFiles()) { resolvedPath = toPosixPath(path.normalize(resolvedPath)); - // Keep relative sibling/child imports unchanged (e.g. ./utils) unless: - // 1. --all-alias is set, OR - // 2. the import crosses the subrepository boundary (src/lib/) + // Keep relative sibling/child imports unchanged (e.g. ./utils) unless --all-alias is set. const isSibling = isRelative && !moduleSpecifier.startsWith(".."); - const importerInsideSubrepo = posixFilePath.startsWith(posixSubrepo + "/"); - const targetInsideSubrepo = resolvedPath.startsWith(posixSubrepo + "/"); - const crossesSubrepo = importerInsideSubrepo !== targetInsideSubrepo; - if (isSibling && !allAlias && !crossesSubrepo) { + if (isSibling && !allAlias) { continue; } @@ -126,18 +116,7 @@ for (const sourceFile of project.getSourceFiles()) { moduleSpecifier.endsWith(".svelte") || moduleSpecifier.endsWith(".d.ts"); - if (resolvedPath.startsWith(posixLibSrc + "/")) { - let rel = resolvedPath.slice(posixLibSrc.length + 1); - if (!hasExtension && (rel.endsWith(".ts") || rel.endsWith(".js"))) { - // Strip extension if the original import did not have one. - if (rel.endsWith(".ts") && !rel.endsWith(".d.ts")) { - rel = rel.slice(0, -3); - } else if (rel.endsWith(".js")) { - rel = rel.slice(0, -3); - } - } - newSpecifier = `@lib/${rel}`; - } else if (resolvedPath.startsWith(posixSrc + "/")) { + if (resolvedPath.startsWith(posixSrc + "/")) { let rel = resolvedPath.slice(posixSrc.length + 1); if (!hasExtension && (rel.endsWith(".ts") || rel.endsWith(".js"))) { // Strip extension if the original import did not have one. diff --git a/utilsdeno/refactor-cli-node-imports.ts b/utilsdeno/refactor-cli-node-imports.ts deleted file mode 100644 index 36f88536..00000000 --- a/utilsdeno/refactor-cli-node-imports.ts +++ /dev/null @@ -1,132 +0,0 @@ -// Refactor Node.js imports in the CLI application to use the barrel compatibility file. -// Use this script by running `deno run --allow-read --allow-write --allow-env refactor-cli-node-imports.ts` from the utilsdeno directory. -// Run with --run flag to apply changes. -import { Project, SyntaxKind, Node } from "npm:ts-morph"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const isDryRun = !Deno.args.includes("--run"); - -if (isDryRun) { - console.log("=== DRY RUN MODE ==="); - console.log( - "To apply changes, run with: deno run --allow-read --allow-write --allow-env refactor-cli-node-imports.ts --run\n" - ); -} else { - console.log("=== RUN MODE: WILL MODIFY FILES ==="); -} - -const project = new Project({ tsConfigFilePath: "../tsconfig.json" }); -project.addSourceFilesAtPaths("../src/apps/cli/**/*.ts"); - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const projectRoot = path.resolve(__dirname, ".."); -const nodeCompatPath = path.resolve(projectRoot, "src", "apps", "cli", "node-compat.ts"); - -function toPosixPath(filePath: string): string { - return filePath.replace(/\\/g, "/"); -} - -const posixProjectRoot = toPosixPath(projectRoot); -const posixSrc = `${posixProjectRoot}/src`; - -function getRelativeImportPath(fromFile: string, toFile: string): string { - let rel = path.relative(path.dirname(fromFile), toFile); - rel = rel.replace(/\\/g, "/"); - if (!rel.startsWith(".") && !rel.startsWith("/")) { - rel = "./" + rel; - } - if (rel.endsWith(".ts")) { - rel = rel.slice(0, -3); - } - return rel; -} - -let modifiedFilesCount = 0; - -for (const sourceFile of project.getSourceFiles()) { - const filePath = sourceFile.getFilePath(); - const posixFilePath = toPosixPath(filePath); - - // Only process CLI source files under src/apps/cli/ - if (!posixFilePath.includes("/src/apps/cli/")) continue; - if ( - posixFilePath.endsWith("node-compat.ts") || - posixFilePath.endsWith("vite.config.ts") || - posixFilePath.endsWith(".spec.ts") || - posixFilePath.endsWith(".test.ts") || - posixFilePath.includes("/_test/") || - posixFilePath.includes("/testdeno/") || - posixFilePath.includes("/test/") - ) { - continue; - } - - const importDeclarations = sourceFile.getImportDeclarations(); - const targetImports: any[] = []; - const namedImportsToAdd: string[] = []; - - for (const impDecl of importDeclarations) { - const specifier = impDecl.getModuleSpecifierValue(); - - // Check if it's a Node.js built-in module we want to redirect - let exportedName = ""; - if (specifier === "fs/promises" || specifier === "node:fs/promises") { - exportedName = "fsPromises"; - } else if (specifier === "fs" || specifier === "node:fs") { - exportedName = "fs"; - } else if (specifier === "path" || specifier === "node:path") { - exportedName = "path"; - } else if (specifier === "node:readline/promises") { - exportedName = "readline"; - } - - if (exportedName) { - const localName = impDecl.getNamespaceImport()?.getText() || impDecl.getDefaultImport()?.getText(); - if (localName) { - targetImports.push({ impDecl, exportedName, localName }); - } - } - } - - if (targetImports.length > 0) { - console.log(`File: ${posixFilePath.slice(posixProjectRoot.length + 1)}`); - - for (const { impDecl, exportedName, localName } of targetImports) { - const { line } = sourceFile.getLineAndColumnAtPos(impDecl.getStart()); - console.log(` Line ${line}: Redirecting "${impDecl.getText()}"`); - - if (exportedName === localName) { - namedImportsToAdd.push(exportedName); - } else { - namedImportsToAdd.push(`${exportedName} as ${localName}`); - } - - if (!isDryRun) { - impDecl.remove(); - } - } - - const relImportPath = getRelativeImportPath(filePath, nodeCompatPath); - console.log(` Adding: import { ${namedImportsToAdd.join(", ")} } from "${relImportPath}"`); - - if (!isDryRun) { - sourceFile.addImportDeclaration({ - namedImports: namedImportsToAdd, - moduleSpecifier: relImportPath, - }); - } - - modifiedFilesCount++; - } -} - -console.log(`\nTotal files to modify: ${modifiedFilesCount}`); - -if (!isDryRun) { - project.saveSync(); - console.log("All changes successfully saved."); -} else { - console.log("Dry run complete. No changes were written to files."); -} diff --git a/utilsdeno/refactor-globals.ts b/utilsdeno/refactor-globals.ts index bd368dfd..ea54a1f4 100644 --- a/utilsdeno/refactor-globals.ts +++ b/utilsdeno/refactor-globals.ts @@ -32,7 +32,6 @@ function toPosixPath(filePath: string): string { const posixProjectRoot = toPosixPath(projectRoot); const posixSrc = `${posixProjectRoot}/src`; -const posixLibSrc = `${posixProjectRoot}/src/lib`; const TARGET_GLOBALS = new Set([ "setTimeout", @@ -191,7 +190,7 @@ for (const sourceFile of project.getSourceFiles()) { if (requiredImports.length > 0) { const existingImport = sourceFile.getImportDeclarations().find((imp) => { const spec = imp.getModuleSpecifierValue(); - return spec === "@lib/common/coreEnvFunctions" || spec === "@lib/common/coreEnvFunctions.ts"; + return spec === "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions" || spec === "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; }); if (existingImport) { @@ -206,7 +205,7 @@ for (const sourceFile of project.getSourceFiles()) { } else { sourceFile.addImportDeclaration({ namedImports: requiredImports, - moduleSpecifier: "@lib/common/coreEnvFunctions.ts", + moduleSpecifier: "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions", }); } } diff --git a/utilsdeno/refactor-import-utils.ts b/utilsdeno/refactor-import-utils.ts deleted file mode 100644 index 96e45dfd..00000000 --- a/utilsdeno/refactor-import-utils.ts +++ /dev/null @@ -1,187 +0,0 @@ -// Delete references to utils.ts and replace them with new imports based on the importMap. -// Use this script by running `deno run --allow-read --allow-write --allow-run refactor-import-utils.ts` from the utilsdeno directory. -import { Project } from "npm:ts-morph"; - -const isDryRun = !Deno.args.includes("--run"); - -if (isDryRun) { - console.log("=== DRY RUN MODE ==="); - console.log( - "To apply changes, run with: deno run --allow-read --allow-write --allow-run refactor-import-utils.ts --run\n" - ); -} - -// const project = new Project({ tsConfigFilePath: "../src/apps/cli/tsconfig.json" }); -const project = new Project({ tsConfigFilePath: "../tsconfig.json" }); - -const importMap = new Map(); - -const targetFiles = [ - "utils.concurrency.ts", - "utils.timer.ts", - "utils.notations.ts", - "utils.database.ts", - "utils.regexp.ts", - "utils.settings.ts", - "utils.patch.ts", - "utils.misc.ts", -]; - -// 1. Map exports from our newly created subfiles -for (const sourceFile of project.getSourceFiles()) { - const filePath = sourceFile.getFilePath(); - const fileName = sourceFile.getBaseName(); - if (filePath.includes("src/lib/src/common/") && targetFiles.includes(fileName)) { - const exports = sourceFile.getExportedDeclarations(); - for (const [name] of exports) { - const relativePath = filePath.split("src/lib/src/")[1].replace(/\.ts$/, ""); - importMap.set(name, `@lib/${relativePath}`); - } - } -} - -// 2. Map exports/imports of octagonal-wheels in utils.ts -const utilsFile = project.getSourceFile("src/lib/src/common/utils.ts"); -if (utilsFile) { - // Parse imports from octagonal-wheels - for (const imp of utilsFile.getImportDeclarations()) { - const moduleSpec = imp.getModuleSpecifierValue(); - if (moduleSpec.startsWith("octagonal-wheels")) { - for (const namedImport of imp.getNamedImports()) { - importMap.set(namedImport.getName(), moduleSpec); - } - } - } - // Parse export declarations from octagonal-wheels - for (const exp of utilsFile.getExportDeclarations()) { - const moduleSpec = exp.getModuleSpecifierValue(); - if (moduleSpec && moduleSpec.startsWith("octagonal-wheels")) { - for (const namedExport of exp.getNamedExports()) { - importMap.set(namedExport.getName(), moduleSpec); - } - } - } -} - -console.log(`Built importMap with ${importMap.size} mappings.\n`); - -let modifiedFilesCount = 0; - -// 3. Loop through all source files and replace imports -for (const sourceFile of project.getSourceFiles()) { - let fileModified = false; - const imports = sourceFile.getImportDeclarations(); - - for (const imp of imports) { - const moduleSpec = imp.getModuleSpecifierValue(); - const isUtilsImport = - moduleSpec === "@lib/common/utils" || - moduleSpec === "@lib/common/utils.ts" || - moduleSpec.endsWith("/common/utils") || - moduleSpec.endsWith("/common/utils.ts"); - - if (isUtilsImport) { - const namedImports = imp.getNamedImports(); - const defaultImport = imp.getDefaultImport(); - - const importsToReplace: Record = {}; - for (const namedImport of namedImports) { - const name = namedImport.getName(); - let newPath = importMap.get(name); - if (newPath) { - // If original ended with .ts and the new path starts with @lib, keep .ts - if (moduleSpec.endsWith(".ts") && newPath.startsWith("@lib/")) { - newPath = newPath + ".ts"; - } - if (!importsToReplace[newPath]) { - importsToReplace[newPath] = []; - } - importsToReplace[newPath].push({ - name, - newPath, - isTypeOnly: namedImport.isTypeOnly() || imp.isTypeOnly(), - }); - } - } - - if (Object.keys(importsToReplace).length > 0 || (defaultImport && importMap.has(defaultImport.getText()))) { - fileModified = true; - - console.log(`File: ${sourceFile.getFilePath().split("obsidian-livesync/")[1]}`); - console.log(` Old: ${imp.getText()}`); - } - - if (!isDryRun) { - // Apply replacements - for (const newPath in importsToReplace) { - const isTypeOnly = importsToReplace[newPath].filter((i) => i.isTypeOnly); - if (isTypeOnly.length > 0) { - sourceFile.insertImportDeclaration(imp.getChildIndex(), { - namedImports: isTypeOnly.map((i) => i.name), - moduleSpecifier: newPath, - isTypeOnly: true, - }); - } - const isValueImport = importsToReplace[newPath].filter((i) => !i.isTypeOnly); - if (isValueImport.length > 0) { - sourceFile.insertImportDeclaration(imp.getChildIndex(), { - namedImports: isValueImport.map((i) => i.name), - moduleSpecifier: newPath, - isTypeOnly: false, - }); - } - for (const { name } of importsToReplace[newPath]) { - const namedImport = imp.getNamedImports().find((ni) => ni.getName() === name); - if (namedImport) { - namedImport.remove(); - } - } - } - } else { - // In dry run, just print what it would do - for (const newPath in importsToReplace) { - const names = importsToReplace[newPath].map((i) => i.name).join(", "); - console.log(` -> Would import { ${names} } from "${newPath}"`); - } - } - - if (defaultImport) { - const name = defaultImport.getText(); - let newPath = importMap.get(name); - if (newPath) { - if (moduleSpec.endsWith(".ts") && newPath.startsWith("@lib/")) { - newPath = newPath + ".ts"; - } - if (!isDryRun) { - sourceFile.insertImportDeclaration(imp.getChildIndex(), { - defaultImport: name, - moduleSpecifier: newPath, - isTypeOnly: imp.isTypeOnly(), - }); - imp.removeDefaultImport(); - } else { - console.log(` -> Would import default ${name} from "${newPath}"`); - } - } - } - - if (!isDryRun) { - if (imp.getNamedImports().length === 0 && !imp.getDefaultImport()) { - imp.remove(); - } - } - } - } - if (fileModified) { - modifiedFilesCount++; - } -} - -console.log(`\nTotal files to modify: ${modifiedFilesCount}`); - -if (!isDryRun) { - project.saveSync(); - console.log("All changes successfully saved."); -} else { - console.log("Dry run complete. No changes were written to files."); -} diff --git a/utilsdeno/refactor-imports.ts b/utilsdeno/refactor-imports.ts deleted file mode 100644 index b7d7a6a2..00000000 --- a/utilsdeno/refactor-imports.ts +++ /dev/null @@ -1,155 +0,0 @@ -// Delete references to types.ts and replace them with new imports based on the importMap. It will also split imports if some are type-only and some are value imports. -// Use this script by running `deno run --allow-read --allow-write --allow-run refactor-imports.ts` from the utilsdeno directory. It will read all source files, find imports from types.ts, and replace them with the new paths based on the importMap. Make sure to review the changes before saving, as it will modify your source files. -import { Project } from "npm:ts-morph"; - -const isDryRun = !Deno.args.includes("--run"); - -if (isDryRun) { - console.log("=== DRY RUN MODE ==="); - console.log( - "To apply changes, run with: deno run --allow-read --allow-write --allow-run refactor-import-utils.ts --run\n" - ); -} - -// const project = new Project({ tsConfigFilePath: "../src/apps/cli/tsconfig.json" }); -const project = new Project({ tsConfigFilePath: "../tsconfig.json" }); - -const importMap = new Map(); -// Build a map of types moved out of Models. -// Under src/lib/src/common/models. -for (const sourceFile of project.getSourceFiles()) { - if (sourceFile.getFilePath().includes("src/lib/src/common/models")) { - const exports = sourceFile.getExportedDeclarations(); - for (const [name, declarations] of exports) { - for (const declaration of declarations) { - if ( - // declaration.getKindName() === "TypeAliasDeclaration" || - // declaration.getKindName() === "InterfaceDeclaration" || - // declaration.getKindName() === "EnumDeclaration" || - true - ) { - // console.log(`Found type export in ${sourceFile.getFilePath()}:`, name); - const relativePath = sourceFile.getFilePath().split("src/lib/src/")[1].replace(/\.ts$/, ""); - importMap.set(name, `@lib/${relativePath}`); - } - } - } - } -} -// Extras - -importMap.set("LOG_LEVEL_NOTICE", "@lib/common/logger"); -importMap.set("LOG_LEVEL_VERBOSE", "@lib/common/logger"); -importMap.set("LOG_LEVEL_INFO", "@lib/common/logger"); -importMap.set("LOG_LEVEL_DEBUG", "@lib/common/logger"); -importMap.set("LOG_LEVEL_URGENT", "@lib/common/logger"); -importMap.set("LOG_LEVEL", "@lib/common/logger"); -importMap.set("Logger", "@lib/common/logger"); - -// console.log("Import map:", importMap); - -// Loop through all files that import from types.ts. -for (const sourceFile of project.getSourceFiles()) { - const imports = sourceFile.getImportDeclarations(); - // if import from types.ts and the file is pointing `/lib/src/common/types.ts` (resolved), then we will check if the imported names exist in the importMap, if yes, we will replace the import path with the new path from importMap. - - for (const imp of imports) { - const moduleSpecifier = imp.getModuleSpecifierValue(); - if (moduleSpecifier.endsWith("types") || moduleSpecifier.endsWith("types.ts")) { - const filePath = sourceFile.getFilePath(); - const lineNumber = imp.getStartLineNumber(); - const resolvedModule = imp.getModuleSpecifierSourceFile(); - if (!resolvedModule || !resolvedModule.getFilePath().includes("/lib/src/common/types.ts")) { - continue; - } - - // Collect imports from types.ts. - const namedImports = imp.getNamedImports(); - const defaultImport = imp.getDefaultImport(); - console.log(`Found import in ${filePath} at line ${lineNumber}:`, { - namedImports: namedImports.map((ni) => ni.getText()), - defaultImport: defaultImport ? defaultImport.getText() : null, - }); - // Group imports by their names and generate new import paths based on the importMap - const importsToReplace: Record = {}; - for (const namedImport of namedImports) { - const name = namedImport.getName(); - const newPath = importMap.get(name); - if (newPath) { - console.log( - `Will replace import of ${name} in ${filePath} at line ${lineNumber} with new path:`, - newPath - ); - if (!importsToReplace[newPath]) { - importsToReplace[newPath] = []; - } - importsToReplace[newPath].push({ - name, - newPath, - isTypeOnly: namedImport.isTypeOnly() || imp.isTypeOnly(), - }); - } - } - - // For each import, generate a new path from importMap and replace it. - // Split the import when it needs to become multiple imports. - - for (const newPath in importsToReplace) { - // First, handle type-only imports. - const isTypeOnly = importsToReplace[newPath].filter((i) => i.isTypeOnly); - if (isTypeOnly.length > 0) { - sourceFile.insertImportDeclaration(imp.getChildIndex(), { - namedImports: isTypeOnly.map((i) => i.name), - moduleSpecifier: newPath, - isTypeOnly: true, - }); - } - // Then, handle non-type-only imports. - const isValueImport = importsToReplace[newPath].filter((i) => !i.isTypeOnly); - if (isValueImport.length > 0) { - sourceFile.insertImportDeclaration(imp.getChildIndex(), { - namedImports: isValueImport.map((i) => i.name), - moduleSpecifier: newPath, - isTypeOnly: false, - }); - } - // Remove the replaced named imports from the old import. - for (const { name } of importsToReplace[newPath]) { - const namedImport = imp.getNamedImports().find((ni) => ni.getName() === name); - if (namedImport) { - namedImport.remove(); - } - } - } - // If there is also a default import and it exists in importMap, replace it too. - if (defaultImport) { - const name = defaultImport.getText(); - const newPath = importMap.get(name); - - if (newPath) { - console.log( - `Replacing default import of ${name} in ${filePath} at line ${lineNumber} with new path:`, - newPath - ); - // Add the new import statement. - sourceFile.insertImportDeclaration(imp.getChildIndex(), { - defaultImport: name, - moduleSpecifier: newPath, - isTypeOnly: imp.isTypeOnly(), - }); - // Remove the default import from the old import. - imp.removeDefaultImport(); - } - } - if (imp.getNamedImports().length === 0 && !imp.getDefaultImport()) { - // Delete the entire import statement if nothing remains. - imp.remove(); - } - } - } -} - -// Save everything at the end. -if (!isDryRun) { - project.saveSync(); -} diff --git a/utilsdeno/refactor-styles.ts b/utilsdeno/refactor-styles.ts index c9546d78..95f46d82 100644 --- a/utilsdeno/refactor-styles.ts +++ b/utilsdeno/refactor-styles.ts @@ -32,7 +32,6 @@ function toPosixPath(filePath: string): string { const posixProjectRoot = toPosixPath(projectRoot); const posixSrc = `${posixProjectRoot}/src`; -const posixLibSrc = `${posixProjectRoot}/src/lib`; function matchStyleAccess(node: Node): { element: Node; propertyName: string; isComputed: boolean } | undefined { if (Node.isPropertyAccessExpression(node)) { diff --git a/utilsdeno/types-add-ignore.ts b/utilsdeno/types-add-ignore.ts deleted file mode 100644 index 3a546525..00000000 --- a/utilsdeno/types-add-ignore.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { Project, SyntaxKind } from "npm:ts-morph"; - -function processFile(filePath: string, origin: string, repoHash: string): string { - const project = new Project(); - const sourceFile = project.addSourceFileAtPath(filePath); - let updated = false; - - // 0. insert a commit hash comment at the top of the file - sourceFile.insertText(0, `// @ts-nocheck\n// REPO: ${origin} Commit hash: ${repoHash}\n`); - updated = true; - - // 1. Replacements for Uint8Array and DataView - let sourceText = sourceFile.getFullText(); - if (sourceText.includes("Uint8Array") || sourceText.includes("DataView")) { - sourceText = sourceText.replace(/Uint8Array/g, "Uint8Array"); - sourceText = sourceText.replace(/DataView/g, "DataView"); - sourceFile.replaceWithText(sourceText); - updated = true; - } - - // 2. Remove EventEmitter import from "events" and declare class EventEmitter inline - const imports = sourceFile.getImportDeclarations(); - imports.forEach((importDecl) => { - if (importDecl.getModuleSpecifierValue() === "events") { - const defaultImport = importDecl.getDefaultImport(); - if (defaultImport && defaultImport.getText() === "EventEmitter") { - importDecl.remove(); - sourceFile.addClass({ - name: "EventEmitter", - isExported: false, - methods: [ - { - name: "on", - parameters: [ - { name: "event", type: "string | symbol" }, - { name: "listener", type: "(...args: any[]) => void" }, - ], - returnType: "this", - }, - { - name: "once", - parameters: [ - { name: "event", type: "string | symbol" }, - { name: "listener", type: "(...args: any[]) => void" }, - ], - returnType: "this", - }, - { - name: "off", - parameters: [ - { name: "event", type: "string | symbol" }, - { name: "listener", type: "(...args: any[]) => void" }, - ], - returnType: "this", - }, - { - name: "emit", - parameters: [ - { name: "event", type: "string | symbol" }, - { name: "args", isRestParameter: true, type: "any[]" }, - ], - returnType: "boolean", - }, - { - name: "addListener", - parameters: [ - { name: "event", type: "string | symbol" }, - { name: "listener", type: "(...args: any[]) => void" }, - ], - returnType: "this", - }, - { - name: "removeListener", - parameters: [ - { name: "event", type: "string | symbol" }, - { name: "listener", type: "(...args: any[]) => void" }, - ], - returnType: "this", - }, - { - name: "removeAllListeners", - parameters: [{ name: "event", isOptional: true, type: "string | symbol" }], - returnType: "this", - }, - ], - }); - updated = true; - } - } - }); - - // 3. Collect targets for inline disable comments - const targetAnyLines = new Set(); - const targetEmptyObjectLines = new Set(); - const targetEmptyInterfaceLines = new Set(); - const targetDuplicateEnumLines = new Set(); - - // 3.1. 'any' type nodes - const anyTypeNodes = sourceFile.getDescendantsOfKind(SyntaxKind.AnyKeyword); - anyTypeNodes.forEach((anyNode: any) => { - const { line } = sourceFile.getLineAndColumnAtPos(anyNode.getStart()); - targetAnyLines.add(line - 1); - }); - - // 3.2. Empty object type literals {} - const typeLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.TypeLiteral); - typeLiterals.forEach((node) => { - if (node.getMembers().length === 0) { - const { line } = sourceFile.getLineAndColumnAtPos(node.getStart()); - targetEmptyObjectLines.add(line - 1); - } - }); - - // 3.3. Empty interfaces - const interfaces = sourceFile.getInterfaces(); - interfaces.forEach((node) => { - if (node.getMembers().length === 0) { - const { line } = sourceFile.getLineAndColumnAtPos(node.getStart()); - targetEmptyInterfaceLines.add(line - 1); - } - }); - - // 3.4. Duplicate enum member values - const enums = sourceFile.getEnums(); - enums.forEach((enumDecl) => { - const values = new Set(); - enumDecl.getMembers().forEach((member) => { - const initValue = member.getInitializer()?.getText(); - if (initValue) { - if (values.has(initValue)) { - const { line } = sourceFile.getLineAndColumnAtPos(member.getStart()); - targetDuplicateEnumLines.add(line - 1); - } else { - values.add(initValue); - } - } - }); - }); - - // 4. Inject ignore comments line by line - const finalSourceText = sourceFile.getFullText(); - const lineBreak = finalSourceText.includes("\r\n") ? "\r\n" : "\n"; - const lines = finalSourceText.split(/\r?\n/); - - // 4.1. Add inline disable to lines that contain 'any' - for (const lineIndex of targetAnyLines) { - const line = lines[lineIndex]; - if (!line) continue; - if (line.includes("eslint-disable-line @typescript-eslint/no-explicit-any")) continue; - lines[lineIndex] = `${line} // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration`; - updated = true; - } - - // 4.2. Add inline disable to lines that contain empty object {} - for (const lineIndex of targetEmptyObjectLines) { - const line = lines[lineIndex]; - if (!line) continue; - if (line.includes("eslint-disable-line") || line.includes("eslint-disable-next-line")) continue; - lines[lineIndex] = - `${line} // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type`; - updated = true; - } - - // 4.3. Add inline disable to lines that contain empty interface - for (const lineIndex of targetEmptyInterfaceLines) { - const line = lines[lineIndex]; - if (!line) continue; - if (line.includes("eslint-disable-line") || line.includes("eslint-disable-next-line")) continue; - lines[lineIndex] = - `${line} // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface`; - updated = true; - } - - // 4.4. Add inline disable to lines with duplicate enums - for (const lineIndex of targetDuplicateEnumLines) { - const line = lines[lineIndex]; - if (!line) continue; - if (line.includes("eslint-disable-line") || line.includes("eslint-disable-next-line")) continue; - lines[lineIndex] = - `${line} // eslint-disable-line @typescript-eslint/no-duplicate-enum-values -- Duplicate enum value`; - updated = true; - } - - const updatedSourceText = lines.join(lineBreak); - if (updated) { - console.log(`Processed file: ${filePath}`); - } - return updatedSourceText; -} - -const targetDir = `./_types`; - -async function processDir(dirPath: string) { - for await (const entry of Deno.readDir(dirPath)) { - if (entry.isDirectory) { - await processDir(`${dirPath}/${entry.name}`); - } - if (entry.isFile && entry.name.endsWith(".d.ts")) { - const filePath = `${dirPath}/${entry.name}`; - console.log(`Processing: ${filePath}`); - const updatedContent = processFile(filePath, repoRemoteOriginStr, gitCommitHashStr); - // Write the file. To revert, regenerate it with npm run lib:build:types. - await Deno.writeTextFile(filePath, updatedContent); - } - } -} - -const subDir = "./src/lib/"; -const repoRemoteOrigins = new Deno.Command("git", { - args: ["remote", "get-url", "origin"], - cwd: subDir, - stdout: "piped", -}).outputSync().stdout; -const repoRemoteOriginStr = new TextDecoder().decode(repoRemoteOrigins).trim(); -console.log(`STAMP: Git remote origin: ${repoRemoteOriginStr}`); -const gitCommitHashSub = new Deno.Command("git", { - args: ["rev-parse", "--short", "HEAD"], - cwd: subDir, - stdout: "piped", -}).outputSync().stdout; -const gitCommitHashStr = new TextDecoder().decode(gitCommitHashSub).trim(); -console.log(`STAMP: Git commit hash: ${gitCommitHashStr}`); -await processDir(targetDir); diff --git a/vite.config.ts b/vite.config.ts index a7c47aee..8b009f0a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -130,7 +130,6 @@ export default defineConfig(({ mode }) => { resolve: { alias: { "@": path.resolve(__dirname, "./src"), - "@lib": path.resolve(__dirname, "./src/lib/src"), src: path.resolve(__dirname, "./src"), }, }, diff --git a/vitest.config.common.ts b/vitest.config.common.ts index 73f023bf..0a7d7375 100644 --- a/vitest.config.common.ts +++ b/vitest.config.common.ts @@ -96,7 +96,6 @@ export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "./src"), - "@lib": path.resolve(__dirname, "./src/lib/src"), src: path.resolve(__dirname, "./src"), }, }, diff --git a/vitest.config.rpc-unit.ts b/vitest.config.rpc-unit.ts deleted file mode 100644 index d3c175e7..00000000 --- a/vitest.config.rpc-unit.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @file vitest.config.rpc-unit.ts - * @description Configuration for running RPC-specific unit tests (such as RpcRoom and transport layers) in Node.js, - * enforcing coverage thresholds on the RPC sub-module. - * This can be run manually to verify RPC-specific coverage, or is matched by the glob patterns in `npm run test:unit`. - */ -import { defineConfig, mergeConfig } from "vitest/config"; -import viteConfig from "./vitest.config.common"; - -export default mergeConfig( - viteConfig, - defineConfig({ - resolve: { - alias: { - obsidian: "", - }, - }, - test: { - name: "rpc-unit-tests", - include: ["src/lib/src/rpc/**/*.unit.spec.ts"], - exclude: ["test/**"], - coverage: { - include: ["src/lib/src/rpc/**/*.ts"], - exclude: ["**/*.unit.spec.ts", "**/index.ts"], - provider: "v8", - reporter: ["text", "json", "html", ["text", { file: "coverage-rpc-text.txt" }]], - thresholds: { - lines: 90, - functions: 90, - branches: 75, - statements: 90, - }, - }, - }, - }) -); diff --git a/vitest.config.ts b/vitest.config.ts index a62992d3..6b632b72 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -38,8 +38,8 @@ export default mergeConfig( // environment: "browser", include: ["test/**/*.test.ts"], coverage: { - include: ["src/**/*.ts", "src/lib/src/**/*.ts", "src/**/*.svelte"], - exclude: ["**/*.test.ts", "src/lib/**"], + include: ["src/**/*.ts", "src/**/*.svelte"], + exclude: ["**/*.test.ts"], provider: "v8", reporter: ["text", "json", "html"], // ignoreEmptyLines: true, diff --git a/vitest.config.unit.ts b/vitest.config.unit.ts index 3012a075..2b1b243e 100644 --- a/vitest.config.unit.ts +++ b/vitest.config.unit.ts @@ -20,7 +20,7 @@ export default mergeConfig( // maxConcurrency: 2, name: "unit-tests", include: ["**/*unit.test.ts", "**/*.unit.spec.ts"], - exclude: ["test/**", "src/apps/**/testdeno/**"], + exclude: ["node_modules/**", "test/**", "src/apps/**/testdeno/**"], coverage: { include: ["src/**/*.ts"], exclude: [ @@ -28,12 +28,8 @@ export default mergeConfig( "**/*unit.test.ts", "**/*.unit.spec.ts", "test/**", - "src/lib/**/*.test.ts", "**/_*", "src/apps/**/testdeno/**", - // "src/apps/**", - // "src/cli/**", - "src/lib/src/cli/**", "**/*_obsolete.ts", ...importOnlyFiles, ], From 14da32cbab2790741241ebc629716a0c78312dff Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 17 Jul 2026 11:32:41 +0000 Subject: [PATCH 003/117] chore: refresh Commonlib package proof --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 40be13dc..b07c760e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4950,7 +4950,7 @@ "node_modules/@vrtmrz/livesync-commonlib": { "version": "0.1.0-package-proof.8", "resolved": "file:../livesync-commonlib-package-boundary/artifacts/vrtmrz-livesync-commonlib-0.1.0-package-proof.8.tgz", - "integrity": "sha512-XiS2BJGYQtBgTdLf9G577q+65wXKjdjFIzsLA1yWhSKgg+nyh/zREEjdAE0y07QyA0Sq8PiIruZB7cYTpqKjvQ==", + "integrity": "sha512-nzoyrszHxY9fLmYm5hsF14ZP3mjkKDcPngE8fJY9mWTFHQPtn0k7owlGQoP/pdqcVW2RowQXbWxkMwsBEn23bw==", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", From d5754f1f5424253efa2e5f35979e6b41d0be3de1 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 17 Jul 2026 11:58:08 +0000 Subject: [PATCH 004/117] docs: keep package proof evidence durable --- docs/adr/2026_07_common_library_package_boundary.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/2026_07_common_library_package_boundary.md b/docs/adr/2026_07_common_library_package_boundary.md index bcdddb36..61d5f177 100644 --- a/docs/adr/2026_07_common_library_package_boundary.md +++ b/docs/adr/2026_07_common_library_package_boundary.md @@ -235,7 +235,7 @@ Every retained barrel must correspond to an explicit `exports` entry, list named ## Implementation Proof -The local proof builds Commonlib `0.1.0-package-proof.8` as one compiled ESM package with a small root, `context`, `browser`, `node`, and `rpc` entries, plus 118 explicit compatibility exports required by the current downstream migration. It publishes neither raw TypeScript nor Svelte source. The reviewed tarball has integrity `sha512-XiS2BJGYQtBgTdLf9G577q+65wXKjdjFIzsLA1yWhSKgg+nyh/zREEjdAE0y07QyA0Sq8PiIruZB7cYTpqKjvQ==`. It can be installed into a clean consumer, imported in Node, type-checked from declarations, and bundled independently for browser context, browser storage, browser services, and workers. +The local package proof builds Commonlib as one compiled ESM package with a small root, `context`, `browser`, `node`, and `rpc` entries, plus the explicit compatibility exports required by the current downstream migration. It publishes neither raw TypeScript nor Svelte source. The generated tarball can be installed into a clean consumer, imported in Node, type-checked from declarations, and bundled independently for browser context, browser storage, browser services, and workers. Release validation records the immutable registry version and checksum separately. The proof found and fixed three boundary defects which source-alias consumption had hidden: compiled JSON imports required explicit output extensions, precompiled Svelte output could not safely be treated as source by the downstream Svelte pipeline, and Vite's default client conditions selected Commonlib's browser worker while building the Node CLI. Packed-consumer regressions cover the first two. The CLI now uses Vite's server conditions and treats every Node built-in reported by Commonlib's Node entry as external; the built CLI is exercised through Deno E2E. Importing root or context also no longer patches DOM prototypes, and translator injection prevents the context entry from loading the complete language catalogue. From 298738fc67c7c7d2cc93c6f6e9d6ec0aed4bd7f7 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 17 Jul 2026 13:28:58 +0000 Subject: [PATCH 005/117] fix: keep packaged dialogs inside mobile safe areas --- package-lock.json | 16 +- package.json | 5 +- .../services/LiveSyncUI/DialogHost.svelte | 12 + src/modules/services/SvelteDialogObsidian.ts | 5 + test/e2e-obsidian/README.md | 10 +- test/e2e-obsidian/runner/session.ts | 2 + test/e2e-obsidian/runner/ui.ts | 38 +++ test/e2e-obsidian/scripts/dialog-mounts.ts | 297 ++++++++++++++++++ .../scripts/hidden-file-snippet-sync.ts | 5 +- test/e2e-obsidian/scripts/local-suite.ts | 1 + 10 files changed, 377 insertions(+), 14 deletions(-) create mode 100644 test/e2e-obsidian/scripts/dialog-mounts.ts diff --git a/package-lock.json b/package-lock.json index b07c760e..7d19d734 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@trystero-p2p/nostr": "^0.24.0", - "@vrtmrz/livesync-commonlib": "file:../livesync-commonlib-package-boundary/artifacts/vrtmrz-livesync-commonlib-0.1.0-package-proof.8.tgz", + "@vrtmrz/livesync-commonlib": "0.1.0-rc.0", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", "idb": "^8.0.3", @@ -59,7 +59,7 @@ "@vitest/browser": "^4.1.8", "@vitest/browser-playwright": "^4.1.8", "@vitest/coverage-v8": "^4.1.8", - "@vrtmrz/obsidian-test-session": "0.1.0", + "@vrtmrz/obsidian-test-session": "0.2.0", "dotenv-cli": "^11.0.0", "esbuild": "0.28.1", "esbuild-plugin-inline-worker": "^0.1.1", @@ -4948,9 +4948,9 @@ } }, "node_modules/@vrtmrz/livesync-commonlib": { - "version": "0.1.0-package-proof.8", - "resolved": "file:../livesync-commonlib-package-boundary/artifacts/vrtmrz-livesync-commonlib-0.1.0-package-proof.8.tgz", - "integrity": "sha512-nzoyrszHxY9fLmYm5hsF14ZP3mjkKDcPngE8fJY9mWTFHQPtn0k7owlGQoP/pdqcVW2RowQXbWxkMwsBEn23bw==", + "version": "0.1.0-rc.0", + "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.0-rc.0.tgz", + "integrity": "sha512-Aa+xC7bG78M7H8leIMmZdTRzA0Xh+ZgmiZJv0pXjy5OdZTPDu1H9M0eRcs5xL2pNBiOLKs0CKiEBql7JgunnQg==", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", @@ -4995,9 +4995,9 @@ } }, "node_modules/@vrtmrz/obsidian-test-session": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-test-session/-/obsidian-test-session-0.1.0.tgz", - "integrity": "sha512-asBOIRTc3xK5GF5ds5mkxN6vsO4RE8o7puvVjoJiGYSlxoFa9jzr8FwAO13CyoOriHF05pOZfTB+eQmL1aNb/A==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-test-session/-/obsidian-test-session-0.2.0.tgz", + "integrity": "sha512-Mnw1wide/KddJHfq6ulpwqdPLV8rIywsY013m9nQvn2GFm7XnBAqZjZpVEolMJH/mqbLlWvU3PhL1UWJLZsIJA==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index f7fef73a..6dbfced5 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "test:e2e:obsidian:cli-help": "tsx test/e2e-obsidian/scripts/cli-help.ts", "test:e2e:obsidian:debug-ui": "tsx test/e2e-obsidian/scripts/debug-ui.ts", "test:e2e:obsidian:smoke": "tsx test/e2e-obsidian/scripts/smoke.ts", + "test:e2e:obsidian:dialog-mounts": "tsx test/e2e-obsidian/scripts/dialog-mounts.ts", "test:e2e:obsidian:vault-reflection": "tsx test/e2e-obsidian/scripts/vault-reflection.ts", "test:e2e:obsidian:couchdb-upload": "tsx test/e2e-obsidian/scripts/couchdb-upload.ts", "test:e2e:obsidian:cli-to-obsidian-sync": "tsx test/e2e-obsidian/scripts/cli-to-obsidian-sync.ts", @@ -101,7 +102,7 @@ "@vitest/browser": "^4.1.8", "@vitest/browser-playwright": "^4.1.8", "@vitest/coverage-v8": "^4.1.8", - "@vrtmrz/obsidian-test-session": "0.1.0", + "@vrtmrz/obsidian-test-session": "0.2.0", "dotenv-cli": "^11.0.0", "esbuild": "0.28.1", "esbuild-plugin-inline-worker": "^0.1.1", @@ -151,7 +152,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@trystero-p2p/nostr": "^0.24.0", - "@vrtmrz/livesync-commonlib": "file:../livesync-commonlib-package-boundary/artifacts/vrtmrz-livesync-commonlib-0.1.0-package-proof.8.tgz", + "@vrtmrz/livesync-commonlib": "0.1.0-rc.0", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", "idb": "^8.0.3", diff --git a/src/modules/services/LiveSyncUI/DialogHost.svelte b/src/modules/services/LiveSyncUI/DialogHost.svelte index 93495b2d..5c5483e2 100644 --- a/src/modules/services/LiveSyncUI/DialogHost.svelte +++ b/src/modules/services/LiveSyncUI/DialogHost.svelte @@ -42,6 +42,18 @@ \ No newline at end of file + diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte b/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte index af42fc80..308d777f 100644 --- a/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte @@ -33,16 +33,17 @@ let replicatingPeerId = $state(null); let communicatingUntil = $state>({}); const COMMUNICATION_HOLD_MS = 2500; - let syncOnReplicationSetting = $state(core.services.setting.currentSettings()?.P2P_SyncOnReplication ?? ""); + // Later setting changes arrive through EVENT_SETTING_SAVED; these values only seed local state at mount time. + const readCurrentSettings = () => core.services.setting.currentSettings(); + const initialSettings = readCurrentSettings(); + let syncOnReplicationSetting = $state(initialSettings?.P2P_SyncOnReplication ?? ""); type P2PRemoteOption = { id: string; name: string; roomSuffix: string; }; let p2pRemoteOptions = $state([]); - let selectedP2PRemoteConfigurationId = $state( - core.services.setting.currentSettings()?.P2P_ActiveRemoteConfigurationId ?? "" - ); + let selectedP2PRemoteConfigurationId = $state(initialSettings?.P2P_ActiveRemoteConfigurationId ?? ""); let selectingP2PRemote = $state(false); function addToList(item: string, list: string): string { diff --git a/src/modules/services/LiveSyncUI/DialogHost.svelte b/src/modules/services/LiveSyncUI/DialogHost.svelte index 5c5483e2..a60d9318 100644 --- a/src/modules/services/LiveSyncUI/DialogHost.svelte +++ b/src/modules/services/LiveSyncUI/DialogHost.svelte @@ -12,33 +12,33 @@ // */ // onSetupContext?(props: DialogSvelteComponentBaseProps): void; // }; - const { setTitle, closeDialog, setResult, mountComponent, getInitialData, onSetupContext }: DialogHostProps = - $props(); + const props: DialogHostProps = $props(); const contextProps = { - setTitle, - closeDialog, - setResult, - getInitialData, - } satisfies DialogSvelteComponentBaseProps + setTitle: (title: string) => props.setTitle(title), + closeDialog: () => props.closeDialog(), + setResult: (result: any) => props.setResult(result), + getInitialData: () => props.getInitialData?.(), + } satisfies DialogSvelteComponentBaseProps; - // Call the onSetupContext function to setup the dialog context - onSetupContext?.(contextProps); + // Context must be established during component initialisation. The callbacks retain live access to the host props. + const setupContext = () => props.onSetupContext?.(contextProps); + setupContext(); /** * Wrapper around setResult to also close the dialog * @param result */ const setResultWrapper = (result: any) => { - setResult(result); - closeDialog(); + props.setResult(result); + props.closeDialog(); }; - const Component = mountComponent; + const Component = $derived(props.mountComponent); let thisElement: HTMLElement;
    - +
    - - -

    LiveSync WebApp E2E

    -

    This page is used by Playwright tests only. window.livesyncTest is exposed by the script below.

    - -
    Loading…
    - - - diff --git a/src/apps/webapp/test/e2e.spec.ts b/src/apps/webapp/test/e2e.spec.ts deleted file mode 100644 index 70c55094..00000000 --- a/src/apps/webapp/test/e2e.spec.ts +++ /dev/null @@ -1,292 +0,0 @@ -/** - * WebApp E2E tests – two-vault scenarios. - * - * Each vault (A and B) runs in its own browser context so that JavaScript - * global state (including Trystero's global signalling tables) is fully - * isolated. The two vaults communicate only through the shared remote - * CouchDB database. - * - * Vault storage is OPFS-backed – no file-picker interaction needed. - * - * Prerequisites: - * - A reachable CouchDB instance whose connection details are in .test.env - * (read automatically by playwright.config.ts). - * - * How to run: - * cd src/apps/webapp && npm run test:e2e - */ - -import { test, expect, type BrowserContext, type Page, type TestInfo } from "@playwright/test"; -import type { LiveSyncTestAPI } from "@/apps/webapp/test-entry"; -import { fileURLToPath, fs, path } from "@vrtmrz/livesync-commonlib/node"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// --------------------------------------------------------------------------- -// Settings helpers -// --------------------------------------------------------------------------- - -function requireEnv(name: string): string { - const v = process.env[name]; - if (!v) throw new Error(`Missing required env variable: ${name}`); - return v; -} - -async function ensureCouchDbDatabase(uri: string, user: string, pass: string, dbName: string): Promise { - const base = uri.replace(/\/+$/, ""); - const dbUrl = `${base}/${encodeURIComponent(dbName)}`; - const auth = Buffer.from(`${user}:${pass}`, "utf-8").toString("base64"); - const response = await fetch(dbUrl, { - method: "PUT", - headers: { - Authorization: `Basic ${auth}`, - }, - }); - - // 201: created, 202: accepted, 412: already exists - if (response.status === 201 || response.status === 202 || response.status === 412) { - return; - } - - const body = await response.text().catch(() => ""); - throw new Error(`Failed to ensure CouchDB database (${response.status}): ${body}`); -} - -function buildSettings(dbName: string): Record { - return { - // Remote database (shared between A and B – this is the replication target) - couchDB_URI: requireEnv("hostname").replace(/\/+$/, ""), - couchDB_USER: process.env["username"] ?? "", - couchDB_PASSWORD: process.env["password"] ?? "", - couchDB_DBNAME: dbName, - - // Core behaviour - isConfigured: true, - liveSync: false, - syncOnSave: false, - syncOnStart: false, - periodicReplication: false, - gcDelay: 0, - savingDelay: 0, - notifyThresholdOfRemoteStorageSize: 0, - - // Encryption off for test simplicity - encrypt: false, - - // Disable plugin/hidden-file sync (not needed in webapp) - usePluginSync: false, - autoSweepPlugins: false, - autoSweepPluginsPeriodic: false, - - //Auto accept perr - P2P_AutoAcceptingPeers: "~.*", - }; -} - -// --------------------------------------------------------------------------- -// Test-page helpers -// --------------------------------------------------------------------------- - -/** Navigate to the test entry page and wait for `window.livesyncTest`. */ -async function openTestPage(ctx: BrowserContext): Promise { - const page = await ctx.newPage(); - await page.goto("/test.html"); - await page.waitForFunction(() => !!(window as any).livesyncTest, { timeout: 20_000 }); - return page; -} - -/** Type-safe wrapper – calls `window.livesyncTest.(...args)` in the page. */ -async function call( - page: Page, - method: M, - ...args: Parameters -): Promise>> { - const invoke = () => - page.evaluate(([m, a]) => (window as any).livesyncTest[m](...a), [method, args] as [ - string, - unknown[], - ]) as Promise>>; - - try { - return await invoke(); - } catch (ex: any) { - const message = String(ex?.message ?? ex); - // Some startup flows may trigger one page reload; recover once. - if ( - message.includes("Execution context was destroyed") || - message.includes("Most likely the page has been closed") - ) { - await page.waitForFunction(() => !!(window as any).livesyncTest, { timeout: 20_000 }); - return await invoke(); - } - throw ex; - } -} - -async function dumpCoverage(page: Page | undefined, label: string, testInfo: TestInfo): Promise { - if (!process.env.PW_COVERAGE || !page || page.isClosed()) { - return; - } - const cov = await page - .evaluate(() => { - const data = (window as any).__coverage__; - if (!data) return null; - // Reset between tests to avoid runaway accumulation. - (window as any).__coverage__ = {}; - return data; - }) - .catch((): null => null); - if (!cov) return; - if (typeof cov === "object" && Object.keys(cov as Record).length === 0) { - return; - } - - const outDir = path.resolve(__dirname, "../.nyc_output"); - fs.mkdirSync(outDir, { recursive: true }); - const name = `${testInfo.testId.replace(/[^a-zA-Z0-9_-]/g, "_")}-${label}.json`; - fs.writeFileSync(path.join(outDir, name), JSON.stringify(cov), "utf-8"); -} - -// --------------------------------------------------------------------------- -// Two-vault E2E suite -// --------------------------------------------------------------------------- - -test.describe("WebApp two-vault E2E", () => { - let ctxA: BrowserContext; - let ctxB: BrowserContext; - let pageA: Page; - let pageB: Page; - - const DB_SUFFIX = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; - const dbName = `${requireEnv("dbname")}-${DB_SUFFIX}`; - const settings = buildSettings(dbName); - - test.beforeAll(async ({ browser }) => { - await ensureCouchDbDatabase( - String(settings.couchDB_URI ?? ""), - String(settings.couchDB_USER ?? ""), - String(settings.couchDB_PASSWORD ?? ""), - dbName - ); - - // Open Vault A and Vault B in completely separate browser contexts. - // Each context has its own JS runtime, IndexedDB and OPFS root, so - // Trystero global state and PouchDB instance names cannot collide. - ctxA = await browser.newContext(); - ctxB = await browser.newContext(); - - pageA = await openTestPage(ctxA); - pageB = await openTestPage(ctxB); - - await call(pageA, "init", "testvault_a", settings as any); - await call(pageB, "init", "testvault_b", settings as any); - }); - - test.afterAll(async () => { - await call(pageA, "shutdown").catch(() => {}); - await call(pageB, "shutdown").catch(() => {}); - await ctxA.close(); - await ctxB.close(); - }); - - test.afterEach(async ({}, testInfo) => { - await dumpCoverage(pageA, "vaultA", testInfo); - await dumpCoverage(pageB, "vaultB", testInfo); - }); - - // ----------------------------------------------------------------------- - // Case 1: Vault A writes a file and can read its metadata back from the - // local database (no replication yet). - // ----------------------------------------------------------------------- - test("Case 1: A writes a file and can get its info", async () => { - const FILE = "e2e/case1-a-only.md"; - const CONTENT = "hello from vault A"; - - const ok = await call(pageA, "putFile", FILE, CONTENT); - expect(ok).toBe(true); - - const info = await call(pageA, "getInfo", FILE); - expect(info).not.toBeNull(); - expect(info!.path).toBe(FILE); - expect(info!.revision).toBeTruthy(); - expect(info!.conflicts).toHaveLength(0); - }); - - // ----------------------------------------------------------------------- - // Case 2: Vault A writes a file, both vaults replicate, and Vault B ends - // up with the file in its local database. - // ----------------------------------------------------------------------- - test("Case 2: A writes a file, both replicate, B receives the file", async () => { - const FILE = "e2e/case2-sync.md"; - const CONTENT = "content from A – should appear in B"; - - await call(pageA, "putFile", FILE, CONTENT); - - // A pushes to remote, B pulls from remote. - await call(pageA, "replicate"); - await call(pageB, "replicate"); - - const infoB = await call(pageB, "getInfo", FILE); - expect(infoB).not.toBeNull(); - expect(infoB!.path).toBe(FILE); - }); - - // ----------------------------------------------------------------------- - // Case 3: Vault A deletes the file it synced in case 2. After both - // vaults replicate, Vault B no longer sees the file. - // ----------------------------------------------------------------------- - test("Case 3: A deletes the file, both replicate, B no longer sees it", async () => { - // This test depends on Case 2 having put e2e/case2-sync.md into both vaults. - const FILE = "e2e/case2-sync.md"; - - await call(pageA, "deleteFile", FILE); - - await call(pageA, "replicate"); - await call(pageB, "replicate"); - - const infoB = await call(pageB, "getInfo", FILE); - // The file should be gone (null means not found or deleted). - expect(infoB).toBeNull(); - }); - - // ----------------------------------------------------------------------- - // Case 4: A and B each independently edit the same file that was already - // synced. After both vaults replicate the editing cycle, both - // vaults report a conflict on that file. - // ----------------------------------------------------------------------- - test("Case 4: concurrent edits from A and B produce a conflict on both sides", async () => { - const FILE = "e2e/case4-conflict.md"; - - // 1) Write a baseline and synchronise so both vaults start from the - // same revision. - await call(pageA, "putFile", FILE, "base content"); - await call(pageA, "replicate"); - await call(pageB, "replicate"); - - // Confirm B has the base file with no conflicts yet. - const baseInfoB = await call(pageB, "getInfo", FILE); - expect(baseInfoB).not.toBeNull(); - expect(baseInfoB!.conflicts).toHaveLength(0); - - // 2) Both vaults write diverging content without syncing in between – - // this creates two competing revisions. - await call(pageA, "putFile", FILE, "content from A (conflict side)"); - await call(pageB, "putFile", FILE, "content from B (conflict side)"); - - // 3) Run replication on both sides. The order mirrors the pattern - // from the CLI two-vault tests (A → remote → B → remote → A). - await call(pageA, "replicate"); - await call(pageB, "replicate"); - await call(pageA, "replicate"); // re-check from A to pick up B's revision - - // 4) At least one side must report a conflict. - const hasConflictA = await call(pageA, "hasConflict", FILE); - const hasConflictB = await call(pageB, "hasConflict", FILE); - - expect( - hasConflictA || hasConflictB, - "Expected a conflict to appear on vault A or vault B after diverging edits" - ).toBe(true); - }); -}); diff --git a/src/apps/webapp/vite.config.ts b/src/apps/webapp/vite.config.ts index c0d8d2e5..8857acf9 100644 --- a/src/apps/webapp/vite.config.ts +++ b/src/apps/webapp/vite.config.ts @@ -1,6 +1,5 @@ import { defineConfig } from "vite"; import { svelte } from "@sveltejs/vite-plugin-svelte"; -import istanbul from "vite-plugin-istanbul"; import { fileURLToPath, fs, path } from "@vrtmrz/livesync-commonlib/node"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, "../../.."); @@ -15,35 +14,9 @@ function readVersion(filePath: string): string | undefined { const packageVersion = readVersion(path.resolve(repoRoot, "package.json")); const manifestVersion = readVersion(path.resolve(repoRoot, "manifest.json")); -const enableCoverage = process.env.PW_COVERAGE === "1"; // https://vite.dev/config/ export default defineConfig({ - plugins: [ - svelte(), - ...(enableCoverage - ? [ - istanbul({ - cwd: repoRoot, - include: ["src/**/*.ts", "src/**/*.svelte"], - exclude: [ - "node_modules", - "dist", - "test", - "coverage", - "src/apps/webapp/test/**", - "playwright.config.ts", - "vite.config.ts", - "**/*.spec.ts", - "**/*.test.ts", - ], - extension: [".js", ".ts", ".svelte"], - requireEnv: false, - cypress: false, - checkProd: false, - }), - ] - : []), - ], + plugins: [svelte()], resolve: { alias: { "@": path.resolve(__dirname, "../../"), @@ -55,12 +28,9 @@ export default defineConfig({ outDir: "dist", emptyOutDir: true, rollupOptions: { - // test.html is used by the Playwright dev-server; include it here - // so the production build doesn't emit warnings about unused inputs. input: { index: path.resolve(__dirname, "index.html"), webapp: path.resolve(__dirname, "webapp.html"), - test: path.resolve(__dirname, "test.html"), }, external: ["crypto"], }, diff --git a/src/common/databaseCompatibility.unit.spec.ts b/src/common/databaseCompatibility.unit.spec.ts index a94e6b5b..33f5a14e 100644 --- a/src/common/databaseCompatibility.unit.spec.ts +++ b/src/common/databaseCompatibility.unit.spec.ts @@ -139,7 +139,7 @@ describe("database compatibility evaluation", () => { }); }); - it("retains an existing legacy review when no structured reason can be reconstructed", () => { + it("compatibility: retains an earlier unstructured review when no structured reason can be reconstructed", () => { const result = evaluateCompatibilityPause({ acknowledgedVersion: "12", currentVersion: 12, @@ -159,7 +159,7 @@ describe("database compatibility evaluation", () => { }); }); - it("scopes the legacy marker to the Vault", () => { + it("compatibility: scopes the earlier review marker to the Vault", () => { expect(legacyDatabaseCompatibilityVersionKey("Example Vault")).toBe("obsidian-live-sync-verExample Vault"); }); }); diff --git a/src/features/HiddenFileSync/CmdHiddenFileSync.unit.spec.ts b/src/features/HiddenFileSync/CmdHiddenFileSync.unit.spec.ts index b8f38cc7..66d00a29 100644 --- a/src/features/HiddenFileSync/CmdHiddenFileSync.unit.spec.ts +++ b/src/features/HiddenFileSync/CmdHiddenFileSync.unit.spec.ts @@ -134,7 +134,7 @@ describe("HiddenFileSync configuration-change notices", () => { expect(progress.done).toHaveBeenCalledOnce(); }); - it("does not surround the initialisation progress with separate gathering and restart Notices", async () => { + it("retirement guard: does not restore separate gathering and restart Notices", async () => { vi.mocked(configureHiddenFileSyncMode).mockImplementation(async (_mode, handlers) => { await handlers.enable(); await handlers.initialise("safe"); diff --git a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts index 0432d637..6bcef72a 100644 --- a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts +++ b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts @@ -88,7 +88,7 @@ describe("LocalDatabaseMaintenance prerequisites", () => { expect(applyPartial).not.toHaveBeenCalled(); }); - it("does not treat the obsolete fixed-revision key as a maintenance prerequisite", async () => { + it("retirement guard: ignores the obsolete fixed-revision key as a maintenance prerequisite", async () => { const { settings, askSelectStringDialogue, applyPartial } = createPrerequisites({ doNotUseFixedRevisionForChunks: false, readChunksOnline: false, diff --git a/src/modules/core/ModuleReplicator.ts b/src/modules/core/ModuleReplicator.ts index 57304ce0..7479df4c 100644 --- a/src/modules/core/ModuleReplicator.ts +++ b/src/modules/core/ModuleReplicator.ts @@ -145,7 +145,8 @@ export class ModuleReplicator extends AbstractModule { } /** - * obsolete method. No longer maintained and will be removed in the future. + * Reconciles local chunks when an older IndexedDB client reports that the remote database was cleaned. + * This compatibility path remains reachable while those clients can still set `remoteCleaned`. * @deprecated v0.24.17 * @param showMessage If true, show message to the user. */ diff --git a/src/modules/core/ModuleReplicator.unit.spec.ts b/src/modules/core/ModuleReplicator.unit.spec.ts index b4849707..d98e1fc5 100644 --- a/src/modules/core/ModuleReplicator.unit.spec.ts +++ b/src/modules/core/ModuleReplicator.unit.spec.ts @@ -116,7 +116,7 @@ describe("ModuleReplicator", () => { }); }); -describe("ModuleReplicator legacy cleanup", () => { +describe("compatibility: cleaned-remote reconciliation for IndexedDB clients", () => { it("keeps its finite replication and balancing work inside the shared activity boundary", async () => { const activityFinished = vi.fn(); const runBoundedRemoteActivity = vi.fn(async (task: () => unknown) => { diff --git a/src/modules/features/SetupManager.unit.spec.ts b/src/modules/features/SetupManager.unit.spec.ts index fdbc8f57..b9eb45b6 100644 --- a/src/modules/features/SetupManager.unit.spec.ts +++ b/src/modules/features/SetupManager.unit.spec.ts @@ -147,7 +147,7 @@ describe("SetupManager", () => { expect(configureManually).toHaveBeenCalledWith(createNewVaultSettings(), UserMode.NewUser); }); - it("onUseSetupURI should normalise imported legacy remote settings before applying", async () => { + it("compatibility: normalises imported flat remote settings from a Setup URI before applying", async () => { const { manager, setting, dialogManager } = createSetupManager(); dialogManager.openWithExplicitCancel .mockResolvedValueOnce(createLegacyRemoteSetting()) @@ -162,7 +162,7 @@ describe("SetupManager", () => { expect(setting.currentSettings().activeConfigurationId).toBe("legacy-couchdb"); }); - it("decodeQR should normalise imported legacy remote settings before applying", async () => { + it("compatibility: normalises imported flat remote settings from QR data before applying", async () => { const { manager, setting, dialogManager } = createSetupManager(); vi.mocked(decodeSettingsFromQRCodeData).mockReturnValue(createLegacyRemoteSetting()); dialogManager.openWithExplicitCancel.mockResolvedValueOnce("compatible-existing-user"); diff --git a/src/serviceFeatures/redFlag.simpleFetch.ts b/src/serviceFeatures/redFlag.simpleFetch.ts index 20170e8d..214cad2e 100644 --- a/src/serviceFeatures/redFlag.simpleFetch.ts +++ b/src/serviceFeatures/redFlag.simpleFetch.ts @@ -14,7 +14,7 @@ import { adjustSettingToRemoteIfNeeded, processVaultInitialisation } from "./red export const SIMPLE_FETCH_STAGE1_REMOTE_WINS = "Overwrite all with remote files"; export const SIMPLE_FETCH_STAGE1_NEWER_WINS = "Compare time and take newer"; -export const SIMPLE_FETCH_STAGE1_LEGACY = "Use the detailed flow"; +export const SIMPLE_FETCH_STAGE1_DETAILED = "Use the detailed flow"; export const SIMPLE_FETCH_STAGE1_CANCEL = "Cancel"; export const SIMPLE_FETCH_STAGE2_REMOTE_DELETE_NONE = "Keep local files even if not on remote"; @@ -27,8 +27,8 @@ export const STAGE2_ABORT = "Cancel all and reboot"; const SIMPLE_FETCH_MODE_KEY = "simple-fetch-mode"; function buildSimpleFetchResult(stage1: string, stage2?: string) { - if (stage1 === SIMPLE_FETCH_STAGE1_LEGACY) { - return { mode: "legacy", options: {} }; + if (stage1 === SIMPLE_FETCH_STAGE1_DETAILED) { + return { mode: "detailed", options: {} }; } if (stage1 === SIMPLE_FETCH_STAGE1_REMOTE_WINS && stage2) { if (![SIMPLE_FETCH_STAGE2_REMOTE_DELETE_ALL, SIMPLE_FETCH_STAGE2_REMOTE_DELETE_NONE].includes(stage2)) { @@ -100,7 +100,7 @@ Firstly, how shall we handle the data retrieved from this remote source? - **${SIMPLE_FETCH_STAGE1_REMOTE_WINS}**: Remote data is the source of truth. If you are new to using Self-hosted LiveSync. This option may be easiest to understand and get started with. It will overwrite all your local files with the remote data, so please make sure you have a backup if there is any important data in your vault. -- **${SIMPLE_FETCH_STAGE1_LEGACY}**: Opens the detailed setup wizard. +- **${SIMPLE_FETCH_STAGE1_DETAILED}**: Opens the detailed setup wizard. If you want to have more control over the synchronisation process, or want to review the changes before applying, you can choose this option to use the detailed flow. `; const stage1 = await host.services.UI.confirm.confirmWithMessage( @@ -109,7 +109,7 @@ Firstly, how shall we handle the data retrieved from this remote source? [ SIMPLE_FETCH_STAGE1_NEWER_WINS, SIMPLE_FETCH_STAGE1_REMOTE_WINS, - SIMPLE_FETCH_STAGE1_LEGACY, + SIMPLE_FETCH_STAGE1_DETAILED, SIMPLE_FETCH_STAGE1_CANCEL, ], SIMPLE_FETCH_STAGE1_NEWER_WINS, @@ -118,7 +118,7 @@ Firstly, how shall we handle the data retrieved from this remote source? if (!stage1 || stage1 === SIMPLE_FETCH_STAGE1_CANCEL) return "cancelled"; - if (stage1 === SIMPLE_FETCH_STAGE1_LEGACY) { + if (stage1 === SIMPLE_FETCH_STAGE1_DETAILED) { return buildSimpleFetchResult(stage1)!; } @@ -204,8 +204,8 @@ export async function askAndPerformFastSetupOnScheduledFetchAll( host.services.appLifecycle.performRestart(); return false; } - if (result.mode === "legacy") { - return undefined; // Let the legacy flow handle it. + if (result.mode === "detailed") { + return undefined; // Let the detailed setup flow handle it. } return await processVaultInitialisation(host, log, async () => { diff --git a/src/serviceFeatures/redFlag.unit.spec.ts b/src/serviceFeatures/redFlag.unit.spec.ts index 9760622b..de8d998c 100644 --- a/src/serviceFeatures/redFlag.unit.spec.ts +++ b/src/serviceFeatures/redFlag.unit.spec.ts @@ -29,7 +29,7 @@ import { synchroniseAllFilesBetweenDBandStorage, } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner"; import { - SIMPLE_FETCH_STAGE1_LEGACY, + SIMPLE_FETCH_STAGE1_DETAILED, SIMPLE_FETCH_STAGE1_NEWER_WINS, SIMPLE_FETCH_STAGE1_REMOTE_WINS, SIMPLE_FETCH_STAGE2_NEWER_CLEANUP, @@ -476,12 +476,12 @@ describe("Red Flag Feature", () => { // but we can verify rebuilder was called. }); - it("should restore legacy fetch flow when requested", async () => { + it("opens the detailed Fetch flow when requested", async () => { const host = createHostMock(); const log = createLoggerMock(); host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL); - host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_LEGACY); + host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_DETAILED); host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce({ vault: "identical", backup: "backup_skipped", @@ -665,11 +665,11 @@ describe("Red Flag Feature", () => { await expect(askSimpleFetchMode(host as any)).resolves.toBe("cancelled"); }); - it("should return legacy mode when selected", async () => { + it("selects the detailed Fetch flow", async () => { const host = createHostMock(); - host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_LEGACY); + host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_DETAILED); - await expect(askSimpleFetchMode(host as any)).resolves.toEqual({ mode: "legacy", options: {} }); + await expect(askSimpleFetchMode(host as any)).resolves.toEqual({ mode: "detailed", options: {} }); }); it("should return remote-only with keep-local option", async () => { @@ -818,12 +818,12 @@ describe("Red Flag Feature", () => { expect(host.mocks.appLifecycle.performRestart).toHaveBeenCalled(); }); - it("should return undefined when legacy mode is selected", async () => { + it("leaves the detailed Fetch flow to its existing handler", async () => { const host = createHostMock(); const log = createLoggerMock(); const cleanupFlag = vi.fn().mockResolvedValue(undefined); - host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_LEGACY); + host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_DETAILED); const result = await askAndPerformFastSetupOnScheduledFetchAll(host as any, log, cleanupFlag); @@ -1477,7 +1477,7 @@ describe("Red Flag Feature", () => { host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL); host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValueOnce({}); - host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_LEGACY); + host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_DETAILED); host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce("cancelled"); const handler = createFetchAllFlagHandler(host as any, log); @@ -1559,7 +1559,7 @@ describe("Red Flag Feature", () => { } as any); host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL); - host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_LEGACY); + host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_DETAILED); host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce({ vault: "identical", extra: {} }); host.mocks.rebuilder.$fetchLocal.mockResolvedValueOnce(); const handler = createFetchAllFlagHandler(host as any, log); diff --git a/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts b/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts index 242b478c..eaaf8950 100644 --- a/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts +++ b/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts @@ -309,7 +309,7 @@ describe("useP2PReplicatorUI commands", () => { expect(ribbon.remove).toHaveBeenCalledOnce(); }); - it("replaces a restored legacy P2P leaf with the current status view without opening another leaf", async () => { + it("compatibility: migrates a restored P2P leaf to the current status view without opening another leaf", async () => { let layoutReady: (() => Promise) | undefined; const legacyLeaf = { setViewState: vi.fn(async () => undefined), diff --git a/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts b/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts index 5e80cbc7..17c4dea6 100644 --- a/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts +++ b/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts @@ -38,7 +38,7 @@ describe("compatibility marker persistence", () => { }); describe("configured CouchDB fixture", () => { - it("starts in the current remote-profile format instead of exercising legacy migration", () => { + it("uses a current remote profile for ordinary configured fixtures", () => { const pluginData = createE2eCouchDbPluginData({ uri: "https://couch.example", username: "alice", diff --git a/test/e2e-obsidian/scripts/settings-ui.ts b/test/e2e-obsidian/scripts/settings-ui.ts index 1b925061..a0f1c791 100644 --- a/test/e2e-obsidian/scripts/settings-ui.ts +++ b/test/e2e-obsidian/scripts/settings-ui.ts @@ -223,6 +223,7 @@ async function verifyEffectiveSettings(): Promise { .getByText("Keep empty folder", { exact: true }) .waitFor({ state: "visible", timeout: uiTimeoutMs }); + // Retirement guard: the removed toggle must not reappear in the current settings pane. const obsoleteToggleCount = await deletionPanel.getByText("Use the trash bin", { exact: true }).count(); if (obsoleteToggleCount !== 0) { throw new Error( From 68d22ade76bd60ad408511a4fe41a5a3e6852b3f Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Thu, 23 Jul 2026 17:38:16 +0000 Subject: [PATCH 083/117] Use established terms in setup guidance --- README.md | 2 +- docs/adr/2026_06_real_obsidian_e2e.md | 2 +- docs/adr/2026_07_p2p_transport_lifecycle.md | 4 ++-- docs/p2p.md | 10 +++++----- docs/p2p_sync_updates_2026.md | 2 +- docs/quick_setup.md | 10 +++++----- docs/recovery.md | 4 ++-- docs/settings.md | 4 ++-- docs/setup_object_storage.md | 16 ++++++++-------- docs/setup_own_server.md | 6 +++--- docs/setup_p2p.md | 14 +++++++------- docs/tips/p2p-sync-tips.md | 2 +- docs/troubleshooting.md | 4 ++-- test/e2e-obsidian/README.md | 14 +++++++------- 14 files changed, 47 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 816388c1..b293b2b4 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Choose a synchronisation method, prepare its server where required, then follow 1. No central data-storage server is required. The project's public signalling relay requires no server provisioning; controlled deployments can provide another compatible relay. 2. Configure the clients by following [Peer-to-Peer Setup](docs/setup_p2p.md). -Each workflow establishes ordinary note synchronisation on the first device, generates the additional-device Setup URI from that working device, and verifies synchronisation in both directions. +Each workflow establishes ordinary note synchronisation on the first device, generates a Setup URI for each additional device from that working device, and verifies synchronisation in both directions. > [!TIP] > Fly.io is no longer free. Fortunately, we can still use IBM Cloudant despite some limitations. Refer to [Set up IBM Cloudant](docs/setup_cloudant.md). diff --git a/docs/adr/2026_06_real_obsidian_e2e.md b/docs/adr/2026_06_real_obsidian_e2e.md index 6682be9c..cade7701 100644 --- a/docs/adr/2026_06_real_obsidian_e2e.md +++ b/docs/adr/2026_06_real_obsidian_e2e.md @@ -200,7 +200,7 @@ Current implementation status: Current implementation status: - The mocked Vitest browser suites, their P2P runner, their root-level relay helpers, and the manual `harness-ci` workflow have been removed after maintained suites covered the critical flows. -- Headless CouchDB and Object Storage combinations, with and without encryption, remain owned by the CLI two-Vault matrix. P2P transport replacement and relay lifecycle remain owned by the CLI Compose E2E suite. Real Obsidian owns the visible CouchDB, Object Storage, and P2P Setup URI workflows, including first-device URI generation, second-device import, and two-way Vault synchronisation. +- Headless CouchDB and Object Storage combinations, with and without encryption, remain owned by the CLI two-Vault matrix. P2P transport replacement and relay lifecycle remain owned by the CLI Compose E2E suite. Real Obsidian owns the visible CouchDB, Object Storage, and P2P Setup URI workflows, including URI generation on the first device, import on the second device, and two-way Vault synchronisation. - The Obsidian compatibility implementation still needed by the Webapp has moved to `src/apps/webapp/obsidianMock.ts`; it is not a retained browser E2E Harness. - Remaining high-value scenarios, including RedFlag and Fast Setup (Simple Fetch) variants, should be added according to their owning integration boundary rather than copied line by line from the retired suite. diff --git a/docs/adr/2026_07_p2p_transport_lifecycle.md b/docs/adr/2026_07_p2p_transport_lifecycle.md index adc53e77..e7f4b671 100644 --- a/docs/adr/2026_07_p2p_transport_lifecycle.md +++ b/docs/adr/2026_07_p2p_transport_lifecycle.md @@ -46,7 +46,7 @@ Lifecycle operations on one `LiveSyncTrysteroReplicator` are serialised. A close Relay sockets retain their Trystero-provided close handlers. LiveSync pauses relay reconnection, closes the sockets, and later resumes reconnection through Trystero's public functions. It does not replace `socket.onclose`, because Trystero uses that handler to retire and recreate shared relay clients correctly. -P2P setup follows the transport's actual ownership model. First-device initialisation resets and scans the local database, but does not attempt to lock, reset, or upload to a non-existent central remote database. Its confirmation dialogues therefore describe preparing this device and do not present the central-server overwrite warnings or remote-configuration option. An additional device performs one explicit peer-selection and finite Fetch pass, then resumes database and Vault reflection. The generic second convergence pass remains reserved for central remote types because repeating it for P2P would ask the user to select the same peer twice. +P2P setup follows the transport's actual ownership model. Initialising the first device resets and scans the local database, but does not attempt to lock, reset, or upload to a non-existent central remote database. Its confirmation dialogues therefore describe preparing this device and do not present warnings about overwriting a central server or an option to fetch its configuration. An additional device selects a peer once, performs Fetch once, then resumes database and Vault reflection. The generic second convergence pass remains reserved for central remote types because repeating it for P2P would ask the user to select the same peer twice. ## Ownership @@ -74,7 +74,7 @@ This interferes with Trystero's shared relay clients. The public pause and resum ## Verification -Commonlib unit tests prove that normal P2P host closure calls `room.leave()` without directly closing Trystero-owned peer connections. Additional package tests cover the action API, replaceable peer-event subscriptions, multiple RPC transport disposers, serialised open and close operations, local-only first-device initialisation, and one-pass additional-device Fetch. +Commonlib unit tests prove that normal P2P host closure calls `room.leave()` without directly closing Trystero-owned peer connections. Additional package tests cover the action API, replaceable peer-event subscriptions, multiple RPC transport disposers, serialised open and close operations, initialisation of the first device without a central remote, and Fetch running once for an additional device. Self-hosted LiveSync unit tests prove that settings and database replacement leave panes on the current replicator, and that an explicit P2P rebuild bypasses the policy intended for ordinary replication. diff --git a/docs/p2p.md b/docs/p2p.md index 1b686815..4e7902d9 100644 --- a/docs/p2p.md +++ b/docs/p2p.md @@ -53,7 +53,7 @@ The **P2P Status** pane is the current Obsidian interface for P2P connections. - LiveSync does not open the pane merely because Obsidian has started. If the pane was already part of the saved Obsidian workspace, Obsidian may restore it. - Workspaces containing the retired P2P pane are migrated to the current status pane. The retired command is no longer exposed. -The active P2P remote is selected independently from the main CouchDB or Object Storage remote. Devices can therefore use P2P as an additional transport without replacing their main remote. +The active P2P remote is selected independently from the main CouchDB or Object Storage remote. Devices can therefore use P2P alongside their main remote without replacing it. ![P2P Status on desktop](../images/p2p-setup/p2p-status-pane.png) @@ -74,9 +74,9 @@ Every participating device must use the same signalling relay set, Group ID, and - A notification contains no Vault data. It only asks the following peer to fetch through the encrypted P2P connection. - Missing a notification does not make an explicit later synchronisation unsafe; **Replicate now** still compares the available data. -The peer's **More actions** menu contains persistent conveniences: +The peer's **More actions** menu can save these choices for that device: -- **Synchronise when this device connects** runs a finite synchronisation when that named peer is discovered. +- **Synchronise when this device connects** runs one synchronisation when that named peer is discovered. - **Follow whenever this device connects** restores following for that named peer. - **Include in the P2P synchronisation command** includes that peer when the command for registered targets is run. @@ -88,11 +88,11 @@ Configure these only after a manual round trip has succeeded. Device names used A device must approve a peer before serving its data. Permanent approval is stored; session approval lasts only for the current Obsidian session. Check the displayed device name before approving a request. -The encrypted Setup URI contains the shared P2P configuration but deliberately omits the device-specific name. Store the Setup URI and its passphrase separately, and generate the additional-device URI from a working first device. +The encrypted Setup URI contains the shared P2P configuration but deliberately omits the device-specific name. Store the Setup URI and its passphrase separately, and generate a Setup URI for another device from a first device which has completed setup. ## Operational limits - At least one device which already has the required data must be online while another device fetches it. - P2P does not provide the continuously available central copy offered by CouchDB or Object Storage. Keep independent backups. -- Mobile operating systems may pause Obsidian in the background. Keep Obsidian visible and the device awake during initial transfer, rebuild, or a large finite synchronisation. +- Mobile operating systems may pause Obsidian in the background. Keep Obsidian visible and the device awake during initial transfer, rebuild, or a large synchronisation. - Changing from CouchDB to P2P is not a repair operation for a stopped CouchDB setup. Diagnose the existing transport first. diff --git a/docs/p2p_sync_updates_2026.md b/docs/p2p_sync_updates_2026.md index c6a31a9f..529d816f 100644 --- a/docs/p2p_sync_updates_2026.md +++ b/docs/p2p_sync_updates_2026.md @@ -2,6 +2,6 @@ This address is retained for links to an earlier P2P guide. The time-specific interface description has been replaced by stable documentation: -- [Set up peer-to-peer synchronisation](setup_p2p.md) for the first device, additional-device Setup URI, approval, and two-way verification. +- [Set up peer-to-peer synchronisation](setup_p2p.md) for configuring the first device, generating a Setup URI for another device, approving the connection, and verifying synchronisation in both directions. - [How peer-to-peer synchronisation works](p2p.md) for signalling, TURN, privacy, the P2P Status pane, and automatic behaviour. - [Peer-to-Peer Synchronisation Tips](tips/p2p-sync-tips.md) for connection troubleshooting. diff --git a/docs/quick_setup.md b/docs/quick_setup.md index b156bcbe..72fee5fe 100644 --- a/docs/quick_setup.md +++ b/docs/quick_setup.md @@ -50,13 +50,13 @@ Create an ordinary test note and allow it to upload before adding another device ## Create a Setup URI for another device -Generate the additional-device Setup URI from the working first device. This captures the settings which that device is actually using, rather than asking another device to reuse the bootstrap URI produced during server provisioning. +Generate a Setup URI for another device from the working first device. This captures the settings which that device is actually using, rather than asking another device to reuse the Setup URI produced during server provisioning. 1. Open the Obsidian command palette on the first device. 2. Run `Self-hosted LiveSync: Copy settings as a new Setup URI`. 3. Enter a new passphrase which will protect this Setup URI, then select `OK`. - ![Masked passphrase for a new additional-device Setup URI](../images/quick-setup/guide-quick-setup-copy-setup-uri-passphrase.png) + ![Masked passphrase for a new Setup URI for another device](../images/quick-setup/guide-quick-setup-copy-setup-uri-passphrase.png) 4. Copy the resulting Setup URI, then select `OK`. @@ -75,7 +75,7 @@ Start with a new or separately backed-up Vault. Do not use a production Vault co 5. Paste the new Setup URI generated by the first device, enter its Setup URI passphrase, and select `Test Settings and Continue`. 6. Review `Setup Complete: Preparing to Fetch Synchronisation Data`, then select `Restart and Fetch Data`. - ![Additional-device Fetch confirmation](../images/quick-setup/guide-quick-setup-second-fetch.png) + ![Fetch confirmation on the additional device](../images/quick-setup/guide-quick-setup-second-fetch.png) 7. For a new or empty Vault, select `Overwrite all with remote files`. For a Vault with local work, stop and choose the appropriate strategy from the [Fast Setup guide](./tips/fast-setup.md). @@ -83,7 +83,7 @@ Start with a new or separately backed-up Vault. Do not use a production Vault co 8. When asked how to handle extra local files, the conservative choice is `Keep local files even if not on remote`. Select the delete option only when the local Vault is disposable and an exact remote copy is intended. - ![Additional-device local file policy](../images/quick-setup/guide-quick-setup-local-file-policy.png) + ![Local file policy on the additional device](../images/quick-setup/guide-quick-setup-local-file-policy.png) 9. Allow retrieval, file reflection, and any requested restart to finish. Keep Obsidian open until the LiveSync progress indicators have cleared. @@ -114,7 +114,7 @@ Use this path when CouchDB is ready but a Setup URI is unavailable. It configure 5. On `Choose a synchronisation remote`, select `CouchDB`, then select `Continue to CouchDB setup`. 6. Enter the complete CouchDB URL, username, password, and database name. - Obsidian Mobile requires HTTPS. Plain HTTP is suitable only for a trusted local connection from a desktop device. - - Use credentials which are allowed to connect to the selected database and, for this first-device path, create it when it does not exist. + - Use credentials which are allowed to connect to the selected database and, when configuring the first device, create it if it does not exist. 7. `Check server requirements` is optional. It sends the displayed credentials to the configured server through Obsidian's internal request API, and some checks require CouchDB administrator access. The initial check is read-only. If it offers a server change, review and confirm that individual change separately. 8. Select `Create or connect to database and continue`. Onboarding requires this connection test to succeed. 9. Review `Setup Complete: Preparing to Initialise Server`, then select `Restart and Initialise Server`. diff --git a/docs/recovery.md b/docs/recovery.md index a4ca48af..9d1227af 100644 --- a/docs/recovery.md +++ b/docs/recovery.md @@ -14,7 +14,7 @@ Use the least destructive operation which matches the evidence: - If the correct data is uncertain, suspend all work with `redflag.md`, preserve every copy, and inspect them before proceeding. - If the central remote is healthy and should win, use **Reset Synchronisation on This Device** or `flag_fetch.md`. - If this device's Vault is healthy and should replace a damaged or unwanted central remote, use **Overwrite Server Data with This Device's Files** or `flag_rebuild.md`. -- If both the Vault and local database are healthy and the only concern is unused storage, Garbage Collection may be appropriate. It is not a damaged-database recovery operation. +- If both the Vault and local database are healthy and the only concern is unused storage, Garbage Collection may be appropriate. It does not repair a damaged database. Do not switch transport, enable P2P, or run Garbage Collection as a substitute for diagnosing a stopped CouchDB or Object Storage setup. @@ -58,7 +58,7 @@ The readable flag is `flag_rebuild.md`; the legacy name `redflag2.md` remains ac For CouchDB and Object Storage, this is destructive to the selected remote state. Other devices may still contain revisions or files which are not present in the authoritative Vault, so keep them stopped until the new remote has been verified and then reset them from that remote. -For a P2P-only setup, there is no central remote database to overwrite. The corresponding first-device preparation rebuilds this device's local LiveSync database from its Vault. +For a P2P-only setup, there is no central remote database to overwrite. Preparing the first device instead rebuilds its local LiveSync database from its Vault. ## Garbage Collection is not Rebuild diff --git a/docs/settings.md b/docs/settings.md index 999ab21a..4dd9521c 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -386,8 +386,8 @@ The subject (`sub`) claim of the JWT, which should match your CouchDB username. The action depends on why the dialogue was opened: -- First-device onboarding uses **Create or connect to database and continue**. It may create the database when it does not exist and the supplied account has permission. -- Additional-device onboarding uses **Connect to existing database and continue**. It does not create a missing database. +- Onboarding for the first device uses **Create or connect to database and continue**. It may create the database when it does not exist and the supplied account has permission. +- Onboarding for an additional device uses **Connect to existing database and continue**. It does not create a missing database. - Adding or editing a saved remote profile uses **Test connection and save**. It does not create a missing database. - Settings mode also offers **Save without connecting**. The existing profile is updated, but automatic synchronisation may fail until the connection is corrected. diff --git a/docs/setup_object_storage.md b/docs/setup_object_storage.md index 9c725cfe..f29f60f1 100644 --- a/docs/setup_object_storage.md +++ b/docs/setup_object_storage.md @@ -1,6 +1,6 @@ # Set up Object Storage -This guide establishes Object Storage synchronisation on a first device, generates an additional-device Setup URI from that working device, and verifies synchronisation in both directions. +This guide establishes Object Storage synchronisation on a first device, generates a Setup URI for another device from that working device, and verifies synchronisation in both directions. Object Storage uses the S3-compatible API. Prepare the following before starting: @@ -12,7 +12,7 @@ Object Storage uses the S3-compatible API. Prepare the following before starting Back up every Vault involved, and do not use Obsidian Sync, iCloud synchronisation, or another synchronisation service on the same Vault. -## Generate the bootstrap Setup URI +## Generate the initial Setup URI The public generator applies the Object Storage preset and records the connection as the selected remote profile. Run it from a trusted terminal: @@ -40,13 +40,13 @@ Use a new bucket prefix, or a prefix whose contents you deliberately intend to r 1. Install and enable Self-hosted LiveSync in the intended Vault. 2. Open onboarding from the `Welcome to Self-hosted LiveSync` Notice. 3. Select `I am setting this up for the first time`, then choose the recommended Setup URI method. -4. Paste the bootstrap Setup URI, enter its passphrase, and select `Test Settings and Continue`. +4. Paste the initial Setup URI, enter its passphrase, and select `Test Settings and Continue`. ![Object Storage Setup URI on the first device](../images/object-storage-setup/guide-object-storage-setup-first-setup-uri.png) 5. Select `Restart and Initialise Server`, then read and accept the final overwrite confirmation only when this Vault is the intended source of truth. - ![Object Storage first-device initialisation](../images/object-storage-setup/guide-object-storage-setup-first-initialise.png) + ![Object Storage initialisation on the first device](../images/object-storage-setup/guide-object-storage-setup-first-initialise.png) ![Final Object Storage overwrite confirmation](../images/object-storage-setup/guide-object-storage-setup-first-rebuild-confirmation.png) @@ -64,7 +64,7 @@ Generate a fresh Setup URI from the working first device: 1. Run `Self-hosted LiveSync: Copy settings as a new Setup URI` from the command palette. 2. Enter a new Setup URI passphrase. - ![Masked passphrase for the additional-device Object Storage Setup URI](../images/object-storage-setup/guide-object-storage-setup-copy-setup-uri-passphrase.png) + ![Masked passphrase for the Object Storage Setup URI for another device](../images/object-storage-setup/guide-object-storage-setup-copy-setup-uri-passphrase.png) 3. Copy the resulting URI. @@ -80,7 +80,7 @@ Start with a new or separately backed-up Vault. 2. Open onboarding, select `I am adding a device to an existing synchronisation setup`, and choose the recommended Setup URI method. 3. Enter the URI generated by the first device and its passphrase. - ![First-device Object Storage Setup URI entered on the second device](../images/object-storage-setup/guide-object-storage-setup-second-setup-uri.png) + ![Object Storage Setup URI from the first device entered on the second device](../images/object-storage-setup/guide-object-storage-setup-second-setup-uri.png) 4. Select `Restart and Fetch Data`. @@ -96,7 +96,7 @@ Start with a new or separately backed-up Vault. Confirm that the first device's test note appears unchanged. Create a second ordinary note on the new device, wait for its journal synchronisation to finish, and confirm that it reaches the first device. Configure optional features only after this two-way check passes. -![First-device note received by the second Object Storage device](../images/object-storage-setup/guide-object-storage-setup-first-to-second.png) +![Note from the first device received by the second Object Storage device](../images/object-storage-setup/guide-object-storage-setup-first-to-second.png) ![Second-device note received by the first Object Storage device](../images/object-storage-setup/guide-object-storage-setup-second-to-first.png) @@ -104,5 +104,5 @@ Confirm that the first device's test note appears unchanged. Create a second ord - Treat the endpoint, bucket, prefix, access key, secret key, Vault passphrase, Setup URI, and Setup URI passphrase as sensitive. - Use a distinct prefix per synchronisation set unless shared data is explicitly intended. -- Do not select first-device initialisation against an existing prefix unless replacing its contents is deliberate. +- Do not initialise the first device against an existing prefix unless replacing its contents is deliberate. - Object Storage is not a Vault backup. Keep independent backups and test restoration separately. diff --git a/docs/setup_own_server.md b/docs/setup_own_server.md index 6268413f..5ac2b753 100644 --- a/docs/setup_own_server.md +++ b/docs/setup_own_server.md @@ -167,7 +167,7 @@ Now `https://tiles-photograph-routine-groundwater.trycloudflare.com` is our serv ## 4. Client Setup > [!TIP] -> A generated Setup URI is the recommended path because it carries the current new-Vault defaults and remote profile. If a Setup URI cannot be generated, follow [Configure CouchDB manually on the first device](./quick_setup.md#configure-couchdb-manually-on-the-first-device), then generate a new Setup URI from that working device for every additional device. +> A generated Setup URI is the recommended path because it carries the current defaults for a new Vault and the selected remote profile. If a Setup URI cannot be generated, follow [Configure CouchDB manually on the first device](./quick_setup.md#configure-couchdb-manually-on-the-first-device), then generate a new Setup URI from that working device for every additional device. ### 1. Generate the setup URI on a desktop device or server ```bash @@ -185,7 +185,7 @@ deno run --minimum-dependency-age=0 --allow-env https://raw.githubusercontent.co > > If `uri_passphrase` is omitted, the generator creates a cryptographically random value and prints it once. -The generator consumes the exact registry-pinned Commonlib release used by the provisioning utility. It creates a configured CouchDB remote profile, applies the current new-Vault defaults, and encodes them with Commonlib's Setup URI contract. +The generator consumes the exact registry-pinned Commonlib release used by the provisioning utility. It creates a configured CouchDB remote profile, applies the current defaults for a new Vault, and encodes them with Commonlib's Setup URI contract. You will then get the following output: @@ -202,7 +202,7 @@ Store the Setup URI and its passphrase separately. Follow [Quick setup](./quick_setup.md#set-up-the-first-device) for the first device. It covers the current onboarding Notice, Setup URI import, server initialisation, and the safety prompts shown for a newly provisioned database. -After ordinary note synchronisation works, [generate a new Setup URI on that first device](./quick_setup.md#create-a-setup-uri-for-another-device), then follow [Add another device](./quick_setup.md#add-another-device). Do not make the second device depend on retaining the provisioning-time bootstrap URI. Configure optional features only after the normal path is verified; [Hidden File Sync has its own guide](./tips/hidden-file-sync.md). +After ordinary note synchronisation works, [generate a new Setup URI on that first device](./quick_setup.md#create-a-setup-uri-for-another-device), then follow [Add another device](./quick_setup.md#add-another-device). Do not make the second device depend on retaining the initial Setup URI produced during provisioning. Configure optional features only after the normal path is verified; [Hidden File Sync has its own guide](./tips/hidden-file-sync.md). --- diff --git a/docs/setup_p2p.md b/docs/setup_p2p.md index a40d83ff..0bf049ec 100644 --- a/docs/setup_p2p.md +++ b/docs/setup_p2p.md @@ -47,7 +47,7 @@ On the working first device: 1. Run `Self-hosted LiveSync: Copy settings as a new Setup URI` from the command palette. 2. Enter a new Setup URI passphrase. - ![Masked passphrase for the additional-device P2P Setup URI](../images/p2p-setup/guide-p2p-setup-copy-setup-uri-passphrase.png) + ![Masked passphrase for the P2P Setup URI for another device](../images/p2p-setup/guide-p2p-setup-copy-setup-uri-passphrase.png) 3. Copy the resulting URI. @@ -61,7 +61,7 @@ Keep the first device online. Store the new URI and its passphrase separately. 2. Open onboarding, select `I am adding a device to an existing synchronisation setup`, and choose the recommended Setup URI method. 3. Enter the Setup URI generated by the first device and its passphrase. - ![First-device P2P Setup URI entered on the second device](../images/p2p-setup/guide-p2p-setup-second-setup-uri.png) + ![P2P Setup URI from the first device entered on the second device](../images/p2p-setup/guide-p2p-setup-second-setup-uri.png) 4. Select `Restart and Fetch Data`. @@ -73,7 +73,7 @@ Keep the first device online. Store the new URI and its passphrase separately. ![P2P local-file policy](../images/p2p-setup/guide-p2p-setup-local-file-policy.png) -6. In `P2P Rebuild`, confirm that the expected first-device name is shown, then select `Sync`. +6. In `P2P Rebuild`, confirm that the expected name of the first device is shown, then select `Sync`. ![Selecting the first device for P2P Rebuild](../images/p2p-setup/guide-p2p-setup-select-first-device.png) @@ -83,16 +83,16 @@ Keep the first device online. Store the new URI and its passphrase separately. 8. Keep both devices open until the test note appears on the second device. - ![First-device note received by the second P2P device](../images/p2p-setup/guide-p2p-setup-first-to-second.png) + ![Note from the first device received by the second P2P device](../images/p2p-setup/guide-p2p-setup-first-to-second.png) ## Verify the return journey -Create a second ordinary note on the second device. Keep automatic announcements disabled and prove the next finite synchronisation explicitly: +Create a second ordinary note on the second device. Keep automatic announcements disabled, then run and verify the next synchronisation explicitly: 1. Open `P2P Status` on both devices. 2. If a peer no longer appears, select `Disconnect` and then `Open connection` on the first device, followed by the second device. The device which joins last is advertised to devices which are already in the room. 3. On the first device, select `Refresh`, verify the second-device name, then select `Replicate now`. -4. On the second device, verify the requesting first-device name and select `Accept` or `Accept Temporarily`. +4. On the second device, verify the name of the requesting first device and select `Accept` or `Accept Temporarily`. ![Explicit return-journey connection request on the second device](../images/p2p-setup/guide-p2p-setup-connection-request-2.png) @@ -119,7 +119,7 @@ An announcement contains no Vault data and does not transfer a change by itself. - Check signalling relay reachability separately from WebRTC connectivity. - Review VPN and TURN options in [Peer-to-Peer Synchronisation Tips](./tips/p2p-sync-tips.md). -## Controlled or self-hosted bootstrap +## Controlled or self-hosted setup The ordinary route above starts in the plug-in UI and can use the project's public signalling relay. For a controlled deployment, prepare your own Nostr-compatible relay and enter it in `Signalling relay URLs` on every device. diff --git a/docs/tips/p2p-sync-tips.md b/docs/tips/p2p-sync-tips.md index 6ce0c9a2..533935ed 100644 --- a/docs/tips/p2p-sync-tips.md +++ b/docs/tips/p2p-sync-tips.md @@ -52,7 +52,7 @@ If the device was asleep, Obsidian was in the background, or the peer disconnect ## Mobile limitations -Keep Obsidian visible and the device awake during initial transfer, rebuild, or a large finite synchronisation. Wake Lock support is best effort and cannot prevent the operating system from suspending or terminating a background application. +Keep Obsidian visible and the device awake during initial transfer, rebuild, or a large synchronisation. Wake Lock support is best effort and cannot prevent the operating system from suspending or terminating a background application. ## Collect evidence diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 4acae22d..be623d3f 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -57,7 +57,7 @@ If the log reports missing chunks or a size mismatch: 2. on a device which has the correct file, run `Recreate missing chunks for all files`, then synchronise; and 3. if the mismatch remains, run `Verify and repair all files` from `Hatch` and review which copy is authoritative. -## A configuration-mismatch dialogue blocks synchronisation +## A configuration mismatch dialogue blocks synchronisation Some settings must match across devices. LiveSync pauses synchronisation when the local and remote values differ rather than propagating an unexpected change silently. @@ -153,4 +153,4 @@ Follow [Recovery and flag files](recovery.md). A `redflag.md` emergency stop rem ## Further technical context -See [Technical Information](tech_info.md) for database and synchronisation internals. Current behaviour belongs in this guide; older defect-specific instructions remain in the release histories. +See [Technical Information](tech_info.md) for database and synchronisation internals. Current behaviour belongs in this guide; instructions for older defects remain in the release histories. diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index bd11e2a3..6fc6d483 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -99,11 +99,11 @@ The mobile pass uses Obsidian's `app.emulateMobile(true)`, a 390 by 844 CSS-pixe The same workflow checks the two remote-activity status boundaries. It first holds a real CouchDB request at the selected fetch implementation and confirms that `🌐N` is visible while `📲` is absent. It then holds the real one-shot replication immediately before its replicator call, confirms that `📲` is visible while no physical request is active, releases it, and requires the finite and bounded activity counts to return to zero, the request and response counts to balance, and both indicators to disappear. Finally, it creates a remote-only chunk, holds the real on-demand fetch immediately before its remote call, makes the same logical active and idle assertions, and verifies that the fetched chunk is written into the local database. These gates make the active states deterministic without replacing the remote request or operation. -`test:e2e:obsidian:couchdb-manual-setup-workflow` follows the visible first-device onboarding path without a bootstrap Setup URI. It enters end-to-end encryption and CouchDB details, runs the read-only server-requirements check, requires the prepared fixture to pass without applying a server fix, and lets the onboarding connection test create the named database. After first-device Rebuild, it creates an ordinary note, asks that working device to generate a Setup URI for a second device, completes Fetch there, and verifies a bidirectional note round-trip. The workflow captures each decision point and the expanded server-check result; password controls remain visually masked. +`test:e2e:obsidian:couchdb-manual-setup-workflow` follows the visible onboarding path for the first device when no Setup URI is available. It enters end-to-end encryption and CouchDB details, runs the read-only `Check server requirements` step, requires the prepared fixture to pass without applying a server fix, and lets the onboarding connection test create the named database. After Rebuild completes on the first device, it creates an ordinary note, asks that working device to generate a Setup URI for a second device, completes Fetch there, and verifies a bidirectional note round-trip. The workflow captures each decision point and the expanded server-check result; password controls remain visually masked. If this status workflow fails while Obsidian is running, it writes a full-page screenshot and a JSON snapshot of the status text and counters under `/tmp/obsidian-livesync-e2e`. The dialogue-mount workflow leaves desktop and mobile screenshots for both representative Svelte routes, and the Hidden File Sync workflow captures the successfully displayed JSON Resolve dialogue before selecting an option. The suite therefore records representative evidence without capturing every interaction. Set `E2E_OBSIDIAN_DIAGNOSTICS_DIR` to use another directory. -The two-Vault workflow performs the missing-marker review once for each isolated Vault. Later process launches reuse the same profile-backed acknowledgement, rather than seeding a replacement or repeatedly applying a first-device decision. The Hidden File Sync scenario is narrower: it starts from an explicitly acknowledged marker because it tests consumer-owned hidden-file behaviour, JSON resolution, target filtering, and grouped mobile Notices rather than duplicating the compatibility workflow. After `app.emulateMobile(true)`, its fixture operations use the active DevTools renderer because Obsidian can remove desktop-only CLI commands in mobile mode. +The two-Vault workflow performs the missing-marker review once for each isolated Vault. Later process launches reuse the same profile-backed acknowledgement, rather than seeding a replacement or repeatedly applying a decision for the first device. The Hidden File Sync scenario is narrower: it starts from an explicitly acknowledged marker because it tests consumer-owned hidden-file behaviour, JSON resolution, target filtering, and grouped mobile Notices rather than duplicating the compatibility workflow. After `app.emulateMobile(true)`, its fixture operations use the active DevTools renderer because Obsidian can remove desktop-only CLI commands in mobile mode. `test:e2e:obsidian:cli-to-obsidian-sync` is the cross-runtime compatibility check for the official LiveSync CLI and the real Obsidian plug-in. Build the plug-in first, and build the local CLI too when no external CLI command is selected. The script uses E2EE, Path Obfuscation, and the current preferred chunk settings to create and synchronise a note through the CLI, starts real Obsidian with an isolated Vault and profile, synchronises the same CouchDB database, and verifies that the plug-in materialises identical note content. This covers the boundary that CLI-only and plug-in-only round trips do not exercise. @@ -124,19 +124,19 @@ LIVESYNC_CLI_COMMAND="docker run --rm --network host --user $(id -u):$(id -g) -- `test:e2e:obsidian:minio-upload` reuses the Object Storage variables from `.test.env` or the process environment. It expects a reachable S3-compatible service, configures Self-hosted LiveSync for Object Storage through `obsidian-cli eval`, creates a note in real Obsidian, runs one-shot Journal Sync, and verifies through the AWS SDK that objects were written under a unique bucket prefix. Adapter tests separately observe an in-progress SDK command, while this real-runtime workflow verifies the resulting request counters advance and rebalance. -`test:e2e:obsidian:object-storage-setup-uri-workflow` generates a public Commonlib-backed bootstrap URI for a unique MinIO prefix, completes visible first-device initialisation, and then asks that working real Obsidian device to create a new Setup URI through the registered command. A second real Obsidian device imports only the device-generated URI. The workflow verifies A-to-B and B-to-A notes, captures the documented onboarding choices, and removes the Object Storage prefix only after both sessions have stopped. +`test:e2e:obsidian:object-storage-setup-uri-workflow` uses the public Commonlib-backed tool to generate the initial Setup URI for a unique MinIO prefix, completes visible initialisation on the first device, and then asks that working real Obsidian device to create a new Setup URI through the registered command. A second real Obsidian device imports only the device-generated URI. The workflow verifies A-to-B and B-to-A notes, captures the documented onboarding choices, and removes the Object Storage prefix only after both sessions have stopped. -`test:e2e:obsidian:p2p-setup-uri-workflow` runs two concurrent isolated real Obsidian sessions against the local Compose Nostr relay fixture. The first device imports a generated bootstrap URI and completes its signalling test with zero peers, creates a Setup URI for the second device through the registered command, and remains online while the second device imports it. The second device must select the expected online source before Fetch can rebuild its local database. The workflow accepts each connection request visibly on the receiving device, verifies the initial A-to-B fetch, checks that the menu for the three persistent per-peer actions remains within the viewport, reconnects both finite P2P sessions in join order, and verifies the B-to-A return journey. Every started session remains tracked until teardown completes. +`test:e2e:obsidian:p2p-setup-uri-workflow` runs two concurrent isolated real Obsidian sessions against the local Compose Nostr relay fixture. The first device imports a generated initial Setup URI and completes its signalling test with zero peers, creates a Setup URI for the second device through the registered command, and remains online while the second device imports it. The second device must select the expected online source before Fetch can rebuild its local database. The workflow accepts each connection request visibly on the receiving device, verifies the initial A-to-B fetch, checks that the menu for the three persistent per-peer actions remains within the viewport, reconnects both P2P sessions in join order, and verifies the B-to-A return journey. Every started session remains tracked until teardown completes. `test:e2e:obsidian:startup-scan` configures a temporary CouchDB database, stops Obsidian, writes a note directly into the vault, restarts Obsidian, and verifies from CouchDB that the boot-time scan picked up the offline file. -`test:e2e:obsidian:setup-uri-workflow` runs the repository's public Commonlib-backed CouchDB provisioning and Setup URI tools against the local CouchDB fixture. It configures the first data-less real Obsidian Vault through the visible onboarding wizard and uses Rebuild. After that device is working, it generates a new Setup URI through the registered command; the second real Obsidian Vault uses that URI for Fetch instead of reusing the provisioning-time bootstrap URI. The workflow verifies ordinary notes from the first device to the second and back again, independently enables Hidden File Sync on each device, and verifies a snippet. The retained Setup URI screenshots show only encrypted URIs and visually masked Setup URI passphrases; plaintext credentials are not captured. Files prefixed with `guide-` capture the relevant dialogue, settings panel, or workspace leaf without transient Notices. Public documentation copies selected images only after visual inspection; the E2E run does not overwrite repository documentation assets. +`test:e2e:obsidian:setup-uri-workflow` runs the repository's public Commonlib-backed CouchDB provisioning and Setup URI tools against the local CouchDB fixture. It configures a new, empty Vault in the first real Obsidian session through the visible onboarding wizard and uses Rebuild. After that device is working, it generates a new Setup URI through the registered command; the second real Obsidian Vault uses that URI for Fetch instead of reusing the initial Setup URI produced by the provisioning tool. The workflow verifies ordinary notes from the first device to the second and back again, independently enables Hidden File Sync on each device, and verifies a snippet. The retained Setup URI screenshots show only encrypted URIs and visually masked Setup URI passphrases; plaintext credentials are not captured. Files prefixed with `guide-` capture the relevant dialogue, settings panel, or workspace leaf without transient Notices. Public documentation copies selected images only after visual inspection; the E2E run does not overwrite repository documentation assets. `test:e2e:obsidian:two-vault-sync` runs a two-vault note synchronisation workflow. It verifies note creation, update, ordinary rename, a case-only file name change within the same directory, deletion, and a separate encrypted round-trip with Path Obfuscation enabled. Its target-filter scenario confirms that one Vault receives and checkpoints a remote document without reflecting it, restarts with the same profile and filter, and then reflects the stored document after the filter is broadened through the settings service. Directory case changes deliberately remain outside this scenario because they require directory-aware rename handling. The optional Markdown conflict check can be enabled with `E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT=true`. It creates divergent revisions in two separate Vaults, performs a conservative merge on one Vault, edits that result again, and requires the other Vault to replace its known deleted losing revision without recreating the conflict. The separate `E2E_OBSIDIAN_INCLUDE_CONFLICT_OPERATIONS=true` check keeps four conflicts active while one Vault edits, deletes, performs a case-only rename, and performs a cross-path rename. It asserts that each operation extends the revision displayed on that device, replicates the exact resulting revision tree, and preserves the other live branch. During focused development, `E2E_OBSIDIAN_ONLY_CONFLICT_OPERATIONS=true` runs that self-contained scope without the ordinary, target-filter, or encrypted scenarios. Both conflict checks remain outside the default local suite. `test:e2e:obsidian:conflict-dialog-policy` creates three real local revision leaves without a remote service and opens the pairwise merge dialogue in Obsidian. It verifies the three-version count, requires the four decision buttons to be stacked vertically, concatenates the displayed pair as a child of the displayed winner, confirms that the untouched leaf remains as one conflict, postpones that remaining pair, restarts the same isolated Vault and profile, and confirms that only the two live versions are reconstructed. It also verifies that an ordinary repeated conflict check does not reopen a postponed dialogue, that **Resolve if conflicted.** explicitly reopens it, and that the active editor retains the appropriate unresolved-conflict warning. The scenario then invokes the same Commonlib consumer boundary used for an incoming replicated document and checks that a postponed warning disappears, an open stale dialogue closes, and the conflict-processing queue completes even when the dialogue closes immediately. This isolates the Obsidian UI contract from transport and second-device setup. The fixture owns one temporary Vault and profile, and the session runner stops Obsidian before removing them. -`test:e2e:obsidian:hidden-file-snippet-sync` runs a two-vault hidden file round-trip. It verifies creation and deletion of a real `.obsidian/snippets/*.css` file, automatic JSON conflict merging for a hidden file with the merged result propagated by a second synchronisation, manual JSON Resolve dialogue application through Obsidian's UI, and per-device target patterns where one vault ignores a hidden file that the other vault synchronises. Initial enablement must open one user-visible progress Notice before the enabled setting is saved, then retain that Notice while its nested rebuild and scan phases continue in the ordinary log. The configured fixture starts with a current CouchDB remote profile, so legacy-profile migration remains the responsibility of the upgrade scenarios and cannot add unrelated Notices to this check. It also covers [issue #555](https://github.com/vrtmrz/obsidian-livesync/issues/555) by requiring several plug-in and settings changes to share one separate mobile-safe action Notice with actionable touch targets; a manually dismissed group must not repeat its acknowledged rows when a later change arrives. +`test:e2e:obsidian:hidden-file-snippet-sync` runs a two-vault hidden file round-trip. It verifies creation and deletion of a real `.obsidian/snippets/*.css` file, automatic JSON conflict merging for a hidden file with the merged result propagated by a second synchronisation, manual JSON Resolve dialogue application through Obsidian's UI, and per-device target patterns where one vault ignores a hidden file that the other vault synchronises. Initial enablement must open one user-visible progress Notice before the enabled setting is saved, then retain that Notice while its nested rebuild and scan phases continue in the ordinary log. The configured fixture starts with a current CouchDB remote profile, so migration from legacy remote settings remains the responsibility of the upgrade scenarios and cannot add unrelated Notices to this check. It also covers [issue #555](https://github.com/vrtmrz/obsidian-livesync/issues/555) by requiring several plug-in and settings changes to share one separate action Notice whose controls remain usable in mobile layouts; a manually dismissed group must not repeat its acknowledged rows when a later change arrives. `test:e2e:obsidian:customisation-sync` runs a two-vault Customisation Sync workflow. It scans a real snippet CSS file, config JSON file, and sample plug-in fixture into per-file Customisation Sync data, synchronises the entries through CouchDB, applies them on the second vault, verifies the resulting `.obsidian` files, propagates a snippet update, and verifies deletion of the source-vault snippet sync data without confusing it with the target vault's own applied copy. @@ -146,7 +146,7 @@ LIVESYNC_CLI_COMMAND="docker run --rm --network host --user $(id -u):$(id -g) -- The workflow first exercises a non-empty legacy settings document which has no `isConfigured` or file-name case value. It verifies that 0.25.83 treats a default-equivalent document as unconfigured. That release can persist the inferred boolean during a later, unrelated settings-save event, so the runner accepts either an absent value or the inferred `false` on disk, then restores the same minimal pre-flag document deliberately before installing 1.0. The target independently proves its direct migration: the Vault remains unconfigured instead of receiving new-Vault recommendations, case-insensitive handling becomes explicit, no compatibility pause or acknowledgement marker is created while onboarding remains pending, and a second 1.0 start is idempotent. The absent marker is deliberately deferred rather than accepted; a later configured start must evaluate it. This fixture rewrite is limited to the missing-flag boundary; the configured transport upgrades use only state created and saved by 0.25.83 itself. -For CouchDB and Object Storage, the workflow then configures 0.25.83 from its own defaults, saves the selected remote, and restarts that release with the same profile before creating history. This both verifies that the old settings persist and lets the old release initialise its replicator from the same saved state as an ordinary existing Vault. The runner waits for that release's asynchronously initialised persistent node identity, creates, edits, renames, and deletes notes, and synchronises each transition before installing the target. Every launch of the upgraded device uses the same isolated Obsidian profile. The session layer closes the renderer before its process-tree fallback, so Chromium persists the legacy compatibility marker naturally; the target must read and migrate that actual profile state to its current namespaced key. The final target restart likewise consumes the marker persisted by the preceding target session. The runner does not reconstruct that device's Vault data, plug-in settings, local database files, device-local state, or remote state. Before the target performs any synchronisation, it must retain the same Vault profile, local database, node identity, remote profile, local checkpoint, and remote milestone. The local node-info document is the identity source of truth; a transient replicator field is used only to confirm that the old asynchronous initialisation has completed. Its first synchronisation must be a no-op: CouchDB document revisions and `update_seq` must remain unchanged, while Object Storage must neither upload nor download journal bodies. The upgraded device then sends a new delta. A separate fresh 1.0 verifier starts from an explicit current-version settings and compatibility fixture, receives the complete surviving history, and returns another delta; it is not part of the legacy-profile migration assertion. The upgraded Vault receives that return journey and retains it across restart. +For CouchDB and Object Storage, the workflow then configures 0.25.83 from its own defaults, saves the selected remote, and restarts that release with the same profile before creating history. This both verifies that the old settings persist and lets the old release initialise its replicator from the same saved state as an ordinary existing Vault. The runner waits for that release's asynchronously initialised persistent node identity, creates, edits, renames, and deletes notes, and synchronises each transition before installing the target. Every launch of the upgraded device uses the same isolated Obsidian profile. The session layer closes the renderer before its process-tree fallback, so Chromium persists the legacy compatibility marker naturally; the target must read and migrate that actual profile state to its current namespaced key. The final target restart likewise consumes the marker persisted by the preceding target session. The runner does not reconstruct that device's Vault data, plug-in settings, local database files, device-local state, or remote state. Before the target performs any synchronisation, it must retain the same Vault profile, local database, node identity, remote profile, local checkpoint, and remote milestone. The local node-info document is the identity source of truth; a transient replicator field is used only to confirm that the old asynchronous initialisation has completed. Its first synchronisation must be a no-op: CouchDB document revisions and `update_seq` must remain unchanged, while Object Storage must neither upload nor download journal bodies. The upgraded device then sends a new delta. A separate fresh 1.0 verifier starts from an explicit fixture containing settings and compatibility state for the current version, receives the complete surviving history, and returns another delta; it is not part of the migration assertion for legacy remote settings. The upgraded Vault receives that return journey and retains it across restart. Before creating stable-release history, the runner waits until the remote Security Seed can be read and only then marks the remote as resolved. Completion of the old release's remote-creation method alone does not prove that this asynchronous fixture boundary is ready. From 24a4ebb8dd346947f024fd321f160c1426ee4e99 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 24 Jul 2026 03:04:39 +0000 Subject: [PATCH 084/117] Use obsidian-test-session 0.2.5 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c0c8f2dc..531210de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "@types/transform-pouch": "^1.0.6", "@typescript-eslint/parser": "8.56.1", "@vitest/coverage-v8": "^4.1.8", - "@vrtmrz/obsidian-test-session": "0.2.4", + "@vrtmrz/obsidian-test-session": "0.2.5", "dotenv-cli": "^11.0.0", "esbuild": "0.28.1", "esbuild-plugin-inline-worker": "^0.1.1", @@ -4839,9 +4839,9 @@ } }, "node_modules/@vrtmrz/obsidian-test-session": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-test-session/-/obsidian-test-session-0.2.4.tgz", - "integrity": "sha512-fyb/6xHea/w9WwWi5u9ZJol1rBnVEk+fa1yM1RzUcf6VUAzmwn5zELWtOw8ZsFTdo9QpwVuAYlRAs35AdmUzqQ==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-test-session/-/obsidian-test-session-0.2.5.tgz", + "integrity": "sha512-ZsI+Yx3z6IEFfh5Ey5mEUBNI0SUD6oDhP7D9LSZVEqPmdJz2JMiag7d8u/p1i6KpsoI2HbDvtw4RHpB+BQReAw==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index dbb67615..8ec2c8e5 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "@types/transform-pouch": "^1.0.6", "@typescript-eslint/parser": "8.56.1", "@vitest/coverage-v8": "^4.1.8", - "@vrtmrz/obsidian-test-session": "0.2.4", + "@vrtmrz/obsidian-test-session": "0.2.5", "dotenv-cli": "^11.0.0", "esbuild": "0.28.1", "esbuild-plugin-inline-worker": "^0.1.1", From bdc44920ac13aae5ae2f9ecf321d831e3b550956 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 24 Jul 2026 05:37:13 +0000 Subject: [PATCH 085/117] Refresh setup guides and capture fixtures --- docs/quick_setup.md | 15 ++++ docs/tips/hidden-file-sync.md | 5 +- ...uide-couchdb-manual-connection-details.png | Bin 0 -> 58625 bytes ...guide-couchdb-manual-connection-method.png | Bin 0 -> 48228 bytes .../guide-couchdb-manual-encryption.png | Bin 0 -> 72751 bytes .../guide-couchdb-manual-remote-selection.png | Bin 0 -> 51248 bytes ...ide-couchdb-manual-server-requirements.png | Bin 0 -> 61972 bytes ...uide-hidden-file-initial-scan-progress.png | Bin 0 -> 30799 bytes ...age-setup-missing-remote-configuration.png | Bin 20113 -> 20728 bytes ...-object-storage-setup-retrieval-method.png | Bin 73017 -> 73011 bytes ...ick-setup-missing-remote-configuration.png | Bin 20113 -> 20728 bytes .../guide-quick-setup-retrieval-method.png | Bin 73017 -> 73011 bytes test/e2e-obsidian/scripts/p2p-pane.ts | 75 ++++++++++++------ 13 files changed, 69 insertions(+), 26 deletions(-) create mode 100644 images/couchdb-manual/guide-couchdb-manual-connection-details.png create mode 100644 images/couchdb-manual/guide-couchdb-manual-connection-method.png create mode 100644 images/couchdb-manual/guide-couchdb-manual-encryption.png create mode 100644 images/couchdb-manual/guide-couchdb-manual-remote-selection.png create mode 100644 images/couchdb-manual/guide-couchdb-manual-server-requirements.png create mode 100644 images/hidden-file-sync/guide-hidden-file-initial-scan-progress.png diff --git a/docs/quick_setup.md b/docs/quick_setup.md index 72fee5fe..f685db48 100644 --- a/docs/quick_setup.md +++ b/docs/quick_setup.md @@ -107,15 +107,30 @@ Use this path when CouchDB is ready but a Setup URI is unavailable. It configure 1. Install and enable Self-hosted LiveSync in the intended Vault. 2. Select the `Welcome to Self-hosted LiveSync` Notice, choose `I am setting this up for the first time`, then confirm that you want to set up a new synchronisation. 3. On `Connection Method`, select `Configure a remote manually`, then select `Proceed with manual configuration`. + + ![Manual remote configuration option during onboarding](../images/couchdb-manual/guide-couchdb-manual-connection-method.png) + 4. On `End-to-End Encryption`, decide how the synchronised data will be protected. - For an ordinary new Vault, enable `End-to-End Encryption` and enter a strong Vault encryption passphrase. - Enable `Obfuscate Properties` if remote document properties should also be concealed. - Store the Vault encryption passphrase securely. It is separate from the passphrase used to protect a Setup URI. + + ![CouchDB Vault encryption settings with the passphrase masked](../images/couchdb-manual/guide-couchdb-manual-encryption.png) + 5. On `Choose a synchronisation remote`, select `CouchDB`, then select `Continue to CouchDB setup`. + + ![CouchDB option in the synchronisation remote choices](../images/couchdb-manual/guide-couchdb-manual-remote-selection.png) + 6. Enter the complete CouchDB URL, username, password, and database name. - Obsidian Mobile requires HTTPS. Plain HTTP is suitable only for a trusted local connection from a desktop device. - Use credentials which are allowed to connect to the selected database and, when configuring the first device, create it if it does not exist. + + ![Manual CouchDB connection fields with the password masked](../images/couchdb-manual/guide-couchdb-manual-connection-details.png) + 7. `Check server requirements` is optional. It sends the displayed credentials to the configured server through Obsidian's internal request API, and some checks require CouchDB administrator access. The initial check is read-only. If it offers a server change, review and confirm that individual change separately. + + ![Successful optional CouchDB server requirements check](../images/couchdb-manual/guide-couchdb-manual-server-requirements.png) + 8. Select `Create or connect to database and continue`. Onboarding requires this connection test to succeed. 9. Review `Setup Complete: Preparing to Initialise Server`, then select `Restart and Initialise Server`. 10. Read the final overwrite warning. Select `I Understand, Overwrite Server` only when this device is intentionally the source of truth and a current backup exists. diff --git a/docs/tips/hidden-file-sync.md b/docs/tips/hidden-file-sync.md index 95094370..e84570d9 100644 --- a/docs/tips/hidden-file-sync.md +++ b/docs/tips/hidden-file-sync.md @@ -47,7 +47,10 @@ A pattern containing only `snippets` does not admit the `.obsidian` parent, so t ![Hidden File Sync initialisation choices](../../images/hidden-file-sync/guide-hidden-file-enable.png) 2. Under `Enable Hidden File Sync`, select the initialisation direction chosen above. -3. Keep Obsidian open while the initial scan and synchronisation finish. +3. Keep Obsidian open while the initial scan and synchronisation finish. A progress Notice appears when preparation begins and remains visible while the initial scan is running. + + ![Hidden File Sync initial scan progress Notice](../../images/hidden-file-sync/guide-hidden-file-initial-scan-progress.png) + 4. Restart Obsidian when the completion Notice recommends it. 5. Confirm that the expected hidden files, and only those files, are present in the remote synchronisation state. diff --git a/images/couchdb-manual/guide-couchdb-manual-connection-details.png b/images/couchdb-manual/guide-couchdb-manual-connection-details.png new file mode 100644 index 0000000000000000000000000000000000000000..27a3e54ee8657b94d000eb0b7c150a2d1ee7f026 GIT binary patch literal 58625 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

    S0dZHMy!mO%ftDJM-uPI40ZyVaY|=XWEHJ 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

    -muj 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 Vv# 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 FoijlJ^iH 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 HR$>1OHD;kP literal 0 HcmV?d00001 diff --git a/images/couchdb-manual/guide-couchdb-manual-remote-selection.png b/images/couchdb-manual/guide-couchdb-manual-remote-selection.png new file mode 100644 index 0000000000000000000000000000000000000000..21c02829c10f7afc1161e74bdbb145d94885b90c GIT binary patch literal 51248 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 literal 0 HcmV?d00001 diff --git a/images/couchdb-manual/guide-couchdb-manual-server-requirements.png b/images/couchdb-manual/guide-couchdb-manual-server-requirements.png new file mode 100644 index 0000000000000000000000000000000000000000..a08541aabf868321eaaa36c04e27908a68051011 GIT binary patch literal 61972 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 DxHh)1 literal 0 HcmV?d00001 diff --git a/images/object-storage-setup/guide-object-storage-setup-missing-remote-configuration.png b/images/object-storage-setup/guide-object-storage-setup-missing-remote-configuration.png index 512b3b6a41d77e736323d917ed9cab6fcc6afb87..79f205fc77c685bb7f15896a45081e60afd4ea4b 100644 GIT binary patch delta 19958 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

    l*=~a(xee;n3l2@E?dvX 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 M$*H4DWK8}44<;qu`v3p{ delta 69101 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≥CeY9gVel5zq&LG0>2vXM=|KU>IL#Om#~{(?oSTN{f9YG=^bso+yO9Iz(-;aN+=l^w|7; zl6T)-+9hHZHO5YBr4pT0*Tfu!abl?b0Z@7c@oM;zFsC*R22{LXi~ z*l5%o1)=i^T-;Gy0QLW!@}R^bo|rQlj#1v6fXU_rvQpY*4kdXJ@ZP)@+%F?;jkFSm zE%6E2N8Tb$QbJ+7?^-KbETP1i(r+yL@!UYZG~idt1Qni5SDAXk(vX73z+XCnMV&wk zjlI{jgUolYhvVgk(6=CrCW%vz$m4Me!%Qt62~F@fI?yfXMhW-L)3g3Iv2l^9tut{E zTu*RQz5Dlnt(qr+PzcG`(fDHrLijbaQLjK}d<<9v6dp~!8rFxZ;d!C|3*9YyD%|Nw zOKJ@wl%w3{=x;Lz`d>ZerU~>0T>Zar%;k66Ni7;tJx8_0K?T){na_lwkOxHu#C&D9 za(IMZzLAHK2w2ZlmFt1#wb2ji*>Jg8B+I%Uj!-cC6#?haD$vcWP@-wX)$q`95bR9Z zFgrgRiovUQFKMJ50#loxf4t#FY~IAqDuX@J&H|{y=$TZ{J&?C|X88%jL6`mq)4p(B z7zB`oc)g3R#3-mK-=w_YZ$`kt;J;>cLV;Gib3RLb^Efb=rc14m>d%Yf{K~AZRoT!< zEDQenomLnc_5@Lll7kC3SJ#?RZ;}yIMqC?xm@UpjcwzW-h+0I^Nhj}g`<>T8iVi|l zVW;B_D9x(J-KippRPlagz;#bRux)ef>$(NubY8Cz<~7^}suiW1(gN z#5jT>045IP=^96FHFyiTYfj(hF;9MdGOk@tcL21tR0Z?z(i!@a@D~s^jUiLC^zI3H ziu^l=&hSgNNWDBY{A#UCb_TrhVUF%_lXL150-RBUMqkuyK zZv>)?_}%g@A9(kfz;p<`;Cx1Dnb2dg;M{n^zD~b`O#4yz+0jy%1V-gU1W=MTW}D=B z1phaOw4GPctjA(rI^%ctjGo`HjFk+*qiNf7?U#(M0TkIRU{Q||xB$6W#*-5C@@eSW zYkq&lHIsP~;#R>rX?tF0rOlL=dV7Kjh=O&1M0|dpAs$W2J9LS#RdgiggW->8%m5~V&>li!HbNi7tlMC} zKb@(UG=9y`wL2>&M5N)w6T*w||!WFQe9Odb8XC zhMx6DA~B@d5I3j759I+5jcbimDCy9#@evNreuP)x08u z_;}`jYrMf*Tr(=%CcKBu?Qypy!#yTZb;^9%a=#0&B_%m+8UehpAhmTEv;sY3*&*@c z^{_I7&rX0Wy01&@T~L=!dPD%yqDfuX#uEn(P`F#*bit49eBdX@~eoS`EoW*;N zQA#sCFsEeIj{n(Czvu>!aof##q+kpHz)QgT3!j*Uj@DP(we{u<7=><5 zC=<%IKizwGTd0Nkts~-R5}jTz`ymAAka%_KyoexbOM656Bs6*OnrxiN$6=mq1YD5_ z=ENy47{dXW-AbayTS^)Q6oPIL!A&{8+j5%g8fQ^QY6>n+1-ATfY^Rlt1U7bxj&5Gv zuuyl&E?GO2G-4yJ;envcl@DL)SI|1NJ%f9fzolbsso+H&{l(g|r&IoaWW9Gh)&Kwh zZybB?&2enmTlPL8D?-^T2^l5(*x4L=q_T>rj4~ow*)md5Hifb)Bg*f7UZ3y#uixd; ze=e7t^YnZ^p7;Che!E@onEijc{OE5GeWLR-{Ji>j%2)FpOs0l|2`+>h5SgfjesB8X zE9>xd+orix#RlpBVozwiT`3mw;4w(>=~pa(Rmk8<70lmi^%H1#(gieAc_n?t`nY4z zgP$^c-I)B&%r)`d(_Eeg;PE-%V9S^#u5$}OLbkkv;4*@c4D(>+MoJMM)AbLRT%r!m zuuU&mTk3cIH*aEM(4tuRL_)Iyk-naQk%LaPT|$S4wbcHEDuw~`!IEs)7oFjsx#1ZR z)@MhRGf#ZhX3)ix#fu$u(Yq|cj2OTsjXqN%M+ks5TChdhp#ed~Z8j1gB-LDjO6#ZD zvfyjFY_a?wFiOi%{$ zY4`_BT(&48{m6U1N#%8wRB(6xJ~^2jCPkb){p5OJ#F5xK9a;8-R>j5Lps!n!FPN@$ zo_~@koQB@p2d&4jmE?=MHhrd|j_PWZv{X45Wc!0dL|^b8OwT8n7?8XM<_UiOCYxm{ z=i;git6~@%{Q%*t=_832UBxFu9d}V*RxH-@y8H}(mF#`2hk0~8dlwfkbC(q6As;C% zS>rgP#U&*N7jo*;SQrG(+)sTugrAC_n}?X7s=+$)PL|zUdbDWF!#EoQvA8I=ay5@!A92^q zcPq)Nm?cRw`Lry9-b%=D8%bcMDQ9T56W4XA(F)_#bWcxpCm1-oEPpwGbFQ0lBSw?F zCTe%c2eE8sXv}_&8tX%6zA{q*&EV&%VG^`YYf2o6B|TX4^yvQ>u}eiUiZNXCo{;pE zFi9}H;hs%f_z{U8>;iQKzrXDb_NZ^?GEFq_hx*YAW*4CsE8 z5}uIemg}HDb#pW}7|wjTLRp(&Dzt>9q$D||CsDe5Spfy?8{e>S$bKfF9?|{F+G$AZ z{Q8UNgxD@iAHu%d-$^Zf2*N(O5z}&5Sn;3x+HPPYlPE8`>6`;BLA^H*S4? zA-`OHD$gyImnFy=wJFEe6P(AY(0A&ZY(w|a`d#BkF-}>%(RxFDzs-a7tL{eU10Fkj znGp?}iv{J%{PaC3D2#Fl-|`dN+Rf)=Y3!k2`IQ;jYt=A_aZa6u_orA$fEdGN19t2S zDGhPy%54ErVXvKMjI#8HKFE23C#D*2VYN(5{7W6GPEjL(DIXOH;o{- zKGrY7XT3%7f+Xr9`>kh3b$j%eqyoPgE0YfF+90H|_71G^Y+0sALbL~#1ueWO@XvMI z&7M8ekxe*esd4Je8;|>q((lL5pOy<|RbPC=KsaNAVaV2bRdAHF!Eo8-fiWr0ffvku zeV$p&pdJ~6WOGtxxSb+>(R;p~?ML^c6syB$82wo>Tb0X&RRh!(LzWjJOqyRV?&n@d zMKMy|1g&&?bi_I|18zP1!Wm@gCDJd_=(>*4Zyb^rDYHvW-%1OGMO7JHaeP6>ei4xy z#Ug|m_-&y>AiENJK8wE|`7nXk&vHpr)jx(qS@O4^c?a*EMi_@kgpl3p0&PjEvoElr zqD)!E7@L8xOQt~Q7mxBCa)pe6Tfvz8K0+~1q+JVj-noZ(%%uH_ocfn<-eLx?*{If& z|H9-_iYy?oy_d1&rF+jTX9`x##%>DV3u81-O8d;|li9t6#kZn>bXX!yIVVz5wNv_* zqMfH{ed#s$a|Ztz6!q-VvCZrMZKX)o_qfFMo?3c0VBlHw<%=bdRk6%*`v}k$+Ts|?A{Iq%Ywn|Ci(h~gp#3Xe; zd>2+yDZD)!BLtqK>27?9Ya_DHrhDXn$mGXJD~4n9(66jaNa4xwQi^;xqAIw`C_zU* z6X@Hs-ULFd1lI9U#1!Hfg%RV~954~P)!9td;-|XJ6mc&9<0@frvX)p?{ z{;=iZ+=92xPz~F@1tD|(7gkve;pGarn4Ufs80dR6^h>y^7h5n?0_(o<`tp6j)}B?M zN;Zxy8H-eiMJo(A(ozxg`Tr>Q95vaF~w!p+7yd-+GioW@nonKCntm)~2mQ zu;Jr19DK4GcX6H0qDP8T<;6hz>*U&oGO%YEkTvwxH=$y++%Br8A=K(+L-;2<_GyHf z?bwwA_#^M_ya|<#hIvC4i`9}U>*o*b62VQ7hEp>O{q>Nqmf-Nq**!^%3euAirEa&8 z-K!H9exXEuM;ZC!+b1{JK-VuYAD%Cc{_|&-zc^eEOq^J3oOQy*jEVV zXaj$SNcNbG3L!W*v#D$)=_V^v+RaC5eJ08;TQeV*UxGP$suEe9%23`nPljVri%4TD`YOk&uKJ{}# zf%W=+Y!VNky&}O}b|FU?)3vhQ{K$gt&!56>8p=uNvF9?};X7@C!g=crLE)*e*~8BI zLvuK)xPN2J%W>?ixF_2hA9&Qj9klhddA>p8lD}3qiTd|rPYA?;H3*vE4e&z%L~d<% zjG_`H=l&SY0e*^CCuC7MofB0B_v@<`yEthomSY5IFj;A#N9kZkN;<@xo&C!6J~P9H z(oF(}1QdDQ@MJ0QQ~ia~_+|d`QyuR~Ya^A*&Wkqk?GZ`=tBE;zSb`BSg-|GqvSihq zf+QSL0j}5CV>p~>mnk`wcCXN^;%|=P-|iPj8=$`u2}&%~+`Tc!35RNU*B)4b(U{SC z@>$NB@h2}5oP~jAk+X(eEFbQA`Kp{b&O^d?!gCk=R$u)8SOnEi3p&MNek1mNZ2|DGGI#p5`+ zG2Q4QhP|h9N8-jGkoc!nnlUhQFax91MFv8IFc{g&xbi+&qtqyO-ez6a{~B3!9juI$ zI!f;=9r%6HtVNfhWY@WAS!;te2bYoLYJokcdN`|Zw67TX2*XQX8E@mqP~;s$JFZMC zBsTiNEnSKxg=zoCMn64_8_dqFYsGv8^$IvSKfxsOr8N-q*zUqZ>Uw1TmCyL`(K;V@ z^_?fK#}ZS4b1A9u-T9P`Sr^}WeI~MM_jFcS6IRCf`EmmqM*J_zFjm-vTdo$hZtf3N zli`xlRE_25^gqs*bcz0^AKhLbk3KFlKo<3m-<=APSCxw4P>n$c$Jbl_tdVn0UT#wA-a1F zNz}d;N+_k;Tgr8d_SO95mv`U4Q$N{+K5)2m_2+YmbhUGR^oGKC94(u1X=BBT&%Bld5e*wb?q4oIldo*KVyp{6IWs}U!c$czo+EZh0&Z}0W>XlN&p)4{>DN6!=11xb# zQ_Qod<{0=ZOR38<(-tCwv5ZDDV4*||)5w2)eDZVz@ttb^)1bP7>dUP zPeS>kwKe~BZ^KX{_j9>K0mTx<6=U^hLrzF!H?8gS^A9iI(bV$veP&rb30AbYgua?= zVk6xv<2K$5Qr$N41B8Cy^J*0T1!llM>&*Xm*_O`Lja#gek&mL9ocqag-th?Je-%f7XUk$21m4ib<*ewRhAiLf2+l){y0vFSGrf z1PMX0%r0`$MgVq_EPK%@41Yh{&Y7Fe{*@fDp(^!HQ*j>^dJ)R--`Oe7K^T}KP2I15 z`HEMN-~EezWLcIA)h0YSMX15^S)Q-K4!EaHVDYQZm1$UkQsu_ST6(dBH8qL{FI;G5 z^T!A1`$NYKu8tIxuseMbgC``jHdgS(O1g&3pNw9Ab1}9=#<1#A&|>&=AfK(9#b`kI zY8YaoeAhqnrpGVBbAXAD?wQ){vjrtvpg8SlXEy4BKo9Ldj3|SS=A1tZE1q^_kh%a7 z`)?wlUtu7V?jhbBEmJf-^cUyb;q}*ACW_^K{?7F?uQ?Y@hYF$B?Z>vXMcEZeerW0D zO$ObPV6i4LR_9vw)xI!Z?)k-=ZU(D1HvYAIMT}`6sXa=H!fe}hra>Ph__W^yuauqy zoNx7~loK$N3rZ$oPSo$q;JHcC7f}8v2^n!F8NO*NeGwZMDn{<=3BiE67xY)K+pfw= zQDguY6UO%so=*{YN;(V-$j29p`}n{}+mw?Um(gTH{iu)abKLvBbS{p75edJEx}jAX z;P51>XQa|% z99+OY{~|TSRl+k5{4V3x73Degq*W^0gh4kzp_$Dn|GrF#SM$bi~)<29x*94D9T zsfMECHl0;T9c@IK!plNK;Lwew(0)%5in&L{ufT6i+3QjqMc4O4%R~YzzHB2QMCckw z@$8}e0EeCbtX}PQc&C2?^^xHbKL52kQ(xQms3!+Dw>H&`OigG^0%Ju3M{K22(G8EP zCZayEA*MMBGT+b}{oYw{e0O*4waw&(n7G@$)LM$iBr>Q^Rj#`edE&zFcExTW{?cIt zPMAG+3nHS_r&EqS;Yhk`j0V&Uz54ulAlK8$)6@>kW|O3XmM+_xPe@El65jl8zm!qt8QIs2=e+ zheBYT3j=xjI!BfWoY?-{VGiyk&^ou1&pjIeU+$KHgzi?03RT-h~b&4bP8GPZVgiNvA!cWz-TK- z%%2hW1~-%fi7?M!`yR|zxyU4e^+}fYAEt_iM|~D1P`>3e(Cf^6-^na_#%lrs?1elr zk>7Q1j=ymnlL`t!IsRif&3m?`9=^*+3V!BIf9u}6VBlATdHGt7O52?sj4y*y-w9IV zzX<=gb>{QgE|WN*)vq|y*%-c~se2)CCE}lOf+Muw;iC75bKW4qn>KipQl_@T^ol1y zMw&V8twNNcG?5B6Y7bF&LF%a;n)o-oExutPxaGG-0zxaW)@c4KnsyVyFk+>|xJ>?` zW5n+dgcxGV-CWNGHrZoUF41Lr65k<-zd5DPA&Zh_mnbK3=aonsCEYy?nT3e<6NlG- z_%c*uY#Rj9%yB%-T*=>%n8v-oabqR8sN=0L=Ejj?Pc{anG)=}`cDo=7Yf7med3pqM zg&}Jd$B?hN5QNG~)!*5if6Z~75+gB=$AX*KC(RAyO0!Soc#=B}11H({ zP>$&4)#>_TD0twDbX?^&NeQW?=kYpAXvs!~zw4;_kT0d)-s;SDa3}VlqnES4|R|U}6*6d$#~M zN$`FL@yz3mSL%Tl45L>u#jYCW=Rbvzb5p_Xj;j zr!*Yph#zO1=;(X{_Z_%1*mqK@XrI8`VSSy9K~Tz8F2^#{6$Juy>=RbjTRf3+scx}N z7-(X&wg$6>WTlSCeqMW-g{(oShkUV{ZjV^RdWe2Z$M` zp26s6V2V*~sg@m6y+p45S@fN{9b~3wdz2y+At6HXG6oHVSWNh2g&_o=hQU1lmA*sN zCFG0y<>&LWbPf_V1|r3{3&S&6GdRZXz`1dY@g>oOQYzJBCZb;H^!t|4 zIL(f`X5a%k3V2*V*Kp9eW36$hgr(#;G|Xf)>PWG5P{fK zm1XDE1~)?&SL#ONyeZ*Z)rteYy4Pr6t`S5a#@?v_3@-s;_?j%00rVH7F4J|(*$OZ} zMHRa!OUYW05vcSoI-|oG**N&Lh=vyl(h(mXD_jH;z@_$a}bEqATpR5r?

    %%)L3aVWx60Zh6^7HrLbM56);k_aF!bslw6IC!e30>O}yzeoAiuE_?yQMiwDpqrjCAe(Al81SbWkrdsHVZ z-<-fp*f^1u6Oc*pkSs&_R7TnfW8}s76>g>Y6>oZ%3A`p}OF5SxNB-)UncDula%J5g zEAkI1YDEni8_D;bqRg@%^0nMbx;I^1#im^-H6YwI_zPX1&u(MX@XYNynlV$Nv!q@i zZ1jw|CjUw2fS27%O*3YuK+3V2=iTQ0XTP<0h%EV=gF+`$TZ(?Qz?B3es@u`s> zWy!tQJ%=3)G`&D4*ud73O_?F4VMy8MVdaMx=j)p>!fwq%mSoj7#+|L|zhIDwX5>)- zK!==>mji(=MYC43K?ACzqc$dUNo}+1HLpc+wx8@-CW@Z&!(UN}ehb;+H^Cp*buV)g zb%g|UZ%f4dSTyp{pV6*C2ahb}54w8{9$whnN7%?k`Pc|L{`J1$9)Zrba+wgN7bBsI zRZ}=)6luD)+69?U^;>sELy-<{r0(6wa)i+PY(Jk1+TWbP)99&2FSv?8GZQI#j9~Lgwg&?dg3;aa$B^~lx%z&a3dkyl*{42227f=+b{Z?nbp;LLHu0|4aizj-h$?Yg2B6IFA@qA=&&Rl*tt zrOt<5!Vry%lw?s&=7+rLeFd@kgTc?w{H@Y$84o@Pd^C;HBb?S3MD11XZKlB~X^><^ zK#QDeDd0DnRb22hy?8o6JA_9WXcxmt;ZKJ9nllcYwA?5MAE;dBSeVbly!_Q|%#AE2 zx1W1Yl;9*ynKc96z~nak+%+~wt!Hngzu$`!zZk%Yk>^B+0{Ea&0ceUB03-D!Siki& zdYFEh>rW>qd%ck_?wc^WFu(zTR3e3*mgEm3`I3%)u`vH-*|_jXQj(L8f3x$`p4oE; zH3jX}c%9Gum>A-u^ZbsO49Io4-6$fBH5j;`-Gx5Gyvvg8zKL|yy+WoEP5ss~^JU^I z#jD_M2rv`l2~$4hU+G2juer8ce<);k&`@Fav|&`c6baK*J|Bz2?YS9CRyj}F6V5mm zVab8+`LIr<3EkRnU`6{-eM#ubx0?gjPW0S?1MV^owfg@u)0HsDJeT+Avd;hRM>K~E zJ4}Byyr-qS9iXOhbuUfgNGGmA@M#vmGSc~9XIM3eUCh;1LnH2JF)(Pz#L4tAoD)NR zOAD*Z@hy^;Ku}Ph4nE4?(eRUHvyfrcUCl(?F?t{T?k#{C%^83_Pt>Je>%DN?H?#5S zIff^w3gWJuD}~D%`aXrmq#Jb8*xh!hhoSt+ITMh%f49YR0=3C@Ov~8}Z&$xqGQ^j5 z-310AlU2#u17JA*@kRbUg6cz7fm8Zb(3!2MDD7K#8I(On5A2dMS3=pgtjSyNC)$@0 zwu+HNE$F0t=F`kB>!&+{PUj(JW%={%lz5n2aDLSb;ueVc?soHI#MUU$q{(ASosU1D zM%JSC>9`*v?M|F(k+5GDRkqCp3XvC#4UOHvPCUY81@5+7q*`Fr(Fl#JR1Of7vBUg zknTWEFf{%Nfwts8S{9jPnHJ=F9@rPSl>G1-;$}M1;&)@RMX*^!f1J8-1hg61WaGOK z(_H?vLjU>B^K71KC69R2OlBz2AJ8bY-$g)w^Jl4h1aW$4(w4tyTE0t~e@8d(1HYn$ z{|LEW7T;S8C4D_zaU>4&%5syjkO&BXf(zXqhA`M3AW(zw;`Y_ggGrX0joR2%^k7a2 z&Q2`DW+CbR_`i@d`TtM&6gbxh7RTR^_JuE{55!k!t)Tn@3loc>_Vg8zgPsyT)!q~L z=v`J>kJMin;RjW_e~nq8)6ER>=DI~)kobID6Z)YRp;1iD1@1bD1{A*I9-0A42P3dz zIt@d?CG!{V1k$Ilq(DLux0~Vt{6S<47=T$^ACGzVf0ien6-;Gn?OKV{;<*Kug0f{u zf_ZGsK3`7ghhBm=C%i;~KTpEm3v=w~$7%FVhzX+WS6+wSUY`N8cQ!5fjzf35NF3pD zJ_#%*aeD}I_`dULPE?Mr}eGJ7Q zjJ{SPh_`^eb78Dm>3tIT7t4oW%g6q|lbv$mPoNF}ZQvL~a46jg-W70ZahRp>_C#yB z97726iJZnfYZ4Sx48<&;th3-S`3H5`pMTPh^^p36;44m@10=PMC@=K(NtxQU=HLs4 z`Y1!h;Z39(fggkoy#t_WWH?#Bi5GeH_fCOThv7X|*jW>`gq?Kg*dck$34R$d%x~fd z9{zVW6%W4k6E%It?MlHNCb)8=VYWYV0?aOmdXnJn=~6J3bU;M5e*{^{EE+6T$OXOO zEsf}4IphpWvu^+ax;|$PRLie;Z+76JX@%O3G!;@OUyc`a8oS-cx5&O6VV9jl>>ooa;TN}ZR!Oxud=B`ENsL7tE=`qE(L?s#DU$+PF&s!E@TVYT&1y1)}*WUsSgn!dInk%V_z~R^`EYB zjaSoPPBD81Ks39~AX3LfO;qpI+{5pexToI4U(#LG6+dr(1gg1L=he*8=;uyh8^eET zBEDI{JM37gP+a*YK}|&)sW3bNCqeHXGK2Ac0Rng@yw=|Y0-K|neF(@zG>y;S*&c}& zcYBMhv8o|{!%IL&Oj&mkdo2rfbv8ASC&dmUsBa!ppp|?;Zc8u0o_~Rk{sjXT`FvSI z2j0R$CigtspUeNuH)A*SXzsb}g?-A-DggeoRq63LiytGwJbUqcm4eA9KF2WSO1U6& z{~0zGy6#4&16%6d^UBxT`ID^HogkI%X)#rJyp}!btV#Uwt9JX9>dfH*zMF#BQOtn2 zI{?v!|Kcov1UycHp*`yjMEejBZ#d(X)s8Q9Cw;vTwxiZ6G(Wc3h=BBC_QrESq-de? zs9F{(l}+pAcGYrnWD77s-BKDIH&>v+)W1OG%;$`^+8cQ@XOzRursJps0?6Ah7RL0u zvHHigjro~thUO?%bZsHBn%Nsl_{{++R7 z&XNhm`Pf3m!tN%KnJdlnZ;dYgd$C=N>E^FrAavZK7`oj0whVN0r8e&xX1*u*qOPM`R zOza`-PfpdeU?$)pJx7!Tke+UYDJ&3bc?Fi>0+c1~9hu04wR7tNiB^{Pvv*m`Ll0)I zr5#F#Wz(iV*21_{k4LN{NAw{OILR20gibx2usBqWiz3=9`D*lUwZFoIen0iSE>gsw z*t^S7?s*mKD%B@460_})&HDb$!*glY(ZrG3GSa_^$Qd6}$Ptw5>6AWn_LUO^X;_Zd zvGehB|C`U-w$GO-?%&=mYnw~XO7PX<_)tiItwNkTL{F2t?BgK zXzo~Hywv=DLSeLLL@Q(|zMOnQz_$0BAFXY-HIFAMNLgbuS?{onLQjyiBWhi12W9xD zV|lSxd*sR4_ z^t{H^KPrq5L+n^!CyiSdBQ07jZ?a665`!^7-7%PI z9#R}q_g^rW3Cw*8sNFw!fw!RTFd>E3&6lvB=1Tot63hbLwC-LR(a>wWqy@Anxm1kZ z+nP*5o1*cdgo-Maf>y|HVIBXaSy^(Em%m>ZE(yEa{~k%lpKNdnZ=@dbjbkT-k$bkX zu)zpnn(NsF(t?`4iCH`$e(fd{JRZSP^VcHM^2hu+dS`guy98JVf)9wxQC3xKq8jx) z1N>fk%VaaCfatuW8`m;c6O@KuU}%~PF+V9*g*j zrA|fFWQ6ol4qSV@wSu1}>tD>QpU+Fsz}qjnDB9{6r6)ebTss>@^s=qa<)Tj-Q^=>- zN;iH|mR9^9L<|Cc9MOsVV-e`aOBB=hH@>*62CGh+pHR88f&oG$1|vc_7Le(c!rsz< zyOHQp4+_<~J{zpzmX2!D!qAL;cJ6*${!obf_Tl-h!0mQs@mmU`_7S)z4f~!^Sx~c{!Xo}Q#REh;DO3} zIIWg3i8ZdurTv&{rK`P8(CF5lCU6|u?kPMVUd+SXaag*?GF!kNhUtlLsurmmpCoyebUegAdEh7~`LY#pD;>ut&sBM@V9+WJ9!@Z_R1QTsX0 zEGzohtwRc$ZGl2$I!4^#dk{?kH;2vH`=U6|4eL~xp_vBW` zg)e>84#FChElOKQtn_#F+pEwOGu83IPQE^Lhg_9WpSQ3jeYYF+;20G|%#y?2B=3!- zDA5h|UeTMT6uzGcQ-}Zymvw9HT!wCk3tz~@UA30Ah!_tHLCVlI;@DpLD=Kcs>Ud(Z z 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

    L@c(^$abPf=0u2wUiaRy9_PrxA>g}7{ub9 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

    l*=~a(xee;n3l2@E?dvX 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 M$*H4DWK8}44<;qu`v3p{ delta 69101 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≥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 z zAYWC8t>()9x(&Mp3At$qhlP998n)>0jN@(i8k*gR3#d0!ie2f6*pw;h@2AU!#ghc+ zc8A-P+sI!#5XF57iMcAwaevrKW>gFt5Q87anDd3X@N?&b_#OXfmfWqkF+iWX9f)4|sZuc&$P z)!4Ga5_Zq$kuY~$qQh^Pz1-SHec5j-h-dlLjNTLmt``?+Ro1o+oELRf1zz}FJX6e{ zE;xL<#et3ZVZ$eV|HI)^#;u++kT4!diko_T`J}CB9+Iu4bRT|Uz_Q50CjL~apNUNN z4I`T^wB=601NH1<{+YjO_PmvNv3vh`r-DDeJMX!?v2rV$H0!1AM&<OoniUZD3F}61 z@fL|bOG-Mrz>~~yL6>$hF1B%zw>0K!2_tc=g?>bv{8Jr*N-=Jl4hj**5qbj2{M9t3 zA3MWg1u?xg0{jr!;MZ}vYmACSX?rAJlEl=Ekt&-WWT`Q=O|Wvh=o3Y9)C!0%_PJ~% zu|$0RuoA&rP#K+YP^qF2JBYM>JZe3r68=~i@giDWpIY%%{2-xM^Ki%^QJhr#JmF#u z{tlt|T}c~mGXXW`@UL5?3b*Q#2^6tPR-Dwv6dX4d*ufrN{92q|48eD2VU=<5cml(o z-gYw@rIs31wl5g#r^;6msM`CRRAnAPGM*!!X@4WSoniTk{C*v+(f)Ps4%^u1&T_Zw z2t6s-bY!7MKlsP40Nu&FPQhv)WaqcMZu!eHs$+b>J?wv4K{`>w1(VbQ{g3n^eS984 ztk!aZvzn0>hz_v1nv=wiVl?S8(|GTuOi#aF=#7fhz44H|5SwW}%;+!-8kPS)ne37#-blE zaB~}SV+RJ8=uQ2265Uv}>AOEB%1JCVBU2x3*8If&Zr2G)4!dwGMn@l^&$mgi%ro60 z`Y=evXE~2<*@!3;{AeVzoKZINixE!V!O0*IyL;=3ZblHlV1rlUool#PV&@nvhw&%m zjCmT9D(BvgvF0yudY~R*Lt2~P{)YAjzgaa*`;^X)P+z5Z!`s^O``)$No}9~kbavfJ z(&!ND@lR+?^J7Y_WYFwZ`!w{jY(;HoG34`lxjo?>)-F=o3SYLFO|_qZV3~9ho8B{_ z$yN6P?Ok+s$LCIx?xjZJ1IIrM)4O+TGQAw>V@G{3jOSOhp4I6iy7`yOBBMxn4UAj} zoy=D49A3z8#Qyekj|qBDT0|1wLDbOGZ?IB>;6GICr&S<6`vz{AID2KCUdd+>kQhU%RTRqqu*(c9v6BLj{5!SE03M=sU|~dqPgELw?D`EPrfUqbL&t8 z2W7vD;RbQ&R`yN|7XnfcU`&j?L?eC$aZZ13J*#X#5!QV@%?^mQ!QhzTJJ%yYq|dht zNO~IbADC?=o>3M(oG*z1qOfsUCo+v&?YvIM$6B18k*WvOBd1ePldw$OOt{1}Jz5}^ z@num2boAR08iHF0)=PYaB}jaKmL8w{ZH6Y#Pqw-h0*xl3mvOny$(Zoa55e*`!EcX= zd`eO+j7wxEyQW%(R`Xt#KY=L_Ze<9Aps}B+{xYyKIg|t3V4^1FT_Ew(RD^3B{DE*r z_(d*t$Aim9QMA<5AK5PQ)R}sQ}%`I-006w=@XH1jdf9VIW>Y@Bo z_~OYSVPA8t|68Ssc`L2&5xms~kJ|Msi9mng-&|Si?XHXv!q6AGZdKU>6K%OZS!8y)migAd5Y|A3DStQ4tgeGz}cW%efNzLGcGf7IAkOZ@u~c zq(-8BR~u;dKm35TQb4JP0qGt>suOk2JrH=N_Xvj9=-O!U9kAf`eHef*`v&rhKuLp` z9zniCXti>Y4G`r3_cK#v&i8-mJ1yYnn8W=K^Vj=uhbYC|yAGVcBq44UB8x}HY*$qo z*vk%KC{cXQT4IG!AtfGm40UmDE9PWyKe~u1zTE6<-`qJm;dC;RJf(n!QM)!8;@=)w ztp9_7al_s#MYd!Bcd`GJ1yvs6aRa#herNEY2%2=F5jLJz|4Z(b{~^nOH9aT|5aMTG zeb$S1kVCio%+*)xV2ax7}0FgCilL3rh(^xP1!#)G>!n(i&`V*EA z2WV1v-O>49uCEo+8BhCg8=7#t%T)8>5MOcmLerF0SSqiWqBmSYP?o*~p|*ZQ{%)MD zpf%<{%vG{j?Ylv6QC#rYNZ{~I{5{9FqSp`lmWyTTQ@DA7K=2R3vzr*JEE>vcAeP-4 z>VgfE{A)>Mp$D5H{Yobz%MgvqeE`9hz8{AXISQAx?sPwbB^^9>vT)0^68bXE4>)Q2 z-*;tqKD~DhFJOMuo-_$kmm46_{evsB77W5o(@TizB}8MBohjX}*r@0;DN}~50wLJn z6do(tivg_|r+aS4^WaV_K26fq#D;lTL$ol!wpRX}?-&ncBLy;Ny1WB6z;1aDSPmd( zipw=mO}F@if%;CvpWQ9E=Y76}z}}>FMtfDK&Zu7Ua+?N*kBmV7m!F1=V}0>(^4l`8 zet3YlnhGKDS8ID_8ipV(a#LZ!Z3w{pw<174%NLXr-Q+~xM%cm?Hf8D79+j2VcSm_B zzoAEou2<@!*bPu=$^+t)o(e27-xs)$eS7Ws=!BM6woO<{({f9B-iy!-I=D+tQzf7><-e!9vO_BLly7G3xny?J~&lN~zppHuk0x{eA>X z3pAZ@{5zBrqqK5am3XGJt3xIM6KrrTS`ENNpEv@$pqB~tkh&~Lx0{?f=5nGP6ddCf z0g)h6!W{5!`(|}rZLe2TJXBeLzeF7Cs!jaEft?0HK=p?e(sZTD;hw5L<|_;tHia|F zjhC(}U1GNdcN1m8pqnxp-M62nT?T`e)A;gDw)DYJ!&SP@GU2ZAl^`q?>ke&pT z%64(taisx<{rF2_*X2wae$~x@5Pb1Z)P7|(fbx^NdSvs;?Qls-IM(7B0c_`tr<7u@ ztZHKf4%q?;&$~WoJB!OyKZZjnX)aU)^rE2y`|l!NPr)=j9oejbF4G|aTUVy6f(>H= zX1(+-IkVVwr4fcDoU}C%t{;%+S)R#KCbid*-;=)20h75;qlL2lO@rOI?K0#1UKtfH z-x9q6Q0OzqN zfgqb-S>#8WgWHbzq=bi!PmiK%C7MVGMXt?uW4><|mghPV-ICLwW3@rJUIIf!m3WUu z_up&xLj<3YZ^x0jt+q6u<`l*K8ctA8 z_l3E}%Q*Ga=IZzdfix^&^V+qr}7x&<)vC@^^sh+^xx{b^J~?j4q0y-KU!^Q z!MRWy$eZVaiZi)pzS%rNZfV;5SypJ4RM48R)79S37FVM*g|nPBTR#np(dS+jVJ4X9 zwCD~9*nl{oj+4O8B!84;npFfxZps$ppi4}Nn&$UR3ugMlQWtMaO&4L8Ee$OR3;r6D zUQNpzhpx7ko?Btkf|LUjbR4`(2>f_*ChbctB4W*T=OxIxvR_*DF&x25vkXz*WE|PU z?T8~^j1z}vcr`Kw6k_=9pq%>NKiM4uv_Slixc;>4m;+vqGRN@1XGOG8GPi_24g@<3 zx9b%Hfar{s{xmc|z3YqewEc-F`QLqOj|+)?0>z20MYOV)`BF2@n8*zB>YUdvev`;2 z7+{JbJnJ0I&=U{Hczw-1DR|Sz1{qFpdMka;p7T`zzVt?d(PS$i%Y zZKFGdjmUi2hcVOg5=?*8ZPb+S*XXz5wk2hBRPO8fqX)O<`6K#U`Ww{q`+|b*^_hid zG-OL%{!!z)UpEz&w1$swzWK|{GB~dv1S>+`C4buMec5Fn&$PY_)Y7|T=qD{?RLym- z;>NHNgyk_tD2_W3sCMf=4SaXWW^+>Z0G089yLVV0lf2GyyHh%Z+l`wI8P04iaZ$>I zK#neU7-v#qmRTD2|FeGO> zX~X5FaC5319toXceDi!j&Dg#DtCT~Ihh&mMFF|SiV4oEIzIGgo_XuqcauTa{fe%)< zjlCzuREPY0qvBTI{%IGP&f83QG0}S0%bcg1<9dVeaOi82l?`OA`UIhRLz1{BLH@1U zYp2|n|7%bi#OiOcv@?@+eifEZ#K973%x z^?zrL3@0l(%==%py4Tn=iJ|zi4o1p=HOU`T_n%-+%Q6BH#`TX-1~Q@8zEYW$SkCy?PTpqby8i6dDB6@t{uUH_Tjbk zboha_ZKNX$*~q?i_jz@&Bk|jPXM!0=f72`OqX=pAE)~AYer%a-y$ccMnXqrZ>6`v; zfIMHjb01-rV8XXUDagpE&yp>0_d=D%0gYdWv1gsvHU}zds_Ep3wivRfW$jiPO`;aL zptU9KUu1p2y>C~$UAXNo@`noyCu?thi$)0cCHwP~}NA@&bY2v7-Ewf(NTDrN> zJ`_Os%B0>neh!$kyap%%cKqj-T$>+@5vPoK z1R2^EPo_~zTO@ng2_IQ`k&VOD-xY24XgPL~apfYq^Bm>f3PVhLs)A5a`jFQ?cg}|_ zut92RrpBMd2I;{5Un*9X0m@8z=i~B6x%Hah7<_vZRpQc~-#=g=`_g8~+)e9_u#RzO z&RY>dKGSsVb|;>0TJk?D(O@Ec?bbt=x_grh)ay|2Wd}Q;I!VRV?V&zK?evmq#?4}M zoMZ342?)eUwU))G=sZ$PyB1($TiY>u3wyr*;N)L+5~J`J%5@?!24b>?wwtQUBi&(f zr8p$TW(-#cyh1)tF+ZBC-~eW@+uf|aMe~`GVkhQ!20NXXpg_HNVvn_nVaD^I!Zaw( z0~0myUeu2Sp3XE2gUt2}RNyz`EZ}4et+l6R#(URO?smrJ)4hPVM$i`jctk?@dsfFfQ4>-tBi3UX zAH!#`X)e5fK~6Lb2L88IS;q*vq$@V2BXa4oiaHPL!$j4mW47tHb0C6b&Eh%^Y@crS z#6D)y!*T1WYp!(3%2}`7G85T8QlF(nzx@q(geO)U(k^gBil2|nvXA~${_iJWLh?jb z0;;Xs%`hTU5`|*q;52?li65&Qn|+FL&ZpoS#qVV?iE2aAS<279BfpgS*Khymw3qwg za{Z60+pMw0do#dtGIc_zNShonFEm0U&Pu}_Cs=j!u%xQ0v#jNWQ_5kr=DZ)$fdY%4 z-a7Y`(!*Q~C1j{9?m|Po*F6U^)7BDHhyB!vY^!uErN7upcq_J{m^Ij7;DS z$Gyg$adxITU1M43X6dLSuhoktAew#p1HE|4`)QY#MEA6|Bd(S0oO|IcvQ)zVfX@4n zXI&{Frj9-7=1~&WE9hH3V`@C*Cj31+fhg1;x`X$I_(mKmPmDyX6ZRkx=xckXW@bQu z2dF8#iZYp<#PX>C2e6wakdF>aIq!N;0nY`!|3+({nL&n7vSDmzn5Y%W?a~3tMh-1W zNqSt}VXVL+4|N_Ty-yqc`L_{^ z_l3~eyaXINIcMcA4eK)Xn6A|!S}B!}j6`Yd0{3<11`5&+FH;n+#f4Jp{QF?Yi2oy9 zG1hXkX4<2P{leMzMz&$a(jfB z{&eR)jgxuVDh8mNWkcrI`i!Lgi#ZzgnO-<`R=OETqajs!SH{Lgk@wp_q04;)`w}pa zYmJ&ZIs*Nr?q+QZ=|_6>1t^ABAK?47XR;eIvF>cCYBfj+9_o~7MzAg zOeIu^3Af7UO}@szNf?{+=zRD5L)1~qs}(HR42Lntsksft4&9vjY}A_fvrlhOC*yId z2Gf2`G~k&(xyW1azJM_#VEL8nJ&rXh}hsn%;VEvcb z&n4I}i;jOUkANq}$N3ck=O2fdB5FUdn;A~$68Rf>%Fm`>-98sMk~w*e83G4}i!1BS zX8#8XDlSi=+*!uydQ`@zXd$-uJ@y-G0tKHd?eZcr(o*QU=p>#E{*}G2&*%j%Oq<4tY6#1a zpMB4o{B-03vTOirI3GZ9TZ&1O&Y6{qh_x$z2XB;5B#(L~2VI1K! zxY$A70r{sIBBOSHAXm`xKKMcKtKL}2--pn8{x5-0qwuC46gAj3Hm(O6ly_?hdKTWo zzV-+3{ER2&7=DxNhyB2iQ4fLVj}5mgdQJV@f<^nE?=?V;UaY}9rk65UH~e1xUxs0( zc)*rJ(i*pRm!4su{o^zHb87QJUwmfaHIE|~8vRxLvRP%0TUT%y-b0`{y>GjEgY!=v zYGfIFMfo#tyQSBci*I&dX>?_7e7lWfd|TC&%W#V!+*1z?FVg;wb>hFRa{Ifk*t_W~ z&AwEG7=C^IJGWi@KS8_*v;+^jXh>F072hnKmHz-H20fvwI}r*5k_i70&N z^P|l&6Wva{#rgpzb72?Wx2OuxVdbw+A_@Ukvkss0D`Aht%qH{@ffKSMbS?RG{ zy8QBeP;Y`j^8XNZT8tt0vK@cd%*`FT&im8!bpo6LO*a?*;evl!FUT4PAXM+-Xx*#l zeqMMPvIVg9ltf%`!O|Yu*6;M^9~Q7`JdqmV{E87RJ_qKJ$6a&WsB5EYRi*IWXGs&` z3CnBER$n%Tj4h6+12ijGHD`QZbtWkJbyl$%abZn-e;T| zZ;?Lmuso@G*0Aqn6oYwX1FSQk(O@wmQpOa$(x9GfHGu!Gfss!0`sWNqAgGyXQ<278 z5o*eQ84Q7#z}121OOo!DAOrXP=_}fq*mi-(2wq=t>if3-%ht`JT4H_WD%}JtjF|`E zIgIS2brBlM6F(= zl%}OxD9%x*ENknT$bQ#NJ=_X;#K-ZXMW!yBonNmAU75qRA;@0=X2t``E3uY%#67{4 zRpTmImRbh1Ey$59eLnV1=iW7eJ*FD}Zo%qYr?H2Oy^6Xs6V#vAUVd^g8nfQRdJ!)M zO|0&X>2oe$d_@csyl%XP1q6rN ziV6Q8RbL$zRonFoNOy%LInx{$=pxN37{#8+j|=K|hm8QYWn%Y>|IxRgZ8r9y#4>W(Wd zqf@xh@aAY%O=JOPhn+H}O<+lqFf6vjykxs=^Kt)jj9S$eJyM!`GHw%LjSi#4WF(jW z=K_`I)xcY&$vpMm!;nZpFC1+e_l5%LXm=CZ@E8=|qRqiI)td^}BvnuXC!3^&0QA9o z?)fE8+7^};j(C&V4-1vCFtaBm3_M2XD;P z5#Mgynqf*I${SLlsAS6OlGB-w8r%$l^EeMFZrJw28-dx+1LLj|7Ln3nPJ1JC6*IiU z--bsgK_HQa<%Fw97a2$W^iNJ6Vl~Dj(q$4T5@GA^FtmaEaMS4S{l0$Q0bkVWmagI< zZ-bwoD_F6lLx|KZ78l;p7VS`Z zKf7%}7V;g5@$XP>GE*(#`n?k-3Bcm191@bFZa&PSyZQ*cH|93JMHP~v#`l_BQ3$cI zFhu|;qg!UmxoNvN9g)?9znDU=14p9L+k-Xa48qvxlGH#Ck)T~9&Ib**3&?rgx!DRI zE9NrD?Lp%q;kRdZ&}wL+gT92}!l7eBB;H%|e&B1~q^tQ*@@{P%qcRGcBP>h(Q#6i^ zLAnsSDJ}I#q$r^`FA7BvcJ?X6%<=wBAfbKyHL9Wu)hloZ0yR2#o_Ra7uUG8U3Eq}7|Xgd{i09m*I{n4K9 z@%-!{!_)=on%~vLAcmP;4*F#mCl8|wUL_mq8wzhw#&3Zxes8iJuu`?pw(tT+*BiJK zv87H@><0xC#j2mMaK|pF)OVOTD1;u?i4MI4IlQo-q0n@mXj}utq5^kBcD!+oH3}QK~7rtqW)gy4-^s+(>PrD_;oV0&D84XJ{CQhLh=xh9Aa|?hk+lLYpiB`X=^-a zP&Aj&0`cLcXNqu2tLx1f9?spt4C=C8)EPK#!SHjvO;)gg{TwrdwEKLci1iE(CSQTt*tR0GTPFokGTdO0v0( zM8DBz-w~IX%1aZ^r>LQM$N>n=NYxZy@JtP0mJDYrcwKw*L4y8Uw+s<<4>shQ+;P8c z{E3Mfd(0Dxbzrc1c?5`gJ4~HSFlCG@34N{(HnWtjne;pUx12l%_t;O+R^Q-_3gi2z zstnE4(3zCK{^%!)SM+jQi7-P*r#8fwzN9wBuLc1Wtk%FYAESqwtc)%IjC6+)z(^ec zKQXA^pFx>I6=BCelL5-dfx8EWx8ypECPirp?q=(diBMrCl3e`ZZADhtT$Dv(TS)YC z%AFs4x|CtHSI@uodzofd&=mcq8}Omgf79W+S5b^wZsI^EwedxP?mj;i{1M3oXcH=w zuXD!QvpmiIrlsCBpl5u%crQa+VAHL>^1ZWV?#lVPsa^%O!WA4-Y%zeU{B zCB$7Yed=bTlUsYfi-{LMclhI-PMi&OC6mT;3E?Kg$e;@VE^IMioMaWU3JPF3fFo-Q z^5);To~&hn;$Qq1w|`(E5pSpe&}DaJ23fT)oAXN#*5$W?EN>>83G^(bzt6&E2e);I zL|k8uxwC=?>h6?Rm{*n;=q640NjHxM*BnH~N`Aj|e5)|4+-(YmQ5A+LRy3aUUm#GMEybRK0Yms@S+09b+#_fq0okt=y6PBJ?rZGscgto)tTfi^;%^s!?1qzh7k)9QIcK!J=;gG47uI;0>wz z@04w6kTg6ZXI$Xd?9ZAg1Yi9obIlfU4VedtG_{9oc^~XPNJXo>6l4$WOrjP_xXm zdtTX9^3M(VZM(`H@;>sSW?9H#a@%m*0)#z;u=Cpnw?J{G?l?MX<&(%tGTs7Zcrv~T z*EE)t(v0IJ$@F8?l=vRofD@mvC}#L)%&UZ_U3uxFP^K5LFVq{WrbDOG=6 zvx!xoo51E-Lt8;(rfLdOn)^qC*1Lq$2sG%)L#Th0Z7I^1f;u!&ZpEM6AtAdvyYQmjkjF zJJs_(D&%-l&sussXR}jHc!|~dr!_!GtQPN9udspQ!_b+Fcg1t0j^H&cC$IL!kXdP& z#d>Tvq0WE0{I_X6hjMU4@Mhr!{zkG`YYj!lYM&1NhYREfUvXFT$Ep0Gg z@>tf4I4SZ|T~)Qv!7mpgX=Es~@-`3U)+fIFhyIBLCi%;laZ3hWPj)RPww=7M*7XQo znxT>yl~o88m@aSRkW{WdqNvhJF{r=y8Rr4dH0c=ge&-5gnjm+bMDV+$$xA2wM5gGD zWFEHNYU5(rqHOv-Sj!WLb@K@K)fU3F*xGk7kRYVM`>ZasWJa^(hNQa@Gd6D5sa@MmH6g0kYUn{qmaQ${S;n)brOo*gH!-oT_v8@p^(VWo|Y_- zJE*b0BxSGs9otuNFSoLo?K$q-ssOO3Jq}vtu-f@(9mIh)1TC!&NeN=^-_a}CVbRZ~ z>5V_C%rms1)rtSR;t0{XpH?(&Y&9~jF97=fYIX#Z!}@*i;_c~#KeS>B(PR}01eGWu zW}2U@ZSNX9k15v)GKdphBGqsP#se8yXwb!aaEvE^w6$VNlB|A$$7$ZW5E*2@93(C4 z9{_nB=F0CCsm!fLkV?-$EP^jFxH`6OfaliBiy z`JJaLhTIj?A>Y`l)-;fc+bOah#j*V%Xz2RE)k(Kq zk<#}1$0Y6q|5gqu{@S>h;LOYfP5@T*J*T-A%qE8d8gx}3VLEOKIB}m_a+@MzEb2mE zk3&t8F1Q{W+E>dCYM78`&+>4x<^g?tF|}hiyN;+19r)!P^YRP@0K!a=TLEGDe=bCT z-J_pLoFi1%8MdylaGv(3w0ewvx~XVW8%B>)8^+G%pQK6f<#D1?r-(dik7XX?rbWQ6 zvR1fDU$R@uN6B-}C4fLu`O*T55WU_EUr1sMXxwjc246nO3Tt0|iE`cHV{ zLBohn%u?AUE4lhY-Cymka;lreT&~rd6N3M7S}LhuJe@)Z^8zZkmOr#JR7%i@ZNJy4 z+GCptan-K9Q50MpLI6Ll9JlX`m_Gw`Ur96?6n;1V7=}(*D(I0XzQNn00D+<20}Qa< z#14}wOm_CJycM=&84&|G^d+!CvTKj)YLhCLxuDoTtivSGHEdx&fJMPdhiF%gaHEMEw@q)3Q|~8 z_V9VBYZ&z8B<)qBVuM4~Imo1UIsvmj03DTE2ShaiYFFy>bRQg`9?%O3nH?1XM?-1u zx5g)yQ(}5lp8k+$O}G)xL}2SQU4deVzx!XPy1|AD+l@-_u+?O}pl$~0v*@|;09huP zW%hxo>=+QTDn}4FYdEh#o@82qZjDOzb3L8^CBx&d@-x`^ypO|s%a(lJY(Gq!{|_A2 zTEqd6wlhy=4SW4h;|yO&mpSAsk>B-K0x7AFox5a?zlVb69@MjqYp-)& zghYTwvydaA4oFZR71#z#eWu%Cm`}eFpAm`dn}Xw#bT#%EQ@nNO<))2rGZD5cgkqv9 zvdghf8eW-GDhNP1#RE_>A=}Ye>h8RE(i1ck_jO|ag0NRL$|>o=B=ecy0J9IZpv4&= z`wckZpUg{5^f@#lL~|YW!)iws&T3uvyn2I7V>44Tz_~~p-&)kG)BzUc5P=83X3A6m zlf)xZ;KF_iXe8=v7jKi^2oZu7%d9&v4v;>?2zc*r>&yC7Llu~S+}B6cBbaA7+=Kx&FyS}@7dW}V%r2N|O{=Uh zeZW{4ksxslO2nUE7S&0y`>=<*lVKi*-~vs%hdO(4QAfs(%^JCxfYbrYaBmQSt_3>c=e0J}R6vK8%;YhkPF1uw zXbM?F2m0MXpi=N$W+{xS@b%lqSPJ_ZlQcMMSZ;Zu7y07_5uu9~4e)o@0u<&P4B(|Y5VuUv_2M#I9Z$QdSe`gE2YBlMe1~nn9 zsHAJA36QB%=I3FE3bS)(3V~;cngeeZu?aAY9_bcIueby!U}rYO{PQr?-nqxmOk6>6 zo#+`_8WZa7=v#p^DjXBO<~m%EGXn*eH{r%ECTdfUGX|YjNlP<-d_bav?{pO9Mizfa zsHAFINdlQ-_%ZA$8Knv5kv(u9M7r48mNb##p?p;&BsvlaA+U>8HmLnO#>(d(N;&A_ zCfL5$9687?S)V#$Ju&z!w&Ue!yKX3A$FEQQL@AByntpN`LXm?&Jv1f^`I&J|fS1Iu zNP#^@<7L-WNlG(Ply0fz(@|r=y>DB@r^H7VqwV>Z@6HYv91H5d9BVx+I3Ij&LSBP~ z%HlLp1Q1UdOz3B8Ho-a!jCF-)z#A#YrNP64qm>@|sJkNNYSCh*iWcwCNxzjPtQunK ziS;4ET1^-@omV|JzECnu70O@HNJ`_jp0)$nY<70mxNUCLcga(bsHgJQ>QjkEO?7qa zUsRgO#ESHUgT^_Jet=iD)GMV06>p);m&hnis@iqFbuRQf;x2-B=_eIZ>YhQw(7C>A z#eHKoqIx)Pqof@M!3_V?`^3;eQz`F3$XHg;Ur!wj=jHsOE)^yZnv?EEWLDD0nMbZR^%gPEKeWz<648v~ z!{qx!O_hj`_)N}F2cb(RQL|cS{KPcG>-N+n1FM!m zs~wY|$r$Y+x=)jn61*Y(7SV&=k=J|hQVmFdScesE`e6v)^5iZm$ghC>iFHIfH!Fe~ z$(-h9otHTQYch_N>hJ zNq_WHMsAcT)5gL|0;#vEY4tzOZ}%nW@Q@?w36t#pg2ARO%-gISo$cE;1_c-wjm)!F z2Dgw}$gej-Y=L5}ZjPqF*Lv3IOCX_zxJy#`I?r~Z{@!E;J$F2J-jgrG6P$2GWq(>`+|Q_us%wqY(YjG&PL<2)bAGx=D{mXLx5g)Wi=3 zjt}JVd>#;25rmym6t1q?t$d*1MNt}vB)I-Lmi!VfxHoW=}pZassf_E=gVICF2 z#x(CGnjmwXq3|Rlh><9U0wOmb*f4PVEN zq}pJer8wweETbdecZ;eYPhY)95y-w&T;{->PjpeE6SjS;g)gDyo;C2ApgFzW>`!eP zdEgSa!I;5bfx=$(p2?W(KK&|%miZK9^quAoVE4};ocV=stb|4U!O@b4(=B4}O-{B6 zdoKJyGMbI8cRLjld_JsVjNH=VW=z|uZffShqV|YB>22T+c7J7Yd^7m~)qjjxN9PwK zirA9#SjZb0=fDdnAsL8qoUENtct5+5U`Ns#k4tbcOkaJiyCM#cm=-Iq|DM4@drK2e zM-&h8!X6?aBfs6+mRuooyLVJjM_1eLn{W)Tvme_(h%g;P2#SAKXhcA4cQz_)F8M`B zsFtK=)!TPb*S)D+Qd+H1wb0|1Gu!3=d5PRrW8*qzq`HVYuG!6{q*P;8uJN1jbi-PuWLi?T0G z#ieg0aZPK4z)(7VdDWP1UE1tmuf&-8<`SP$xzNy`D_2JzG(Ocv#eH3ArC3OKM5&|* zl9H+`o1{$t5|>N>yJ;tGjx$m#ACO2HY&XPzlRcR18vkbm)bkS?vzo?S6Owkcq63e` zBfhn`uE#Nor9S%coVT|CUNYkO9NJaM@&t!YS-Uv|Tyd2ia_oJ=IkzP}6MtaNv8?xn z<&yAY$S4DTLq0j9 zUe&ZX+g@X2gNMF;id(*)eI!M6LDhP-)lL8&Qs7vP2DU2|sYM4iB47{D1BS^?bI9pGxNMB>n>oKj1LoWr* z;ithk5d`*Z?=i|mn5^7C;Fw1{b{fBTnFIuBpUWM8x*D1yr@?ncy1TUBgw05a%FJ!z z8lj!th${DD@!(>^PlGH-vr-;BUu6+x8I?0|MZ8Fvxs6X+w*XVMZ9zY=7?pze0xaLN zsb!>3B&xpHxZ}RS$9hi8Y47jyf*{jV%_Ah2Uk01+PZ8gI&WWC-%WmfC;~!?ywh=Uz zVLa)L!b^%(ym_(kW=J$zNkl0{XA+yo$ve1@JIC0>u(NKC{!T_-@*;~`v|0gh^WI)$ zF1vsU*!Jx|%l=gdqZMrudZ8dbw~|fB3CxM2KRlSRV>g_H^;}ZN^LfxQC>D4Fpk$2R zX-xLCB0#Awd%rgWZW^~`ZXU^zmsawZLI`C1?Nyac4@weO2#)F`d8BmHh6xd!X=Xhg zc5cyajL$nK3&fZ#=^lk6zKrlTBy#tbX8WXkP-7hRu5`;+_bBMSP(B7OzaBWo3KHY& zkBHdcx5;#SpjR?yZS|SS^?)BtbrhPUr^)vjwwCvst_^3#AO-?mGIq@9Tt=~xGt_@s zcEJ{*FM%q6LXx+@nKizUj2+X$pY?JHcYipiyg`LJeAauN$wa=%X+BM4L2i?%ACiuK zVl&CF(+`u^|JuLdGDSzkk~&XQ`GWTjOMCA2c8 z`^YLUIG6HpQSSv`i+(JX(kuM;<-k4;Flx5EC}-!s%<#|Qi6M64A0}+!&s=g4!)L{_ z8YQa>Y17twm>VXR%q6R&K%bn?)ju@7uX-eq8k1u7BTaU%TtVL*k60aR8m97LKPNRk z8_c{()%*b38Ut!&JwoKw|NLr$W26>S#G|2ANT{wytsG6T=hJU8q+0`ejikFMmAnjb zF#|4EVHV%-!KBQ$2HmcgsvA=)?{1i=TeEmsEY4_U1sD$Y-57$u4kGDfkH4xYtl)IxHeoo#K_rDR70vw5H!0apZrgsT8eJz%abG42->h^ykPXCpm|G`7bdszzEFno`{P>g{8V^T)=32vI8$iG63YfXh^mnv-tG5z&e^=_(5y#;0a9LYLx} zk0{p^Lob*;O$k_A`qiUS($hf=mXRe?BM}+a|9^kl)JUbEgg~#OhdzN= zsw@m$=g~iG#JA3HU^XE*WVx_9Nt6uDhay&@)tcU3CG*Ce0t`>bn};tT=n7Ov{SQ** zCLo8OOj?LaMb=hXFRfVnnZ8{}3+FqtDZk>G$qat`0?&otvcizRy?i~4V7UC|gW!w@+yu#&b2$O9K` zC z;OvLOfWVPVVSp`OiZJn;HHQKsdSlKfWl; zn=d@D(&5(4isvcjiAfRGbdoJ;Q#SDDizx~VU03{|c;m7f4hyAng%5oH&wnnWE(78J zFA9rK4LD&zt=6*klhO;_rtF=?R$yRYa9;;`+>PdsDCYIiyx+2>VAp%Rp{`$ox>gq4^16020PSC%@!Xov+8dMFmOd)@n0-@JZ=y_xwL`lAyVVLB4(5ws@%wxc zFZE8!8+;gBAy03$(Hx%+m zv()XoZ9%*cSK21|3}$bpRx0g?=#qElU?>_QVSkOPT-i#J4*48bD1?)ImLYtoCNhmk zT=dM~0~}mx$`!)Gxwx!N54XReK#Txflr7EqhTNqjv{8PXz&Q5-?<~sJtPTWf(5I$= z32lC zY2LY0zdb!{aB;jsroOxQ=B}Xn_9+nN@vL%-Om67fv#l)Iz!p2Q2heVeLuc;VM{^gg zf4MlJdA=iw`qIU7nbgX>2f@$Hma*pFd4pIe6{B{)g2M8Xw$vZ+!MtBdkeYw@8n8uE zv{Bm{^dk%4g$3jqn|~E;`0$DDhEao0kE@`7R=~2O4U*|0qMNgY-83N#o;`?{%E_ds zX@Xwt>VbtJ=Cg;I)ZtKE+3@I_|8JDQB@w(UZrJqVe|S*`iV3!9xcvnzOEjQTBHnET z_3B02>0^L&c`)p*TXxYD2Ka1s!3~@i^f}LGk)c9k@vkNDaWGbx0VW9t62M-)*pZJL zpS<*kuem(G`|`=FxTgY3a{rU*q8E(L1!O^;d5dH=p3kBG}k zJQ-9k#oLdRGac6d6QY56kVLQWM)H}D6<0J(n-%C*pLEQ2P^JyuOgVzqXsg*AeCtF< zcukAMLqcjTjMg9Wv9qIJJ@EJ@{4}W#9b;H^C_Gg2^wa3q<1RihRF~ewgaRI;I18f94uvNLw$V1 zHMR$C-*d367(q@qyy{>bM$8XvVZ~m;3H$t^ZYMV+JOf!jP+*>#>Fay7c4}7g_Vlq? zS`q4=OS%Qo;fE6S)J)uz17`U|!jks+hj9Fk5Kd*r4FdKr=1n`Xf*pf}g+CcoDV*z;fjZfOI+NlNp7Hw^6x~2-yLcyUnx~NTN@%3HmtXOb zpPUtmucc8+-0g#yF@!p-1Zsw}awuAjZ=mS$g2N$At1q)?^8Ow#=Z>DMUMOZgM0F9)aUu|y3qjR6>FNDiE2xA)Oa}@v_7zT< z&4`2){9rd^Ke`Hj+SD=pQ$eQF0f9mCtoCo+a(Y%ChY|UE_k#>NneK$wfe2BFc=9bE z^>GG|Im1r@=Z_QZF^TUNR1ez4uMW{OoeIM>(!*WU;qDJ$m*9CO$ zOp~l*{ehUvCSf;?`cjHCB*f?aAu2ob>)FtzUAWNO1qRMzG%}05R!+$!M>A9FNOs8k zW-hF|^@87hLdB%C!i?QYR*=*u)Vz>U8N(V?py7Y}1KG@NF<+h}Mnr?|Q)*2dP_rX5 zgmqpUzx@EE2yqw}B@yg7uHK6EKdbJlJ~`(8`qreH{gVA^MP|R{gPE!qZtG;;#J3_w zOBBbw2Z@p;h;Lozpd7!3oFW!gqDBZP2aGTI+71>%8|6R*cB|*lWlQn7zRDi3PGq6d zxArSY`SalhUzEt|L_Mj zlIK%Bj`>4~Wc!r(VX)5U3!Wf0pG8752UVzCJ57pDnebP)qdwbpVt?@&#p_y60_di* zPc{}#^rqyH6>icjE5|bYj;}Y~C807EnsnoMiL9ZO;cVy{Rk#wTp0+ow=_bPn#74d5 zem6-rTfomm-RxG=BXmXBXXhqoxS75_q18<0T`}j5^L6n0x9iMnP z6Jf^;HD)pU{MuOuAbl^B=w8MvEGKtOe%zp$kH2D<^IY}9wNF zP7kucpuZe`GFnKs5+%l9Y=JDRC~hs*(O5A`a*J}FOUfUdO0`ZLykY+ag>O3cO>7+6 z7_$;VXynl`oiZKbpRmS`UhhybD)ZiQ4sLJpCwhqjye0e!92ZoG+W}9b!w2E>ic|Ur z=ZZG0w{WJ2y{~D*K8=aP@t4}*iY-3ty$j^=`sm5eh{GZmrzkIM#ly0v0o6Sa1hkYA zIY+IVP^#cQ@X6j1iR4$4>?}&V!q`*Ky5ujq^ZwQ#Lpo#;^1_k8t!Ijk7mtOy^DQ18)a#)CX=o?*6@$^f!aBR`;#c zQ&73CDk0SOe5sr5^u;edaVR6La0m&c=xDnQM;;3Bpa4ifEk zVgquKKP|GY#%4^B^+lrbwq5k9`TwX}Awz^>ck3>G^?kpCAkZNO=f~s%}7j)Cv`_e zL{Itw6?UR_YPC1sQ*=ME)f#y+$D}eVWei)13SN;A`&*RSJhNKcs6;#5^i|KI6l+w* zZyR38Ln+adND&NAUVDbBQsW^}{6HEwc;V^8pGsaka^HwditW9}4aWYIT78jBx24eM zU$h=ZZ+t%*WC%-O5_%weeqhkxD!h@!6Xukh+574b4p;)LSu&xnR-0{fTnFDEKqOta zTqN!}U4;d|%?ftL0$r6;^^43rS;C=Z!G#$^a@2i=G_tQ*{x+|lN*ht=oxkjkyV{*B z6oLxpz!^yUE>%iEsJNQ;;PN9bq zA^4MD>9(07eV+VjV$Dg`{w{=~Y(mQz7V3+v`WneaQ$;&0t>mArYv*oVP-ciGsP|Q3 zRA;~m`L&%jE*lQys5x5?g=ZlHLy&E4Q`R)_HGbxfR)!Ex%L%u0+(l}^ClRmlsPay5 zx?s1?NSN{Z?p9z&W0>@wsdljitzr@$PzC$G5h#fmlxi)ia16D%4uyW!!8<5E)|v|& zTzx7(8$zA%&cY!~@Sdc$TK&@Z_$NdCW!Erng$5<;x0=bdFVI`A6YHw}Quge3B_+q!V}7&17q5SQeo&5w zWxiE*;nMiD^G9vLQnp#DeJl+NAVCacxMEzk}i4w$$E-wf9 zUcx<`fGI}Fo_zLx@^9`1iRE&_nVB(SRs5pw3*B$Xl1WX+G0dWV z2g5+AR{E4J(e}Q_$D1g%+q7>t5m`Gsl_(hCg>ZCpHw&zfsu!!sJ@!V(NR&RG5 zsc~s{v=bSE&3kODNMb*;YuuZvHADDG+dJkmb8!)~P~ML;Q~5aY+eBdtk0eRZrIyak zN(p+v*~Vzu&#T8F3pMPp?`MfDKK`%G#Xm&&-xS`~w95x8F@x#H%HMbSiRU_aMRgma zvDG-~9Edj4U?ox!O6ew27o4 zCEi_g6qP@LT<8o&!*?;MHJHMioqBx-ckcm-vN?_K&J@d2I`36qN*^yKU zGQy~>9Wb9LmP^}i;pTU3dm5xsd?LIPX(sAhO-VVzw*7+%oay{6*zZQqd0(e};Lv)& zRe5VJDnfJ^++l)rY&xc?kK0ElnPmect{So=sz}cM$;$mwkf0z~!F z^I%f+Y_>p}Rr23nwb|opouy)oV|G%!J1V!WJ`9qXx5<05%V$;+GkA*|E8mvNE)uHE zjQnvL^j>)%^L+sZMNDoc+25|Y9Tdu!b;U(inNiybmA< z<{a%^c0C-9=0EWK+#?ySB@rK!-z&Y5W_jzmGVx%OACAX^PL5Eb;M=>xJAf8=Ls$0==TN&FwL?yl_GhVaAE zoJwTllYGc6M(EYgzix358rgOYqxWNhoLnQz6N9ufq7+h&sf!V1NtI5{Zt~s{LI;s+ zs>Fvl2cPBtKScJ1T^V32Oo6E#?ewVHZwSy3YdoXnK7&%96robsJh~Pel62rzTcjh) zA`YAS`8MkE;kQvbij?C4o@OOdDhJJfOjL}=V{guGnk8sG^2jWif1jFodwTy&;X-@- z2*eSabM-_5;C3v*I<&1u7gel$`7-hoK=6ot4PnlAk}U+a9&~wkmrzMP$hC_+fJp^T zr0fO?LBUZ9y@4u{5z)Ih#)Qq~E6sJB#>K7!O)t$I8BP zdH0Xl!)dL>t2U4vW>)$B!e27IF_Zi^lxj_YvGC8=8Hj^XdGPp2cFL8~+O)?0Az@1h zr!Ea#z}IFBA4HwLf~DRMaMDIlE&<37+NhVO0x^^pY}b7N1(~Y6e+ofTjX#sGxBMvj z_Ux2IRAZ9m6j`tc(QYMAeonngHD?=rR;7Yt2Rt$)cb8kf?>Oz~_w!nT@~vkOMLPB7 z?l!Fab%F)}NjU=$xZ3WGb;sk&M-J0^Mq1S3<;Br^*i7324yg;7Q8qTF6$hz3UzbV$ z;Q57t%Mi#!tfcj`Kuz{lJQh!DpXr&+lS(Eyw?L?)^9cPIy?PeEukaZn0c${{BEppN zWy1I=ET;$80yWFrIEJvV3RySA@lyqfxeEpU^IU2BYnoCl$}sQO)J`CV0+i;zc=rdu z!Di|9g%mRzL8o5wO`=M0k!0pcy4C&%>;eIBl)MupI#ntP90~Fikl^uKS-2J z7T1VC`eohfn^TaG1taU~%Qu2;J6OsScE&`S%*_0P zL}d7L_HAR){Qa8Lu z8CBCiW(r?HgulJL?X@{G(eYz0QVKo*8iwper1wWe%US*2H+i0-K?OezbdT$;Ng0!a zeF)LgdIO3d(~C8Y6?}Yke7-BzmMJP^29CJ+_hY61f%>c*c{L;DX8{_&S?4(FwAB)} zaTv>g^sWSD)DE7hPCu?`q1G(z7TrR}P|f=n?pibbh}6WTvX?FraF#8*;s3Y4{u> z$DW4ZFuAcJHQP%bzR>7(DZ$Pu$;BCE0#^cR<0oIDws-3Oy*10dhl7=HC8D|^GgDX$ z)3PMq%I1*cSSf`)sL5neUJ_V3VB+5c!oSzC2&sSb;DyX|*&5(fm;^!2G>tdwLq?L` z+@98ZZN9?mWS7fiLwt9>N)6s6rwRx^v9rIfLba7qem$MG^o9-AM4bKVMBOTC)y{95 zwm+kt4gIahY4{0VBY{(kU|Gvuv-O))w3|k&(4UEa{I;8>VTE?axD|K*rLIwn^fZ#i zF~5nb?6f@wX^+JER*Hlbq+Y-qkZ>#3E#T(rC}?S*P26auZHn?+2rhf}OD3P}jUB3| zD=8v;Kb(Iu{9d=2)XtSaC9i8{g@^#|IaEr`#Mon z)-#W9*T1)kbUA7suO#>?bIje1Vz*xFv`r@Ty!xhJ;sSdb7qxeH@A-IY-9w0-bk92F zWAXSZAvrV7IwEt6dlj2VVp;YMQTj_+laxKHJeQ&8=x^yrk%Xnn-xP6Jf2gD4%1KQ1%X02RaZP7NA^QiN%_F;U7c`Ylt~>c;W! ze>7+HxP&QxoffYndNu=FC^_Cgk@pBM5c0 z)cHhc*P`Sex4-^G+Rz8D#lTxnxWMfIuSbEDC%qfB;MJ2>n$;`auDL^R@GFtZirKg% zrL8C5)>Bl5e{N8MC^%b``%(2)c9?qSEagQLhoYM~{!-`cNkpi1Gp{JR4^g&F;smH0DAF}IC;s|Wm z_=MG>KYwSm{?Kd?ZF{}hLy^tj#raSzx?E58Xw776_&t!b9Qu*GkuUCqQ*e6YVTq>O zDC4tZCUY%+owfV?`9hv@Z*l{3drNHORrJ#*rl_!F!w8!|_mShe!MycZuEPea*LVT< z;%pXpx#u~L)1vt~3~h3@Q_JR`mdwlWbGA}-s2jgcI%#MhpgzTjr0I&h=$E2CQtRlW zZ*C2F@UtUBb#XW8k#HsYS{3;Jdd0(7>1gqSa_0?_?+rnR8qSg7i!sjV)_L*oZ2nWz z?@@mc+$Itn2|2OD%7v8sIFX6x$mj%(6!!}DO}#V5T(!~2{ZDECxn69SLg}x@D)Pi0 zT@4{n?C}?vxkK4ao5=$t!3x%jmgU9YA#WyrR?6kfJdD|Y{VwVn8t7unYI`1Co154J zkm#w(#Prp;L&08a@%u#i4#K$8pAA*Y!{5SKZuGAGhX|YN-wkl-TX+}x-u=wI{4O`)um9Pkf^v_y}N^%qttfS;R`Ty!6 z1IHHbtr-y3M20C3s?ghV6eP&+`$jAHg9DOHHTows;|wv zI+=g2)P`Jl->BNSK0>apxEXwuNy*P1bMtHPVE*4u35W0F>RXkr+|tCev`1}wY%y+S zkpv%JNIklCWEDGgg%ELdX?IF1Q|U&R!gA`%MGp05AMTCEZ}*sS&OXbdBqqWh3#~4^ zz-YiDV6!T%fAlr|^Qf#?e!cWiKbE4Eb!+v<&NQ2W&*z1;WP{Y+rudGU*>}@ve}^51 z2F3(G?>Xr9Q&j=B+a6RPTJMA&8DX*Eb9AV!%{sY(wkjW+btaiIgEVraSZ^tmT9LwL z|M2%0vgD8NWm~t>hQx4D4s6C%@%E~DOJ5mXHozSXCXofGjS|eeEv_#zYT6mwqqF&O zt~4dH65iS`yp&?f55XWwp8)|LiO`2h^}HP4pHB6IgV9l^oOQ$HO?XLyB~k|BF4cAj zLo-5Kw_1izqj6Ux^>9XO^}49N^EdV9`}YUv^U zffAe22TeYHJ;%=gEMAtX(b;M{*39OpuHPuke1>#tku;(t*fbW9Ib*9gpyB$M!J!v_#EpLv+E|&CNT9ZdC=i<|T_&g=ouM zUuRU})tfN{3=cf-MLC7wAWhQ!w~=d$dAaBzgV0wNsgm~~7-Oaiu;qzz-Ncqz&Vo;<&lgfPMYfruGayxnKJ`om+P}*$olA zmssI9AWbBZ2u5SpQ6jIsG$0E(BA=ihlc@05RTkF3vjFH0X*lv-k`PWpNlD4&*@m2~ zX=z#%PZgr|{l!1NNWr5x3PLT#fI;=v{D3|6;#Kk0_%C{Pj-!H_g}~N4Y1@s^bH6RS zpM-oS;xLj4qHySgYK2_5L$4{|2{VX&L18Eh1L=0_wygGt;d-HRM$}E`8?N7PAMs_8 z0VxTwLh#)b94i861=YGnrxTC}8Zg9};d;?cBp3k6%9|&!K;#ixVSfhGGCVQhe@Na3 zg6^ic47i;k+!u|UoW57T#n$zU3z3Xg(ZGhp1Z6r4=K|N?zy>FyP8??ZRa~8nL8P~1 z4)yo>T?@3+q}&L60+1fV;={t#t#p78Uz@I5dlAW!a%1v<<*)cCkz&_nyB!4PWJ8<* zi0?V*)aLo6q!>vM9PVI2eySwaB)HoNsGR7Mwdb(341Bg~FbD>pRx?p9K;7KHW{uo= zB~1rfq+51*Jq1G*;h=u1g;g!3Vd7k`bLlo&wfBgef#xJ?7RL+=^NR?kFztPyd7F9h z#B`pMA*BAm8oD+9;)?i7y!q%=8M|1ds;4q$ew_kiuI7 z{ZiI-)PDs`N06_^kRKL?t-a{^=?>s9?q-fY^KJvcPd^1IgB;v6TJr0Wxu~UZI<(Ou zB5NpYybVB_Kg2&4+okM9CgG^E3tE%eM_Wero1pD$P%z!e+&ot*@{Um3@ATy4U}t&g zP~IJo-4@aYoxQu@{T9OuCDe59z`$4RYfLj=oG1eoXkK8LJcHcm4)7$+ud%(4@^}08 zpR8TzR#aZ$N2o_i4|U~@nuE#?j~z@{I?sueY$sehv~^_Rcd+~Yo~wVObEqX=(E_HT zS*b<$VF56X+`hfP08c5f%LysZ;^mdDws2x~XH-_b5=QZN_5X$JO-KKZEkIqtSX-Z?Yn7rcR0z2nPzpy;e(K1^J-hN;gdg&OnUaUOM zK+JCG2X5^L6|GvEK@TgN*I~_ic@7V6A_9=m&0^ADQ5oawqwe^15b+lO`W- zj`(+-YV10jxJaL6+e5emfJPSP$Z;-uEJycNxy$gFWY99#X@beq|8( z;z-TXh8Mci1!@Sq<7@jNgh~Cm7K$f1#S(pYl;V5bPsw)+;*Bh?+4gdvumA@|zu$nA zb1~?0uq!zA%%H}K`~!X(s7Gu*1;T_jqO5NJDEyhT^ED}+3X{47w;F2)2M3X?>kwQ6 zBDN|P9)p^$IpPnnJ3Cs>G@h`tbwB6}BcKZTd}nUZnc#5m5{xq_A&Pho$7ZRAI;;Ha za+-ZtWv6k=bXM2S_-#-1?dEZ^-T|znjGTxZBue1SQm9hW7^JA|qOYT~O0tQo9enw< znMfhb?L{!ArUb0q=&WdL1K>N|Y{;MopTWDo4-=f5cpuzPTDpKi99G?E`m4anWk57Pl0G z^e?6ht@IP#6aIU*bg}#gZ6(U34PwRSI+TT<@}n^Kx9j7!dL-^Sm#xN+DYUA$EKvtj7Q;(Av~8U=k6h zepJGd>3_?4;gE}b%kNu2pTJm|rtE~ujq_0RJ4!-?_I~XdSIn;ZKW)8rTvTlrH7p8} z(kUg~paR0s(w!12NP|i@N*pAlyIVp8MOr~XkWjh=B&0(?kxmi#_V|3y+xI`-KNx1t zoVc#F*IsMwfMtoVDJeT8=hH#asV-uws&Yg_d0!<<&3Qw?fiwVkLjl z4XG=#bz%s=@^zW`7}^#Zhp2rrQQ3P4yMaCbjY7UlOrB}*fb~t&j&ru7?1jGHe{ZCH zK2AxBWI{7Y@j=BpLyG}T6YnJ}Z2CUi)}R({5MYX`6tjI3m8$h6uae%y0=ess3Rhq5 z#?s(M5;w$#c-r1$t+B2iCM)=4vn^n3L3L(BrASOm4Vps2B7~TPR>H58+w&n9F>eQl z#^Kvmv^o#mjm{!U4;d(M{L;0~(baG<&@6eC?2P!P;my%1*R=RMx=Xw%B#z>ExF?5& ziJBJ4uIq#ii^#ZTVY9gY6Zt{n-AzZO878t1hM$&Zr7p3WdmUmO+y{v&)|+f2Vjh~l z_7_Ko;fLN#>(ckY@{M$vl_)8+L_#HCRTeGhmaE1@108@u}p$+;MQ zl|=H>rw>pDgil-MQFo9k8Jb`22NB?lW1#05eb#R*hAU0W>ef8eNg~$WhiIA45}EMXEVr%5gtsEVbs`jxP|D9*L*go%j=1bn+6+239KoGg zWV@l&NVe|PN`+&bBAF?I!*$C;BWAm-In!*nkJj^M7&7_1NAEFHON~f>eTD6~O-09y z2O+}#y*S=Q(O5!xbAF2{pDjQK3F%l&oW1#?ieTc=nWW|P>~(AW;RH@L!9+2#eju$k zFY7;xsX;7Mf~!MWiNCNeC~0H5MF+ecwE`7aN|rd;HTHr5>d!b}d?0z-N^BWIt3*he z{mBT&`(-KrbByopqAhrrAIOd$bwV|gg#>`xrigLjJ5*>-vpl}tO+Zq}Yw0r;`WL^M zjj)`pFRC+mkMPkQiP2Wnn0Q8QrN>

    L@c(^$abPf=0u2wUiaRy9_PrxA>g}7{ub9 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 { throw new Error("Transient Obsidian notices did not become quiet before the P2P status screenshot."); } -const basePluginData = createE2eCouchDbPluginData( - { - uri: "http://127.0.0.1:5984", - username: "", - password: "", - dbName: "p2p-pane-ui-only", - }, - { - notifyThresholdOfRemoteStorageSize: -1, - periodicReplication: false, - P2P_Enabled: false, - P2P_AutoStart: false, - syncAfterMerge: false, - syncOnEditorSave: false, - syncOnFileOpen: false, - syncOnSave: false, - syncOnStart: false, - } -); +function createBaseP2PPluginData(): Record { + return createE2eCouchDbPluginData( + { + uri: "http://127.0.0.1:5984", + username: "", + password: "", + dbName: "p2p-pane-ui-only", + }, + { + notifyThresholdOfRemoteStorageSize: -1, + periodicReplication: false, + P2P_Enabled: false, + P2P_AutoStart: false, + syncAfterMerge: false, + syncOnEditorSave: false, + syncOnFileOpen: false, + syncOnSave: false, + syncOnStart: false, + } + ); +} + +function createConfiguredP2PPluginData(): Record { + const pluginData = { + ...createBaseP2PPluginData(), + P2P_roomID: "configured-p2p-room", + P2P_passphrase: "configured-p2p-passphrase", + }; + upsertRemoteConfigurationInPlace(pluginData as ObsidianLiveSyncSettings, "p2p", { + id: "e2e-p2p", + name: "P2P Remote", + activateForP2P: true, + }); + return pluginData; +} async function withP2PSession( binary: string, @@ -170,18 +199,14 @@ async function main(): Promise { throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); } - await withP2PSession(binary, cli.binary, basePluginData, async () => { + await withP2PSession(binary, cli.binary, createBaseP2PPluginData(), async () => { await assertP2PUIIsOptIn(); }); await withP2PSession( binary, cli.binary, - { - ...basePluginData, - P2P_roomID: "configured-p2p-room", - P2P_passphrase: "configured-p2p-passphrase", - }, + createConfiguredP2PPluginData(), async () => { await assertConfiguredP2PUIIsAvailable(); const desktopScreenshot = await verifyP2PStatusPane("p2p-status-pane.png", false); From 32b72e4b103b3b9bdd89a9f70285339976bb8bb7 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 24 Jul 2026 05:39:40 +0000 Subject: [PATCH 086/117] Normalise Chinese setup guide line endings --- docs/setup_own_server_cn.md | 304 ++++++++++++++++++------------------ 1 file changed, 152 insertions(+), 152 deletions(-) diff --git a/docs/setup_own_server_cn.md b/docs/setup_own_server_cn.md index 1c88ed61..809c02e3 100644 --- a/docs/setup_own_server_cn.md +++ b/docs/setup_own_server_cn.md @@ -1,152 +1,152 @@ -# 在你自己的服务器上设置 CouchDB - -## 目录 -- [配置 CouchDB](#配置-CouchDB) -- [运行 CouchDB](#运行-CouchDB) - - [Docker CLI](#docker-cli) - - [Docker Compose](#docker-compose) -- [创建数据库](#创建数据库) -- [从移动设备访问](#从移动设备访问) - - [移动设备测试](#移动设备测试) - - [设置你的域名](#设置你的域名) ---- - -> 注:提供了 [docker-compose.yml 和 ini 文件](https://github.com/vrtmrz/self-hosted-livesync-server) 可以同时启动 Caddy 和 CouchDB。推荐直接使用该 docker-compose 配置进行搭建。(若使用,请查阅链接中的文档,而不是这个文档) - -## 配置 CouchDB - -设置 CouchDB 的最简单方法是使用 [CouchDB docker image]((https://hub.docker.com/_/couchdb)). - -需要修改一些 `local.ini` 中的配置,以让它可以用于 Self-hosted LiveSync,如下: - -``` -[couchdb] -single_node=true -max_document_size = 50000000 - -[chttpd] -require_valid_user = true -max_http_request_size = 4294967296 - -[chttpd_auth] -require_valid_user = true -authentication_redirect = /_utils/session.html - -[httpd] -WWW-Authenticate = Basic realm="couchdb" -enable_cors = true - -[cors] -origins = app://obsidian.md,capacitor://localhost,http://localhost -credentials = true -headers = accept, authorization, content-type, origin, referer -methods = GET, PUT, POST, HEAD, DELETE -max_age = 3600 -``` - -## 运行 CouchDB - -### Docker CLI - -你可以通过指定 `local.ini` 配置运行 CouchDB: - -``` -$ docker run --rm -it -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /path/to/local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb -``` -*记得将上述命令中的 local.ini 挂载路径替换成实际的存放路径* - -后台运行: -``` -$ docker run -d --restart always -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /path/to/local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb -``` -*记得将上述命令中的 local.ini 挂载路径替换成实际的存放路径* - -### Docker Compose -创建一个文件夹, 将你的 `local.ini` 放在文件夹内, 然后在文件夹内创建 `docker-compose.yml`. 请确保对 `local.ini` 有读写权限并且确保在容器运行后能创建 `data` 文件夹. 文件夹结构大概如下: -``` -obsidian-livesync -├── docker-compose.yml -└── local.ini -``` - -可以参照以下内容编辑 `docker-compose.yml`: -```yaml -services: - couchdb: - image: couchdb - container_name: obsidian-livesync - user: 1000:1000 - environment: - - COUCHDB_USER=admin - - COUCHDB_PASSWORD=password - volumes: - - ./data:/opt/couchdb/data - - ./local.ini:/opt/couchdb/etc/local.ini - ports: - - 5984:5984 - restart: unless-stopped -``` - -最后, 创建并启动容器: -``` -# -d will launch detached so the container runs in background -docker-compose up -d -``` - -## 创建数据库 - -CouchDB 部署成功后, 需要手动创建一个数据库, 方便插件连接并同步. - -1. 访问 `http://localhost:5984/_utils`, 输入帐号密码后进入管理页面 -2. 点击 Create Database, 然后根据个人喜好创建数据库 - -## 从移动设备访问 -如果你想要从移动设备访问 Self-hosted LiveSync,你需要一个合法的 SSL 证书。 - -### 移动设备测试 -测试时,[localhost.run](http://localhost.run/) 这一类的反向隧道服务很实用。(非必须,只是用于终端设备不方便 ssh 的时候的备选方案) - -``` -$ ssh -R 80:localhost:5984 nokey@localhost.run -Warning: Permanently added the RSA host key for IP address '35.171.254.69' to the list of known hosts. - -=============================================================================== -Welcome to localhost.run! - -Follow your favourite reverse tunnel at [https://twitter.com/localhost_run]. - -**You need a SSH key to access this service.** -If you get a permission denied follow Gitlab's most excellent howto: -https://docs.gitlab.com/ee/ssh/ -*Only rsa and ed25519 keys are supported* - -To set up and manage custom domains go to https://admin.localhost.run/ - -More details on custom domains (and how to enable subdomains of your custom -domain) at https://localhost.run/docs/custom-domains - -To explore using localhost.run visit the documentation site: -https://localhost.run/docs/ - -=============================================================================== - - -** your connection id is xxxxxxxxxxxxxxxxxxxxxxxxxxxx, please mention it if you send me a message about an issue. ** - -xxxxxxxx.localhost.run tunneled with tls termination, https://xxxxxxxx.localhost.run -Connection to localhost.run closed by remote host. -Connection to localhost.run closed. -``` - -https://xxxxxxxx.localhost.run 即为临时服务器地址。 - -### 设置你的域名 - -设置一个指向你服务器的 A 记录,并根据需要设置反向代理。 - -Note: 不推荐将 CouchDB 挂载到根目录 -可以使用 Caddy 很方便的给服务器加上 SSL 功能 - -提供了 [docker-compose.yml 和 ini 文件](https://github.com/vrtmrz/self-hosted-livesync-server) 可以同时启动 Caddy 和 CouchDB。 - -注意检查服务器日志,当心恶意访问。 +# 在你自己的服务器上设置 CouchDB + +## 目录 +- [配置 CouchDB](#配置-CouchDB) +- [运行 CouchDB](#运行-CouchDB) + - [Docker CLI](#docker-cli) + - [Docker Compose](#docker-compose) +- [创建数据库](#创建数据库) +- [从移动设备访问](#从移动设备访问) + - [移动设备测试](#移动设备测试) + - [设置你的域名](#设置你的域名) +--- + +> 注:提供了 [docker-compose.yml 和 ini 文件](https://github.com/vrtmrz/self-hosted-livesync-server) 可以同时启动 Caddy 和 CouchDB。推荐直接使用该 docker-compose 配置进行搭建。(若使用,请查阅链接中的文档,而不是这个文档) + +## 配置 CouchDB + +设置 CouchDB 的最简单方法是使用 [CouchDB docker image]((https://hub.docker.com/_/couchdb)). + +需要修改一些 `local.ini` 中的配置,以让它可以用于 Self-hosted LiveSync,如下: + +``` +[couchdb] +single_node=true +max_document_size = 50000000 + +[chttpd] +require_valid_user = true +max_http_request_size = 4294967296 + +[chttpd_auth] +require_valid_user = true +authentication_redirect = /_utils/session.html + +[httpd] +WWW-Authenticate = Basic realm="couchdb" +enable_cors = true + +[cors] +origins = app://obsidian.md,capacitor://localhost,http://localhost +credentials = true +headers = accept, authorization, content-type, origin, referer +methods = GET, PUT, POST, HEAD, DELETE +max_age = 3600 +``` + +## 运行 CouchDB + +### Docker CLI + +你可以通过指定 `local.ini` 配置运行 CouchDB: + +``` +$ docker run --rm -it -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /path/to/local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb +``` +*记得将上述命令中的 local.ini 挂载路径替换成实际的存放路径* + +后台运行: +``` +$ docker run -d --restart always -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /path/to/local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb +``` +*记得将上述命令中的 local.ini 挂载路径替换成实际的存放路径* + +### Docker Compose +创建一个文件夹, 将你的 `local.ini` 放在文件夹内, 然后在文件夹内创建 `docker-compose.yml`. 请确保对 `local.ini` 有读写权限并且确保在容器运行后能创建 `data` 文件夹. 文件夹结构大概如下: +``` +obsidian-livesync +├── docker-compose.yml +└── local.ini +``` + +可以参照以下内容编辑 `docker-compose.yml`: +```yaml +services: + couchdb: + image: couchdb + container_name: obsidian-livesync + user: 1000:1000 + environment: + - COUCHDB_USER=admin + - COUCHDB_PASSWORD=password + volumes: + - ./data:/opt/couchdb/data + - ./local.ini:/opt/couchdb/etc/local.ini + ports: + - 5984:5984 + restart: unless-stopped +``` + +最后, 创建并启动容器: +``` +# -d will launch detached so the container runs in background +docker-compose up -d +``` + +## 创建数据库 + +CouchDB 部署成功后, 需要手动创建一个数据库, 方便插件连接并同步. + +1. 访问 `http://localhost:5984/_utils`, 输入帐号密码后进入管理页面 +2. 点击 Create Database, 然后根据个人喜好创建数据库 + +## 从移动设备访问 +如果你想要从移动设备访问 Self-hosted LiveSync,你需要一个合法的 SSL 证书。 + +### 移动设备测试 +测试时,[localhost.run](http://localhost.run/) 这一类的反向隧道服务很实用。(非必须,只是用于终端设备不方便 ssh 的时候的备选方案) + +``` +$ ssh -R 80:localhost:5984 nokey@localhost.run +Warning: Permanently added the RSA host key for IP address '35.171.254.69' to the list of known hosts. + +=============================================================================== +Welcome to localhost.run! + +Follow your favourite reverse tunnel at [https://twitter.com/localhost_run]. + +**You need a SSH key to access this service.** +If you get a permission denied follow Gitlab's most excellent howto: +https://docs.gitlab.com/ee/ssh/ +*Only rsa and ed25519 keys are supported* + +To set up and manage custom domains go to https://admin.localhost.run/ + +More details on custom domains (and how to enable subdomains of your custom +domain) at https://localhost.run/docs/custom-domains + +To explore using localhost.run visit the documentation site: +https://localhost.run/docs/ + +=============================================================================== + + +** your connection id is xxxxxxxxxxxxxxxxxxxxxxxxxxxx, please mention it if you send me a message about an issue. ** + +xxxxxxxx.localhost.run tunneled with tls termination, https://xxxxxxxx.localhost.run +Connection to localhost.run closed by remote host. +Connection to localhost.run closed. +``` + +https://xxxxxxxx.localhost.run 即为临时服务器地址。 + +### 设置你的域名 + +设置一个指向你服务器的 A 记录,并根据需要设置反向代理。 + +Note: 不推荐将 CouchDB 挂载到根目录 +可以使用 Caddy 很方便的给服务器加上 SSL 功能 + +提供了 [docker-compose.yml 和 ini 文件](https://github.com/vrtmrz/self-hosted-livesync-server) 可以同时启动 Caddy 和 CouchDB。 + +注意检查服务器日志,当心恶意访问。 From 058da1f34ce16f296396c788a6ce5dfec98d4b02 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 24 Jul 2026 05:55:37 +0000 Subject: [PATCH 087/117] Polish setup guide wording --- docs/quick_setup.md | 2 +- docs/tips/hidden-file-sync.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quick_setup.md b/docs/quick_setup.md index f685db48..a77b3834 100644 --- a/docs/quick_setup.md +++ b/docs/quick_setup.md @@ -119,7 +119,7 @@ Use this path when CouchDB is ready but a Setup URI is unavailable. It configure 5. On `Choose a synchronisation remote`, select `CouchDB`, then select `Continue to CouchDB setup`. - ![CouchDB option in the synchronisation remote choices](../images/couchdb-manual/guide-couchdb-manual-remote-selection.png) + ![CouchDB option in the list of synchronisation remotes](../images/couchdb-manual/guide-couchdb-manual-remote-selection.png) 6. Enter the complete CouchDB URL, username, password, and database name. - Obsidian Mobile requires HTTPS. Plain HTTP is suitable only for a trusted local connection from a desktop device. diff --git a/docs/tips/hidden-file-sync.md b/docs/tips/hidden-file-sync.md index e84570d9..4fd89a09 100644 --- a/docs/tips/hidden-file-sync.md +++ b/docs/tips/hidden-file-sync.md @@ -47,7 +47,7 @@ A pattern containing only `snippets` does not admit the `.obsidian` parent, so t ![Hidden File Sync initialisation choices](../../images/hidden-file-sync/guide-hidden-file-enable.png) 2. Under `Enable Hidden File Sync`, select the initialisation direction chosen above. -3. Keep Obsidian open while the initial scan and synchronisation finish. A progress Notice appears when preparation begins and remains visible while the initial scan is running. +3. Keep Obsidian open while the initial scan and synchronisation finish. A progress Notice appears when preparation begins and remains visible until the initial scan has finished. ![Hidden File Sync initial scan progress Notice](../../images/hidden-file-sync/guide-hidden-file-initial-scan-progress.png) From d48945258367e00b2866e112f0440c0e7223135d Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 24 Jul 2026 08:51:30 +0000 Subject: [PATCH 088/117] Improve troubleshooting and compatible setting handling --- CONTRIBUTING.md | 5 + _tools/inspect-troubleshooting-docs.ts | 138 +++++ .../inspect-troubleshooting-docs.unit.spec.ts | 17 + docs/settings.md | 6 + docs/troubleshooting.md | 13 +- docs/tweak_mismatch_dialogue.png | Bin 47151 -> 32256 bytes package.json | 1 + .../ModuleResolveMismatchedTweaks.ts | 47 +- ...ModuleResolveMismatchedTweaks.unit.spec.ts | 125 +++- .../SettingDialogue/LiveSyncSetting.ts | 3 +- .../features/SettingDialogue/PaneAdvanced.ts | 4 +- .../features/SettingDialogue/SettingPane.ts | 1 + test/e2e-obsidian/README.md | 2 +- test/e2e-obsidian/scripts/dialog-mounts.ts | 553 +++++++++++++++++- updates.md | 3 + 15 files changed, 868 insertions(+), 50 deletions(-) create mode 100644 _tools/inspect-troubleshooting-docs.ts create mode 100644 _tools/inspect-troubleshooting-docs.unit.spec.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a1ca3b9f..a97333b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,11 @@ Before submitting a pull request, you must run verification scripts locally to e ```bash npm run test:unit ``` +- When changing the troubleshooting or recovery guides, inspect their current English UI labels and local references: + ```bash + npm run inspect:troubleshooting + ``` + This read-only Inspector prints JSON containing `ok`, `checkedFiles`, `checkedLocalReferences`, and `errors`, and exits unsuccessfully when a contract is stale. If you have the capability and a suitable environment (such as Linux and Docker), running the CLI End-to-End (E2E) tests is also highly appreciated. Instructions are detailed in [devs.md](devs.md). If you cannot run E2E tests locally, please explicitly ask to run the tests on the CI by stating 'Please run CI tests' in your pull request description. diff --git a/_tools/inspect-troubleshooting-docs.ts b/_tools/inspect-troubleshooting-docs.ts new file mode 100644 index 00000000..e7de1545 --- /dev/null +++ b/_tools/inspect-troubleshooting-docs.ts @@ -0,0 +1,138 @@ +import { access, readFile } from "node:fs/promises"; +import { dirname, relative, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +type InspectionError = { + check: "current-label" | "local-reference" | "retired-label"; + file: string; + detail: string; +}; + +export type TroubleshootingDocsInspection = { + ok: boolean; + checkedFiles: string[]; + checkedLocalReferences: number; + errors: InspectionError[]; +}; + +const guidePaths = ["docs/troubleshooting.md", "docs/recovery.md", "docs/tips/p2p-sync-tips.md"] as const; +const messageCataloguePath = "src/common/messagesJson/en.json"; +const markdownLinkPattern = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^)]*["'])?\)/gu; + +function repositoryRootFromThisFile(): string { + return resolve(dirname(fileURLToPath(import.meta.url)), ".."); +} + +function normaliseReferenceTarget(rawTarget: string): string { + const withoutAngles = rawTarget.startsWith("<") && rawTarget.endsWith(">") ? rawTarget.slice(1, -1) : rawTarget; + return decodeURIComponent(withoutAngles); +} + +function isExternalReference(target: string): boolean { + return /^(?:https?:|mailto:|obsidian:)/u.test(target); +} + +async function inspectLocalReferences( + repositoryRoot: string, + documentPath: string, + document: string, + errors: InspectionError[] +): Promise { + let checked = 0; + for (const match of document.matchAll(markdownLinkPattern)) { + const rawTarget = match[1]; + if (!rawTarget) continue; + const target = normaliseReferenceTarget(rawTarget); + if (isExternalReference(target) || target.startsWith("#")) continue; + + const [pathPart] = target.split("#", 1); + if (!pathPart) continue; + checked++; + const referencedPath = resolve(repositoryRoot, dirname(documentPath), pathPart); + try { + await access(referencedPath); + } catch { + errors.push({ + check: "local-reference", + file: documentPath, + detail: `Missing local reference: ${relative(repositoryRoot, referencedPath)}`, + }); + } + } + return checked; +} + +export async function inspectTroubleshootingDocs( + repositoryRoot = repositoryRootFromThisFile() +): Promise { + const errors: InspectionError[] = []; + const documents = new Map(); + for (const guidePath of guidePaths) { + documents.set(guidePath, await readFile(resolve(repositoryRoot, guidePath), "utf8")); + } + + const troubleshooting = documents.get("docs/troubleshooting.md")!; + const catalogue = JSON.parse(await readFile(resolve(repositoryRoot, messageCataloguePath), "utf8")) as Record< + string, + string + >; + const requiredMessageKeys = [ + "TweakMismatchResolve.Action.UseConfigured", + "TweakMismatchResolve.Action.UseMine", + "TweakMismatchResolve.Action.UseRemote", + "TweakMismatchResolve.Action.Dismiss", + "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown", + ] as const; + + for (const messageKey of requiredMessageKeys) { + const label = catalogue[messageKey]; + if (!label) { + errors.push({ + check: "current-label", + file: messageCataloguePath, + detail: `The English message catalogue does not define ${messageKey}.`, + }); + continue; + } + if (!troubleshooting.includes(label)) { + errors.push({ + check: "current-label", + file: "docs/troubleshooting.md", + detail: `The guide does not include the current UI label '${label}'.`, + }); + } + } + + for (const retiredLabel of ["`Update with mine`", "`Use configured`", "`Sync settings via Markdown files`"]) { + if (troubleshooting.includes(retiredLabel)) { + errors.push({ + check: "retired-label", + file: "docs/troubleshooting.md", + detail: `The guide still includes the retired label ${retiredLabel}.`, + }); + } + } + + let checkedLocalReferences = 0; + for (const [guidePath, document] of documents) { + checkedLocalReferences += await inspectLocalReferences(repositoryRoot, guidePath, document, errors); + } + + return { + ok: errors.length === 0, + checkedFiles: [...guidePaths], + checkedLocalReferences, + errors, + }; +} + +async function runCli(): Promise { + const result = await inspectTroubleshootingDocs(); + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + if (!result.ok) process.exitCode = 1; +} + +const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : undefined; +if (invokedPath === import.meta.url) { + await runCli(); +} diff --git a/_tools/inspect-troubleshooting-docs.unit.spec.ts b/_tools/inspect-troubleshooting-docs.unit.spec.ts new file mode 100644 index 00000000..f184a162 --- /dev/null +++ b/_tools/inspect-troubleshooting-docs.unit.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; +import { inspectTroubleshootingDocs } from "./inspect-troubleshooting-docs"; + +describe("troubleshooting documentation contract", () => { + it("uses current English UI labels and resolves every local guide reference", async () => { + const result = await inspectTroubleshootingDocs(); + + expect(result.checkedFiles).toEqual([ + "docs/troubleshooting.md", + "docs/recovery.md", + "docs/tips/p2p-sync-tips.md", + ]); + expect(result.checkedLocalReferences).toBeGreaterThan(0); + expect(result.errors).toEqual([]); + expect(result.ok).toBe(true); + }); +}); diff --git a/docs/settings.md b/docs/settings.md index 4dd9521c..16af6883 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -959,6 +959,12 @@ If disabled(toggled), chunks will be split on the UI thread (Previous behaviour) Setting key: processSmallFilesInUIThread If enabled, the file under 1kb will be processed in the UI thread. +#### Automatically align compatible chunk settings + +Setting key: autoAcceptCompatibleTweak + +Current releases enable this by default when the differences are limited to compatible chunk settings. The side with the newer recorded modification time is used for the chunk hash algorithm, chunk size, or splitter version; the remote value is used when neither side has a recorded time or the times are equal. No dialogue or database reconstruction is required. Existing content remains readable, but changing these values can reduce chunk reuse. Turn this off to review compatible differences manually. Any difference which also involves an incompatible setting always requires an explicit decision. + ### 8. Compatibility (Trouble addressed) #### Do not check configuration mismatch before replication diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index be623d3f..41812ed0 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -61,9 +61,14 @@ If the log reports missing chunks or a size mismatch: Some settings must match across devices. LiveSync pauses synchronisation when the local and remote values differ rather than propagating an unexpected change silently. -- Choose `Update with mine` only when this device's setting is the intended shared value. -- Choose `Use configured` to accept the value already stored for the synchronisation group. -- `Dismiss` postpones the decision, but synchronisation remains paused until it is resolved. +Current releases automatically align compatible settings which control how new chunks are created, by default and where possible. This applies to the chunk hash algorithm, chunk size, and splitter version. Existing content remains readable across these choices, although using different choices can reduce chunk reuse and increase storage or transfer work. An explicit opt-out retains the manual review. A mismatch involving encryption, path obfuscation, file-name case handling, or any combination which includes one of those settings always remains a manual decision. + +The available actions depend on when the mismatch is found: + +- While checking a remote profile, `Use configured settings` accepts the shared values already stored in that remote. `Dismiss` leaves this device's settings unchanged. +- For a mismatch found before synchronisation, `Apply settings to this device` accepts the remote values. Choose `Update remote database settings` only when this device's values are intended to become the shared values. +- When the change requires local or remote reconstruction, the action itself states that Fetch or Rebuild will follow. Make sure that the intended authoritative copy is available before choosing it. +- `Dismiss` postpones a mismatch found before synchronisation. Synchronisation remains paused until the mismatch is resolved. ![Configuration mismatch dialogue](tweak_mismatch_dialogue.png) @@ -75,7 +80,7 @@ Historic defect notices and renamed controls are retained in the [0.25 release h Generate an encrypted Setup URI from a working device. This preserves the intended remote profiles and selections while allowing the additional device to keep its own device-specific name. Store the URI and its passphrase separately. -For deliberate setting changes during normal use, use `Sync settings via Markdown files` under `Sync settings`. +For deliberate setting changes during normal use, use `Sync Settings via Markdown` under `Sync settings`. ### Choose a Setup URI passphrase diff --git a/docs/tweak_mismatch_dialogue.png b/docs/tweak_mismatch_dialogue.png index 073a999be8fbdd8e47fd21221f3445a9834b023e..d25566cada62529216f7393b3cc07999510e2e40 100644 GIT binary patch literal 32256 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 literal 47151 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 vWiE;X3wT)ezaBawNMFW>zyk~E) { const items = Object.keys( TweakValuesShouldMatchedTemplate @@ -64,33 +62,17 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule { ); if (!hasOnlyCompatibleLossyMismatches) return undefined; + let autoAcceptCompatibleTweak = this.settings.autoAcceptCompatibleTweak; if (this.settings.autoAcceptCompatibleTweak === undefined) { - if (this._hasNotifiedAutoAcceptCompatibleUndefined) { - return undefined; - } - this._hasNotifiedAutoAcceptCompatibleUndefined = true; - const CHOICE_ENABLE = $msg("TweakMismatchResolve.Action.EnableAutoAcceptCompatible"); - const CHOICE_DISABLE = $msg("TweakMismatchResolve.Action.DisableAutoAcceptCompatible"); - const CHOICES = [CHOICE_ENABLE, CHOICE_DISABLE] as const; - const message = $msg("TweakMismatchResolve.Message.AutoAcceptCompatibleUndefined"); - const ret = await this.core.confirm.askSelectStringDialogue(message, CHOICES, { - title: $msg("TweakMismatchResolve.Title.AutoAcceptCompatible"), - timeout: 0, - defaultAction: CHOICE_ENABLE, - }); - if (ret !== CHOICE_ENABLE) { - return undefined; - } - await this.services.setting.applyPartial( - { - autoAcceptCompatibleTweak: true, - }, - true - ); - Logger("Auto-accept for compatible tweak mismatch has been enabled."); + // Keep the settings object stable: settings panes and an in-flight replication retry can + // retain this reference while the default is persisted. + this.settings.autoAcceptCompatibleTweak = true; + await this.services.setting.saveSettingData(); + autoAcceptCompatibleTweak = true; + Logger("Automatic alignment of compatible chunk settings has been enabled."); } - if (this.settings.autoAcceptCompatibleTweak !== true) return undefined; + if (autoAcceptCompatibleTweak !== true) return undefined; return this._selectNewerTweakSide(current, preferred); } @@ -215,7 +197,7 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule { } else if (rebuildRecommended) { CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE, [preferred, false]]); CHOICE_AND_VALUES.push([CHOICE_USE_MINE, [true, false]]); - CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_WITH_REBUILD, [true, true]]); + CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_WITH_REBUILD, [preferred, true]]); CHOICE_AND_VALUES.push([CHOICE_USE_MINE_WITH_REBUILD, [true, true]]); } else { CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE, [preferred, false]]); @@ -255,9 +237,16 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule { return "CHECKAGAIN"; } if (conf) { - this.settings = { ...this.settings, ...conf }; - await this.core.replicator.setPreferredRemoteTweakSettings(this.settings); + // ReplicationService retains the current settings object while it performs the immediate + // CHECKAGAIN retry. Update that object in place so the retry observes the accepted values. + Object.assign(this.settings, extractObject(TweakValuesTemplate, conf)); await this.services.setting.saveSettingData(); + if (!rebuildRequired) { + // The failed replication has settled before mismatch resolution runs. Reinitialise the + // chunk-generation managers now so hash and splitter changes take effect before retrying. + await this.localDatabase.managers.reinitialise(); + } + await this.core.replicator.setPreferredRemoteTweakSettings(this.settings); if (rebuildRequired) { await this.core.rebuilder.$fetchLocal(); } diff --git a/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.unit.spec.ts b/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.unit.spec.ts index 68fc0661..21f773e0 100644 --- a/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.unit.spec.ts +++ b/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.unit.spec.ts @@ -1,9 +1,16 @@ import { describe, expect, it, vi } from "vitest"; -import { DEFAULT_SETTINGS, REMOTE_COUCHDB, type RemoteDBSettings, type TweakValues } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { + DEFAULT_SETTINGS, + REMOTE_COUCHDB, + type RemoteDBSettings, + type TweakValues, +} from "@vrtmrz/livesync-commonlib/compat/common/types"; import { ModuleResolvingMismatchedTweaks } from "./ModuleResolveMismatchedTweaks"; function createModule(settingsOverride: Partial = {}) { - const askSelectStringDialogue = vi.fn(async () => undefined); + const askSelectStringDialogue = vi.fn(async (..._args: unknown[]): Promise => undefined); + const applyPartial = vi.fn(async (_partial: Record): Promise => undefined); + const reinitialise = vi.fn(async () => undefined); const core = { _services: { API: { @@ -15,6 +22,12 @@ function createModule(settingsOverride: Partial = {}) { }, setting: { saveSettingData: vi.fn(async () => undefined), + applyPartial, + }, + }, + localDatabase: { + managers: { + reinitialise, }, }, settings: { @@ -26,6 +39,9 @@ function createModule(settingsOverride: Partial = {}) { askSelectStringDialogue, }, } as any; + applyPartial.mockImplementation(async (partial: Record) => { + core.settings = { ...core.settings, ...partial }; + }); Object.defineProperty(core, "services", { get() { @@ -34,10 +50,35 @@ function createModule(settingsOverride: Partial = {}) { }); const module = new ModuleResolvingMismatchedTweaks(core); - return { module, core, askSelectStringDialogue }; + return { module, core, askSelectStringDialogue, applyPartial, reinitialise }; } describe("ModuleResolvingMismatchedTweaks", () => { + it("should enable and auto-accept compatible mismatches when the preference is undefined", async () => { + const { module, core, askSelectStringDialogue, applyPartial } = createModule({ + autoAcceptCompatibleTweak: undefined, + hashAlg: "xxhash64", + tweakModified: 100, + }); + const initialSettings = core.settings; + + const preferred = { + ...(DEFAULT_SETTINGS as unknown as TweakValues), + hashAlg: "xxhash32", + tweakModified: 200, + } as Partial; + + const [conf, rebuild] = await module._checkAndAskResolvingMismatchedTweaks(preferred); + + expect(conf).toEqual(preferred); + expect(rebuild).toBe(false); + expect(core.settings).toBe(initialSettings); + expect(core.settings.autoAcceptCompatibleTweak).toBe(true); + expect(core._services.setting.saveSettingData).toHaveBeenCalledTimes(1); + expect(applyPartial).not.toHaveBeenCalled(); + expect(askSelectStringDialogue).not.toHaveBeenCalled(); + }); + it("should auto-accept compatible mismatches on connect check using newer remote tweakModified", async () => { const { module, askSelectStringDialogue } = createModule({ autoAcceptCompatibleTweak: true, @@ -58,6 +99,28 @@ describe("ModuleResolvingMismatchedTweaks", () => { expect(askSelectStringDialogue).not.toHaveBeenCalled(); }); + it.each([ + { label: "neither side has a recorded time", currentModified: 0, preferredModified: 0 }, + { label: "the recorded times are equal", currentModified: 200, preferredModified: 200 }, + ])("should use the remote compatible value when $label", async ({ currentModified, preferredModified }) => { + const { module, askSelectStringDialogue } = createModule({ + autoAcceptCompatibleTweak: true, + hashAlg: "xxhash64", + tweakModified: currentModified, + }); + const preferred = { + ...(DEFAULT_SETTINGS as unknown as TweakValues), + hashAlg: "xxhash32", + tweakModified: preferredModified, + } as Partial; + + const [conf, rebuild] = await module._checkAndAskResolvingMismatchedTweaks(preferred); + + expect(conf).toEqual(preferred); + expect(rebuild).toBe(false); + expect(askSelectStringDialogue).not.toHaveBeenCalled(); + }); + it("should fallback to manual confirmation when mismatches are mixed on connect check", async () => { const { module, askSelectStringDialogue } = createModule({ autoAcceptCompatibleTweak: true, @@ -80,6 +143,24 @@ describe("ModuleResolvingMismatchedTweaks", () => { expect(askSelectStringDialogue).toHaveBeenCalledTimes(1); }); + it("should fetch after applying a compatible remote setting when the user selects the rebuild option", async () => { + const { module, askSelectStringDialogue } = createModule({ + autoAcceptCompatibleTweak: false, + hashAlg: "xxhash64", + }); + askSelectStringDialogue.mockResolvedValueOnce("Apply settings to this device, and fetch again"); + + const preferred = { + ...(DEFAULT_SETTINGS as unknown as TweakValues), + hashAlg: "xxhash32", + } as TweakValues; + + const [conf, rebuild] = await module._checkAndAskResolvingMismatchedTweaks(preferred); + + expect(conf).toEqual(preferred); + expect(rebuild).toBe(true); + }); + it("should auto-accept compatible mismatches on remote-config check using newer local tweakModified", async () => { const { module, askSelectStringDialogue } = createModule({ autoAcceptCompatibleTweak: true, @@ -105,4 +186,42 @@ describe("ModuleResolvingMismatchedTweaks", () => { expect(result).toEqual({ result: false, requireFetch: false }); expect(askSelectStringDialogue).not.toHaveBeenCalled(); }); + + it("should apply remote compatible settings in place and reinitialise managers before retrying", async () => { + const { module, core, reinitialise } = createModule({ + autoAcceptCompatibleTweak: true, + hashAlg: "xxhash64", + tweakModified: 100, + }); + const initialSettings = core.settings; + const preferred = { + ...(DEFAULT_SETTINGS as unknown as TweakValues), + hashAlg: "xxhash32", + tweakModified: 200, + } as TweakValues; + const calls: string[] = []; + core._services.tweakValue = { + checkAndAskResolvingMismatched: vi.fn(async () => [preferred, false]), + }; + core._services.setting.saveSettingData = vi.fn(async () => { + calls.push("save"); + }); + core.replicator = { + tweakSettingsMismatched: true, + preferredTweakValue: preferred, + setPreferredRemoteTweakSettings: vi.fn(async () => { + calls.push("set-preferred"); + }), + }; + reinitialise.mockImplementation(async () => { + calls.push("reinitialise"); + }); + + const result = await module._askResolvingMismatchedTweaks(); + + expect(result).toBe("CHECKAGAIN"); + expect(core.settings).toBe(initialSettings); + expect(core.settings.hashAlg).toBe("xxhash32"); + expect(calls).toEqual(["save", "reinitialise", "set-preferred"]); + }); }); diff --git a/src/modules/features/SettingDialogue/LiveSyncSetting.ts b/src/modules/features/SettingDialogue/LiveSyncSetting.ts index 96ee66ef..b7876678 100644 --- a/src/modules/features/SettingDialogue/LiveSyncSetting.ts +++ b/src/modules/features/SettingDialogue/LiveSyncSetting.ts @@ -206,7 +206,8 @@ export class LiveSyncSetting extends Setting { const setValue = wrapMemo((value: boolean) => { toggle.setValue(opt?.invert ? !value : value); }); - this.invalidateValue = () => setValue(LiveSyncSetting.env.editingSettings[key] ?? false); + this.invalidateValue = () => + setValue(LiveSyncSetting.env.editingSettings[key] ?? opt?.defaultToggleValue ?? false); this.invalidateValue(); toggle.onChange(async (value) => { diff --git a/src/modules/features/SettingDialogue/PaneAdvanced.ts b/src/modules/features/SettingDialogue/PaneAdvanced.ts index 6c9c85a5..bfa0d02e 100644 --- a/src/modules/features/SettingDialogue/PaneAdvanced.ts +++ b/src/modules/features/SettingDialogue/PaneAdvanced.ts @@ -35,7 +35,9 @@ export function paneAdvanced(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme clampMin: 10, onUpdate: this.onlyOnCouchDB, }); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("autoAcceptCompatibleTweak"); + new Setting(paneEl) + .setClass("wizardHidden") + .autoWireToggle("autoAcceptCompatibleTweak", { defaultToggleValue: true }); // new Setting(paneEl) // .setClass("wizardHidden") // .autoWireToggle("sendChunksBulk", { onUpdate: onlyOnCouchDB }) diff --git a/src/modules/features/SettingDialogue/SettingPane.ts b/src/modules/features/SettingDialogue/SettingPane.ts index e34fdfbe..b3cd4e1b 100644 --- a/src/modules/features/SettingDialogue/SettingPane.ts +++ b/src/modules/features/SettingDialogue/SettingPane.ts @@ -75,6 +75,7 @@ export type AutoWireOption = { holdValue?: boolean; isPassword?: boolean; invert?: boolean; + defaultToggleValue?: boolean; onUpdate?: OnUpdateFunc; obsolete?: boolean; }; diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index 6fc6d483..e0069e12 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -83,7 +83,7 @@ The underlying `test:e2e:obsidian:` scripts remain available for an im `test:e2e:obsidian:onboarding-invitation` starts an unconfigured temporary Vault with no plug-in data and verifies that startup selects Commonlib's new-Vault recommendations, offers the setup wizard without opening it, and does not scan Vault files automatically. It checks the invitation action and introduction in mobile test mode, then uses the permanent command to reopen the wizard on the desktop. This scenario owns the unconfigured-startup boundary only; configured compatibility review remains covered by `settings-ui`, and the setup workflows remain covered by their dedicated scenarios. -`test:e2e:obsidian:dialog-mounts` starts a temporary real Obsidian session and exercises remote selection and CouchDB settings through `SetupManager`, plus Setup URI entry through the registered command. It verifies the compatibility pause and remote-size review, the distinction between a central data-storage server and P2P signalling, the explicit tested and untested CouchDB save actions, the internal-API warning, and the Setup URI controls. It captures the representative dialogues on desktop and mobile, checks the mobile layout and vertically stacked actions, closes each route through its normal controls, and verifies that the remote-selection promise settles without an error. These UI-only checks do not apply a remote configuration or contact a remote service. +`test:e2e:obsidian:dialog-mounts` starts a temporary real Obsidian session and exercises remote selection and CouchDB settings through `SetupManager`, plus Setup URI entry through the registered command. It verifies the compatibility pause and remote-size review, the distinction between a central data-storage server and P2P signalling, the explicit tested and untested CouchDB save actions, the internal-API warning, the Setup URI controls, automatic adjustment when differences are limited to compatible chunk settings, and both manual configuration-mismatch routes. The same session opens the live log and generated full report, reaches the `Hatch` recovery controls, writes and removes its own persistent log, and runs the missing-chunk recreation and file-verification actions against the empty disposable Vault. It captures representative desktop and mobile dialogues, checks the mobile layout and vertically stacked actions, closes each route through its normal controls, and verifies that each mounted operation settles without an error. It does not apply a remote configuration, contact a remote service, or claim to repair a deliberately damaged database. `test:e2e:obsidian:settings-ui` starts with a pending compatibility review and verifies the dedicated pause summary, its detailed explanation, and the explicit resume action in a temporary real Obsidian session. It captures the desktop summary and the iPhone-sized summary and detail dialogues; the mobile checks cover viewport containment, horizontal overflow, safe-area containment, and the close control's touch target. It confirms that the acknowledged internal version advances only after the review is accepted, and checks that the Change Log contains no acknowledgement control. It then selects the Synchronisation Settings pane and verifies that the deletion panel still exposes the effective 'Keep empty folder' setting without presenting the legacy `trashInsteadDelete` control, whose value no longer changes Obsidian deletion behaviour. diff --git a/test/e2e-obsidian/scripts/dialog-mounts.ts b/test/e2e-obsidian/scripts/dialog-mounts.ts index 2065b7de..c12586e1 100644 --- a/test/e2e-obsidian/scripts/dialog-mounts.ts +++ b/test/e2e-obsidian/scripts/dialog-mounts.ts @@ -6,6 +6,7 @@ import { assertMobileDialogueLayout, assertMobileNoticeLayout, setObsidianMobile import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; import { captureObsidianDialogue, + captureObsidianElement, captureObsidianPage, obsidianRemoteDebuggingPort, withObsidianPage, @@ -13,6 +14,7 @@ import { import { createTemporaryVault } from "../runner/vault.ts"; const dialogRunStateKey = "__livesyncE2EDialogMount"; +const repairRunStateKey = "__livesyncE2ETroubleshootingRepair"; const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_DIALOG_TIMEOUT_MS ?? 10000); type DialogueMode = "desktop" | "mobile"; @@ -20,6 +22,7 @@ type DialogueMode = "desktop" | "mobile"; type DialogueRunState = { done: boolean; error?: string; + expected?: unknown; kind: string; result?: unknown; }; @@ -27,18 +30,39 @@ type DialogueRunState = { type SetupManagerHandle = { constructor: { name: string }; onSelectServer?: (settings: unknown, remoteType: string) => Promise; + _askUseRemoteConfiguration?: (settings: unknown, preferred: unknown) => Promise; + _checkAndAskResolvingMismatchedTweaks?: (preferred: unknown) => Promise; + __addLog?: (message: string) => void; }; type LiveSyncTestPlugin = { core: { + fileHandler: { + createAllChunks(force: boolean): Promise; + }; modules: SetupManagerHandle[]; - settings: unknown; + settings: Record; }; }; +type ObsidianSettingsController = { + open(): void; + openTabById(tabId: string): void; +}; + +type ObsidianVaultFile = { + path: string; +}; + type ObsidianTestApp = { commands?: { executeCommandById(commandId: string): boolean }; plugins?: { plugins: Record }; + setting?: ObsidianSettingsController; + vault?: { + delete(file: ObsidianVaultFile, force: boolean): Promise; + getFiles(): ObsidianVaultFile[]; + read(file: ObsidianVaultFile): Promise; + }; }; type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp }; @@ -78,7 +102,62 @@ async function openSetupUriDialogue(): Promise { } } -async function assertDialogueRunCompleted(): Promise { +async function openConfigurationMismatchDialogue( + kind: "connected" | "connected-rebuild-recommended" | "remote-configuration" +): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.evaluate( + ({ stateKey, kind }) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + const resolver = plugin.core.modules.find( + (module) => module.constructor.name === "ModuleResolvingMismatchedTweaks" + ); + if (resolver === undefined) throw new Error("Could not find ModuleResolvingMismatchedTweaks"); + if (kind === "connected-rebuild-recommended") { + plugin.core.settings.autoAcceptCompatibleTweak = false; + } + + const preferred = + kind === "connected-rebuild-recommended" + ? { + ...plugin.core.settings, + hashAlg: plugin.core.settings.hashAlg === "xxhash32" ? "xxhash64" : "xxhash32", + } + : { + ...plugin.core.settings, + enableCompression: !Boolean(plugin.core.settings.enableCompression), + }; + const state: DialogueRunState = { + kind: `configuration-mismatch-${kind}`, + done: false, + expected: preferred, + }; + (globalThis as unknown as Record)[stateKey] = state; + const operation = + kind === "remote-configuration" + ? resolver._askUseRemoteConfiguration?.(plugin.core.settings, preferred) + : resolver._checkAndAskResolvingMismatchedTweaks?.(preferred); + if (operation === undefined) { + throw new Error(`The configuration mismatch resolver does not support ${kind}.`); + } + void operation.then( + (result) => { + state.result = result; + state.done = true; + }, + (error: unknown) => { + state.error = error instanceof Error ? error.message : String(error); + state.done = true; + } + ); + }, + { stateKey: dialogRunStateKey, kind } + ); + }); +} + +async function assertDialogueRunCompleted(): Promise { const state = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { await page.waitForFunction( (stateKey) => @@ -92,11 +171,12 @@ async function assertDialogueRunCompleted(): Promise { ); }); if (!state) { - throw new Error("The remote selection dialogue did not record its completion state."); + throw new Error("The mounted dialogue did not record its completion state."); } if (state.error) { - throw new Error(`The remote selection dialogue failed: ${state.error}`); + throw new Error(`The mounted dialogue failed: ${state.error}`); } + return state; } async function verifyRemoteSizeNoticeAndDialogue(): Promise<{ @@ -347,6 +427,442 @@ async function verifySetupUriDialogue(mode: DialogueMode): Promise { return screenshotPath; } +async function verifyCompatibleMismatchAutoAdjustment(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.evaluate((stateKey) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + const resolver = plugin.core.modules.find( + (module) => module.constructor.name === "ModuleResolvingMismatchedTweaks" + ); + if (typeof resolver?._checkAndAskResolvingMismatchedTweaks !== "function") { + throw new Error("Could not find the configuration mismatch resolver"); + } + plugin.core.settings.autoAcceptCompatibleTweak = undefined; + const currentModified = + typeof plugin.core.settings.tweakModified === "number" ? plugin.core.settings.tweakModified : 0; + const preferred = { + ...plugin.core.settings, + hashAlg: plugin.core.settings.hashAlg === "xxhash32" ? "xxhash64" : "xxhash32", + tweakModified: currentModified + 1, + }; + const state: DialogueRunState = { + kind: "configuration-mismatch-compatible-auto-adjustment", + done: false, + expected: preferred, + }; + (globalThis as unknown as Record)[stateKey] = state; + void resolver._checkAndAskResolvingMismatchedTweaks(preferred).then( + (result) => { + state.result = result; + state.done = true; + }, + (error: unknown) => { + state.error = error instanceof Error ? error.message : String(error); + state.done = true; + } + ); + }, dialogRunStateKey); + }); + + const state = await assertDialogueRunCompleted(); + if (!Array.isArray(state.result) || state.result.length !== 2) { + throw new Error("The compatible mismatch did not return its settings and rebuild decision."); + } + const [appliedSettings, shouldRebuild] = state.result; + const expectedSettings = state.expected; + if ( + typeof appliedSettings !== "object" || + appliedSettings === null || + typeof expectedSettings !== "object" || + expectedSettings === null || + !("hashAlg" in appliedSettings) || + !("hashAlg" in expectedSettings) || + appliedSettings.hashAlg !== expectedSettings.hashAlg || + shouldRebuild !== false + ) { + throw new Error("The compatible mismatch was not adjusted to the newer setting without a rebuild."); + } + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const autoAcceptEnabled = await page.evaluate(() => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + return plugin?.core.settings.autoAcceptCompatibleTweak; + }); + if (autoAcceptEnabled !== true) { + throw new Error("Compatible mismatch auto-adjustment was not persisted as the default."); + } + for (const title of ["Auto-Accept Available", "Configuration Mismatch Detected"]) { + const dialogue = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: title }), + }); + if ((await dialogue.count()) !== 0) { + throw new Error(`Compatible mismatch auto-adjustment unexpectedly opened '${title}'.`); + } + } + }); +} + +async function verifyCompatibleAlignmentSettingDefault(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const persistedValue = await page.evaluate(() => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + return plugin.core.settings.autoAcceptCompatibleTweak; + }); + if (persistedValue !== undefined) { + throw new Error( + `The default-display fixture expected an undefined preference, received ${persistedValue}.` + ); + } + + await page.evaluate(() => { + const setting = (globalThis as ObsidianTestGlobal).app?.setting; + if (setting === undefined) throw new Error("Obsidian settings are unavailable"); + setting.open(); + setting.openTabById("obsidian-livesync"); + }); + const liveSyncSettings = page.locator(".sls-setting"); + await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await liveSyncSettings.locator('.sls-setting-menu-btn[title="Advanced"]').click({ timeout: uiTimeoutMs }); + const settingItem = liveSyncSettings.locator(".setting-item").filter({ + has: page.getByText("Auto-accept compatible tweak mismatches", { exact: true }), + }); + await settingItem.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const toggle = settingItem.locator(".checkbox-container"); + if (!(await toggle.evaluate((element) => element.classList.contains("is-enabled")))) { + throw new Error("The automatic compatible-setting policy was displayed as disabled while still undefined."); + } + }); +} + +async function verifyConfigurationMismatchDialogues(): Promise<{ general: string; fetch: string }> { + await verifyCompatibleMismatchAutoAdjustment(); + await openConfigurationMismatchDialogue("remote-configuration"); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Use Remote Configuration" }), + }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal + .getByRole("button", { name: "Use configured settings", exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal.getByRole("button", { name: "Dismiss", exact: true }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + await assertDialogueRunCompleted(); + + await openConfigurationMismatchDialogue("connected"); + const generalScreenshotPath = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-configuration-mismatch-dialogue.png", + async (page) => { + const container = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }), + }); + const modal = container.locator(".modal").last(); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + for (const action of ["Apply settings to this device", "Update remote database settings"]) { + await modal + .getByRole("button", { name: action, exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + await modal.getByRole("button", { name: /Dismiss$/u }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + for (const retiredAction of ["Use configured", "Update with mine"]) { + if ((await modal.getByRole("button", { name: retiredAction, exact: true }).count()) !== 0) { + throw new Error(`The mismatch dialogue still exposes the retired action '${retiredAction}'.`); + } + } + const actions = modal.locator(".setting-item-control").last(); + const flexDirection = await actions.evaluate((element) => getComputedStyle(element).flexDirection); + if (flexDirection !== "column") { + throw new Error(`Expected vertically stacked mismatch actions, received ${flexDirection}.`); + } + return modal; + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }), + }); + await modal.getByRole("button", { name: /Dismiss$/u }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + await assertDialogueRunCompleted(); + + await openConfigurationMismatchDialogue("connected-rebuild-recommended"); + const fetchScreenshotPath = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-configuration-mismatch-fetch-dialogue.png", + async (page) => { + const container = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }), + }); + const modal = container.locator(".modal").last(); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal + .getByRole("button", { name: "Apply settings to this device, and fetch again", exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + return modal; + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }), + }); + await modal + .getByRole("button", { name: "Apply settings to this device, and fetch again", exact: true }) + .click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + const fetchResult = await assertDialogueRunCompleted(); + if (!Array.isArray(fetchResult.result) || fetchResult.result.length !== 2) { + throw new Error("The configuration-mismatch Fetch action did not return its settings and Fetch decision."); + } + const [appliedSettings, shouldFetch] = fetchResult.result; + const expectedSettings = fetchResult.expected; + if ( + typeof appliedSettings !== "object" || + appliedSettings === null || + typeof expectedSettings !== "object" || + expectedSettings === null || + !("hashAlg" in appliedSettings) || + !("hashAlg" in expectedSettings) || + appliedSettings.hashAlg !== expectedSettings.hashAlg || + shouldFetch !== true + ) { + throw new Error("The configuration-mismatch Fetch action did not apply the remote setting before Fetch."); + } + + return { general: generalScreenshotPath, fetch: fetchScreenshotPath }; +} + +async function executeRegisteredCommand(commandId: string): Promise { + const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + return await page.evaluate( + (id) => (globalThis as ObsidianTestGlobal).app?.commands?.executeCommandById(id) === true, + commandId + ); + }); + if (!opened) { + throw new Error(`The command was not registered or could not be executed: ${commandId}`); + } +} + +async function verifyLogAndReportSurfaces(): Promise<{ log: string; report: string }> { + await executeRegisteredCommand("obsidian-livesync:view-log"); + const logScreenshot = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-show-log.png", + async (page) => { + const logPane = page.locator(".logpane"); + await logPane.waitFor({ state: "visible", timeout: uiTimeoutMs }); + for (const label of ["Wrap", "Auto scroll", "Pause"]) { + await logPane.getByText(label, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + await logPane.getByRole("button", { name: "Close", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + return logPane; + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const logPane = page.locator(".logpane"); + await logPane.getByRole("button", { name: "Close", exact: true }).click({ timeout: uiTimeoutMs }); + await logPane.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + await executeRegisteredCommand("obsidian-livesync:dump-debug-info"); + const reportScreenshot = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-full-report.png", + async (page) => { + const modal = page.locator(".modal-container").filter({ + hasText: "Your Debug info is ready to be copied", + }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const report = await modal.locator("textarea").inputValue({ timeout: uiTimeoutMs }); + if (!report.includes("# ---- Debug Info Dump ----")) { + throw new Error("The full-report dialogue did not contain the generated debug report."); + } + await modal.getByRole("button", { name: "OK", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + return modal.locator(".modal").last(); + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + hasText: "Your Debug info is ready to be copied", + }); + await modal.getByRole("button", { name: "OK", exact: true }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + return { log: logScreenshot, report: reportScreenshot }; +} + +async function verifyHatchSurfacesAndSafeActions(): Promise { + const screenshotPath = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-hatch.png", + async (page) => { + await page.evaluate(() => { + const setting = (globalThis as ObsidianTestGlobal).app?.setting; + if (setting === undefined) throw new Error("Obsidian settings are unavailable"); + setting.open(); + setting.openTabById("obsidian-livesync"); + }); + const liveSyncSettings = page.locator(".sls-setting"); + await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await liveSyncSettings.locator('.sls-setting-menu-btn[title="Hatch"]').click({ timeout: uiTimeoutMs }); + for (const label of [ + "Write logs into the file", + "Recreate missing chunks for all files", + "Verify and repair all files", + ]) { + await liveSyncSettings.locator(".setting-item-name", { hasText: label }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + } + await liveSyncSettings.getByRole("button", { name: "Recreate all", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await liveSyncSettings.getByRole("button", { name: "Verify all", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await liveSyncSettings + .locator(".setting-item-name", { hasText: "Recreate missing chunks for all files" }) + .scrollIntoViewIfNeeded(); + return liveSyncSettings; + } + ); + + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const liveSyncSettings = page.locator(".sls-setting"); + const logSetting = liveSyncSettings.locator(".setting-item").filter({ + has: page.getByText("Write logs into the file", { exact: true }), + }); + await logSetting.locator(".checkbox-container").click({ timeout: uiTimeoutMs }); + await page.waitForFunction( + () => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + return plugin?.core.settings.writeLogToTheFile === true; + }, + undefined, + { timeout: uiTimeoutMs } + ); + + const persistentLogMarker = "E2E persistent troubleshooting log"; + await page.evaluate((marker) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + const module = plugin.core.modules.find((candidate) => candidate.constructor.name === "ModuleLog"); + if (typeof module?.__addLog !== "function") throw new Error("Could not find ModuleLog"); + module.__addLog(marker); + }, persistentLogMarker); + await page.waitForFunction( + async (marker) => { + const vault = (globalThis as ObsidianTestGlobal).app?.vault; + if (vault === undefined) return false; + const logFile = vault.getFiles().find((file) => file.path.startsWith("livesync_log_")); + if (logFile === undefined) return false; + return (await vault.read(logFile)).includes(marker); + }, + persistentLogMarker, + { timeout: uiTimeoutMs } + ); + + // Saving a toggle refreshes the settings pane. Resolve the visible control again so the + // second action does not target the detached pre-save element. + const refreshedLogToggle = page + .locator(".sls-setting:visible .setting-item:visible") + .filter({ + has: page.getByText("Write logs into the file", { exact: true }), + }) + .locator(".checkbox-container:visible") + .last(); + await refreshedLogToggle.click({ timeout: uiTimeoutMs }); + await page.waitForFunction( + () => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + return plugin?.core.settings.writeLogToTheFile === false; + }, + undefined, + { timeout: uiTimeoutMs } + ); + await page.evaluate(async () => { + const vault = (globalThis as ObsidianTestGlobal).app?.vault; + if (vault === undefined) throw new Error("Obsidian Vault is unavailable"); + const logFile = vault.getFiles().find((file) => file.path.startsWith("livesync_log_")); + if (logFile === undefined) throw new Error("The persistent troubleshooting log was not created"); + await vault.delete(logFile, true); + }); + await page.waitForFunction( + () => + !(globalThis as ObsidianTestGlobal).app?.vault + ?.getFiles() + .some((file) => file.path.startsWith("livesync_log_")), + undefined, + { timeout: uiTimeoutMs } + ); + + await page.evaluate((stateKey) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + const original = plugin.core.fileHandler.createAllChunks.bind(plugin.core.fileHandler); + const state: DialogueRunState = { kind: "recreate-missing-chunks", done: false }; + (globalThis as unknown as Record)[stateKey] = state; + plugin.core.fileHandler.createAllChunks = async (force) => { + try { + state.result = await original(force); + } catch (error) { + state.error = error instanceof Error ? error.message : String(error); + } finally { + state.done = true; + plugin.core.fileHandler.createAllChunks = original; + } + }; + }, repairRunStateKey); + await liveSyncSettings.getByRole("button", { name: "Recreate all", exact: true }).click({ + timeout: uiTimeoutMs, + }); + await page.waitForFunction( + (stateKey) => + (globalThis as unknown as Record)[stateKey]?.done === true, + repairRunStateKey, + { timeout: uiTimeoutMs } + ); + const repairState = await page.evaluate( + (stateKey) => (globalThis as unknown as Record)[stateKey], + repairRunStateKey + ); + if (repairState?.error) { + throw new Error(`Recreate missing chunks failed: ${repairState.error}`); + } + + await liveSyncSettings.getByRole("button", { name: "Verify all", exact: true }).click({ + timeout: uiTimeoutMs, + }); + await page + .locator(".notice") + .filter({ hasText: /^done$/u }) + .waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + }); + + return screenshotPath; +} + async function verifyMobileStartupReviews(): Promise<{ compatibilityReview: string; remoteSizeReview: string }> { const compatibilityReviewScreenshot = await captureObsidianDialogue( obsidianRemoteDebuggingPort(), @@ -430,13 +946,14 @@ async function main(): Promise { dbName: "dialog-mounts-ui-only", }, { - notifyThresholdOfRemoteStorageSize: -1, - syncOnStart: false, - syncOnSave: false, - syncOnEditorSave: false, - syncOnFileOpen: false, - syncAfterMerge: false, - periodicReplication: false, + notifyThresholdOfRemoteStorageSize: -1, + syncOnStart: false, + syncOnSave: false, + syncOnEditorSave: false, + syncOnFileOpen: false, + syncAfterMerge: false, + periodicReplication: false, + useAdvancedMode: true, } ), }); @@ -480,6 +997,20 @@ async function main(): Promise { ); const setupUriScreenshot = await verifySetupUriDialogue("desktop"); console.log(`Setup URI dialogue mounted and closed successfully. Screenshot: ${setupUriScreenshot}`); + await verifyCompatibleAlignmentSettingDefault(); + console.log("The undefined compatible-setting preference is displayed with its effective enabled default."); + const mismatchScreenshots = await verifyConfigurationMismatchDialogues(); + console.log( + `A mismatch limited to compatible chunk settings was adjusted without a dialogue, current manual mismatch actions mounted successfully, and the Fetch action applied the remote setting before scheduling Fetch. Screenshots: ${mismatchScreenshots.general}, ${mismatchScreenshots.fetch}` + ); + const troubleshootingScreenshots = await verifyLogAndReportSurfaces(); + console.log( + `Show log and the generated full-report dialogue were reached through their registered commands. Screenshots: ${troubleshootingScreenshots.log}, ${troubleshootingScreenshots.report}` + ); + const hatchScreenshot = await verifyHatchSurfacesAndSafeActions(); + console.log( + `Hatch repair controls were reachable, safe empty-fixture runs completed, and persistent logging was enabled, verified, disabled, and removed. Screenshot: ${hatchScreenshot}` + ); await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), true, uiTimeoutMs); try { diff --git a/updates.md b/updates.md index af3b5ee7..4853a544 100644 --- a/updates.md +++ b/updates.md @@ -19,10 +19,13 @@ Earlier releases remain available in the [0.25 release history](https://github.c - P2P is now presented only after it has been configured: its status pane no longer opens at start-up, its ribbon icon remains hidden for CouchDB-only Vaults, and the retired P2P pane command has been removed. The current pane distinguishes announcing changes, following a peer, and persistent per-device actions. Setup and guidance now distinguish the required signalling relay from optional TURN, and describe the public signalling relay's privacy and availability limits. - First-device P2P setup now accepts a successfully opened signalling room without requiring another peer to be online. Additional-device Fetch still requires selecting a source peer and completing `P2P Rebuild`. - Manual CouchDB setup now distinguishes creating a first database from connecting an additional device to an existing one. Settings mode can save an unverified profile explicitly, while onboarding requires a successful connection, and each proposed server-configuration fix requires separate confirmation. +- Differences limited to the chunk hash algorithm, chunk size, or splitter version are now aligned automatically by default. Existing content remains readable, while an explicit opt-out and any difference which also involves an incompatible setting retain manual review. ### Fixed - Answering or externally closing a merge dialogue immediately no longer leaves conflict processing waiting for a response which has already occurred. +- Choosing **Apply settings to this device, and fetch again** for a compatible configuration mismatch now applies the remote settings before Fetch, instead of updating the remote database with this device's settings. +- Accepted settings which control how new chunks are created now take effect before synchronisation is retried, rather than leaving the previous hash or splitter active until restart. ### Testing From 6afeb0b4099d2a022ff4ad54e2f4009e4b479027 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 24 Jul 2026 09:54:44 +0000 Subject: [PATCH 089/117] Record beta.2 history and remove release-note links --- updates.md | 30 +++++++++++++++++++++--------- versions.json | 3 ++- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/updates.md b/updates.md index 4853a544..4b3bd275 100644 --- a/updates.md +++ b/updates.md @@ -2,19 +2,18 @@ Well then, everyone: it has been roughly a year since I declared the 0.25 beta. During that time, we have concentrated mainly on fixing defects and completing the features that the project needed. -Version 1.0 has been in mind for some time. We have now brought together the work intended to make it possible: stronger CI, more detailed tests, an E2E runner suited to synchronisation, and testing tools for physical devices. These now form a coherent [Kit](https://github.com/vrtmrz/fancy-kit) rather than a collection of isolated pieces. With those foundations in place, it seems that the time has finally come to reshape the structure of this repository. +Version 1.0 has been in mind for some time. We have now brought together the work intended to make it possible: stronger CI, more detailed tests, an E2E runner suited to synchronisation, and testing tools for physical devices. These now form a coherent Kit rather than a collection of isolated pieces. With those foundations in place, it seems that the time has finally come to reshape the structure of this repository. -None of this would have been possible without your issue reports, pull requests, sponsorship, and the support provided through [OpenAI's Codex for Open Source](https://openai.com/form/codex-for-oss/). I would like to express my gratitude once again. As with every pull request contributed to the project, code produced with Codex and similar tools is reviewed and audited by me, vrtmrz. Anyone interested in how I manage that process can refer to [my dotfiles](https://github.com/vrtmrz/dotfiles). +None of this would have been possible without your issue reports, pull requests, sponsorship, and the support provided through OpenAI's Codex for Open Source. I would like to express my gratitude once again. As with every pull request contributed to the project, code produced with Codex and similar tools is reviewed and audited by me, vrtmrz. Anyone interested in how I manage that process can refer to my dotfiles. This will call for your help once again. I would be very grateful for your co-operation as we build a sounder foundation for the project and its future development. -Earlier releases remain available in the [0.25 release history](https://github.com/vrtmrz/obsidian-livesync/blob/1.0.0-beta.0/docs/releases/0.25.md) and the [legacy release history](https://github.com/vrtmrz/obsidian-livesync/blob/1.0.0-beta.0/docs/releases/legacy.md). +Earlier releases remain available in the 0.25 release history and the legacy release history. ## Unreleased ### Improved -- Choosing **Not now** on a merge conflict now postpones repeated dialogues for that conflict while the active file retains an unresolved-conflict warning. Three or more live versions show their current count and are reviewed one deterministic pair at a time; completed pairs remain resolved across restart. The existing conflict commands can reopen a postponed conflict explicitly, and a later conflict prompts again after the current one has been resolved. - Enabling Hidden File Sync now opens one progress Notice before its setting is saved and reuses that Notice throughout the initial file scan, instead of stacking separate phase and restart Notices. - P2P is now presented only after it has been configured: its status pane no longer opens at start-up, its ribbon icon remains hidden for CouchDB-only Vaults, and the retired P2P pane command has been removed. The current pane distinguishes announcing changes, following a peer, and persistent per-device actions. Setup and guidance now distinguish the required signalling relay from optional TURN, and describe the public signalling relay's privacy and availability limits. - First-device P2P setup now accepts a successfully opened signalling room without requiring another peer to be online. Additional-device Fetch still requires selecting a source peer and completing `P2P Rebuild`. @@ -23,15 +22,29 @@ Earlier releases remain available in the [0.25 release history](https://github.c ### Fixed -- Answering or externally closing a merge dialogue immediately no longer leaves conflict processing waiting for a response which has already occurred. - Choosing **Apply settings to this device, and fetch again** for a compatible configuration mismatch now applies the remote settings before Fetch, instead of updating the remote database with this device's settings. - Accepted settings which control how new chunks are created now take effect before synchronisation is retried, rather than leaving the previous hash or splitter active until restart. ### Testing -- Added revision-tree regressions and focused real-Obsidian scenarios for multiple-version review and restart between resolution stages. - Added regressions for P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, and mobile dialogues. +## 1.0.0-beta.2 + +23rd July, 2026 + +### Improved + +- Choosing **Not now** on a merge conflict now postpones repeated dialogues for that conflict while the active file retains an unresolved-conflict warning. Three or more live versions show their current count and are reviewed one deterministic pair at a time; completed pairs remain resolved across restart. The existing conflict commands can reopen a postponed conflict explicitly, and a later conflict prompts again after the current one has been resolved. + +### Fixed + +- Answering or externally closing a merge dialogue immediately no longer leaves conflict processing waiting for a response which has already occurred. + +### Testing + +- Added revision-tree regressions and focused real-Obsidian scenarios for multiple-version review and restart between resolution stages. + ## 1.0.0-beta.1 22nd July, 2026 @@ -63,10 +76,10 @@ Earlier releases remain available in the [0.25 release history](https://github.c - An unconfigured installation now waits for you to start setup. A long-lived Notice offers the setup action, and **Open onboarding wizard** remains available from the command palette instead of the dialogue opening automatically. - The setup wizard now creates named remote profiles for CouchDB, Object Storage, and P2P. Current Setup URIs preserve their profile names and selections, and the wizard reserves Rebuild or Fetch before the ordinary start-up scan begins. - Peer-to-Peer Synchronisation (P2P) and Hidden File Sync are supported opt-in features. JWT authentication, ignore files, automatic newer-file conflict resolution, and Garbage Collection V3 remain previews. Customisation Sync remains a supported advanced workflow. -- Data Compression remains available after measurement showed a modest, workload-dependent reduction in stored and transferred chunk data. Its benefits, costs, and reason for remaining disabled by default in 1.0 are described in the [Data Compression specification](https://github.com/vrtmrz/obsidian-livesync/blob/1.0.0-beta.0/docs/specs_data_compression.md). +- Data Compression remains available after measurement showed a modest, workload-dependent reduction in stored and transferred chunk data. Its benefits, costs, and reason for remaining disabled by default in 1.0 are described in the Data Compression specification. - Compatibility review now runs before Config Doctor without overlapping it. Existing Vaults retain their automatic synchronisation choices and explicit file-name case setting. For installations created by earlier releases, LiveSync preserves whether setup had been completed and saves a missing legacy case setting as case-insensitive. - P2P connections now restart reliably after settings are reapplied or the local database is reset. Setup on an additional device asks you to select the source device once. Disconnecting leaves the LiveSync room and closes its signalling relay connections so that reconnecting can establish a new room. -- Action buttons are stacked vertically, long setup dialogues keep their controls reachable on mobile screens, and persistent Notices no longer cover close controls. Hidden File Sync reload and restart requests are grouped into one message, including the case reported in [issue #555](https://github.com/vrtmrz/obsidian-livesync/issues/555). +- Action buttons are stacked vertically, long setup dialogues keep their controls reachable on mobile screens, and persistent Notices no longer cover close controls. Hidden File Sync reload and restart requests are grouped into one message, including the case reported in issue #555. - Warnings about estimated remote storage size now appear as long-lived clickable Notices instead of timed dialogues. Initial uploads and Rebuild operations no longer prompt to send every chunk in advance; ordinary replication completes the transfer. - Removed the obsolete **Use the trash bin** control and the setting for fixed chunk revisions. Remote deletion still follows Obsidian's preference, and chunk revisions remain content-derived. The Change Log remains available but no longer opens automatically or tracks unread versions. @@ -83,7 +96,6 @@ Earlier releases remain available in the [0.25 release history](https://github.c ### Miscellaneous - Self-hosted LiveSync now owns its translation catalogue. Commonlib provides English messages to other applications, and translation contributions can be made directly to the Self-hosted LiveSync repository. -- Detailed architecture, compatibility, and verification notes are available in [pull request #1033](https://github.com/vrtmrz/obsidian-livesync/pull/1033) and its linked specifications and decisions. ### Testing diff --git a/versions.json b/versions.json index 3c168a92..a0f3dfb2 100644 --- a/versions.json +++ b/versions.json @@ -7,5 +7,6 @@ "0.25.82": "1.7.2", "0.25.83": "1.7.2", "1.0.0-beta.0": "1.7.2", - "1.0.0-beta.1": "1.7.2" + "1.0.0-beta.1": "1.7.2", + "1.0.0-beta.2": "1.7.2" } From 0e5475b7e3421daebc4357d3733b085cea36c264 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 24 Jul 2026 16:13:49 +0000 Subject: [PATCH 090/117] Limit commands to applicable contexts --- .../adr/2026_07_multiple_remote_onboarding.md | 2 +- ...elease_notes_and_database_compatibility.md | 2 +- docs/p2p.md | 2 +- docs/settings.md | 2 +- docs/setup_p2p.md | 2 +- .../messages/LiveSyncProvisionalMessages.ts | 2 + .../CmdConfigSync.command.unit.spec.ts | 96 ++++++++++++ src/features/ConfigSync/CmdConfigSync.ts | 10 +- .../HiddenFileSync/CmdHiddenFileSync.ts | 35 +++-- .../CmdHiddenFileSync.unit.spec.ts | 65 +++++++- .../CmdLocalDatabaseMainte.ts | 22 ++- .../CmdLocalDatabaseMainte.unit.spec.ts | 71 ++++++++- src/modules/essential/ModuleBasicMenu.ts | 21 ++- .../essential/ModuleBasicMenu.unit.spec.ts | 139 ++++++++++++++++++ .../features/SettingDialogue/PaneSetup.ts | 5 +- src/serviceFeatures/setupObsidian/qrCode.ts | 6 +- .../setupObsidian/qrCode.unit.spec.ts | 40 +++++ .../setupObsidian/setupManagerHandlers.ts | 5 - .../setupManagerHandlers.unit.spec.ts | 5 +- src/serviceFeatures/setupObsidian/setupUri.ts | 20 ++- .../setupObsidian/setupUri.unit.spec.ts | 53 +++++++ src/serviceFeatures/useP2PReplicatorUI.ts | 49 ++++-- .../useP2PReplicatorUI.unit.spec.ts | 91 +++++++++++- styles.css | 4 + test/e2e-obsidian/README.md | 2 +- .../scripts/onboarding-invitation.ts | 50 +++++-- updates.md | 1 + 27 files changed, 727 insertions(+), 75 deletions(-) create mode 100644 src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts create mode 100644 src/modules/essential/ModuleBasicMenu.unit.spec.ts diff --git a/docs/adr/2026_07_multiple_remote_onboarding.md b/docs/adr/2026_07_multiple_remote_onboarding.md index 3ad81596..db468c0d 100644 --- a/docs/adr/2026_07_multiple_remote_onboarding.md +++ b/docs/adr/2026_07_multiple_remote_onboarding.md @@ -77,7 +77,7 @@ Commonlib unit tests cover preserving existing profiles, opaque-ID insertion, ge Self-hosted LiveSync unit tests cover preserving modern Setup URI profiles and their active selection, retaining legacy Setup URI and QR migration, adding CouchDB and Object Storage profiles beside an existing profile, independent P2P selection, fresh P2P selection as both main and P2P remote, and cancellation without mutation. -The real-Obsidian onboarding E2E owns the invitation, dialogue presentation, safe-area and touch-target checks, cancellation, and command reopening. It does not contact a remote or submit credentials. Remote connection correctness remains owned by the CouchDB, Object Storage, P2P, and two-Vault suites. The end-to-end Setup URI and provisioning acceptance workflow remains a separate release gate. +The real-Obsidian onboarding E2E owns the invitation, dialogue presentation, safe-area and touch-target checks, cancellation, and reopening from the Setup pane. It does not contact a remote or submit credentials. Remote connection correctness remains owned by the CouchDB, Object Storage, P2P, and two-Vault suites. The end-to-end Setup URI and provisioning acceptance workflow remains a separate release gate. ## Consequences diff --git a/docs/adr/2026_07_release_notes_and_database_compatibility.md b/docs/adr/2026_07_release_notes_and_database_compatibility.md index 15b21c1e..a444e40d 100644 --- a/docs/adr/2026_07_release_notes_and_database_compatibility.md +++ b/docs/adr/2026_07_release_notes_and_database_compatibility.md @@ -60,7 +60,7 @@ Keep configured-state inference separate from new-Vault initialisation. If an ex ### Onboarding activation and initialisation -- Keep an unconfigured Vault outside database initialisation, offline scanning, and configured-only checks. Offer setup through the long-lived onboarding Notice and the permanent command instead of opening a competing dialogue automatically. +- Keep an unconfigured Vault outside database initialisation, offline scanning, and configured-only checks. Offer setup through the long-lived onboarding Notice, and allow the wizard to be reopened from the Setup pane instead of opening a competing dialogue automatically. - For new-device onboarding, reserve Rebuild before enabling and saving the accepted settings. - For an unconfigured existing device, reserve Fetch before enabling and saving imported or manually confirmed settings. - Suspend the current runtime after the flag has been written, apply the accepted settings through the scheduler's preparation callback, and request restart only after that callback succeeds. diff --git a/docs/p2p.md b/docs/p2p.md index 4e7902d9..ff47468a 100644 --- a/docs/p2p.md +++ b/docs/p2p.md @@ -48,7 +48,7 @@ A TURN provider cannot read LiveSync's encrypted Vault contents, but it can obse The **P2P Status** pane is the current Obsidian interface for P2P connections. -- The command **Self-hosted LiveSync: P2P Sync : Open P2P Status** remains available from the command palette. +- After a P2P configuration exists, the command **Self-hosted LiveSync: P2P Sync : Open P2P Status** is available from the command palette. - The P2P ribbon icon appears only after a P2P configuration exists. - LiveSync does not open the pane merely because Obsidian has started. If the pane was already part of the saved Obsidian workspace, Obsidian may restore it. - Workspaces containing the retired P2P pane are migrated to the current status pane. The retired command is no longer exposed. diff --git a/docs/settings.md b/docs/settings.md index 16af6883..b371d330 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -40,7 +40,7 @@ Internal database or settings compatibility reviews use a separate safety dialog This pane is used for setting up Self-hosted LiveSync. There are several options to set up Self-hosted LiveSync. -An unconfigured installation does not open the onboarding dialogue automatically or scan the Vault into the local database. A long-lived Notice offers the onboarding action, and **Open onboarding wizard** remains available from the command palette after that Notice closes. +An unconfigured installation does not open the onboarding dialogue automatically or scan the Vault into the local database. A long-lived Notice offers the onboarding action. If the Notice is dismissed, open **Self-hosted LiveSync settings** → **Setup** → **Rerun Onboarding Wizard**. Choose the new-device path when this device owns the files which should initialise synchronisation. Choose the existing-device path when it should receive an established remote state. The wizard reserves Rebuild or Fetch respectively before enabling the settings and requesting a restart, so the selected initialisation runs before the ordinary start-up scan. diff --git a/docs/setup_p2p.md b/docs/setup_p2p.md index 0bf049ec..fb13a9c1 100644 --- a/docs/setup_p2p.md +++ b/docs/setup_p2p.md @@ -34,7 +34,7 @@ Before starting: ![P2P local database confirmation on the first device](../images/p2p-setup/guide-p2p-setup-first-rebuild-confirmation.png) 7. Keep optional features disabled until ordinary note synchronisation works. -8. Open `Self-hosted LiveSync: P2P Sync : Open P2P Status` from the command palette. After a P2P profile exists, the P2P ribbon icon provides the same destination. Select `Open connection` if signalling is disconnected. +8. After saving the P2P profile, open `Self-hosted LiveSync: P2P Sync : Open P2P Status` from the command palette. The P2P ribbon icon provides the same destination. Select `Open connection` if signalling is disconnected. ![First P2P device connected to the signalling relay](../images/p2p-setup/guide-p2p-setup-first-device-connected.png) diff --git a/src/common/messages/LiveSyncProvisionalMessages.ts b/src/common/messages/LiveSyncProvisionalMessages.ts index 5d031336..0c9057ba 100644 --- a/src/common/messages/LiveSyncProvisionalMessages.ts +++ b/src/common/messages/LiveSyncProvisionalMessages.ts @@ -64,6 +64,8 @@ export const liveSyncProvisionalEnglishMessages = { "This file has unresolved conflicts.": "This file has unresolved conflicts.", "This file has ${COUNT} unresolved versions. They will be reviewed one pair at a time.": "This file has ${COUNT} unresolved versions. They will be reviewed one pair at a time.", + "Sync now": "Sync now", + "Apply pending changes now": "Apply pending changes now", } as const; export type LiveSyncProvisionalMessageKey = keyof typeof liveSyncProvisionalEnglishMessages; diff --git a/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts b/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts new file mode 100644 index 00000000..e09efe85 --- /dev/null +++ b/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts @@ -0,0 +1,96 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@/deps.ts", () => ({ + addIcon: vi.fn(), + diff_match_patch: class DiffMatchPatch {}, + normalizePath: vi.fn((path: string) => path), + Notice: class Notice {}, + parseYaml: vi.fn(), + Platform: {}, +})); +vi.mock("./PluginDialogModal.ts", () => ({ + PluginDialogModal: class PluginDialogModal {}, +})); +vi.mock("@/features/HiddenFileCommon/JsonResolveModal.ts", () => ({ + JsonResolveModal: class JsonResolveModal {}, +})); +vi.mock("@/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts", () => ({ + ConflictResolveModal: class ConflictResolveModal {}, +})); +vi.mock("@/features/LiveSyncCommands.ts", () => ({ + LiveSyncCommands: class LiveSyncCommands { + core!: { services: unknown }; + get services() { + return this.core.services; + } + }, +})); +vi.mock("@/common/types.ts", () => ({ + ICXHeader: "ix:", + PERIODIC_PLUGIN_SWEEP: 60, +})); +vi.mock("@/common/utils.ts", () => ({ + EVEN: Symbol("even"), + disposeMemoObject: vi.fn(), + isCustomisationSyncMetadata: vi.fn(), + isPluginMetadata: vi.fn(), + memoIfNotExist: vi.fn(), + memoObject: vi.fn(), + retrieveMemoObject: vi.fn(), + scheduleTask: vi.fn(), +})); +vi.mock("@/common/PeriodicProcessor.ts", () => ({ + PeriodicProcessor: class PeriodicProcessor {}, +})); +vi.mock("@/common/events.ts", () => ({ + EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG: "open-plugin-sync", + eventHub: { + onEvent: vi.fn(), + }, +})); +vi.mock("@/common/translation", () => ({ + $msg: vi.fn((message: string) => message), +})); +vi.mock("@/common/obsidianCommunityPlugins.ts", () => ({ + getObsidianCommunityPluginManager: vi.fn(), +})); + +import { ConfigSync } from "./CmdConfigSync"; + +describe("ConfigSync commands", () => { + it("shows the Customisation Sync command only whilst the feature is enabled", () => { + const commands: Array<{ + id: string; + checkCallback?: (checking: boolean) => boolean | void; + }> = []; + const settings = { + usePluginSync: false, + }; + const showPluginSyncModal = vi.fn(); + const configSync = Object.create(ConfigSync.prototype) as ConfigSync; + Object.assign(configSync, { + core: { + settings, + services: { + API: { + addCommand: vi.fn((command) => commands.push(command)), + }, + }, + }, + addRibbonIcon: vi.fn(() => ({ + addClass: vi.fn(), + })), + showPluginSyncModal, + }); + + configSync.onload(); + + const command = commands.find(({ id }) => id === "livesync-plugin-dialog-ex"); + expect(command?.checkCallback?.(true)).toBe(false); + + settings.usePluginSync = true; + expect(command?.checkCallback?.(true)).toBe(true); + expect(command?.checkCallback?.(false)).toBe(true); + expect(showPluginSyncModal).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/features/ConfigSync/CmdConfigSync.ts b/src/features/ConfigSync/CmdConfigSync.ts index f5ef2c80..b4a40841 100644 --- a/src/features/ConfigSync/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -454,8 +454,14 @@ export class ConfigSync extends LiveSyncCommands { this.services.API.addCommand({ id: "livesync-plugin-dialog-ex", name: "Show customization sync dialog", - callback: () => { - this.showPluginSyncModal(); + checkCallback: (checking) => { + if (!this.isThisModuleEnabled()) { + return false; + } + if (!checking) { + this.showPluginSyncModal(); + } + return true; }, }); this.addRibbonIcon("custom-sync", $msg("cmdConfigSync.showCustomizationSync"), () => { diff --git a/src/features/HiddenFileSync/CmdHiddenFileSync.ts b/src/features/HiddenFileSync/CmdHiddenFileSync.ts index 6d1147aa..92826376 100644 --- a/src/features/HiddenFileSync/CmdHiddenFileSync.ts +++ b/src/features/HiddenFileSync/CmdHiddenFileSync.ts @@ -54,10 +54,7 @@ import { EVENT_SETTING_SAVED, eventHub } from "@/common/events.ts"; import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; import type { LiveSyncCore } from "@/main.ts"; import { tryGetFilePath } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc"; -import { - configureHiddenFileSyncMode, - type ConfigureHiddenFileSyncResult, -} from "./configureHiddenFileSyncMode.ts"; +import { configureHiddenFileSyncMode, type ConfigureHiddenFileSyncResult } from "./configureHiddenFileSyncMode.ts"; import type { OptionalSyncFeatureMode } from "@/features/optionalSyncFeatures.ts"; import { getObsidianCommunityPluginManager } from "@/common/obsidianCommunityPlugins.ts"; type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce"; @@ -109,37 +106,45 @@ export class HiddenFileSync extends LiveSyncCommands { this.services.API.addCommand({ id: "livesync-sync-internal", name: "(re)initialise hidden files between storage and database", - callback: () => { - if (this.isReady()) { + checkCallback: (checking) => { + if (!this.isManualCommandAvailable()) return false; + if (!checking) { void this.initialiseInternalFileSync("safe", true); } + return true; }, }); this.services.API.addCommand({ id: "livesync-scaninternal-storage", name: "Scan hidden file changes on the storage", - callback: () => { - if (this.isReady()) { + checkCallback: (checking) => { + if (!this.isManualCommandAvailable()) return false; + if (!checking) { void this.scanAllStorageChanges(true); } + return true; }, }); this.services.API.addCommand({ id: "livesync-scaninternal-database", name: "Scan hidden file changes on the local database", - callback: () => { - if (this.isReady()) { + checkCallback: (checking) => { + if (!this.isManualCommandAvailable()) return false; + if (!checking) { void this.scanAllDatabaseChanges(true); } + return true; }, }); this.services.API.addCommand({ id: "livesync-internal-scan-offline-changes", name: "Scan and apply all offline hidden-file changes", - callback: () => { - if (this.isReady()) { + checkCallback: (checking) => { + if (!this.isManualCommandAvailable()) return false; + if (!checking) { void this.applyOfflineChanges(true); } + return true; }, }); eventHub.onEvent(EVENT_SETTING_SAVED, () => { @@ -191,12 +196,16 @@ export class HiddenFileSync extends LiveSyncCommands { } isReady() { - if (!this._isMainReady) return false; + if (!this._isMainReady()) return false; if (this._isMainSuspended()) return false; if (!this.isThisModuleEnabled()) return false; return true; } + private isManualCommandAvailable() { + return this.settings.useAdvancedMode && this.isReady() && this._isDatabaseReady(); + } + async performStartupScan(showNotice: boolean) { await this.applyOfflineChanges(showNotice); } diff --git a/src/features/HiddenFileSync/CmdHiddenFileSync.unit.spec.ts b/src/features/HiddenFileSync/CmdHiddenFileSync.unit.spec.ts index 66d00a29..7e7685d2 100644 --- a/src/features/HiddenFileSync/CmdHiddenFileSync.unit.spec.ts +++ b/src/features/HiddenFileSync/CmdHiddenFileSync.unit.spec.ts @@ -8,13 +8,16 @@ vi.mock("@/features/HiddenFileCommon/JsonResolveModal.ts", () => ({ vi.mock("@/features/LiveSyncCommands.ts", () => ({ LiveSyncCommands: class LiveSyncCommands { plugin!: { app: unknown }; - core!: { services: unknown }; + core!: { services: unknown; settings: unknown }; get app() { return this.plugin.app; } get services() { return this.core.services; } + get settings() { + return this.core.settings; + } }, })); vi.mock("./configureHiddenFileSyncMode.ts", () => ({ @@ -25,6 +28,66 @@ import { HiddenFileSync } from "./CmdHiddenFileSync.ts"; import { configureHiddenFileSyncMode } from "./configureHiddenFileSyncMode.ts"; describe("HiddenFileSync configuration-change notices", () => { + it("shows manual Hidden File Sync commands only when the feature, Advanced mode, and runtime are ready", () => { + const commands: Array<{ + id: string; + checkCallback?: (checking: boolean) => boolean | void; + }> = []; + const settings = { + syncInternalFiles: false, + useAdvancedMode: false, + }; + const hiddenFileSync = Object.create(HiddenFileSync.prototype) as HiddenFileSync; + Object.assign(hiddenFileSync, { + core: { + settings, + services: { + API: { + addCommand: vi.fn((command) => commands.push(command)), + }, + }, + }, + _isMainReady: vi.fn(() => true), + _isMainSuspended: vi.fn(() => false), + _isDatabaseReady: vi.fn(() => true), + }); + + hiddenFileSync.onload(); + + const commandIds = [ + "livesync-sync-internal", + "livesync-scaninternal-storage", + "livesync-scaninternal-database", + "livesync-internal-scan-offline-changes", + ]; + for (const commandId of commandIds) { + const command = commands.find(({ id }) => id === commandId); + expect(command?.checkCallback?.(true)).toBe(false); + } + + settings.syncInternalFiles = true; + settings.useAdvancedMode = true; + for (const commandId of commandIds) { + const command = commands.find(({ id }) => id === commandId); + expect(command?.checkCallback?.(true)).toBe(true); + } + }); + + it("does not report Hidden File Sync as ready before the main runtime is ready", () => { + const hiddenFileSync = Object.create(HiddenFileSync.prototype) as HiddenFileSync; + Object.assign(hiddenFileSync, { + core: { + settings: { + syncInternalFiles: true, + }, + }, + _isMainReady: vi.fn(() => false), + _isMainSuspended: vi.fn(() => false), + }); + + expect(hiddenFileSync.isReady()).toBe(false); + }); + it("groups plug-in reloads and an Obsidian restart into one finished Notice", async () => { const noticeGroups = { setItem: vi.fn(), diff --git a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts index 05a32e8c..37a9c7e5 100644 --- a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts +++ b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts @@ -4,6 +4,8 @@ import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, + REMOTE_COUCHDB, + REMOTE_P2P, type DocumentID, type EntryDoc, type EntryLeaf, @@ -38,16 +40,28 @@ export class LocalDatabaseMaintenance extends LiveSyncCommands { id: "analyse-database", name: "Analyse Database Usage (advanced)", icon: "database-search", - callback: async () => { - await this.analyseDatabase(); + checkCallback: (checking) => { + if (!this.settings.useAdvancedMode || !this._isDatabaseReady()) return false; + if (!checking) { + void this.analyseDatabase(); + } + return true; }, }); this.plugin.addCommand({ id: "gc-v3", name: "Garbage Collection V3 (advanced, beta)", icon: "trash-2", - callback: async () => { - await this.gcv3(); + checkCallback: (checking) => { + const isApplicableRemote = + this.settings.remoteType === REMOTE_COUCHDB || this.settings.remoteType === REMOTE_P2P; + if (!this.settings.useEdgeCaseMode || !this._isDatabaseReady() || !isApplicableRemote) { + return false; + } + if (!checking) { + void this.gcv3(); + } + return true; }, }); eventHub.onEvent(EVENT_ANALYSE_DB_USAGE, () => this.analyseDatabase()); diff --git a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts index 6bcef72a..19be24ee 100644 --- a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts +++ b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts @@ -1,5 +1,31 @@ import { describe, expect, it, vi } from "vitest"; -import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types"; + +vi.mock("octagonal-wheels/number", () => ({ + sizeToHumanReadable: vi.fn((value: number) => `${value} B`), +})); +vi.mock("octagonal-wheels/concurrency/lock_v2", () => ({ + serialized: vi.fn((_key: string, task: () => unknown) => task()), +})); +vi.mock("octagonal-wheels/collection", () => ({ + arrayToChunkedArray: vi.fn((values: unknown[]) => [values]), +})); +vi.mock("@/features/LiveSyncCommands", () => ({ + LiveSyncCommands: class LiveSyncCommands { + core!: { settings: unknown }; + get settings() { + return this.core.settings; + } + }, +})); +vi.mock("@/common/events", () => ({ + EVENT_ANALYSE_DB_USAGE: "analyse", + EVENT_REQUEST_PERFORM_GC_V3: "gc", + eventHub: { + onEvent: vi.fn(), + }, +})); +import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { LocalDatabaseMaintenance } from "./CmdLocalDatabaseMainte"; import { ensureLocalDatabaseMaintenancePrerequisites } from "./maintenancePrerequisites"; function createPrerequisites(settingsOverride: Partial = {}) { @@ -22,6 +48,49 @@ function createPrerequisites(settingsOverride: Partial } describe("LocalDatabaseMaintenance prerequisites", () => { + it("shows database analysis in Advanced mode and Garbage Collection only in applicable Edge Case mode", () => { + const commands: Array<{ + id: string; + checkCallback?: (checking: boolean) => boolean | void; + }> = []; + const settings: { + useAdvancedMode: boolean; + useEdgeCaseMode: boolean; + remoteType: string; + } = { + useAdvancedMode: false, + useEdgeCaseMode: false, + remoteType: REMOTE_COUCHDB, + }; + const maintenance = Object.create(LocalDatabaseMaintenance.prototype) as LocalDatabaseMaintenance; + Object.assign(maintenance, { + plugin: { + addCommand: vi.fn((command) => commands.push(command)), + }, + core: { + settings, + }, + _isDatabaseReady: vi.fn(() => true), + }); + + maintenance.onload(); + + const analyse = commands.find(({ id }) => id === "analyse-database"); + const garbageCollect = commands.find(({ id }) => id === "gc-v3"); + expect(analyse?.checkCallback?.(true)).toBe(false); + expect(garbageCollect?.checkCallback?.(true)).toBe(false); + + settings.useAdvancedMode = true; + expect(analyse?.checkCallback?.(true)).toBe(true); + expect(garbageCollect?.checkCallback?.(true)).toBe(false); + + settings.useEdgeCaseMode = true; + expect(garbageCollect?.checkCallback?.(true)).toBe(true); + + settings.remoteType = REMOTE_MINIO; + expect(garbageCollect?.checkCallback?.(true)).toBe(false); + }); + it("asks to disable on-demand chunk fetching before maintenance actions", async () => { const { settings, askSelectStringDialogue, applyPartial } = createPrerequisites(); diff --git a/src/modules/essential/ModuleBasicMenu.ts b/src/modules/essential/ModuleBasicMenu.ts index 2c67ab34..5eb6d235 100644 --- a/src/modules/essential/ModuleBasicMenu.ts +++ b/src/modules/essential/ModuleBasicMenu.ts @@ -2,13 +2,14 @@ import type { LiveSyncCore } from "@/main"; import { LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger"; import { fireAndForget } from "octagonal-wheels/promises"; import { AbstractModule } from "@/modules/AbstractModule"; +import { $msg } from "@/common/translation"; // Separated Module for basic menu commands, which are not related to obsidian specific features. It is expected to be used in other platforms with minimal changes. // However, it is odd that it has here at all; it really ought to be in each respective feature. It will likely be moved eventually. Until now, addCommand pointed to Obsidian's version. export class ModuleBasicMenu extends AbstractModule { _everyOnloadStart(): Promise { this.addCommand({ id: "livesync-replicate", - name: "Replicate now", + name: $msg("Sync now"), callback: async () => { await this.services.replication.replicate(); }, @@ -56,14 +57,18 @@ export class ModuleBasicMenu extends AbstractModule { this.addCommand({ id: "livesync-scan-files", name: "Scan storage and database again", - callback: async () => { - await this.services.vault.scanVault(true); + checkCallback: (checking) => { + if (!this.settings.useAdvancedMode) return false; + if (!checking) { + fireAndForget(() => this.services.vault.scanVault(true)); + } + return true; }, }); this.addCommand({ id: "livesync-runbatch", - name: "Run pended batch processes", + name: $msg("Apply pending changes now"), callback: async () => { await this.services.fileProcessing.commitPendingFileEvents(); }, @@ -73,8 +78,12 @@ export class ModuleBasicMenu extends AbstractModule { this.addCommand({ id: "livesync-abortsync", name: "Abort synchronization immediately", - callback: () => { - this.core.replicator.terminateSync(); + checkCallback: (checking) => { + if (!this.settings.useAdvancedMode) return false; + if (!checking) { + this.core.replicator.terminateSync(); + } + return true; }, }); return Promise.resolve(true); diff --git a/src/modules/essential/ModuleBasicMenu.unit.spec.ts b/src/modules/essential/ModuleBasicMenu.unit.spec.ts new file mode 100644 index 00000000..10a175e7 --- /dev/null +++ b/src/modules/essential/ModuleBasicMenu.unit.spec.ts @@ -0,0 +1,139 @@ +import { describe, expect, it, vi } from "vitest"; +import type { Command } from "@/deps"; +import { ModuleBasicMenu } from "./ModuleBasicMenu"; + +type RegisteredCommand = Command & { + checkCallback?: (checking: boolean) => boolean | void; +}; + +function createFixture() { + const commands: RegisteredCommand[] = []; + const settings = { + liveSync: false, + useAdvancedMode: false, + enableDebugTools: false, + }; + const services = { + API: { + addLog: vi.fn(), + addCommand: vi.fn((command: RegisteredCommand) => { + commands.push(command); + return command; + }), + registerWindow: vi.fn(), + addRibbonIcon: vi.fn(), + registerProtocolHandler: vi.fn(), + }, + replication: { + replicate: vi.fn(async () => undefined), + }, + vault: { + getActiveFilePath: vi.fn((): string | null => "note.md"), + scanVault: vi.fn(async () => undefined), + }, + control: { + applySettings: vi.fn(async () => undefined), + }, + setting: { + saveSettingData: vi.fn(async () => undefined), + }, + appLifecycle: { + isSuspended: vi.fn(() => false), + setSuspended: vi.fn(), + }, + fileProcessing: { + commitPendingFileEvents: vi.fn(async () => true), + }, + UI: { + promptCopyToClipboard: vi.fn(async (_title: string, _value: string) => true), + }, + path: { + path2id: vi.fn(async () => "f:note"), + }, + }; + const core = { + settings, + _services: services, + services, + localDatabase: { + getDBEntry: vi.fn(async () => false), + localDatabase: { + get: vi.fn(async () => ({ + _id: "f:note", + _rev: "2-current", + _conflicts: [], + path: "note.md", + ctime: 100, + mtime: 200, + size: 12, + type: "plain", + children: ["h:private-chunk-id"], + eden: {}, + })), + }, + getDBEntryMeta: vi.fn(async () => ({ + _id: "f:note", + _rev: "2-current", + _conflicts: [], + path: "note.md", + ctime: 100, + mtime: 200, + size: 12, + type: "plain", + datatype: "plain", + data: "", + children: ["h:private-chunk-id"], + eden: {}, + })), + allDocsRaw: vi.fn(async () => ({ + rows: [{ id: "h:private-chunk-id", key: "h:private-chunk-id", value: { rev: "1-chunk" } }], + })), + }, + storageAccess: { + isExistsIncludeHidden: vi.fn(async () => true), + statHidden: vi.fn(async () => ({ ctime: 100, mtime: 200, size: 12, type: "file" })), + }, + replicator: { + terminateSync: vi.fn(), + }, + }; + const module = new ModuleBasicMenu(core as never); + + return { + commands, + core, + module, + services, + settings, + getCommand(id: string) { + const command = commands.find((candidate) => candidate.id === id); + expect(command, `command ${id}`).toBeDefined(); + return command!; + }, + }; +} + +describe("ModuleBasicMenu command palette", () => { + it("uses clear user-facing names without changing the established command IDs", async () => { + const fixture = createFixture(); + + await fixture.module._everyOnloadStart(); + + expect(fixture.getCommand("livesync-replicate").name).toBe("Sync now"); + expect(fixture.getCommand("livesync-runbatch").name).toBe("Apply pending changes now"); + }); + + it("keeps maintenance commands out of the normal palette", async () => { + const fixture = createFixture(); + + await fixture.module._everyOnloadStart(); + + expect(fixture.getCommand("livesync-scan-files").checkCallback?.(true)).toBe(false); + expect(fixture.getCommand("livesync-abortsync").checkCallback?.(true)).toBe(false); + + fixture.settings.useAdvancedMode = true; + expect(fixture.getCommand("livesync-scan-files").checkCallback?.(true)).toBe(true); + expect(fixture.getCommand("livesync-abortsync").checkCallback?.(true)).toBe(true); + }); + +}); diff --git a/src/modules/features/SettingDialogue/PaneSetup.ts b/src/modules/features/SettingDialogue/PaneSetup.ts index 866d403b..1fd66675 100644 --- a/src/modules/features/SettingDialogue/PaneSetup.ts +++ b/src/modules/features/SettingDialogue/PaneSetup.ts @@ -12,7 +12,7 @@ import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts import type { PageFunctions } from "./SettingPane.ts"; import { visibleOnly } from "./SettingPane.ts"; import { request } from "@/deps.ts"; -import { SetupManager, UserMode } from "@/modules/features/SetupManager.ts"; +import { SetupManager } from "@/modules/features/SetupManager.ts"; import { LiveSyncError } from "@vrtmrz/livesync-commonlib/compat/common/LSError"; import { createCoreSettingsAfterFullReset, @@ -40,8 +40,7 @@ export function paneSetup( .addButton((text) => { text.setButtonText($msg("Rerun Wizard")).onClick(async () => { const setupManager = this.core.getModule(SetupManager); - await setupManager.onOnboard(UserMode.ExistingUser); - // await this.plugin.moduleSetupObsidian.onBoardingWizard(true); + await setupManager.startOnBoarding(); }); }); diff --git a/src/serviceFeatures/setupObsidian/qrCode.ts b/src/serviceFeatures/setupObsidian/qrCode.ts index 6498c1fb..844719c2 100644 --- a/src/serviceFeatures/setupObsidian/qrCode.ts +++ b/src/serviceFeatures/setupObsidian/qrCode.ts @@ -65,7 +65,11 @@ export function useSetupQRCodeFeature(host: NecessaryServices<"API" | "UI" | "se host.services.API.addCommand({ id: "livesync-setting-qr", name: "Show settings as a QR code", - callback: () => fireAndForget(encodeSetupSettingsAsQR(host)), + checkCallback: (checking) => { + if (!host.services.setting.currentSettings().isConfigured) return false; + if (!checking) fireAndForget(encodeSetupSettingsAsQR(host)); + return true; + }, }); host.services.context.events.onEvent(EVENT_REQUEST_SHOW_SETUP_QR, () => fireAndForget(() => encodeSetupSettingsAsQR(host)) diff --git a/src/serviceFeatures/setupObsidian/qrCode.unit.spec.ts b/src/serviceFeatures/setupObsidian/qrCode.unit.spec.ts index 26f7884d..61244f7c 100644 --- a/src/serviceFeatures/setupObsidian/qrCode.unit.spec.ts +++ b/src/serviceFeatures/setupObsidian/qrCode.unit.spec.ts @@ -114,4 +114,44 @@ describe("setupObsidian/qrCode", () => { ); expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_SHOW_SETUP_QR, expect.any(Function)); }); + + it("keeps the QR command out of the palette until setup is complete", async () => { + const addHandler = vi.fn(); + const commands: Array<{ + id: string; + checkCallback?: (checking: boolean) => boolean | void; + }> = []; + const settings = { isConfigured: false }; + const host = { + services: { + context: createServiceContext(), + API: { + addCommand: vi.fn((command) => commands.push(command)), + }, + appLifecycle: { + onLoaded: { + addHandler, + }, + }, + setting: { + currentSettings: vi.fn(() => settings), + }, + UI: { + confirm: { + confirmWithMessage: vi.fn(), + }, + }, + }, + } as any; + + useSetupQRCodeFeature(host); + const loadedHandler = addHandler.mock.calls[0][0] as () => Promise; + await loadedHandler(); + + const command = commands.find((candidate) => candidate.id === "livesync-setting-qr")!; + expect(command.checkCallback?.(true)).toBe(false); + + settings.isConfigured = true; + expect(command.checkCallback?.(true)).toBe(true); + }); }); diff --git a/src/serviceFeatures/setupObsidian/setupManagerHandlers.ts b/src/serviceFeatures/setupObsidian/setupManagerHandlers.ts index b12ae1da..d745a563 100644 --- a/src/serviceFeatures/setupObsidian/setupManagerHandlers.ts +++ b/src/serviceFeatures/setupObsidian/setupManagerHandlers.ts @@ -47,11 +47,6 @@ export function useSetupManagerHandlersFeature( setupManager: SetupManager ) { host.services.appLifecycle.onLoaded.addHandler(() => { - host.services.API.addCommand({ - id: "livesync-open-onboarding", - name: "Open onboarding wizard", - callback: () => fireAndForget(() => openOnboarding(setupManager)), - }); host.services.API.addCommand({ id: "livesync-opensetupuri", name: "Use the copied setup URI (Formerly Open setup URI)", diff --git a/src/serviceFeatures/setupObsidian/setupManagerHandlers.unit.spec.ts b/src/serviceFeatures/setupObsidian/setupManagerHandlers.unit.spec.ts index a5493495..bdae06ca 100644 --- a/src/serviceFeatures/setupObsidian/setupManagerHandlers.unit.spec.ts +++ b/src/serviceFeatures/setupObsidian/setupManagerHandlers.unit.spec.ts @@ -109,7 +109,7 @@ describe("setupObsidian/setupManagerHandlers", () => { expect(preventDefault).toHaveBeenCalledOnce(); }); - it("useSetupManagerHandlersFeature should register onLoaded handler that wires command and events", async () => { + it("keeps onboarding out of the command palette while wiring the setup URI command and events", async () => { const addHandler = vi.fn(); const addCommand = vi.fn(); const events = { onEvent: vi.fn() }; @@ -147,10 +147,9 @@ describe("setupObsidian/setupManagerHandlers", () => { const loadedHandler = addHandler.mock.calls[0][0] as () => Promise; await loadedHandler(); - expect(addCommand).toHaveBeenCalledWith( + expect(addCommand).not.toHaveBeenCalledWith( expect.objectContaining({ id: "livesync-open-onboarding", - name: "Open onboarding wizard", }) ); expect(addCommand).toHaveBeenCalledWith( diff --git a/src/serviceFeatures/setupObsidian/setupUri.ts b/src/serviceFeatures/setupObsidian/setupUri.ts index 9a8d8c65..a4fe4aec 100644 --- a/src/serviceFeatures/setupObsidian/setupUri.ts +++ b/src/serviceFeatures/setupObsidian/setupUri.ts @@ -50,19 +50,33 @@ export function useSetupURIFeature(host: NecessaryServices<"API" | "UI" | "setti host.services.API.addCommand({ id: "livesync-copysetupuri", name: "Copy settings as a new setup URI", - callback: () => fireAndForget(copySetupURI(host, log)), + checkCallback: (checking) => { + if (!host.services.setting.currentSettings().isConfigured) return false; + if (!checking) fireAndForget(copySetupURI(host, log)); + return true; + }, }); host.services.API.addCommand({ id: "livesync-copysetupuri-short", name: "Copy settings as a new setup URI (With customization sync)", - callback: () => fireAndForget(copySetupURI(host, log, false)), + checkCallback: (checking) => { + const settings = host.services.setting.currentSettings(); + if (!settings.isConfigured || !settings.usePluginSync) return false; + if (!checking) fireAndForget(copySetupURI(host, log, false)); + return true; + }, }); host.services.API.addCommand({ id: "livesync-copysetupurifull", name: "Copy settings as a new setup URI (Full)", - callback: () => fireAndForget(copySetupURIFull(host, log)), + checkCallback: (checking) => { + const settings = host.services.setting.currentSettings(); + if (!settings.isConfigured || !settings.useAdvancedMode) return false; + if (!checking) fireAndForget(copySetupURIFull(host, log)); + return true; + }, }); host.services.context.events.onEvent(EVENT_REQUEST_COPY_SETUP_URI, () => diff --git a/src/serviceFeatures/setupObsidian/setupUri.unit.spec.ts b/src/serviceFeatures/setupObsidian/setupUri.unit.spec.ts index 91b280a0..27ec04ef 100644 --- a/src/serviceFeatures/setupObsidian/setupUri.unit.spec.ts +++ b/src/serviceFeatures/setupObsidian/setupUri.unit.spec.ts @@ -156,4 +156,57 @@ describe("setupObsidian/setupUri", () => { expect(addCommand).toHaveBeenCalledWith(expect.objectContaining({ id: "livesync-copysetupurifull" })); expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_COPY_SETUP_URI, expect.any(Function)); }); + + it("shows Setup URI variants only when their configuration level is relevant", async () => { + const addHandler = vi.fn(); + const commands: Array<{ + id: string; + checkCallback?: (checking: boolean) => boolean | void; + }> = []; + const settings = { + isConfigured: false, + usePluginSync: false, + useAdvancedMode: false, + }; + const host = { + services: { + context: createServiceContext(), + API: { + addCommand: vi.fn((command) => commands.push(command)), + addLog: vi.fn(), + }, + appLifecycle: { + onLoaded: { + addHandler, + }, + }, + setting: { + currentSettings: vi.fn(() => settings), + }, + UI: { + confirm: { + askString: vi.fn(() => "pass"), + }, + promptCopyToClipboard: vi.fn(() => true), + }, + }, + } as any; + + useSetupURIFeature(host); + const loadedHandler = addHandler.mock.calls[0][0] as () => Promise; + await loadedHandler(); + + const command = (id: string) => commands.find((candidate) => candidate.id === id)!; + expect(command("livesync-copysetupuri").checkCallback?.(true)).toBe(false); + + settings.isConfigured = true; + expect(command("livesync-copysetupuri").checkCallback?.(true)).toBe(true); + expect(command("livesync-copysetupuri-short").checkCallback?.(true)).toBe(false); + expect(command("livesync-copysetupurifull").checkCallback?.(true)).toBe(false); + + settings.usePluginSync = true; + settings.useAdvancedMode = true; + expect(command("livesync-copysetupuri-short").checkCallback?.(true)).toBe(true); + expect(command("livesync-copysetupurifull").checkCallback?.(true)).toBe(true); + }); }); diff --git a/src/serviceFeatures/useP2PReplicatorUI.ts b/src/serviceFeatures/useP2PReplicatorUI.ts index 418c41fa..a30f6182 100644 --- a/src/serviceFeatures/useP2PReplicatorUI.ts +++ b/src/serviceFeatures/useP2PReplicatorUI.ts @@ -67,7 +67,12 @@ export function useP2PReplicatorUI( showWindow: (type: string) => Promise; showWindowOnRight?: (type: string) => Promise; registerWindow: (type: string, factory: (leaf: WorkspaceLeaf) => unknown) => void; - addCommand: (command: { id: string; name: string; callback: () => void }) => unknown; + addCommand: (command: { + id: string; + name: string; + callback?: () => void; + checkCallback?: (checking: boolean) => boolean | void; + }) => unknown; addRibbonIcon: ( icon: string, title: string, @@ -146,8 +151,12 @@ export function useP2PReplicatorUI( api.addCommand({ id: "open-p2p-server-status", name: "P2P Sync : Open P2P Status", - callback: () => { - void openStatusPane(); + checkCallback: (checking) => { + if (!hasP2PConfiguration(host.services.setting.currentSettings())) return false; + if (!checking) { + void openStatusPane(); + } + return true; }, }); host.services.API.addCommand({ @@ -155,11 +164,15 @@ export function useP2PReplicatorUI( name: "Replicate P2P to default peer", checkCallback: (isChecking: boolean) => { const settings = host.services.setting.currentSettings(); - if (isChecking) { - if (settings.remoteType == REMOTE_P2P) return false; - return replicator.replicator?.server?.isServing ?? false; + const isAvailable = + hasP2PConfiguration(settings) && + settings.remoteType !== REMOTE_P2P && + (replicator.replicator?.server?.isServing ?? false); + if (!isAvailable) return false; + if (!isChecking) { + runOpenReplication(); } - runOpenReplication(); + return true; }, }); host.services.API.addCommand({ @@ -167,11 +180,15 @@ export function useP2PReplicatorUI( name: "Replicate now by P2P", checkCallback: (isChecking: boolean) => { const settings = host.services.setting.currentSettings(); - if (isChecking) { - if (settings.remoteType == REMOTE_P2P) return false; - return replicator.replicator?.server?.isServing ?? false; + const isAvailable = + hasP2PConfiguration(settings) && + settings.remoteType !== REMOTE_P2P && + (replicator.replicator?.server?.isServing ?? false); + if (!isAvailable) return false; + if (!isChecking) { + runOpenReplication(); } - runOpenReplication(); + return true; }, }); @@ -179,10 +196,14 @@ export function useP2PReplicatorUI( id: "p2p-sync-targets", name: "P2P: Sync with targets", checkCallback: (isChecking: boolean) => { - if (isChecking) { - return replicator.replicator?.server?.isServing ?? false; + const isAvailable = + hasP2PConfiguration(host.services.setting.currentSettings()) && + (replicator.replicator?.server?.isServing ?? false); + if (!isAvailable) return false; + if (!isChecking) { + void replicator.replicator?.replicateFromCommand(true); } - void replicator.replicator?.replicateFromCommand(true); + return true; }, }); diff --git a/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts b/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts index eaaf8950..b37e3944 100644 --- a/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts +++ b/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts @@ -85,7 +85,12 @@ describe("useP2PReplicatorUI commands", () => { onSettingLoaded: { addHandler: vi.fn() }, onLayoutReady: { addHandler: vi.fn() }, }, - setting: { currentSettings: vi.fn(() => ({ remoteType: "COUCHDB" })) }, + setting: { + currentSettings: vi.fn(() => ({ + remoteType: "COUCHDB", + P2P_Enabled: true, + })), + }, replicator: { runFiniteReplicationActivity }, }, } as any; @@ -143,7 +148,11 @@ describe("useP2PReplicatorUI commands", () => { }); it("retains only the current P2P status command and routes existing open requests to it", async () => { - const commands: Array<{ id: string; callback?: () => void }> = []; + const commands: Array<{ + id: string; + callback?: () => void; + checkCallback?: (checking: boolean) => boolean | void; + }> = []; let initialise: (() => Promise) | undefined; const showWindow = vi.fn(async () => undefined); const showWindowOnRight = vi.fn(async () => undefined); @@ -183,12 +192,90 @@ describe("useP2PReplicatorUI commands", () => { expect(commands.map((command) => command.id)).not.toContain("open-p2p-replicator"); expect(commands.map((command) => command.id)).toContain("open-p2p-server-status"); + expect(commands.find((command) => command.id === "open-p2p-server-status")?.checkCallback?.(true)).toBe(false); eventHub.emitEvent(EVENT_REQUEST_OPEN_P2P); await vi.waitFor(() => expect(showWindowOnRight).toHaveBeenCalledWith("p2p-status")); expect(showWindow).not.toHaveBeenCalledWith("p2p"); }); + it("shows P2P commands only when a P2P configuration exists and their runtime prerequisites are met", async () => { + const commands: Array<{ + id: string; + checkCallback?: (checking: boolean) => boolean | void; + }> = []; + let initialise: (() => Promise) | undefined; + let settings: Record = { + remoteType: "COUCHDB", + remoteConfigurations: {}, + }; + const host = { + services: { + context: createServiceContext(), + API: { + showWindow: vi.fn(async () => undefined), + showWindowOnRight: vi.fn(async () => undefined), + registerWindow: vi.fn(), + addCommand: vi.fn((command) => commands.push(command)), + addRibbonIcon: vi.fn(), + }, + appLifecycle: { + onInitialise: { + addHandler: vi.fn((handler) => { + initialise = handler; + }), + }, + onSettingLoaded: { addHandler: vi.fn() }, + onLayoutReady: { addHandler: vi.fn() }, + }, + setting: { + currentSettings: vi.fn(() => settings), + onSettingSaved: { addHandler: vi.fn() }, + }, + replicator: { runFiniteReplicationActivity: vi.fn() }, + }, + } as any; + const p2p = { + replicator: { + server: { isServing: true }, + openReplication: vi.fn(), + replicateFromCommand: vi.fn(), + }, + } as any; + + useP2PReplicatorUI(host, {} as any, p2p); + await initialise?.(); + + for (const commandId of [ + "open-p2p-server-status", + "replicate-now-by-p2p-default-peer", + "replicate-now-by-p2p", + "p2p-sync-targets", + ]) { + expect(commands.find(({ id }) => id === commandId)?.checkCallback?.(true)).toBe(false); + } + + settings = { + ...settings, + remoteConfigurations: { + peer: { + id: "peer", + name: "Peer", + uri: "sls+p2p://room?passphrase=secret", + isEncrypted: false, + }, + }, + }; + for (const commandId of [ + "open-p2p-server-status", + "replicate-now-by-p2p-default-peer", + "replicate-now-by-p2p", + "p2p-sync-targets", + ]) { + expect(commands.find(({ id }) => id === commandId)?.checkCallback?.(true)).toBe(true); + } + }); + it("does not open the P2P status pane automatically when the workspace becomes ready", async () => { let layoutReady: (() => Promise) | undefined; const showWindow = vi.fn(async () => undefined); diff --git a/styles.css b/styles.css index 3f315599..273fe736 100644 --- a/styles.css +++ b/styles.css @@ -355,6 +355,10 @@ body { justify-content: center; } +body:not(.is-mobile):has(.sls-setting) .notice:has(.sls-onboarding-invitation-action) { + margin-right: 96px; +} + .sls-review-harness { box-sizing: border-box; max-width: 100%; diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index e0069e12..06b05ac2 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -81,7 +81,7 @@ The underlying `test:e2e:obsidian:` scripts remain available for an im `test:contract:context:cli` builds the Node CLI and runs its existing Deno setup, put, read, list, information, remove, conflict-resolution, and revision workflow. `test:contract:context:obsidian` builds the plug-in and runs the real-Obsidian smoke test, including the Context inspection. These runtime scripts are local validation entry points and are not added to the default CI gate by this change. -`test:e2e:obsidian:onboarding-invitation` starts an unconfigured temporary Vault with no plug-in data and verifies that startup selects Commonlib's new-Vault recommendations, offers the setup wizard without opening it, and does not scan Vault files automatically. It checks the invitation action and introduction in mobile test mode, then uses the permanent command to reopen the wizard on the desktop. This scenario owns the unconfigured-startup boundary only; configured compatibility review remains covered by `settings-ui`, and the setup workflows remain covered by their dedicated scenarios. +`test:e2e:obsidian:onboarding-invitation` starts an unconfigured temporary Vault with no plug-in data and verifies that startup selects Commonlib's new-Vault recommendations, offers the setup wizard without opening it, and does not scan Vault files automatically. It checks the invitation action and introduction in mobile test mode, then reopens the wizard from **Self-hosted LiveSync settings** → **Setup** on the desktop. This scenario owns the unconfigured-startup boundary only; configured compatibility review remains covered by `settings-ui`, and the setup workflows remain covered by their dedicated scenarios. `test:e2e:obsidian:dialog-mounts` starts a temporary real Obsidian session and exercises remote selection and CouchDB settings through `SetupManager`, plus Setup URI entry through the registered command. It verifies the compatibility pause and remote-size review, the distinction between a central data-storage server and P2P signalling, the explicit tested and untested CouchDB save actions, the internal-API warning, the Setup URI controls, automatic adjustment when differences are limited to compatible chunk settings, and both manual configuration-mismatch routes. The same session opens the live log and generated full report, reaches the `Hatch` recovery controls, writes and removes its own persistent log, and runs the missing-chunk recreation and file-verification actions against the empty disposable Vault. It captures representative desktop and mobile dialogues, checks the mobile layout and vertically stacked actions, closes each route through its normal controls, and verifies that each mounted operation settles without an error. It does not apply a remote configuration, contact a remote service, or claim to repair a deliberately damaged database. diff --git a/test/e2e-obsidian/scripts/onboarding-invitation.ts b/test/e2e-obsidian/scripts/onboarding-invitation.ts index 82818632..8367fb82 100644 --- a/test/e2e-obsidian/scripts/onboarding-invitation.ts +++ b/test/e2e-obsidian/scripts/onboarding-invitation.ts @@ -26,7 +26,10 @@ type UnconfiguredStartupEvidence = { }; type ObsidianTestApp = { - commands?: { executeCommandById(commandId: string): boolean }; + setting?: { + open(): void; + openTabById(tabId: string): void; + }; }; type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp }; @@ -151,14 +154,38 @@ async function captureAndCloseIntro(filename: string, mobile: boolean): Promise< return screenshot; } -async function openPermanentCommand(): Promise { - const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { - return await page.evaluate( - (commandId) => (globalThis as ObsidianTestGlobal).app?.commands?.executeCommandById(commandId) === true, - "obsidian-livesync:livesync-open-onboarding" - ); +async function openOnboardingFromSettings(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.evaluate(() => { + const setting = (globalThis as ObsidianTestGlobal).app?.setting; + if (setting === undefined) throw new Error("Obsidian settings are unavailable"); + setting.open(); + setting.openTabById("obsidian-livesync"); + }); + + const liveSyncSettings = page.locator(".sls-setting"); + await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await liveSyncSettings.locator('.sls-setting-menu-btn[title="Setup"]').click({ timeout: uiTimeoutMs }); + + const onboardingSetting = liveSyncSettings.locator(".setting-item").filter({ + has: page.locator(".setting-item-name").filter({ hasText: "Rerun Onboarding Wizard" }), + }); + await onboardingSetting.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await onboardingSetting + .getByRole("button", { name: "Rerun Wizard", exact: true }) + .click({ timeout: uiTimeoutMs }); + await onboardingDialogue(page).waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); +} + +async function closeSettings(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const settingsContainer = page.locator(".modal-container").filter({ + has: page.locator(".sls-setting"), + }); + await settingsContainer.locator(".modal-close-button").click({ timeout: uiTimeoutMs }); + await settingsContainer.waitFor({ state: "hidden", timeout: uiTimeoutMs }); }); - if (!opened) throw new Error("The permanent onboarding command was not registered."); } async function main(): Promise { @@ -190,8 +217,9 @@ async function main(): Promise { console.log(`Fresh Vault startup evidence: ${JSON.stringify(evidence)}`); const desktopInvitation = await captureDesktopInvitation(); - await openPermanentCommand(); - const commandIntro = await captureAndCloseIntro("onboarding-intro-command-desktop.png", false); + await openOnboardingFromSettings(); + const settingsIntro = await captureAndCloseIntro("onboarding-intro-settings-desktop.png", false); + await closeSettings(); const mobileInvitation = await captureAndSelectMobileInvitation(); const mobileIntro = await captureAndCloseIntro("onboarding-intro-mobile.png", true); @@ -200,7 +228,7 @@ async function main(): Promise { desktopInvitation, mobileInvitation, mobileIntro, - commandIntro, + settingsIntro, ].join(", ")}` ); } finally { diff --git a/updates.md b/updates.md index 4b3bd275..27f20885 100644 --- a/updates.md +++ b/updates.md @@ -14,6 +14,7 @@ Earlier releases remain available in the 0.25 release history and the legacy rel ### Improved +- Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands keep their identifiers, so hotkeys already assigned to them continue to work. The onboarding wizard can be reopened from **Self-hosted LiveSync settings** → **Setup**. - Enabling Hidden File Sync now opens one progress Notice before its setting is saved and reuses that Notice throughout the initial file scan, instead of stacking separate phase and restart Notices. - P2P is now presented only after it has been configured: its status pane no longer opens at start-up, its ribbon icon remains hidden for CouchDB-only Vaults, and the retired P2P pane command has been removed. The current pane distinguishes announcing changes, following a peer, and persistent per-device actions. Setup and guidance now distinguish the required signalling relay from optional TURN, and describe the public signalling relay's privacy and availability limits. - First-device P2P setup now accepts a successfully opened signalling room without requiring another peer to be online. Additional-device Fetch still requires selecting a source peer and completing `P2P Rebuild`. From 658ad6dfe4e46810bd8775bbadbf9da7e8ddba28 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 24 Jul 2026 16:15:02 +0000 Subject: [PATCH 091/117] Describe saved connections consistently --- docs/adr/2026_07_multiple_remote_onboarding.md | 4 ++-- docs/settings.md | 6 +++--- src/common/messages/LiveSyncProvisionalMessages.ts | 2 ++ src/common/translation.unit.spec.ts | 2 ++ .../features/SettingDialogue/PaneRemoteConfig.ts | 4 ++-- test/e2e-obsidian/scripts/settings-ui.ts | 11 +++++++++++ 6 files changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/adr/2026_07_multiple_remote_onboarding.md b/docs/adr/2026_07_multiple_remote_onboarding.md index db468c0d..ff07cdec 100644 --- a/docs/adr/2026_07_multiple_remote_onboarding.md +++ b/docs/adr/2026_07_multiple_remote_onboarding.md @@ -65,7 +65,7 @@ The special meaning would duplicate `activeConfigurationId`, make a user-visible ### Add profile naming and full list editing to onboarding -That would make the first-run path longer and duplicate the established Remote Databases interface. Automatic descriptive names and later renaming keep this change limited to data integrity and consistent selection. +That would make the first-run path longer and duplicate the established Saved connections interface. Automatic descriptive names and later renaming keep this change limited to data integrity and consistent selection. ### Replace the compatibility fields immediately @@ -81,7 +81,7 @@ The real-Obsidian onboarding E2E owns the invitation, dialogue presentation, saf ## Consequences -- Manual onboarding and the Remote Databases pane share one Commonlib profile contract. +- Manual onboarding and the Saved connections list share one Commonlib profile contract. - Existing profiles survive reconfiguration, and a newly configured connection becomes explicitly selectable. - Modern imports retain user-assigned profile identity and names. - Legacy Setup URIs continue to work through an isolated compatibility boundary. diff --git a/docs/settings.md b/docs/settings.md index b371d330..89836438 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -169,9 +169,9 @@ Show verbose log. Please enable when you report the logs ## 3. Remote Configuration -### 1. Remote Server +### 1. Connection settings -Self-hosted LiveSync supports multiple remote connection profiles under **Remote Server** -> **Remote Databases**. This allows you to save and switch between multiple databases or bucket configurations in a single vault. +Self-hosted LiveSync stores multiple remote connection profiles under **Connection settings** → **Saved connections**. Each profile represents a CouchDB database, an Object Storage connection, or a P2P configuration, and several profiles can be kept in one Vault. Each profile has an opaque identifier and a presentation name. The name does not need to be unique and is not used to select the profile. The main remote and the P2P remote are selected independently, so code and settings imports must preserve both selections rather than relying on a special identifier such as `default`. @@ -185,7 +185,7 @@ Each profile has an opaque identifier and a presentation name. The name does not Setting key: remoteType -The active remote server type. This is automatically projected to the legacy configuration when you activate a connection profile. +The active connection type. This is automatically projected to the legacy configuration when you activate a connection profile. ### 2. Notification diff --git a/src/common/messages/LiveSyncProvisionalMessages.ts b/src/common/messages/LiveSyncProvisionalMessages.ts index 0c9057ba..ad064588 100644 --- a/src/common/messages/LiveSyncProvisionalMessages.ts +++ b/src/common/messages/LiveSyncProvisionalMessages.ts @@ -66,6 +66,8 @@ export const liveSyncProvisionalEnglishMessages = { "This file has ${COUNT} unresolved versions. They will be reviewed one pair at a time.", "Sync now": "Sync now", "Apply pending changes now": "Apply pending changes now", + "Connection settings": "Connection settings", + "Saved connections": "Saved connections", } as const; export type LiveSyncProvisionalMessageKey = keyof typeof liveSyncProvisionalEnglishMessages; diff --git a/src/common/translation.unit.spec.ts b/src/common/translation.unit.spec.ts index 45fb4719..f2da8cee 100644 --- a/src/common/translation.unit.spec.ts +++ b/src/common/translation.unit.spec.ts @@ -30,6 +30,8 @@ describe("LiveSync-owned translation catalogue", () => { it("uses LiveSync-owned provisional English without extending Commonlib's message contract", () => { expect($msg("This file has unresolved conflicts.")).toBe("This file has unresolved conflicts."); expect($msg("More actions for ${DEVICE}", { DEVICE: "phone" })).toBe("More actions for phone"); + expect($msg("Connection settings")).toBe("Connection settings"); + expect($msg("Saved connections")).toBe("Saved connections"); expect( $msg("This file has ${COUNT} unresolved versions. They will be reviewed one pair at a time.", { COUNT: "3", diff --git a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts index 4b1e9504..e3ae96f5 100644 --- a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts +++ b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts @@ -133,8 +133,8 @@ export function paneRemoteConfig( } { // TODO: very WIP. need to refactor the UI. - void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleRemoteServer"), () => {}).then((paneEl) => { - const actions = new Setting(paneEl).setName("Remote Databases"); + void addPanel(paneEl, $msg("Connection settings"), () => {}).then((paneEl) => { + const actions = new Setting(paneEl).setName($msg("Saved connections")); // actions.addButton((button) => // button // .setButtonText("Change Remote and Setup") diff --git a/test/e2e-obsidian/scripts/settings-ui.ts b/test/e2e-obsidian/scripts/settings-ui.ts index a0f1c791..93b85786 100644 --- a/test/e2e-obsidian/scripts/settings-ui.ts +++ b/test/e2e-obsidian/scripts/settings-ui.ts @@ -214,6 +214,17 @@ async function verifyEffectiveSettings(): Promise { throw new Error("The Change Log still contains a compatibility or release-note acknowledgement control."); } + await liveSyncSettings.locator('.sls-setting-menu-btn[title="Remote Configuration"]').click(); + const connectionPanel = liveSyncSettings + .locator("h4.sls-setting-panel-title") + .filter({ hasText: "Connection settings" }) + .locator(".."); + await connectionPanel.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await connectionPanel.getByText("Saved connections", { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await liveSyncSettings.locator('.sls-setting-menu-btn[title="Sync Settings"]').click(); const deletionPanel = liveSyncSettings .locator("h4.sls-setting-panel-title") From 07cba4ce83ef37d0ced1307ff47c6305be39b177 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 24 Jul 2026 16:15:46 +0000 Subject: [PATCH 092/117] Keep unreadable revisions for explicit repair --- docs/settings.md | 16 +- docs/specs_conflict_resolution.md | 20 +- docs/troubleshooting.md | 15 +- package.json | 1 + .../messages/LiveSyncProvisionalMessages.ts | 50 +- .../coreFeatures/ModuleConflictResolver.ts | 7 +- .../ModuleConflictResolver.unit.spec.ts | 23 + src/modules/essential/ModuleBasicMenu.ts | 12 +- .../essential/ModuleBasicMenu.unit.spec.ts | 31 ++ .../features/SettingDialogue/PaneHatch.ts | 499 ++++++++++++------ src/serviceFeatures/fileDatabaseInfo.ts | 454 ++++++++++++++++ .../fileDatabaseInfo.unit.spec.ts | 413 +++++++++++++++ src/serviceFeatures/fileRepair.ts | 109 ++++ src/serviceFeatures/fileRepair.unit.spec.ts | 172 ++++++ styles.css | 43 ++ test/e2e-obsidian/README.md | 5 +- test/e2e-obsidian/scripts/dialog-mounts.ts | 6 +- test/e2e-obsidian/scripts/local-suite.ts | 1 + test/e2e-obsidian/scripts/revision-repair.ts | 334 ++++++++++++ test/e2e-obsidian/scripts/run-focused.ts | 1 + updates.md | 4 +- 21 files changed, 2036 insertions(+), 180 deletions(-) create mode 100644 src/serviceFeatures/fileDatabaseInfo.ts create mode 100644 src/serviceFeatures/fileDatabaseInfo.unit.spec.ts create mode 100644 src/serviceFeatures/fileRepair.ts create mode 100644 src/serviceFeatures/fileRepair.unit.spec.ts create mode 100644 test/e2e-obsidian/scripts/revision-repair.ts diff --git a/docs/settings.md b/docs/settings.md index 89836438..c50558fe 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -698,6 +698,12 @@ Open the dialogue #### Make report to inform the issue +#### Copy database information for a file + +Select a file to copy its local database information. The command **Copy database information for the active file** performs the same inspection for the file open in the editor. + +The report includes the Vault-relative path, document and chunk identifiers, local database revisions, conflicts, and local chunk availability. It does not query the remote or include file contents. Paths and identifiers can still be private metadata, so review the report before sharing it. + #### Write logs into the file Setting key: writeLogToTheFile @@ -721,17 +727,19 @@ Stop reflecting database changes to storage files. ### 3. Recovery and Repair -#### Recreate missing chunks for all files +#### Recreate chunks for current Vault files -This will recreate chunks for all files. If there were missing chunks, this may fix the errors. +Recreate chunks from files currently present in the Vault. This can repair missing chunks for those exact current contents after they have been confirmed as authoritative. It cannot reconstruct unavailable historical or conflict content. #### Resolve All conflicted files by the newer one -Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one. +After confirmation, resolve every conflict by modification time. This logically deletes every version except the newest one. It is a destructive policy choice and cannot recover content which is already unavailable. #### Verify and repair all files -Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep. +Compare each Vault file with every current live revision in the local database. Each winner and conflict revision is shown separately with its exact revision identifier, local chunk availability, and relationship to the current Vault file. Unavailable shared ancestors are reported separately because they prevent conservative three-way merging but are not live revisions which can be discarded. + +`Retry reading revision` retries the configured chunk-retrieval path without changing the revision tree. `Discard unreadable revision` is offered only for an exact current live revision which remains unreadable; after confirmation, it creates a logical deletion for that revision. Prefer recovery from another replica or backup before discarding it. #### Check and convert non-path-obfuscated files diff --git a/docs/specs_conflict_resolution.md b/docs/specs_conflict_resolution.md index 569105c6..cbeb048c 100644 --- a/docs/specs_conflict_resolution.md +++ b/docs/specs_conflict_resolution.md @@ -46,6 +46,22 @@ The all-branch history check prevents a resolved conflict from being recreated m The compatibility implementation currently selects the newer modification time for differing binary conflicts even when the general **Always overwrite with a newer file** option is disabled. This is existing behaviour, not a new 1.0 guarantee. Changing it to explicit selection only is a separate compatibility decision. +## Unreadable revisions and repair + +A document revision can remain in the PouchDB tree while one or more chunks needed to reconstruct its content are unavailable. Missing content is not evidence that the revision is obsolete. LiveSync therefore leaves an unreadable winner or conflict revision in the tree instead of deleting it during automatic conflict processing. + +**Hatch** → **Verify and repair all files** inspects the current winner, every current conflict revision, and the nearest shared ancestor for each conflict. It reports exact revision identifiers and local chunk availability separately: + +- **Retry reading revision** attempts the configured chunk-retrieval path again. It does not change the revision tree. +- **Discard unreadable revision** is available only for a current winner or conflict revision which remains unreadable when the action is performed. It requires confirmation and creates a logical deletion for that exact revision. +- A shared ancestor is informational. An ancestor which is no longer a live revision cannot be discarded independently through this workflow. If its body is unavailable, conservative three-way merge remains disabled, although readable live revisions can still be selected manually. + +Logical deletion does not recreate missing bytes, purge the document history, or prove that the deleted version was unimportant. Another replica or backup may still contain the missing chunks. Recover from that source before discarding a revision whenever possible. + +**Recreate chunks for current Vault files** can recreate chunks only from files which are readable in the current Vault. It cannot reconstruct unique bytes from an unavailable historical or conflict revision. + +A generation-one revision has no parent. When its body is unavailable, LiveSync cannot preserve a changed Vault file as a sibling branch without inventing ancestry. It leaves the operation unresolved. Recover the missing chunks from another replica or backup, or explicitly discard that live revision. If the current Vault file is the intended replacement, it can be stored after the unreadable revision has been logically deleted. + ### Two devices independently create the same path If two devices create the same full synchronised path before either device has @@ -220,8 +236,8 @@ Do not: ## Verification -Commonlib's real-PouchDB and injected-boundary unit tests cover unequal branch lengths, exact shared ancestry, deterministic ordering of multiple live leaves, a sensible stage followed by reconstruction of a manual pair, content below a deleted losing leaf, recorded and reconstructed branch identity, ambiguous matches, conflict-time editing, logical deletion, case-only rename, cross-path rename, and safe unproven fallbacks. +Commonlib's real-PouchDB and injected-boundary unit tests cover unequal branch lengths, exact shared ancestry, deterministic ordering of multiple live leaves, a sensible stage followed by reconstruction of a manual pair, content below a deleted losing leaf, recorded and reconstructed branch identity, ambiguous matches, conflict-time editing, missing-body preservation when parent metadata is available, refusal to invent a parent for a generation-one revision, logical deletion, case-only rename, cross-path rename, and safe unproven fallbacks. LiveSync's optional real-Obsidian two-Vault checks have two scopes. `E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT=true` resolves and edits a Markdown conflict, propagates it to a Vault which still displays the deleted losing content, and requires one live result to remain. `E2E_OBSIDIAN_INCLUDE_CONFLICT_OPERATIONS=true` edits, deletes, case-renames, and cross-path-renames files while conflicts remain active; it verifies the parent revision of each resulting branch, replicates those exact trees, and confirms that the other live branches remain intact. -The focused `test:e2e:obsidian:conflict-dialog-policy` scenario creates three live versions in one real Obsidian Vault. It verifies the count warning, commits a concatenated child of the displayed winner, confirms that the untouched leaf remains as one conflict, postpones that remaining pair, restarts the isolated Obsidian profile, and confirms that only the live pair is reconstructed. It also verifies that an incoming resolution closes a stale dialogue, completes the waiting conflict operation, and clears the warning. +The focused `test:e2e:obsidian:conflict-dialog-policy` scenario creates three live versions in one real Obsidian Vault. It verifies the count warning, commits a concatenated child of the displayed winner, confirms that the untouched leaf remains as one conflict, postpones that remaining pair, restarts the isolated Obsidian profile, and confirms that only the live pair is reconstructed. It also verifies that an incoming resolution closes a stale dialogue, completes the waiting conflict operation, and clears the warning. The repair scenario removes a referenced local chunk, confirms that the exact unreadable live revision remains in the tree, and exercises explicit retry and discard controls without deleting another live revision. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 41812ed0..ee85985e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -53,9 +53,16 @@ Check Obsidian's `Detect all file extensions`, LiveSync selectors, ignore files, If the log reports missing chunks or a size mismatch: -1. restart Obsidian once to rule out an interrupted fetch; -2. on a device which has the correct file, run `Recreate missing chunks for all files`, then synchronise; and -3. if the mismatch remains, run `Verify and repair all files` from `Hatch` and review which copy is authoritative. +1. stop editing the affected file and keep a separate copy of any readable content; +2. restart Obsidian once to rule out an interrupted fetch; +3. synchronise a device or restore a backup which still has the correct content; +4. on that healthy device, run `Recreate chunks for current Vault files`, then synchronise; +5. run `Verify and repair all files` from `Hatch`; review the winner, every conflict revision, and any unavailable shared ancestor separately; and +6. use `Discard unreadable revision` only after confirming that the exact revision is no longer recoverable or wanted. + +`Retry reading revision` does not change the revision tree. `Discard unreadable revision` creates a logical deletion for one current winner or conflict revision after rechecking it. It does not purge history or reconstruct missing content. An unavailable non-live ancestor cannot be deleted through this workflow; it disables conservative three-way merge but does not prevent explicit selection between readable live revisions. + +`Recreate chunks for current Vault files` uses current Vault content. It cannot recreate unique bytes which exist only in an unreadable historical or conflict revision. ## A configuration mismatch dialogue blocks synchronisation @@ -110,6 +117,8 @@ Enable Obsidian's `Detect all file extensions`, then check LiveSync selectors, i Run `Generate full report for opening the issue with debug info` to copy the current settings summary and recent verbose log lines. Remove credentials, remote URLs, Vault names, file contents, and other private information before sharing it. +When a problem concerns one file, run **Copy database information for the active file**, or use **Hatch** → **Copy database information for a file** to select another file. The report describes this device's local database view, including the Vault-relative path, document and chunk identifiers, local database revisions, conflicts, and local chunk availability. It does not query the remote server or include file contents. Treat paths and identifiers as private metadata before sharing. + Use `Show log` for live inspection. Logs are intentionally kept in memory for a limited time to reduce accidental disclosure. Enable `Write logs into the file` only while reproducing a problem, then disable it and remove the file after review because persistent logging affects performance and may contain private data. ![Write logs into the file](../images/write_logs_into_the_file.png) diff --git a/package.json b/package.json index 00867d6b..ad11c68b 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "test:e2e:obsidian:onboarding-invitation": "tsx test/e2e-obsidian/scripts/onboarding-invitation.ts", "test:e2e:obsidian:dialog-mounts": "tsx test/e2e-obsidian/scripts/dialog-mounts.ts", "test:e2e:obsidian:conflict-dialog-policy": "tsx test/e2e-obsidian/scripts/conflict-dialog-policy.ts", + "test:e2e:obsidian:revision-repair": "tsx test/e2e-obsidian/scripts/revision-repair.ts", "test:e2e:obsidian:settings-ui": "tsx test/e2e-obsidian/scripts/settings-ui.ts", "test:e2e:obsidian:review-harness": "tsx test/e2e-obsidian/scripts/review-harness.ts", "test:e2e:obsidian:p2p-pane": "tsx test/e2e-obsidian/scripts/p2p-pane.ts", diff --git a/src/common/messages/LiveSyncProvisionalMessages.ts b/src/common/messages/LiveSyncProvisionalMessages.ts index ad064588..4618edc9 100644 --- a/src/common/messages/LiveSyncProvisionalMessages.ts +++ b/src/common/messages/LiveSyncProvisionalMessages.ts @@ -9,8 +9,7 @@ export const liveSyncProvisionalEnglishMessages = { "This first setup has several short steps because it confirms encryption, the connection method, and which device provides the initial data. Once it is complete, additional devices can reuse a Setup URI.": "This first setup has several short steps because it confirms encryption, the connection method, and which device provides the initial data. Once it is complete, additional devices can reuse a Setup URI.", - "Setup Complete: Preparing to Fetch from Another Device": - "Setup Complete: Preparing to Fetch from Another Device", + "Setup Complete: Preparing to Fetch from Another Device": "Setup Complete: Preparing to Fetch from Another Device", "The P2P connection has been configured successfully. The initial synchronisation data must now be fetched from an online source device.": "The P2P connection has been configured successfully. The initial synchronisation data must now be fetched from an online source device.", "After restarting, select an online source device for the initial Fetch. The local LiveSync database on this device will be rebuilt from that source. Unsynchronised files in this Vault may conflict with the fetched data.": @@ -66,6 +65,53 @@ export const liveSyncProvisionalEnglishMessages = { "This file has ${COUNT} unresolved versions. They will be reviewed one pair at a time.", "Sync now": "Sync now", "Apply pending changes now": "Apply pending changes now", + "Copy database information for the active file": "Copy database information for the active file", + "Copy database information for a file": "Copy database information for a file", + "Copy revision, conflict, and local chunk availability information, including document and chunk identifiers but not file contents.": + "Copy revision, conflict, and local chunk availability information, including document and chunk identifiers but not file contents.", + "Choose file": "Choose file", + "Choose a file to inspect": "Choose a file to inspect", + "Database information for ${FILE}": "Database information for ${FILE}", + "All revisions and chunk availability below are a snapshot of this device's local database; the remote is not queried. Review the Vault-relative path, document identifier, content-derived chunk identifiers, and metadata before sharing this report. File contents are omitted.": + "All revisions and chunk availability below are a snapshot of this device's local database; the remote is not queried. Review the Vault-relative path, document identifier, content-derived chunk identifiers, and metadata before sharing this report. File contents are omitted.", + "Vault file: modified ${TIME}, size ${SIZE}": "Vault file: modified ${TIME}, size ${SIZE}", + "Vault file: missing": "Vault file: missing", + "Local database document: missing": "Local database document: missing", + "${ROLE}: ${REVISION}": "${ROLE}: ${REVISION}", + "Winner revision": "Winner revision", + "Conflict revision": "Conflict revision", + "Unknown revision": "Unknown revision", + "Logical deletion": "Logical deletion", + "Readable on this device; recorded size ${RECORDED}, decoded size ${ACTUAL}": + "Readable on this device; recorded size ${RECORDED}, decoded size ${ACTUAL}", + "Unreadable on this device; ${COUNT} referenced chunks are missing or deleted": + "Unreadable on this device; ${COUNT} referenced chunks are missing or deleted", + "Matches the current Vault file": "Matches the current Vault file", + "Differs from the current Vault file": "Differs from the current Vault file", + "Retry reading revision": "Retry reading revision", + "Discard unreadable revision": "Discard unreadable revision", + "Discard database revision ${REVISION} of ${FILE}? This creates a logical deletion for that exact live revision. Missing content cannot be recovered by this action.": + "Discard database revision ${REVISION} of ${FILE}? This creates a logical deletion for that exact live revision. Missing content cannot be recovered by this action.", + "Revision metadata is unavailable on this device": "Revision metadata is unavailable on this device", + "Shared ancestor ${REVISION} is not readable on this device. Automatic three-way merging may be unavailable, but the live revisions remain available for explicit review.": + "Shared ancestor ${REVISION} is not readable on this device. Automatic three-way merging may be unavailable, but the live revisions remain available for explicit review.", + "No shared ancestor is available for this conflict. The live revisions remain available for explicit review.": + "No shared ancestor is available for this conflict. The live revisions remain available for explicit review.", + "Show revision history": "Show revision history", + "Use Vault file in local database": "Use Vault file in local database", + "Restore database winner to Vault": "Restore database winner to Vault", + "Copy database information": "Copy database information", + "Recreate chunks for current Vault files": "Recreate chunks for current Vault files", + "Recreate chunks from the files currently present in this Vault. This cannot reconstruct unavailable historical or conflict content.": + "Recreate chunks from the files currently present in this Vault. This cannot reconstruct unavailable historical or conflict content.", + "Recreate current chunks": "Recreate current chunks", + "Resolve every conflict by modification time? This logically deletes every version except the newest one and cannot recover content which is already unavailable.": + "Resolve every conflict by modification time? This logically deletes every version except the newest one and cannot recover content which is already unavailable.", + "Resolve all conflicts by the newest version": "Resolve all conflicts by the newest version", + "Verify and repair all files": "Verify and repair all files", + "Compare each Vault file with every live local-database revision. Unreadable conflict versions remain visible until you retry or explicitly discard an exact revision.": + "Compare each Vault file with every live local-database revision. Unreadable conflict versions remain visible until you retry or explicitly discard an exact revision.", + "Verify all": "Verify all", "Connection settings": "Connection settings", "Saved connections": "Saved connections", } as const; diff --git a/src/modules/coreFeatures/ModuleConflictResolver.ts b/src/modules/coreFeatures/ModuleConflictResolver.ts index 3acb0f55..85d060a1 100644 --- a/src/modules/coreFeatures/ModuleConflictResolver.ts +++ b/src/modules/coreFeatures/ModuleConflictResolver.ts @@ -86,8 +86,11 @@ export class ModuleConflictResolver extends AbstractModule { return MISSING_OR_ERROR; } if (rightLeaf == false) { - // Conflicted item could not load, delete this. - return await this.services.conflict.resolveByDeletingRevision(path, rightRev, "MISSING OLD REV"); + // A locally unreadable conflict leaf may still be recoverable from another + // replica or backup. Keep it visible for explicit repair instead of treating + // missing chunks as evidence that the branch is obsolete. + this._log(`could not read conflicted revision ${rightRev}:${path}`, LOG_LEVEL_NOTICE); + return MISSING_OR_ERROR; } const isSame = leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted; diff --git a/src/modules/coreFeatures/ModuleConflictResolver.unit.spec.ts b/src/modules/coreFeatures/ModuleConflictResolver.unit.spec.ts index c1348c43..18bfb618 100644 --- a/src/modules/coreFeatures/ModuleConflictResolver.unit.spec.ts +++ b/src/modules/coreFeatures/ModuleConflictResolver.unit.spec.ts @@ -4,6 +4,7 @@ import { DEFAULT_SETTINGS, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, + MISSING_OR_ERROR, type FilePathWithPrefix, type MetaEntry, } from "@vrtmrz/livesync-commonlib/compat/common/types"; @@ -189,6 +190,28 @@ describe("ModuleConflictResolver independent same-path creation", () => { }); describe("ModuleConflictResolver sensible merge hand-off", () => { + it("keeps an unreadable non-winner revision unresolved", async () => { + const path = "missing-conflict-body.md" as FilePathWithPrefix; + const { module, resolveByDeletingRevision, tryAutoMerge } = createModule(); + tryAutoMerge.mockResolvedValue({ + leftRev: "3-current", + rightRev: "2-unreadable", + leftLeaf: { + rev: "3-current", + data: "Readable current body\n", + ctime: 1, + mtime: 3, + deleted: false, + }, + rightLeaf: false, + }); + + const result = await module.checkConflictAndPerformAutoMerge(path); + + expect(result).toBe(MISSING_OR_ERROR); + expect(resolveByDeletingRevision).not.toHaveBeenCalled(); + }); + it("stores the merged body and removes the resolved conflict leaf", async () => { const path = "sensible.md" as FilePathWithPrefix; const { module, resolveByDeletingRevision, tryAutoMerge } = createModule(); diff --git a/src/modules/essential/ModuleBasicMenu.ts b/src/modules/essential/ModuleBasicMenu.ts index 5eb6d235..06d27450 100644 --- a/src/modules/essential/ModuleBasicMenu.ts +++ b/src/modules/essential/ModuleBasicMenu.ts @@ -3,6 +3,7 @@ import { LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger"; import { fireAndForget } from "octagonal-wheels/promises"; import { AbstractModule } from "@/modules/AbstractModule"; import { $msg } from "@/common/translation"; +import { copyFileDatabaseInfo } from "@/serviceFeatures/fileDatabaseInfo"; // Separated Module for basic menu commands, which are not related to obsidian specific features. It is expected to be used in other platforms with minimal changes. // However, it is odd that it has here at all; it really ought to be in each respective feature. It will likely be moved eventually. Until now, addCommand pointed to Obsidian's version. export class ModuleBasicMenu extends AbstractModule { @@ -16,11 +17,14 @@ export class ModuleBasicMenu extends AbstractModule { }); this.addCommand({ id: "livesync-dump", - name: "Dump information of this doc ", - callback: () => { + name: $msg("Copy database information for the active file"), + checkCallback: (checking) => { const file = this.services.vault.getActiveFilePath(); - if (!file) return; - fireAndForget(() => this.localDatabase.getDBEntry(file, {}, true, false)); + if (!file) return false; + if (!checking) { + fireAndForget(() => copyFileDatabaseInfo(this.core, file)); + } + return true; }, }); this.addCommand({ diff --git a/src/modules/essential/ModuleBasicMenu.unit.spec.ts b/src/modules/essential/ModuleBasicMenu.unit.spec.ts index 10a175e7..af5871d3 100644 --- a/src/modules/essential/ModuleBasicMenu.unit.spec.ts +++ b/src/modules/essential/ModuleBasicMenu.unit.spec.ts @@ -136,4 +136,35 @@ describe("ModuleBasicMenu command palette", () => { expect(fixture.getCommand("livesync-abortsync").checkCallback?.(true)).toBe(true); }); + it("keeps active-file database information available and opens it in a copy dialogue", async () => { + const fixture = createFixture(); + + await fixture.module._everyOnloadStart(); + + const command = fixture.getCommand("livesync-dump"); + expect(command.name).toBe("Copy database information for the active file"); + expect(command.checkCallback?.(true)).toBe(true); + + command.checkCallback?.(false); + + await vi.waitFor(() => { + expect(fixture.services.UI.promptCopyToClipboard).toHaveBeenCalledOnce(); + }); + const [title, report] = fixture.services.UI.promptCopyToClipboard.mock.calls[0]; + expect(title).toBe("Database information for note.md"); + expect(report).toContain("note.md"); + expect(report).toContain("2-current"); + expect(report).toContain("h:private-chunk-id"); + expect(report).toContain("1-chunk"); + expect(fixture.core.localDatabase.getDBEntry).not.toHaveBeenCalled(); + }); + + it("hides the active-file database report when no file is active", async () => { + const fixture = createFixture(); + fixture.services.vault.getActiveFilePath.mockReturnValue(null); + + await fixture.module._everyOnloadStart(); + + expect(fixture.getCommand("livesync-dump").checkCallback?.(true)).toBe(false); + }); }); diff --git a/src/modules/features/SettingDialogue/PaneHatch.ts b/src/modules/features/SettingDialogue/PaneHatch.ts index 3897046c..4cf67e40 100644 --- a/src/modules/features/SettingDialogue/PaneHatch.ts +++ b/src/modules/features/SettingDialogue/PaneHatch.ts @@ -3,14 +3,13 @@ import { type DocumentID, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, - type LoadedEntry, type MetaEntry, type FilePath, type EntryDoc, } from "@vrtmrz/livesync-commonlib/compat/common/types"; -import { createBlob, getFileRegExp, isDocContentSame, readAsBlob } from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import { readAsBlob } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger"; -import { addPrefix, shouldBeIgnored, stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path"; +import { shouldBeIgnored } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path"; import { $msg } from "@/common/translation"; import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; @@ -21,12 +20,24 @@ import { EVENT_REQUEST_RUN_FIX_INCOMPLETE, eventHub, } from "@/common/events.ts"; -import { ICHeader, ICXHeader, PSCHeader } from "@/common/types.ts"; +import { ICHeader } 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 { isNotFoundError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc"; +import { + chooseAndCopyFileDatabaseInfo, + collectFileDatabaseInfoPaths, + copyFileDatabaseInfo, + retryReadFileDatabaseRevision, +} from "@/serviceFeatures/fileDatabaseInfo.ts"; +import { + discardUnreadableLiveRevision, + inspectFileRepair, + type FileRepairInspection, + type FileRepairRevision, +} from "@/serviceFeatures/fileRepair.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"); @@ -67,6 +78,18 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, await this.app.commands.executeCommandById("obsidian-livesync:dump-debug-info"); }) ); + new Setting(paneEl) + .setName($msg("Copy database information for a file")) + .setDesc( + $msg( + "Copy revision, conflict, and local chunk availability information, including document and chunk identifiers but not file contents." + ) + ) + .addButton((button) => + button.setButtonText($msg("Choose file")).onClick(async () => { + await chooseAndCopyFileDatabaseInfo(this.core); + }) + ); new Setting(paneEl) .setName($msg("Analyse database usage")) .setDesc( @@ -99,132 +122,300 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, }); void addPanel(paneEl, "Recovery and Repair").then((paneEl) => { - const addResult = async (path: string, file: FilePathWithPrefix | false, fileOnDB: LoadedEntry | false) => { - const storageFileStat = file ? await this.core.storageAccess.statHidden(file) : null; - resultArea.appendChild( - this.createEl(resultArea, "div", {}, (el) => { - el.appendChild(this.createEl(el, "h6", { text: path })); - el.appendChild( - this.createEl(el, "div", {}, (infoGroupEl) => { - infoGroupEl.appendChild( - this.createEl(infoGroupEl, "div", { - text: `Storage : Modified: ${!storageFileStat ? `Missing:` : `${new Date(storageFileStat.mtime).toLocaleString()}, Size:${storageFileStat.size}`}`, - }) - ); - infoGroupEl.appendChild( - this.createEl(infoGroupEl, "div", { - text: `Database: Modified: ${!fileOnDB ? `Missing:` : `${new Date(fileOnDB.mtime).toLocaleString()}, Size:${fileOnDB.size} (actual size:${readAsBlob(fileOnDB).size})`}`, - }) - ); - }) + const resultArea = paneEl.createDiv({ text: "", cls: "sls-repair-results" }); + const addActionButton = ( + parent: HTMLElement, + text: string, + action: (button: HTMLButtonElement) => Promise | void, + warning = false + ) => { + this.createEl(parent, "button", { text }, (button) => { + if (warning) { + button.addClass("mod-warning"); + } + button.onClickEvent(async () => { + button.disabled = true; + try { + await action(button); + } finally { + if (button.isConnected) { + button.disabled = false; + } + } + }); + }); + }; + const storeStorageInDatabase = async (path: string): Promise => { + if (path.startsWith(".")) { + const addOn = this.core.getAddOn(HiddenFileSync.name); + if (!addOn) { + return false; + } + const file = (await addOn.scanInternalFiles()).find((entry) => entry.path === path); + if (!file) { + Logger(`Failed to find the file in the internal files: ${path}`, LOG_LEVEL_NOTICE); + return false; + } + return Boolean(await addOn.storeInternalFileToDatabase(file, true)); + } + return Boolean(await this.core.fileHandler.storeFileToDB(path as FilePath, true)); + }; + const applyWinnerToStorage = async ( + path: string, + revision: FileRepairRevision + ): Promise => { + if (revision.loadedEntry === false) { + return false; + } + if (revision.loadedEntry.path.startsWith(ICHeader)) { + const addOn = this.core.getAddOn(HiddenFileSync.name); + return addOn + ? Boolean(await addOn.extractInternalFileFromDatabase(path as FilePath, true)) + : false; + } + return Boolean(await this.core.fileHandler.dbToStorage(revision.loadedEntry as MetaEntry, null, true)); + }; + const addRepairResult = (inspection: FileRepairInspection) => { + const { information, revisions } = inspection; + const path = information.path; + const card = this.createEl(resultArea, "div", { cls: "sls-repair-result" }); + const refresh = async () => { + card.remove(); + const refreshed = await inspectFileRepair(this.core, path); + if (refreshed.requiresAttention) { + addRepairResult(refreshed); + } else { + Logger(`Verification no longer reports a problem for ${path}`, LOG_LEVEL_NOTICE); + } + }; + + this.createEl(card, "h6", { text: path }); + if (information.storage.exists) { + this.createEl(card, "div", { + text: $msg("Vault file: modified ${TIME}, size ${SIZE}", { + TIME: new Date(information.storage.mtime ?? 0).toLocaleString(), + SIZE: `${information.storage.size ?? 0}`, + }), + }); + } else { + this.createEl(card, "div", { text: $msg("Vault file: missing") }); + } + if (!information.database.exists) { + this.createEl(card, "div", { text: $msg("Local database document: missing") }); + } + + const addRevision = (revision: FileRepairRevision) => { + const { metadata } = revision; + const revisionEl = this.createEl(card, "div", { cls: "sls-repair-revision" }); + this.createEl(revisionEl, "div", { + text: $msg("${ROLE}: ${REVISION}", { + ROLE: revision.role === "winner" ? $msg("Winner revision") : $msg("Conflict revision"), + REVISION: metadata.revision ?? $msg("Unknown revision"), + }), + cls: "sls-repair-revision-title", + }); + if (metadata.deleted) { + this.createEl(revisionEl, "div", { text: $msg("Logical deletion") }); + } else if (revision.contentReadable) { + this.createEl(revisionEl, "div", { + text: $msg("Readable on this device; recorded size ${RECORDED}, decoded size ${ACTUAL}", { + RECORDED: `${metadata.recordedSize}`, + ACTUAL: `${revision.loadedEntry === false ? 0 : readAsBlob(revision.loadedEntry).size}`, + }), + }); + } else { + const missing = metadata.chunks.filter( + ({ embedded, localDatabaseState }) => + !embedded && localDatabaseState !== "available" ); - if (fileOnDB && file) { - el.appendChild( - this.createEl(el, "button", { text: "Show history" }, (buttonEl) => { - buttonEl.onClickEvent(() => { - eventHub.emitEvent(EVENT_REQUEST_SHOW_HISTORY, { - file: file, - fileOnDB: fileOnDB, - }); - }); - }) - ); + this.createEl(revisionEl, "div", { + text: $msg("Unreadable on this device; ${COUNT} referenced chunks are missing or deleted", { + COUNT: `${missing.length}`, + }), + cls: "mod-warning", + }); + if (missing.length > 0) { + this.createEl(revisionEl, "code", { + text: missing + .slice(0, 3) + .map(({ id }) => id) + .join(", ") + (missing.length > 3 ? ", …" : ""), + }); } - if (file) { - el.appendChild( - this.createEl(el, "button", { text: "Storage -> Database" }, (buttonEl) => { - buttonEl.onClickEvent(async () => { - if (file.startsWith(".")) { - const addOn = this.core.getAddOn(HiddenFileSync.name); - if (addOn) { - const file = (await addOn.scanInternalFiles()).find((e) => e.path == path); - if (!file) { - Logger( - `Failed to find the file in the internal files: ${path}`, - LOG_LEVEL_NOTICE - ); - return; - } - if (!(await addOn.storeInternalFileToDatabase(file, true))) { - Logger( - `Failed to store the file to the database (Hidden file): ${file.path}`, - LOG_LEVEL_NOTICE - ); - return; - } - } - } else { - if (!(await this.core.fileHandler.storeFileToDB(file, true))) { - Logger( - `Failed to store the file to the database: ${file}`, - LOG_LEVEL_NOTICE - ); - return; + } + if (revision.contentMatchesStorage === true) { + this.createEl(revisionEl, "div", { text: $msg("Matches the current Vault file") }); + } else if (revision.contentMatchesStorage === false) { + this.createEl(revisionEl, "div", { text: $msg("Differs from the current Vault file") }); + } + + if (!metadata.deleted && !revision.contentReadable && metadata.revision) { + const actions = this.createEl(revisionEl, "div", { cls: "sls-repair-actions" }); + addActionButton(actions, $msg("Retry reading revision"), async () => { + const loaded = await retryReadFileDatabaseRevision(this.core, path, metadata.revision!); + Logger( + loaded + ? `Revision ${metadata.revision} of ${path} is readable after retry` + : `Revision ${metadata.revision} of ${path} remains unreadable`, + LOG_LEVEL_NOTICE + ); + await refresh(); + }); + addActionButton( + actions, + $msg("Discard unreadable revision"), + async () => { + const confirmed = + (await this.core.confirm.askYesNoDialog( + $msg( + "Discard database revision ${REVISION} of ${FILE}? This creates a logical deletion for that exact live revision. Missing content cannot be recovered by this action.", + { + REVISION: metadata.revision!, + FILE: path, } + ), + { + title: $msg("Discard unreadable revision"), + defaultOption: "No", } - el.remove(); - }); - }) - ); - } - if (fileOnDB) { - el.appendChild( - this.createEl(el, "button", { text: "Database -> Storage" }, (buttonEl) => { - buttonEl.onClickEvent(async () => { - if (fileOnDB.path.startsWith(ICHeader)) { - const addOn = this.core.getAddOn(HiddenFileSync.name); - if (addOn) { - if ( - !(await addOn.extractInternalFileFromDatabase(path as FilePath, true)) - ) { - Logger( - `Failed to store the file to the database (Hidden file): ${file}`, - LOG_LEVEL_NOTICE - ); - return; - } - } - } else { - if ( - !(await this.core.fileHandler.dbToStorage( - fileOnDB as MetaEntry, - null, - true - )) - ) { - Logger( - `Failed to store the file to the storage: ${fileOnDB.path}`, - LOG_LEVEL_NOTICE - ); - return; - } + )) === "yes"; + if (!confirmed) { + return; + } + const result = await discardUnreadableLiveRevision( + this.core, + path, + metadata.revision! + ); + Logger( + `Discard unreadable revision ${metadata.revision} of ${path}: ${result}`, + result === "discarded" ? LOG_LEVEL_NOTICE : LOG_LEVEL_VERBOSE + ); + await refresh(); + }, + true + ); + } + }; + revisions.forEach(addRevision); + + for (const revision of information.database.unavailableConflictRevisions) { + const revisionEl = this.createEl(card, "div", { cls: "sls-repair-revision" }); + this.createEl(revisionEl, "div", { + text: $msg("${ROLE}: ${REVISION}", { + ROLE: $msg("Conflict revision"), + REVISION: revision, + }), + cls: "sls-repair-revision-title", + }); + this.createEl(revisionEl, "div", { + text: $msg("Revision metadata is unavailable on this device"), + cls: "mod-warning", + }); + const actions = this.createEl(revisionEl, "div", { cls: "sls-repair-actions" }); + addActionButton(actions, $msg("Retry reading revision"), async () => { + await retryReadFileDatabaseRevision(this.core, path, revision); + await refresh(); + }); + addActionButton( + actions, + $msg("Discard unreadable revision"), + async () => { + const confirmed = + (await this.core.confirm.askYesNoDialog( + $msg( + "Discard database revision ${REVISION} of ${FILE}? This creates a logical deletion for that exact live revision. Missing content cannot be recovered by this action.", + { + REVISION: revision, + FILE: path, } - el.remove(); - }); - }) - ); + ), + { + title: $msg("Discard unreadable revision"), + defaultOption: "No", + } + )) === "yes"; + if (!confirmed) { + return; + } + await discardUnreadableLiveRevision(this.core, path, revision); + await refresh(); + }, + true + ); + } + + for (const base of information.database.mergeBases) { + if (base.contentAvailableLocally) { + continue; + } + this.createEl(card, "div", { + text: base.revision + ? $msg( + "Shared ancestor ${REVISION} is not readable on this device. Automatic three-way merging may be unavailable, but the live revisions remain available for explicit review.", + { + REVISION: base.revision, + } + ) + : $msg( + "No shared ancestor is available for this conflict. The live revisions remain available for explicit review." + ), + cls: "sls-repair-ancestor-warning", + }); + } + + const winner = revisions.find(({ role }) => role === "winner"); + const actions = this.createEl(card, "div", { cls: "sls-repair-actions" }); + if (winner?.loadedEntry && information.storage.exists) { + const winnerEntry = winner.loadedEntry; + addActionButton(actions, $msg("Show revision history"), () => { + eventHub.emitEvent(EVENT_REQUEST_SHOW_HISTORY, { + file: path as FilePathWithPrefix, + fileOnDB: winnerEntry, + }); + }); + } + if ( + information.storage.exists && + information.database.conflictCount === 0 && + (!winner || winner.contentReadable) + ) { + addActionButton(actions, $msg("Use Vault file in local database"), async () => { + if (!(await storeStorageInDatabase(path))) { + Logger(`Failed to store the Vault file in the local database: ${path}`, LOG_LEVEL_NOTICE); + return; } - return el; - }) - ); + await refresh(); + }); + } + if ( + !information.storage.exists && + information.database.conflictCount === 0 && + winner?.loadedEntry + ) { + addActionButton(actions, $msg("Restore database winner to Vault"), async () => { + if (!(await applyWinnerToStorage(path, winner))) { + Logger(`Failed to restore the database winner to the Vault: ${path}`, LOG_LEVEL_NOTICE); + return; + } + await refresh(); + }); + } + addActionButton(actions, $msg("Copy database information"), async () => { + await copyFileDatabaseInfo(this.core, path); + }); }; - const checkBetweenStorageAndDatabase = async (file: FilePathWithPrefix, fileOnDB: LoadedEntry) => { - const dataContent = readAsBlob(fileOnDB); - const content = createBlob(await this.core.storageAccess.readHiddenFileBinary(file)); - if (await isDocContentSame(content, dataContent)) { - Logger(`Compare: SAME: ${file}`); - } else { - Logger(`Compare: CONTENT IS NOT MATCHED! ${file}`, LOG_LEVEL_NOTICE); - void addResult(file, file, fileOnDB); - } - }; new Setting(paneEl) - .setName("Recreate missing chunks for all files") - .setDesc("This will recreate chunks for all files. If there were missing chunks, this may fix the errors.") + .setName($msg("Recreate chunks for current Vault files")) + .setDesc( + $msg( + "Recreate chunks from the files currently present in this Vault. This cannot reconstruct unavailable historical or conflict content." + ) + ) .addButton((button) => button - .setButtonText("Recreate all") + .setButtonText($msg("Recreate current chunks")) .setCta() .onClick(async () => { await this.core.fileHandler.createAllChunks(true); @@ -240,40 +431,40 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, .setButtonText("Resolve All") .setCta() .onClick(async () => { + const confirmed = + (await this.core.confirm.askYesNoDialog( + $msg( + "Resolve every conflict by modification time? This logically deletes every version except the newest one and cannot recover content which is already unavailable." + ), + { + title: $msg("Resolve all conflicts by the newest version"), + defaultOption: "No", + } + )) === "yes"; + if (!confirmed) { + return; + } await this.services.conflict.resolveAllConflictedFilesByNewerOnes(); }) ); new Setting(paneEl) - .setName("Verify and repair all files") + .setName($msg("Verify and repair all files")) .setDesc( - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep." + $msg( + "Compare each Vault file with every live local-database revision. Unreadable conflict versions remain visible until you retry or explicitly discard an exact revision." + ) ) .addButton((button) => button - .setButtonText("Verify all") + .setButtonText($msg("Verify all")) .setDisabled(false) .setCta() .onClick(async () => { + resultArea.replaceChildren(); Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify"); - const ignorePatterns = getFileRegExp(this.core.settings, "syncInternalFilesIgnorePatterns"); - const targetPatterns = getFileRegExp(this.core.settings, "syncInternalFilesTargetPatterns"); this.core.localDatabase.clearCaches(); - Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify"); - const files = this.core.settings.syncInternalFiles - ? await this.core.storageAccess.getFilesIncludeHidden("/", targetPatterns, ignorePatterns) - : await this.core.storageAccess.getFileNames(); - const documents = [] as FilePath[]; - - const adn = this.core.localDatabase.findAllDocs(); - for await (const i of adn) { - const path = this.services.path.getPath(i); - if (path.startsWith(ICXHeader)) continue; - if (path.startsWith(PSCHeader)) continue; - if (!this.core.settings.syncInternalFiles && path.startsWith(ICHeader)) continue; - documents.push(stripAllPrefixes(path)); - } - const allPaths = [...new Set([...documents, ...files])]; + const allPaths = await collectFileDatabaseInfoPaths(this.core); let i = 0; const incProc = () => { i++; @@ -295,28 +486,21 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, : false; const fileOnStorage = stat != null ? stat : false; if (!(await this.services.vault.isTargetFile(path))) return incProc(); - const releaser = await semaphore.acquire(1); if (fileOnStorage && this.services.vault.isFileSizeTooLarge(fileOnStorage.size)) return incProc(); + const releaser = await semaphore.acquire(1); try { - const isHiddenFile = path.startsWith("."); - const dbPath = isHiddenFile ? addPrefix(path, ICHeader) : path; - const fileOnDB = await this.core.localDatabase.getDBEntry(dbPath); - if (fileOnDB && this.services.vault.isFileSizeTooLarge(fileOnDB.size)) + const inspection = await inspectFileRepair(this.core, path); + const winner = inspection.revisions.find(({ role }) => role === "winner"); + if ( + winner && + this.services.vault.isFileSizeTooLarge(winner.metadata.recordedSize) + ) return incProc(); - - if (!fileOnDB && fileOnStorage) { - Logger(`Compare: Not found on the local database: ${path}`, LOG_LEVEL_NOTICE); - void addResult(path, path, false); - return incProc(); - } - if (fileOnDB && !fileOnStorage) { - Logger(`Compare: Not found on the storage: ${path}`, LOG_LEVEL_NOTICE); - void addResult(path, false, fileOnDB); - return incProc(); - } - if (fileOnStorage && fileOnDB) { - await checkBetweenStorageAndDatabase(path, fileOnDB); + if (inspection.requiresAttention) { + addRepairResult(inspection); + } else { + Logger(`Compare: SAME: ${path}`); } } catch (ex) { Logger(`Error while processing ${path}`, LOG_LEVEL_NOTICE); @@ -335,7 +519,6 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, // Logger(`${i}/${files.length}\n`, LOG_LEVEL_NOTICE, "verify-processed"); }) ); - const resultArea = paneEl.createDiv({ text: "" }); new Setting(paneEl) .setName("Check and convert non-path-obfuscated files") .setDesc("") diff --git a/src/serviceFeatures/fileDatabaseInfo.ts b/src/serviceFeatures/fileDatabaseInfo.ts new file mode 100644 index 00000000..40fc01e3 --- /dev/null +++ b/src/serviceFeatures/fileDatabaseInfo.ts @@ -0,0 +1,454 @@ +import { $msg } from "@/common/translation"; +import type { + FilePath, + FilePathWithPrefix, + LoadedEntry, + ObsidianLiveSyncSettings, +} from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { getFileRegExp } from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import { isNotFoundError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc"; +import { ICHeader, ICXHeader, PSCHeader } from "@vrtmrz/livesync-commonlib/compat/common/models/fileaccess.const"; +import type { StorageAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/StorageAccess"; +import type { LiveSyncLocalDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/LiveSyncLocalDB"; +import type { IPathService, IUIService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService"; +import { addPrefix, stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path"; + +type DatabaseMeta = LoadedEntry & { + _rawStorageType: string | null; + _legacyBodyPresent: boolean; + _revs_info?: Array<{ + rev: string; + status: string; + }>; +}; + +export type FileDatabaseInfoCore = { + localDatabase: Pick< + LiveSyncLocalDB, + "allDocsRaw" | "findAllDocs" | "getDBEntryFromMeta" | "getDBEntry" | "localDatabase" + >; + services: { + path: Pick; + UI: IUIService; + }; + settings: ObsidianLiveSyncSettings; + storageAccess: Pick< + StorageAccess, + "getFileNames" | "getFilesIncludeHidden" | "isExistsIncludeHidden" | "statHidden" + >; +}; + +export type RevisionDatabaseInfo = { + documentId: string; + revision: string | null; + current: boolean; + deleted: boolean; + storageType: string; + storageLayout: "chunked" | "legacy-inline"; + ctime: number; + mtime: number; + recordedSize: number; + revisionHistory: Array<{ + revision: string; + status: string; + }>; + chunkReferences: number; + uniqueChunkReferences: number; + embeddedChunkReferences: number; + locallyStoredChunkReferences: number; + contentAvailableLocally: boolean; + chunks: Array<{ + id: string; + referenceCount: number; + embedded: boolean; + storedInLocalDatabase: boolean; + localDatabaseState: "available" | "deleted" | "missing"; + localDatabaseRevision: string | null; + }>; +}; + +export type FileDatabaseMergeBaseInfo = { + winnerRevision: string; + conflictRevision: string; + revision: string | null; + metadataAvailableLocally: boolean; + contentAvailableLocally: boolean; + missingChunkIds: string[]; + unavailableSharedRevisions: string[]; +}; + +export type FileDatabaseInfo = { + path: string; + databasePath: FilePathWithPrefix | FilePath; + storage: { + exists: boolean; + ctime?: number; + mtime?: number; + size?: number; + }; + database: { + source: "local database on this device"; + remoteQueried: false; + exists: boolean; + currentRevision: string | null; + conflictCount: number; + conflictRevisions: string[]; + unavailableConflictRevisions: string[]; + revisions: RevisionDatabaseInfo[]; + mergeBases: FileDatabaseMergeBaseInfo[]; + }; +}; + +const REPORT_WARNING = + "All revisions and chunk availability below are a snapshot of this device's local database; the remote is not queried. Review the Vault-relative path, document identifier, content-derived chunk identifiers, and metadata before sharing this report. File contents are omitted."; + +function toDatabasePath(path: string): FilePathWithPrefix | FilePath { + if (path.startsWith(".")) { + return addPrefix(path as FilePath, ICHeader); + } + return path as FilePath; +} + +type RawDatabaseDocument = { + _id: string; + _rev?: string; + _conflicts?: string[]; + _deleted?: boolean; + _revs_info?: Array<{ + rev: string; + status: string; + }>; + children?: string[]; + ctime?: number; + deleted?: boolean; + data?: string | string[]; + eden?: Record; + mtime?: number; + size?: number; + type?: string; +}; + +async function getLocalDatabaseMeta( + core: FileDatabaseInfoCore, + path: FilePathWithPrefix | FilePath, + options: PouchDB.Core.GetOptions +): Promise { + const documentId = await core.services.path.path2id(path); + let raw: RawDatabaseDocument; + try { + raw = await core.localDatabase.localDatabase.get(documentId, options); + } catch (error) { + if (isNotFoundError(error)) { + return false; + } + throw error; + } + + if (raw.type === "leaf") { + return false; + } + if (raw.type && raw.type !== "notes" && raw.type !== "newnote" && raw.type !== "plain") { + return false; + } + + const rawStorageType = raw.type ?? null; + const legacy = rawStorageType === null || rawStorageType === "notes"; + const type = legacy ? "notes" : rawStorageType; + const legacyBodyPresent = + legacy && (typeof raw.data === "string" || (Array.isArray(raw.data) && raw.data.every((item) => typeof item === "string"))); + return { + _id: raw._id, + _rev: raw._rev, + _conflicts: raw._conflicts, + _revs_info: raw._revs_info, + path, + data: legacyBodyPresent ? raw.data : "", + ctime: raw.ctime ?? 0, + mtime: raw.mtime ?? 0, + size: raw.size ?? 0, + children: type === "newnote" || type === "plain" ? (raw.children ?? []) : [], + datatype: type === "newnote" ? "newnote" : "plain", + deleted: raw.deleted ?? raw._deleted, + type, + eden: raw.eden ?? {}, + _rawStorageType: rawStorageType, + _legacyBodyPresent: legacyBodyPresent, + } as DatabaseMeta; +} + +async function collectRevisionDatabaseInfo( + core: FileDatabaseInfoCore, + meta: DatabaseMeta, + current: boolean +): Promise { + const legacy = meta._rawStorageType === null || meta._rawStorageType === "notes"; + const children = legacy ? [] : "children" in meta ? meta.children : []; + const uniqueChildren = [...new Set(children)]; + const referenceCounts = new Map(); + for (const child of children) { + referenceCounts.set(child, (referenceCounts.get(child) ?? 0) + 1); + } + const embeddedChildren = new Set( + Object.keys("eden" in meta && meta.eden ? meta.eden : {}).filter((id) => uniqueChildren.includes(id)) + ); + const localRows = + uniqueChildren.length === 0 + ? [] + : ( + await core.localDatabase.allDocsRaw({ + keys: uniqueChildren, + include_docs: false, + }) + ).rows; + const localChunkStates = new Map( + localRows + .filter((row) => "value" in row) + .map( + (row) => + [ + row.key, + { + state: row.value.deleted ? ("deleted" as const) : ("available" as const), + revision: row.value.rev, + }, + ] as const + ) + ); + + return { + documentId: meta._id, + revision: meta._rev ?? null, + current, + deleted: Boolean(meta.deleted ?? meta._deleted), + storageType: meta._rawStorageType ?? "absent", + storageLayout: legacy ? "legacy-inline" : "chunked", + ctime: meta.ctime, + mtime: meta.mtime, + recordedSize: meta.size, + revisionHistory: (meta._revs_info ?? []).map(({ rev, status }) => ({ + revision: rev, + status, + })), + chunkReferences: children.length, + uniqueChunkReferences: uniqueChildren.length, + embeddedChunkReferences: children.filter((id) => embeddedChildren.has(id)).length, + locallyStoredChunkReferences: children.filter((id) => localChunkStates.get(id)?.state === "available").length, + contentAvailableLocally: legacy + ? meta._legacyBodyPresent + : uniqueChildren.every( + (id) => embeddedChildren.has(id) || localChunkStates.get(id)?.state === "available" + ), + chunks: uniqueChildren.map((id) => { + const localState = localChunkStates.get(id); + return { + id, + referenceCount: referenceCounts.get(id) ?? 0, + embedded: embeddedChildren.has(id), + storedInLocalDatabase: localState?.state === "available", + localDatabaseState: localState?.state ?? "missing", + localDatabaseRevision: localState?.revision ?? null, + }; + }), + }; +} + +function revisionHistory(meta: DatabaseMeta): Array<{ revision: string; status: string }> { + const history = (meta._revs_info ?? []).map(({ rev, status }) => ({ + revision: rev, + status, + })); + if (meta._rev && !history.some(({ revision }) => revision === meta._rev)) { + history.unshift({ + revision: meta._rev, + status: "available", + }); + } + return history; +} + +function missingChunkIds(info: RevisionDatabaseInfo): string[] { + return info.chunks + .filter(({ embedded, localDatabaseState }) => !embedded && localDatabaseState !== "available") + .map(({ id }) => id); +} + +export async function inspectFileDatabaseInfo(core: FileDatabaseInfoCore, path: string): Promise { + const storageExists = await core.storageAccess.isExistsIncludeHidden(path); + const storageStat = storageExists ? await core.storageAccess.statHidden(path) : null; + const databasePath = toDatabasePath(path); + const currentMeta = await getLocalDatabaseMeta(core, databasePath, { + conflicts: true, + revs: true, + revs_info: true, + }); + + const revisions: RevisionDatabaseInfo[] = []; + const conflictRevisions = currentMeta === false ? [] : (currentMeta._conflicts ?? []); + const unavailableConflictRevisions: string[] = []; + const mergeBases: FileDatabaseMergeBaseInfo[] = []; + const metadataByRevision = new Map(); + if (currentMeta !== false && currentMeta._rev) { + metadataByRevision.set(currentMeta._rev, currentMeta); + } + const getRevisionMeta = async (revision: string): Promise => { + const cached = metadataByRevision.get(revision); + if (cached !== undefined) { + return cached; + } + const meta = await getLocalDatabaseMeta(core, databasePath, { + rev: revision, + revs: true, + revs_info: true, + }); + metadataByRevision.set(revision, meta); + return meta; + }; + + if (currentMeta) { + revisions.push(await collectRevisionDatabaseInfo(core, currentMeta, true)); + for (const revision of conflictRevisions) { + const conflictMeta = await getRevisionMeta(revision); + if (conflictMeta) { + revisions.push(await collectRevisionDatabaseInfo(core, conflictMeta, false)); + const winnerHistory = revisionHistory(currentMeta); + const conflictHistory = revisionHistory(conflictMeta); + const conflictHistoryByRevision = new Map( + conflictHistory.map(({ revision: historyRevision, status }) => [historyRevision, status]) + ); + const sharedHistory = winnerHistory.filter(({ revision: historyRevision }) => + conflictHistoryByRevision.has(historyRevision) + ); + const sharedRevision = sharedHistory[0]?.revision ?? null; + const unavailableSharedRevisions = sharedHistory + .filter( + ({ revision: historyRevision, status }) => + status !== "available" || + conflictHistoryByRevision.get(historyRevision) !== "available" + ) + .map(({ revision: historyRevision }) => historyRevision); + const sharedMeta = sharedRevision ? await getRevisionMeta(sharedRevision) : false; + const sharedInfo = sharedMeta + ? await collectRevisionDatabaseInfo(core, sharedMeta, false) + : undefined; + mergeBases.push({ + winnerRevision: currentMeta._rev ?? "", + conflictRevision: revision, + revision: sharedRevision, + metadataAvailableLocally: Boolean(sharedMeta), + contentAvailableLocally: sharedInfo?.contentAvailableLocally ?? false, + missingChunkIds: sharedInfo ? missingChunkIds(sharedInfo) : [], + unavailableSharedRevisions, + }); + } else { + unavailableConflictRevisions.push(revision); + } + } + } + + const report: FileDatabaseInfo = { + path, + databasePath, + storage: storageStat + ? { + exists: true, + ctime: storageStat.ctime, + mtime: storageStat.mtime, + size: storageStat.size, + } + : { + exists: false, + }, + database: { + source: "local database on this device", + remoteQueried: false, + exists: currentMeta !== false, + currentRevision: currentMeta ? (currentMeta._rev ?? null) : null, + conflictCount: conflictRevisions.length, + conflictRevisions, + unavailableConflictRevisions, + revisions, + mergeBases, + }, + }; + + return report; +} + +export async function readFileDatabaseRevisionLocally( + core: FileDatabaseInfoCore, + path: string, + revision: string +): Promise { + const databasePath = toDatabasePath(path); + const meta = await getLocalDatabaseMeta(core, databasePath, { + rev: revision, + revs: true, + revs_info: true, + }); + if (!meta) { + return false; + } + const info = await collectRevisionDatabaseInfo(core, meta, false); + if (info.deleted || !info.contentAvailableLocally) { + return false; + } + return await core.localDatabase.getDBEntryFromMeta(meta, false, false); +} + +export async function retryReadFileDatabaseRevision( + core: FileDatabaseInfoCore, + path: string, + revision: string +): Promise { + return await core.localDatabase.getDBEntry(toDatabasePath(path), { rev: revision }, false, true, true); +} + +export async function buildFileDatabaseInfoReport(core: FileDatabaseInfoCore, path: string): Promise { + const report = await inspectFileDatabaseInfo(core, path); + return `${$msg(REPORT_WARNING)} + +\`\`\`json +${JSON.stringify(report, null, 2)} +\`\`\``; +} + +export async function copyFileDatabaseInfo(core: FileDatabaseInfoCore, path: string): Promise { + const report = await buildFileDatabaseInfoReport(core, path); + return await core.services.UI.promptCopyToClipboard( + $msg("Database information for ${FILE}", { FILE: path }), + report + ); +} + +export async function collectFileDatabaseInfoPaths(core: FileDatabaseInfoCore): Promise { + const ignorePatterns = getFileRegExp(core.settings, "syncInternalFilesIgnorePatterns"); + const targetPatterns = getFileRegExp(core.settings, "syncInternalFilesTargetPatterns"); + const storagePaths = core.settings.syncInternalFiles + ? await core.storageAccess.getFilesIncludeHidden("/", targetPatterns, ignorePatterns) + : await core.storageAccess.getFileNames(); + const databasePaths: string[] = []; + + for await (const entry of core.localDatabase.findAllDocs()) { + const prefixedPath = entry.path; + if (prefixedPath.startsWith(ICXHeader) || prefixedPath.startsWith(PSCHeader)) { + continue; + } + if (!core.settings.syncInternalFiles && prefixedPath.startsWith(ICHeader)) { + continue; + } + databasePaths.push(stripAllPrefixes(prefixedPath)); + } + + return [...new Set([...storagePaths, ...databasePaths])].sort((left, right) => + left < right ? -1 : left > right ? 1 : 0 + ); +} + +export async function chooseAndCopyFileDatabaseInfo(core: FileDatabaseInfoCore): Promise { + const paths = await collectFileDatabaseInfoPaths(core); + const selected = await core.services.UI.confirm.askSelectString($msg("Choose a file to inspect"), paths); + if (!selected) { + return false; + } + return await copyFileDatabaseInfo(core, selected); +} diff --git a/src/serviceFeatures/fileDatabaseInfo.unit.spec.ts b/src/serviceFeatures/fileDatabaseInfo.unit.spec.ts new file mode 100644 index 00000000..1d1b6207 --- /dev/null +++ b/src/serviceFeatures/fileDatabaseInfo.unit.spec.ts @@ -0,0 +1,413 @@ +import { describe, expect, it, vi } from "vitest"; +import { + buildFileDatabaseInfoReport, + chooseAndCopyFileDatabaseInfo, + collectFileDatabaseInfoPaths, + inspectFileDatabaseInfo, + readFileDatabaseRevisionLocally, + retryReadFileDatabaseRevision, +} from "./fileDatabaseInfo"; + +async function* documents(paths: string[]) { + for (const path of paths) { + yield { + _id: `f:${path}`, + path, + }; + } +} + +function createCore() { + const current = { + _id: "f:note", + _rev: "3-current", + _conflicts: ["2-conflict"], + _revs_info: [ + { rev: "3-current", status: "available" }, + { rev: "2-parent", status: "missing" }, + ], + path: "note.md", + ctime: 100, + mtime: 300, + size: 42, + type: "plain", + datatype: "plain", + data: "secret current body", + children: ["h:private-current", "h:private-current", "h:private-embedded", "h:private-deleted"], + eden: { + "h:private-embedded": { + data: "secret embedded body", + epoch: 1, + }, + }, + }; + const conflict = { + ...current, + _rev: "2-conflict", + _conflicts: undefined, + _revs_info: [{ rev: "2-conflict", status: "available" }], + mtime: 200, + data: "secret conflict body", + children: ["h:private-missing"], + eden: {}, + }; + const promptCopyToClipboard = vi.fn(async (_title: string, _value: string) => true); + const askSelectString = vi.fn(async () => "db-only.md"); + const core = { + settings: { + syncInternalFiles: false, + syncInternalFilesIgnorePatterns: "", + syncInternalFilesTargetPatterns: "", + }, + storageAccess: { + isExistsIncludeHidden: vi.fn(async () => true), + statHidden: vi.fn(async () => ({ + ctime: 90, + mtime: 310, + size: 45, + type: "file", + })), + getFileNames: vi.fn(async () => ["z.md", "a.md"]), + getFilesIncludeHidden: vi.fn(async () => [".obsidian/app.json", "a.md"]), + }, + localDatabase: { + getDBEntryFromMeta: vi.fn(async (meta: typeof current) => ({ + ...meta, + data: ["loaded body"], + })), + getDBEntry: vi.fn(async () => current), + localDatabase: { + get: vi.fn(async (_id: string, options?: { rev?: string }) => + options?.rev === "2-conflict" ? conflict : current + ), + }, + allDocsRaw: vi.fn(async ({ keys }: { keys: string[] }) => ({ + rows: [ + ...(keys.includes("h:private-current") + ? [ + { + id: "h:private-current", + key: "h:private-current", + value: { rev: "1-chunk" }, + }, + ] + : []), + ...(keys.includes("h:private-deleted") + ? [ + { + id: "h:private-deleted", + key: "h:private-deleted", + value: { rev: "4-deleted-chunk", deleted: true }, + }, + ] + : []), + ], + })), + findAllDocs: vi.fn(() => documents(["db-only.md", "i:.obsidian/app.json", "ix:ignore", "ps:setting"])), + }, + services: { + path: { + path2id: vi.fn(async () => "f:note"), + }, + UI: { + promptCopyToClipboard, + confirm: { + askSelectString, + }, + }, + }, + }; + return { + askSelectString, + conflict, + core, + current, + promptCopyToClipboard, + }; +} + +describe("file database information", () => { + it("reports document and chunk revisions without exposing file contents", async () => { + const { core } = createCore(); + + const report = await buildFileDatabaseInfoReport(core as never, "note.md"); + + expect(report).toContain('"path": "note.md"'); + expect(report).toContain('"documentId": "f:note"'); + expect(report).toContain('"revision": "3-current"'); + expect(report).toContain('"revision": "2-conflict"'); + expect(report).toContain('"storageType": "plain"'); + expect(report).toContain('"storageLayout": "chunked"'); + expect(report).toContain('"contentAvailableLocally": false'); + expect(report).toContain('"id": "h:private-current"'); + expect(report).toContain('"localDatabaseRevision": "1-chunk"'); + expect(report).toContain('"referenceCount": 2'); + expect(report).toContain('"id": "h:private-embedded"'); + expect(report).toContain('"embedded": true'); + expect(report).toContain('"id": "h:private-deleted"'); + expect(report).toContain('"localDatabaseState": "deleted"'); + expect(report).toContain('"localDatabaseRevision": "4-deleted-chunk"'); + expect(report).toContain('"id": "h:private-missing"'); + expect(report).toContain('"localDatabaseState": "missing"'); + expect(report).toContain('"localDatabaseRevision": null'); + expect(report).not.toContain("secret current body"); + expect(report).not.toContain("secret conflict body"); + expect(report).not.toContain("secret embedded body"); + }); + + it.each([ + { + name: "notes", + document: { + type: "notes", + data: "secret legacy body", + }, + storageType: "notes", + }, + { + name: "an absent type", + document: { + type: undefined, + data: ["secret", " legacy body"], + }, + storageType: "absent", + }, + ])("reports $name as legacy inline storage without exposing its body", async ({ document, storageType }) => { + const { core, current } = createCore(); + core.localDatabase.localDatabase.get.mockResolvedValue({ + ...current, + ...document, + _conflicts: [], + children: ["h:must-not-be-treated-as-a-chunk"], + } as never); + + const info = await inspectFileDatabaseInfo(core as never, "note.md"); + const report = await buildFileDatabaseInfoReport(core as never, "note.md"); + + expect(info.database.revisions).toEqual([ + expect.objectContaining({ + storageType, + storageLayout: "legacy-inline", + chunkReferences: 0, + contentAvailableLocally: true, + }), + ]); + expect(report).not.toContain("secret legacy body"); + expect(report).not.toContain("h:must-not-be-treated-as-a-chunk"); + }); + + it("reports the exact shared ancestor and its missing chunks for each conflict", async () => { + const { conflict, core, current } = createCore(); + const parent = { + ...current, + _rev: "2-parent", + _conflicts: undefined, + _revs_info: [ + { rev: "2-parent", status: "available" }, + { rev: "1-root", status: "missing" }, + ], + children: ["h:missing-parent"], + eden: {}, + }; + core.localDatabase.localDatabase.get.mockImplementation(async (_id: string, options?: { rev?: string }) => { + if (options?.rev === "2-conflict") { + return { + ...conflict, + _revs_info: [ + { rev: "2-conflict", status: "available" }, + { rev: "2-parent", status: "available" }, + { rev: "1-root", status: "missing" }, + ], + }; + } + if (options?.rev === "2-parent") { + return parent; + } + return { + ...current, + _revs_info: [ + { rev: "3-current", status: "available" }, + { rev: "2-parent", status: "available" }, + { rev: "1-root", status: "missing" }, + ], + }; + }); + + const info = await inspectFileDatabaseInfo(core as never, "note.md"); + + expect(info.database.mergeBases).toEqual([ + { + winnerRevision: "3-current", + conflictRevision: "2-conflict", + revision: "2-parent", + metadataAvailableLocally: true, + contentAvailableLocally: false, + missingChunkIds: ["h:missing-parent"], + unavailableSharedRevisions: ["1-root"], + }, + ]); + }); + + it("does not decode a revision whose chunks are not all available locally", async () => { + const { core } = createCore(); + + await expect(readFileDatabaseRevisionLocally(core as never, "note.md", "3-current")).resolves.toBe(false); + + expect(core.localDatabase.getDBEntryFromMeta).not.toHaveBeenCalled(); + }); + + it("decodes an exact revision after confirming that every chunk is available locally", async () => { + const { core, current } = createCore(); + core.localDatabase.localDatabase.get.mockResolvedValue({ + ...current, + children: ["h:available"], + eden: {}, + } as never); + core.localDatabase.allDocsRaw.mockResolvedValue({ + rows: [ + { + id: "h:available", + key: "h:available", + value: { rev: "1-available" }, + }, + ], + }); + + await expect(readFileDatabaseRevisionLocally(core as never, "note.md", "3-current")).resolves.toEqual( + expect.objectContaining({ + data: ["loaded body"], + }) + ); + + expect(core.localDatabase.getDBEntryFromMeta).toHaveBeenCalledWith( + expect.objectContaining({ + _rev: "3-current", + }), + false, + false + ); + }); + + it("retries an exact revision through the configured chunk retrieval path", async () => { + const { core } = createCore(); + + await retryReadFileDatabaseRevision(core as never, "note.md", "2-conflict"); + + expect(core.localDatabase.getDBEntry).toHaveBeenCalledWith( + "note.md", + { rev: "2-conflict" }, + false, + true, + true + ); + }); + + it("reports the exact revision as locally available after retry recovers its missing chunk", async () => { + const { conflict, core } = createCore(); + let recovered = false; + core.localDatabase.getDBEntry.mockImplementation(async () => { + recovered = true; + return conflict as never; + }); + core.localDatabase.allDocsRaw.mockImplementation(async ({ keys }: { keys: string[] }) => ({ + rows: + recovered && keys.includes("h:private-missing") + ? [ + { + id: "h:private-missing", + key: "h:private-missing", + value: { rev: "1-recovered" }, + }, + ] + : [], + })); + + await expect( + retryReadFileDatabaseRevision(core as never, "note.md", "2-conflict") + ).resolves.not.toBe(false); + const information = await inspectFileDatabaseInfo(core as never, "note.md"); + + expect( + information.database.revisions.find(({ revision }) => revision === "2-conflict") + ).toEqual( + expect.objectContaining({ + contentAvailableLocally: true, + chunks: [ + expect.objectContaining({ + id: "h:private-missing", + localDatabaseState: "available", + localDatabaseRevision: "1-recovered", + }), + ], + }) + ); + }); + + it("keeps the exact revision identifiers when conflict metadata is unavailable", async () => { + const { conflict, core, current } = createCore(); + core.localDatabase.localDatabase.get.mockImplementation(async (_id: string, options?: { rev?: string }) => { + if (options?.rev === "2-unavailable") { + throw Object.assign(new Error("missing"), { status: 404 }); + } + if (options?.rev === "2-conflict") { + return conflict; + } + return { ...current, _conflicts: ["2-conflict", "2-unavailable"] }; + }); + + const report = await buildFileDatabaseInfoReport(core as never, "note.md"); + + expect(report).toContain('"conflictRevisions"'); + expect(report).toContain('"2-conflict"'); + expect(report).toContain('"2-unavailable"'); + expect(report).toContain('"unavailableConflictRevisions"'); + }); + + it("reads an existing local document even when current synchronisation filters exclude its path", async () => { + const { core, current } = createCore(); + core.services.path.path2id.mockResolvedValue("f:ignored"); + core.localDatabase.localDatabase.get.mockResolvedValue({ + ...current, + _id: "f:ignored", + _rev: "5-ignored", + _conflicts: [], + _revs_info: [], + path: "ignored.md", + ctime: 10, + mtime: 20, + size: 30, + children: [], + }); + + const report = await buildFileDatabaseInfoReport(core as never, "ignored.md"); + + expect(report).toContain('"exists": true'); + expect(report).toContain('"documentId": "f:ignored"'); + expect(report).toContain('"revision": "5-ignored"'); + }); + + it("offers the union of storage and database paths and excludes inactive internal namespaces", async () => { + const { core } = createCore(); + + await expect(collectFileDatabaseInfoPaths(core as never)).resolves.toEqual(["a.md", "db-only.md", "z.md"]); + + core.settings.syncInternalFiles = true; + await expect(collectFileDatabaseInfoPaths(core as never)).resolves.toEqual([ + ".obsidian/app.json", + "a.md", + "db-only.md", + ]); + }); + + it("copies the selected file report through the existing copy dialogue", async () => { + const { askSelectString, core, promptCopyToClipboard } = createCore(); + + await expect(chooseAndCopyFileDatabaseInfo(core as never)).resolves.toBe(true); + + expect(askSelectString).toHaveBeenCalledWith("Choose a file to inspect", ["a.md", "db-only.md", "z.md"]); + expect(promptCopyToClipboard).toHaveBeenCalledWith( + "Database information for db-only.md", + expect.stringContaining('"path": "db-only.md"') + ); + }); +}); diff --git a/src/serviceFeatures/fileRepair.ts b/src/serviceFeatures/fileRepair.ts new file mode 100644 index 00000000..f5cef48b --- /dev/null +++ b/src/serviceFeatures/fileRepair.ts @@ -0,0 +1,109 @@ +import type { LoadedEntry } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { createBlob, isDocContentSame, readAsBlob } from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import type { StorageAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/StorageAccess"; +import type { IFileHandler } from "@vrtmrz/livesync-commonlib/compat/interfaces/FileHandler"; +import { + inspectFileDatabaseInfo, + readFileDatabaseRevisionLocally, + type FileDatabaseInfo, + type FileDatabaseInfoCore, + type RevisionDatabaseInfo, +} from "./fileDatabaseInfo"; + +export type FileRepairCore = FileDatabaseInfoCore & { + fileHandler: Pick; + storageAccess: FileDatabaseInfoCore["storageAccess"] & Pick; +}; + +export type FileRepairRevision = { + role: "winner" | "conflict"; + metadata: RevisionDatabaseInfo; + contentReadable: boolean; + contentMatchesStorage: boolean | null; + loadedEntry: LoadedEntry | false; +}; + +export type FileRepairInspection = { + information: FileDatabaseInfo; + revisions: FileRepairRevision[]; + requiresAttention: boolean; +}; + +export type DiscardUnreadableRevisionResult = + | "discarded" + | "failed" + | "no-longer-live" + | "revision-is-readable"; + +export async function inspectFileRepair(core: FileRepairCore, path: string): Promise { + const information = await inspectFileDatabaseInfo(core, path); + const storageContent = information.storage.exists + ? createBlob(await core.storageAccess.readHiddenFileBinary(path)) + : undefined; + const revisions: FileRepairRevision[] = []; + + for (const metadata of information.database.revisions) { + const loadedEntry = + metadata.deleted || !metadata.contentAvailableLocally + ? false + : await readFileDatabaseRevisionLocally(core, path, metadata.revision ?? ""); + const contentReadable = metadata.deleted || loadedEntry !== false; + const contentMatchesStorage = + storageContent && loadedEntry !== false + ? await isDocContentSame(storageContent, readAsBlob(loadedEntry)) + : null; + revisions.push({ + role: metadata.current ? "winner" : "conflict", + metadata, + contentReadable, + contentMatchesStorage, + loadedEntry, + }); + } + + const winner = revisions.find(({ role }) => role === "winner"); + const databaseAndStorageDiffer = + information.storage.exists !== information.database.exists || + (information.storage.exists && + winner !== undefined && + (winner.metadata.deleted || winner.contentMatchesStorage === false)) || + (!information.storage.exists && winner !== undefined && !winner.metadata.deleted); + const unreadableLiveRevision = + information.database.unavailableConflictRevisions.length > 0 || + revisions.some(({ contentReadable }) => !contentReadable); + const requiresAttention = + databaseAndStorageDiffer || + information.database.conflictCount > 0 || + unreadableLiveRevision || + (information.database.exists && winner === undefined); + + return { + information, + revisions, + requiresAttention, + }; +} + +export async function discardUnreadableLiveRevision( + core: FileRepairCore, + path: string, + revision: string +): Promise { + const latest = await inspectFileDatabaseInfo(core, path); + const liveRevisions = [ + latest.database.currentRevision, + ...latest.database.conflictRevisions, + ].filter((candidate): candidate is string => candidate !== null); + if (!liveRevisions.includes(revision)) { + return "no-longer-live"; + } + + const metadata = latest.database.revisions.find((candidate) => candidate.revision === revision); + const metadataUnavailable = latest.database.unavailableConflictRevisions.includes(revision); + if (!metadataUnavailable && (metadata?.deleted || metadata?.contentAvailableLocally)) { + return "revision-is-readable"; + } + + const deleted = await core.fileHandler.deleteRevisionFromDB(latest.databasePath, revision); + return deleted ? "discarded" : "failed"; +} diff --git a/src/serviceFeatures/fileRepair.unit.spec.ts b/src/serviceFeatures/fileRepair.unit.spec.ts new file mode 100644 index 00000000..9d066877 --- /dev/null +++ b/src/serviceFeatures/fileRepair.unit.spec.ts @@ -0,0 +1,172 @@ +import { describe, expect, it, vi } from "vitest"; +import { + discardUnreadableLiveRevision, + inspectFileRepair, +} from "./fileRepair"; + +function createCore() { + const current = { + _id: "f:note", + _rev: "3-current", + _conflicts: ["2-conflict"], + _revs_info: [{ rev: "3-current", status: "available" }], + path: "note.md", + ctime: 1, + mtime: 3, + size: 7, + type: "plain", + children: ["h:current"], + eden: {}, + }; + const conflict = { + ...current, + _rev: "2-conflict", + _conflicts: undefined, + _revs_info: [{ rev: "2-conflict", status: "available" }], + mtime: 2, + children: ["h:missing-conflict"], + }; + const deleteRevisionFromDB = vi.fn(async () => true); + const core = { + settings: { + syncInternalFiles: false, + syncInternalFilesIgnorePatterns: "", + syncInternalFilesTargetPatterns: "", + }, + storageAccess: { + isExistsIncludeHidden: vi.fn(async () => true), + statHidden: vi.fn(async () => ({ + ctime: 1, + mtime: 3, + size: 7, + type: "file", + })), + readHiddenFileBinary: vi.fn(async () => new TextEncoder().encode("current").buffer), + getFileNames: vi.fn(async () => ["note.md"]), + getFilesIncludeHidden: vi.fn(async () => ["note.md"]), + }, + localDatabase: { + localDatabase: { + get: vi.fn(async (_id: string, options?: { rev?: string }) => + options?.rev === "2-conflict" ? conflict : current + ), + }, + allDocsRaw: vi.fn(async ({ keys }: { keys: string[] }) => ({ + rows: keys.includes("h:current") + ? [ + { + id: "h:current", + key: "h:current", + value: { rev: "1-current" }, + }, + ] + : [], + })), + getDBEntryFromMeta: vi.fn(async (meta: typeof current) => ({ + ...meta, + data: [meta._rev === "3-current" ? "current" : "conflict"], + })), + getDBEntry: vi.fn(async () => false), + findAllDocs: vi.fn(async function* () { + yield current; + }), + }, + fileHandler: { + deleteRevisionFromDB, + }, + services: { + path: { + path2id: vi.fn(async () => "f:note"), + }, + UI: { + confirm: {}, + }, + }, + }; + return { + conflict, + core, + current, + deleteRevisionFromDB, + }; +} + +describe("file repair inspection", () => { + it("shows the winner and every conflict revision independently", async () => { + const { core } = createCore(); + + const inspection = await inspectFileRepair(core as never, "note.md"); + + expect(inspection.revisions).toEqual([ + expect.objectContaining({ + role: "winner", + contentReadable: true, + contentMatchesStorage: true, + metadata: expect.objectContaining({ + revision: "3-current", + }), + }), + expect.objectContaining({ + role: "conflict", + contentReadable: false, + contentMatchesStorage: null, + metadata: expect.objectContaining({ + revision: "2-conflict", + }), + }), + ]); + expect(inspection.requiresAttention).toBe(true); + }); + + it("rechecks liveness and readability before discarding an exact revision", async () => { + const { core, deleteRevisionFromDB } = createCore(); + + await expect( + discardUnreadableLiveRevision(core as never, "note.md", "2-conflict") + ).resolves.toBe("discarded"); + await expect( + discardUnreadableLiveRevision(core as never, "note.md", "3-current") + ).resolves.toBe("revision-is-readable"); + + expect(deleteRevisionFromDB).toHaveBeenCalledOnce(); + expect(deleteRevisionFromDB).toHaveBeenCalledWith("note.md", "2-conflict"); + }); + + it("allows an exact unreadable generation-one winner to be discarded explicitly", async () => { + const { core, current, deleteRevisionFromDB } = createCore(); + current._rev = "1-root"; + current._conflicts = []; + current.children = ["h:missing-root"]; + core.localDatabase.allDocsRaw.mockResolvedValue({ rows: [] }); + + const inspection = await inspectFileRepair(core as never, "note.md"); + + expect(inspection.revisions).toEqual([ + expect.objectContaining({ + role: "winner", + contentReadable: false, + metadata: expect.objectContaining({ + revision: "1-root", + }), + }), + ]); + await expect( + discardUnreadableLiveRevision(core as never, "note.md", "1-root") + ).resolves.toBe("discarded"); + expect(deleteRevisionFromDB).toHaveBeenCalledWith("note.md", "1-root"); + }); + + it("refuses to discard a revision which stopped being a live leaf", async () => { + const { core, current, deleteRevisionFromDB } = createCore(); + core.localDatabase.localDatabase.get.mockResolvedValue({ + ...current, + _conflicts: [], + }); + + await expect( + discardUnreadableLiveRevision(core as never, "note.md", "2-conflict") + ).resolves.toBe("no-longer-live"); + + expect(deleteRevisionFromDB).not.toHaveBeenCalled(); + }); +}); diff --git a/styles.css b/styles.css index 273fe736..3d63babd 100644 --- a/styles.css +++ b/styles.css @@ -595,6 +595,49 @@ body.is-mobile .livesync-compatibility-review-notice { word-break: break-all; } +.sls-repair-results { + display: grid; + gap: var(--size-4-3); +} + +.sls-repair-result { + padding: var(--size-4-3); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + background: var(--background-secondary); +} + +.sls-repair-revision { + margin-top: var(--size-4-2); + padding: var(--size-4-2); + border-left: 3px solid var(--background-modifier-border); + background: var(--background-primary-alt); +} + +.sls-repair-revision-title { + font-weight: var(--font-semibold); + overflow-wrap: anywhere; +} + +.sls-repair-revision code { + display: block; + margin-top: var(--size-4-1); + overflow-wrap: anywhere; + white-space: normal; +} + +.sls-repair-ancestor-warning { + margin-top: var(--size-4-2); + color: var(--text-warning); +} + +.sls-repair-actions { + display: flex; + flex-wrap: wrap; + gap: var(--size-4-2); + margin-top: var(--size-4-2); +} + /* Diff navigation */ .diff-options-row { display: flex; diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index 06b05ac2..f06c7b57 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -93,7 +93,7 @@ The mobile pass uses Obsidian's `app.emulateMobile(true)`, a 390 by 844 CSS-pixe `test:e2e:obsidian:p2p-pane` starts one unconfigured CouchDB-only session and one configured P2P session. It proves that the command remains registered while the retired command, automatic pane, and unconfigured ribbon entry are absent. For the configured profile, it verifies that the ribbon and current status command reach the pane without opening it at start-up, checks its connection control and horizontal layout, and captures unobstructed desktop and mobile screenshots. It deliberately uses no relay or peer: replacement of the active replicator is covered by focused unit tests, the Deno and Compose CLI P2P lifecycle suite covers the headless transport, and `p2p-setup-uri-workflow` owns the visible transfer path between two real Obsidian sessions. -`test:e2e:obsidian:local-suite` builds the plug-in and, unless `LIVESYNC_CLI_COMMAND` selects an external CLI, the local LiveSync CLI. It then runs discovery, smoke, the onboarding invitation, Svelte dialogue mounting, settings UI, the Review Harness, the P2P status pane, Vault reflection, CouchDB upload and manual setup, CLI-to-Obsidian synchronisation, Object Storage upload and Setup URI round-trip, P2P Setup URI round-trip, startup scan, provisioned CouchDB Setup URI, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export in sequence. Start the local CouchDB, MinIO, and P2P relay fixtures before running it, or use `test:e2e:obsidian:local-suite:services` to let the wrapper stop leftover fixtures, start fresh fixtures, and stop them again after the run. +`test:e2e:obsidian:local-suite` builds the plug-in and, unless `LIVESYNC_CLI_COMMAND` selects an external CLI, the local LiveSync CLI. It then runs discovery, smoke, the onboarding invitation, Svelte dialogue mounting, revision repair, settings UI, the Review Harness, the P2P status pane, Vault reflection, CouchDB upload and manual setup, CLI-to-Obsidian synchronisation, Object Storage upload and Setup URI round-trip, P2P Setup URI round-trip, startup scan, provisioned CouchDB Setup URI, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export in sequence. Start the local CouchDB, MinIO, and P2P relay fixtures before running it, or use `test:e2e:obsidian:local-suite:services` to let the wrapper stop leftover fixtures, start fresh fixtures, and stop them again after the run. `test:e2e:obsidian:couchdb-upload` reuses the CouchDB variables from `.test.env` or the process environment. It expects a reachable CouchDB service, creates a unique database, starts from configured plug-in data without the device-local compatibility marker, and verifies the copied-or-restored Vault explanation in the actual compatibility dialogue. It captures the summary and details, resumes explicitly, confirms that the marker was recorded, creates a note in real Obsidian, commits the note into the local database, runs one-shot synchronisation, and verifies that the remote database contains both the metadata document and its chunk documents. @@ -136,6 +136,8 @@ LIVESYNC_CLI_COMMAND="docker run --rm --network host --user $(id -u):$(id -g) -- `test:e2e:obsidian:conflict-dialog-policy` creates three real local revision leaves without a remote service and opens the pairwise merge dialogue in Obsidian. It verifies the three-version count, requires the four decision buttons to be stacked vertically, concatenates the displayed pair as a child of the displayed winner, confirms that the untouched leaf remains as one conflict, postpones that remaining pair, restarts the same isolated Vault and profile, and confirms that only the two live versions are reconstructed. It also verifies that an ordinary repeated conflict check does not reopen a postponed dialogue, that **Resolve if conflicted.** explicitly reopens it, and that the active editor retains the appropriate unresolved-conflict warning. The scenario then invokes the same Commonlib consumer boundary used for an incoming replicated document and checks that a postponed warning disappears, an open stale dialogue closes, and the conflict-processing queue completes even when the dialogue closes immediately. This isolates the Obsidian UI contract from transport and second-device setup. The fixture owns one temporary Vault and profile, and the session runner stops Obsidian before removing them. +`test:e2e:obsidian:revision-repair` creates two live revisions in a temporary real Obsidian Vault, removes a chunk used only by the non-winning revision, and proves that automatic conflict checking does not discard the unreadable branch. **Verify and repair all files** must show the winner and conflict separately, identify the exact unreadable revision and missing chunk, and leave the revision tree unchanged when reading is retried. The scenario then verifies both the cancellation path and the explicit confirmation path for discarding that exact unreadable live revision, requires the winner to remain unchanged, and captures the repair card. It uses no remote service; a retry is therefore expected to remain unreadable unless the chunk is already available locally. + `test:e2e:obsidian:hidden-file-snippet-sync` runs a two-vault hidden file round-trip. It verifies creation and deletion of a real `.obsidian/snippets/*.css` file, automatic JSON conflict merging for a hidden file with the merged result propagated by a second synchronisation, manual JSON Resolve dialogue application through Obsidian's UI, and per-device target patterns where one vault ignores a hidden file that the other vault synchronises. Initial enablement must open one user-visible progress Notice before the enabled setting is saved, then retain that Notice while its nested rebuild and scan phases continue in the ordinary log. The configured fixture starts with a current CouchDB remote profile, so migration from legacy remote settings remains the responsibility of the upgrade scenarios and cannot add unrelated Notices to this check. It also covers [issue #555](https://github.com/vrtmrz/obsidian-livesync/issues/555) by requiring several plug-in and settings changes to share one separate action Notice whose controls remain usable in mobile layouts; a manually dismissed group must not repeat its acknowledged rows when a later change arrives. `test:e2e:obsidian:customisation-sync` runs a two-vault Customisation Sync workflow. It scans a real snippet CSS file, config JSON file, and sample plug-in fixture into per-file Customisation Sync data, synchronises the entries through CouchDB, applies them on the second vault, verifies the resulting `.obsidian` files, propagates a snippet update, and verifies deletion of the source-vault snippet sync data without confusing it with the target vault's own applied copy. @@ -187,6 +189,7 @@ Useful environment variables: - `E2E_OBSIDIAN_SKIP_EXTRACT=true`: download the AppImage without extracting it. - `E2E_OBSIDIAN_SMOKE_TIMEOUT_MS`: smoke timeout in milliseconds. - `E2E_OBSIDIAN_DIALOG_TIMEOUT_MS`: timeout for a representative Svelte dialogue to mount, expose its principal controls, and close; default is 10 seconds. +- `E2E_OBSIDIAN_REVISION_REPAIR_TIMEOUT_MS`: timeout for each visible revision-repair control and result; default is 15 seconds. - `E2E_OBSIDIAN_SETTINGS_TIMEOUT_MS`: timeout for the settings pane and its deletion controls to become visible; default is 10 seconds. - `E2E_OBSIDIAN_REVIEW_HARNESS_TIMEOUT_MS`: timeout for Review Harness view and action boundaries; default is 15 seconds. - `E2E_OBSIDIAN_P2P_PANE_TIMEOUT_MS`: timeout for the P2P status pane and its principal connection control; default is 10 seconds. diff --git a/test/e2e-obsidian/scripts/dialog-mounts.ts b/test/e2e-obsidian/scripts/dialog-mounts.ts index c12586e1..f2109068 100644 --- a/test/e2e-obsidian/scripts/dialog-mounts.ts +++ b/test/e2e-obsidian/scripts/dialog-mounts.ts @@ -722,7 +722,7 @@ async function verifyHatchSurfacesAndSafeActions(): Promise { await liveSyncSettings.locator('.sls-setting-menu-btn[title="Hatch"]').click({ timeout: uiTimeoutMs }); for (const label of [ "Write logs into the file", - "Recreate missing chunks for all files", + "Recreate chunks for current Vault files", "Verify and repair all files", ]) { await liveSyncSettings.locator(".setting-item-name", { hasText: label }).waitFor({ @@ -730,7 +730,7 @@ async function verifyHatchSurfacesAndSafeActions(): Promise { timeout: uiTimeoutMs, }); } - await liveSyncSettings.getByRole("button", { name: "Recreate all", exact: true }).waitFor({ + await liveSyncSettings.getByRole("button", { name: "Recreate current chunks", exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs, }); @@ -739,7 +739,7 @@ async function verifyHatchSurfacesAndSafeActions(): Promise { timeout: uiTimeoutMs, }); await liveSyncSettings - .locator(".setting-item-name", { hasText: "Recreate missing chunks for all files" }) + .locator(".setting-item-name", { hasText: "Recreate chunks for current Vault files" }) .scrollIntoViewIfNeeded(); return liveSyncSettings; } diff --git a/test/e2e-obsidian/scripts/local-suite.ts b/test/e2e-obsidian/scripts/local-suite.ts index de2ef89d..9dfc225f 100644 --- a/test/e2e-obsidian/scripts/local-suite.ts +++ b/test/e2e-obsidian/scripts/local-suite.ts @@ -15,6 +15,7 @@ const testSteps: Step[] = [ { name: "smoke", args: ["run", "test:e2e:obsidian:smoke"] }, { name: "onboarding invitation", args: ["run", "test:e2e:obsidian:onboarding-invitation"] }, { name: "Svelte dialogue mounts", args: ["run", "test:e2e:obsidian:dialog-mounts"] }, + { name: "revision repair", args: ["run", "test:e2e:obsidian:revision-repair"] }, { name: "settings UI", args: ["run", "test:e2e:obsidian:settings-ui"] }, { name: "Review Harness", args: ["run", "test:e2e:obsidian:review-harness"] }, { name: "P2P status pane", args: ["run", "test:e2e:obsidian:p2p-pane"] }, diff --git a/test/e2e-obsidian/scripts/revision-repair.ts b/test/e2e-obsidian/scripts/revision-repair.ts new file mode 100644 index 00000000..405c20c9 --- /dev/null +++ b/test/e2e-obsidian/scripts/revision-repair.ts @@ -0,0 +1,334 @@ +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { evalObsidianJson } from "../runner/cli.ts"; +import { + createE2eObsidianDeviceLocalState, + waitForLiveSyncCoreReady, + waitForLocalDatabaseEntry, +} from "../runner/liveSyncWorkflow.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { captureObsidianElement, withObsidianPage } from "../runner/ui.ts"; +import { createTemporaryVault } from "../runner/vault.ts"; + +const path = "revision-repair.md"; +const baseContent = "Revision repair\n\nShared base.\n"; +const branchContents = [ + `Revision repair\n\nLeft branch.\n${"L".repeat(4096)}\n`, + `Revision repair\n\nRight branch.\n${"R".repeat(4096)}\n`, +] as const; +const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_REVISION_REPAIR_TIMEOUT_MS ?? 15000); + +type BrokenRevisionFixture = { + winnerRevision: string; + conflictRevision: string; + missingChunkId: string; +}; + +type RevisionTree = { + winnerRevision: string; + conflictRevisions: string[]; +}; + +type ObsidianSettingsController = { + open(): void; + openTabById(tabId: string): void; +}; + +type ObsidianTestGlobal = typeof globalThis & { + app?: { + setting?: ObsidianSettingsController; + }; +}; + +async function createAndOpenBaseFile(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const content=${JSON.stringify(baseContent)};`, + "let file=app.vault.getAbstractFileByPath(path);", + "if(!file) file=await app.vault.create(path,content);", + "await app.workspace.getLeaf(false).openFile(file);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + env + ); +} + +async function createBrokenConflict( + cliBinary: string, + env: NodeJS.ProcessEnv, + baseRevision: string +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const baseRevision=${JSON.stringify(baseRevision)};`, + `const contents=${JSON.stringify(branchContents)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const id=await core.services.path.path2id(path);", + "for(const [index,content] of contents.entries()){", + " const blob=new Blob([content],{type:'text/plain'});", + " const now=Date.now()+index;", + " const result=await core.localDatabase.putDBEntry({", + " _id:id,path,data:blob,ctime:now,mtime:now,", + " size:(await blob.arrayBuffer()).byteLength,children:[],", + " datatype:'plain',type:'plain',eden:{},", + " },false,baseRevision);", + " if(!result?.ok) throw new Error(`Could not create repair conflict: ${path}`);", + "}", + "const tree=await core.localDatabase.localDatabase.get(id,{conflicts:true});", + "const conflictRevision=tree._conflicts?.[0];", + "if(!tree._rev||!conflictRevision){", + " throw new Error(`Repair fixture did not produce two live revisions: ${path}`);", + "}", + "const conflict=await core.localDatabase.localDatabase.get(id,{rev:conflictRevision});", + "const embedded=new Set(Object.keys(conflict.eden??{}));", + "const missingChunkId=(conflict.children??[]).find((child)=>!embedded.has(child));", + "if(!missingChunkId){", + " throw new Error(`Repair fixture did not create an independent chunk: ${conflictRevision}`);", + "}", + "const chunk=await core.localDatabase.localDatabase.get(missingChunkId);", + "await core.localDatabase.localDatabase.remove(chunk);", + "core.localDatabase.clearCaches();", + "const unreadable=await core.localDatabase.getDBEntry(path,{rev:conflictRevision},false,true,true);", + "if(unreadable!==false){", + " throw new Error(`The selected revision remained readable after its chunk was removed: ${conflictRevision}`);", + "}", + "return JSON.stringify({", + " winnerRevision:tree._rev,", + " conflictRevision,", + " missingChunkId,", + "});", + "})()", + ].join(""), + env + ); +} + +async function readRevisionTree(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const id=await core.services.path.path2id(path);", + "const tree=await core.localDatabase.localDatabase.get(id,{conflicts:true});", + "return JSON.stringify({", + " winnerRevision:tree._rev,", + " conflictRevisions:tree._conflicts??[],", + "});", + "})()", + ].join(""), + env + ); +} + +async function requestConflictCheck(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "core.localDatabase.clearCaches();", + "await core.services.conflict.queueCheckFor(path);", + "await core.services.conflict.ensureAllProcessed();", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + env + ); +} + +async function main(): Promise { + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) { + throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + } + const cliBinary = cli.binary; + const vault = await createTemporaryVault("obsidian-livesync-revision-repair-"); + let session: ObsidianLiveSyncSession | undefined; + try { + session = await startObsidianLiveSyncSession({ + binary, + cliBinary, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: { + doctorProcessedVersion: "1.0.0", + isConfigured: true, + liveSync: false, + remoteType: "", + couchDB_URI: "", + couchDB_DBNAME: "", + couchDB_USER: "", + couchDB_PASSWORD: "", + remoteConfigurations: {}, + activeConfigurationId: "", + notifyThresholdOfRemoteStorageSize: -1, + periodicReplication: false, + syncAfterMerge: false, + syncOnEditorSave: false, + syncOnFileOpen: false, + syncOnSave: false, + syncOnStart: false, + disableMarkdownAutoMerge: true, + showMergeDialogOnlyOnActive: true, + useEden: false, + }, + localStorageEntries: createE2eObsidianDeviceLocalState(vault.name), + }); + await waitForLiveSyncCoreReady(cliBinary, session.cliEnv); + await createAndOpenBaseFile(cliBinary, session.cliEnv); + const base = await waitForLocalDatabaseEntry(cliBinary, session.cliEnv, path); + const fixture = await createBrokenConflict(cliBinary, session.cliEnv, base.rev); + + await requestConflictCheck(cliBinary, session.cliEnv); + const afterAutomaticCheck = await readRevisionTree(cliBinary, session.cliEnv); + if ( + afterAutomaticCheck.winnerRevision !== fixture.winnerRevision || + !afterAutomaticCheck.conflictRevisions.includes(fixture.conflictRevision) + ) { + throw new Error( + `Automatic conflict checking discarded the unreadable revision: ${JSON.stringify({ + fixture, + afterAutomaticCheck, + })}` + ); + } + + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + await page.evaluate(() => { + const setting = (globalThis as ObsidianTestGlobal).app?.setting; + if (setting === undefined) throw new Error("Obsidian settings are unavailable"); + setting.open(); + setting.openTabById("obsidian-livesync"); + }); + const settings = page.locator(".sls-setting"); + await settings.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await settings.locator('.sls-setting-menu-btn[title="Hatch"]').click({ timeout: uiTimeoutMs }); + const verifySetting = settings.locator(".setting-item").filter({ + has: page.getByText("Verify and repair all files", { exact: true }), + }); + await verifySetting.getByRole("button", { name: "Verify all", exact: true }).click({ + timeout: uiTimeoutMs, + }); + const card = settings.locator(".sls-repair-result").filter({ hasText: path }); + await card.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const brokenRevision = card + .locator(".sls-repair-revision") + .filter({ hasText: fixture.conflictRevision }); + await brokenRevision + .getByText(/Unreadable on this device/u) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await brokenRevision.getByText(fixture.missingChunkId, { exact: false }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + if ((await card.locator(".sls-repair-revision").count()) !== 2) { + throw new Error("Verify and Repair did not render the winner and conflict revision separately."); + } + + await brokenRevision.getByRole("button", { name: "Retry reading revision", exact: true }).click({ + timeout: uiTimeoutMs, + }); + await settings + .locator(".sls-repair-result") + .filter({ hasText: path }) + .locator(".sls-repair-revision") + .filter({ hasText: fixture.conflictRevision }) + .getByText(/Unreadable on this device/u) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + + const afterRetry = await readRevisionTree(cliBinary, session.cliEnv); + if (!afterRetry.conflictRevisions.includes(fixture.conflictRevision)) { + throw new Error(`Retry changed the revision tree: ${JSON.stringify(afterRetry)}`); + } + + const screenshot = await captureObsidianElement( + session.remoteDebuggingPort, + "revision-repair-unreadable-conflict.png", + (page) => page.locator(".sls-repair-result").filter({ hasText: path }) + ); + + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const settings = page.locator(".sls-setting"); + const brokenRevision = () => + settings + .locator(".sls-repair-result") + .filter({ hasText: path }) + .locator(".sls-repair-revision") + .filter({ hasText: fixture.conflictRevision }); + await brokenRevision() + .getByRole("button", { name: "Discard unreadable revision", exact: true }) + .click({ timeout: uiTimeoutMs }); + const confirmation = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Discard unreadable revision" }), + }); + await confirmation.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await confirmation.getByRole("button", { name: "No", exact: true }).click({ timeout: uiTimeoutMs }); + await confirmation.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + const afterCancellation = await readRevisionTree(cliBinary, session.cliEnv); + if (!afterCancellation.conflictRevisions.includes(fixture.conflictRevision)) { + throw new Error(`Cancelling discard changed the revision tree: ${JSON.stringify(afterCancellation)}`); + } + + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const settings = page.locator(".sls-setting"); + const brokenRevision = settings + .locator(".sls-repair-result") + .filter({ hasText: path }) + .locator(".sls-repair-revision") + .filter({ hasText: fixture.conflictRevision }); + await brokenRevision + .getByRole("button", { name: "Discard unreadable revision", exact: true }) + .click({ timeout: uiTimeoutMs }); + const confirmation = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Discard unreadable revision" }), + }); + await confirmation.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await confirmation.getByRole("button", { name: "Yes", exact: true }).click({ timeout: uiTimeoutMs }); + await settings + .locator(".sls-repair-revision") + .filter({ hasText: fixture.conflictRevision }) + .waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + const afterDiscard = await readRevisionTree(cliBinary, session.cliEnv); + if ( + afterDiscard.winnerRevision !== fixture.winnerRevision || + afterDiscard.conflictRevisions.length !== 0 + ) { + throw new Error( + `Explicit discard did not remove only the selected unreadable revision: ${JSON.stringify({ + fixture, + afterDiscard, + })}` + ); + } + + console.log( + "Real Obsidian kept an unreadable conflict revision through automatic checking and retry, rendered every live revision separately, required confirmation, and discarded only the selected revision." + ); + console.log(`Repair screenshot: ${screenshot}`); + } finally { + if (session) { + await session.app.stop(); + } + await vault.dispose(); + } +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exit(1); +}); diff --git a/test/e2e-obsidian/scripts/run-focused.ts b/test/e2e-obsidian/scripts/run-focused.ts index f9e7dd93..7f3274d1 100644 --- a/test/e2e-obsidian/scripts/run-focused.ts +++ b/test/e2e-obsidian/scripts/run-focused.ts @@ -8,6 +8,7 @@ const focusedScenarios = new Set([ "smoke", "onboarding-invitation", "dialog-mounts", + "revision-repair", "settings-ui", "review-harness", "p2p-pane", diff --git a/updates.md b/updates.md index 27f20885..bacce5b9 100644 --- a/updates.md +++ b/updates.md @@ -14,6 +14,7 @@ Earlier releases remain available in the 0.25 release history and the legacy rel ### Improved +- **Verify and repair all files** now reports the database winner, every conflict revision, missing chunks, and unavailable shared ancestors separately. It can retry an exact revision without changing the tree, while discarding an unreadable live revision requires explicit confirmation. - Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands keep their identifiers, so hotkeys already assigned to them continue to work. The onboarding wizard can be reopened from **Self-hosted LiveSync settings** → **Setup**. - Enabling Hidden File Sync now opens one progress Notice before its setting is saved and reuses that Notice throughout the initial file scan, instead of stacking separate phase and restart Notices. - P2P is now presented only after it has been configured: its status pane no longer opens at start-up, its ribbon icon remains hidden for CouchDB-only Vaults, and the retired P2P pane command has been removed. The current pane distinguishes announcing changes, following a peer, and persistent per-device actions. Setup and guidance now distinguish the required signalling relay from optional TURN, and describe the public signalling relay's privacy and availability limits. @@ -23,12 +24,13 @@ Earlier releases remain available in the 0.25 release history and the legacy rel ### Fixed +- An unreadable conflict revision is no longer deleted automatically merely because its chunks are unavailable on the current device. - Choosing **Apply settings to this device, and fetch again** for a compatible configuration mismatch now applies the remote settings before Fetch, instead of updating the remote database with this device's settings. - Accepted settings which control how new chunks are created now take effect before synchronisation is retried, rather than leaving the previous hash or splitter active until restart. ### Testing -- Added regressions for P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, and mobile dialogues. +- Added regressions for revision repair, P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, and mobile dialogues. ## 1.0.0-beta.2 From 20fb027055e904c97ac8a6d7f688ba894619f8e9 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 24 Jul 2026 17:09:22 +0000 Subject: [PATCH 093/117] Protect conflict chunks during garbage collection --- docs/recovery.md | 2 +- docs/settings.md | 8 +- docs/specs_conflict_resolution.md | 2 + docs/specs_garbage_collection.md | 63 ++++++++ docs/troubleshooting.md | 2 +- .../CmdLocalDatabaseMainte.ts | 39 ++--- .../CmdLocalDatabaseMainte.unit.spec.ts | 135 +++++++++++++++++- .../SettingDialogue/PaneMaintenance.ts | 2 +- updates.md | 3 +- 9 files changed, 220 insertions(+), 36 deletions(-) create mode 100644 docs/specs_garbage_collection.md diff --git a/docs/recovery.md b/docs/recovery.md index 9d1227af..1d98ce04 100644 --- a/docs/recovery.md +++ b/docs/recovery.md @@ -69,7 +69,7 @@ Garbage Collection removes unreferenced chunks while preserving the current data - all relevant devices have synchronised; and - the remaining historical and deletion state is understood. -Deleted documents and tombstones are not free, and historical revisions may keep chunks reachable. Garbage Collection therefore cannot promise the smallest possible remote. +Deleted documents, tombstones, live conflicts, and retained metadata are not free. Live conflict branches keep the chunks needed for review, while an ordinary superseded linear revision does not protect its former chunks. Garbage Collection can therefore make old content unreadable and cannot promise the smallest possible remote. Review the [Garbage Collection V3 specification](specs_garbage_collection.md) before using it. Rebuild is a different operation. It reconstructs the database from a chosen authoritative state and is the more certain way to remove unwanted history or repair a damaged remote, but it is also more disruptive and can discard changes which exist only elsewhere. diff --git a/docs/settings.md b/docs/settings.md index c50558fe..08e545a4 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -12,7 +12,7 @@ The following status applies to optional and compatibility features in the 1.0 l | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Supported, opt-in | Peer-to-Peer Synchronisation, Hidden File Sync, and Customisation Sync | Maintained and covered by focused real-runtime tests. Enable them only where their separate setup and operational constraints are acceptable. | | Maintained, advanced | Data Compression | Available as an explicit storage and bandwidth trade-off. It remains disabled by default because the measured processing and memory costs outweigh the mixed-dataset saving. | -| Beta or experimental | JWT authentication, ignore files, automatic newer-file conflict resolution, and Garbage Collection V3 | Retained for explicit testing and specialised use. They remain disabled by default and are not part of the minimum supported setup. | +| Beta or experimental | JWT authentication, ignore files, automatic newer-file conflict resolution, and Garbage Collection V3 for CouchDB | Retained for explicit testing and specialised use. They remain disabled by default and are not part of the minimum supported setup. | | Compatibility only | V1 dynamic iteration counts, the old IndexedDB adapter, non-current hash algorithms, Eden chunks, and the stored `doNotUseFixedRevisionForChunks` key | Existing settings and data remain readable. New Vaults use the current defaults, and compatibility controls are shown only where a migration or recovery path still needs them. | | Icon | Description | @@ -1047,6 +1047,12 @@ Purge all download/upload cache. Delete all data on the remote server. +### 6. Garbage Collection V3 (CouchDB only) + +Garbage Collection V3 identifies chunk documents which are not reachable from any current file or live conflict branch, creates logical deletions for those chunks locally, propagates the deletions to CouchDB, and requests remote compaction. + +Use it only when the Vault, local database, and remote are healthy, and every relevant device has synchronised. It can make an ordinary superseded file revision unreadable when no live state still needs its chunks. It does not repair corruption or replace a deliberate rebuild. See the [Garbage Collection V3 specification](specs_garbage_collection.md). + ### 7. Reset #### Delete local database to reset or uninstall Self-hosted LiveSync diff --git a/docs/specs_conflict_resolution.md b/docs/specs_conflict_resolution.md index cbeb048c..f74c137d 100644 --- a/docs/specs_conflict_resolution.md +++ b/docs/specs_conflict_resolution.md @@ -60,6 +60,8 @@ Logical deletion does not recreate missing bytes, purge the document history, or **Recreate chunks for current Vault files** can recreate chunks only from files which are readable in the current Vault. It cannot reconstruct unique bytes from an unavailable historical or conflict revision. +Garbage Collection V3 treats every live conflict revision and its nearest available shared ancestor as reachable. Their locally available chunks are retained until the conflict is resolved. After resolution, chunks used only by the discarded branch or no-longer-needed merge ancestry can become eligible for collection. See the [Garbage Collection V3 specification](specs_garbage_collection.md). + A generation-one revision has no parent. When its body is unavailable, LiveSync cannot preserve a changed Vault file as a sibling branch without inventing ancestry. It leaves the operation unresolved. Recover the missing chunks from another replica or backup, or explicitly discard that live revision. If the current Vault file is the intended replacement, it can be stored after the unreadable revision has been logically deleted. ### Two devices independently create the same path diff --git a/docs/specs_garbage_collection.md b/docs/specs_garbage_collection.md new file mode 100644 index 00000000..c10647b5 --- /dev/null +++ b/docs/specs_garbage_collection.md @@ -0,0 +1,63 @@ +# Garbage Collection V3 + +Garbage Collection V3 is a beta maintenance operation for CouchDB remotes. It removes chunk documents which are no longer required by the current local revision tree, propagates those logical deletions to CouchDB, and then requests remote compaction. + +It is not a repair operation. Use it only when the Vault, the local LiveSync database, and the CouchDB remote are healthy, every relevant device has synchronised, and recoverable backups exist. + +## Supported scope + +Garbage Collection V3 is available in Edge Case mode for CouchDB. It is not offered for Object Storage or P2P: + +- Object Storage has a different journal and object lifecycle. +- P2P has no central database to compact and cannot provide the accepted-device progress information required by this workflow. + +The operation requires **Fetch chunks on demand** to be off so that its local reachability result is not confused with chunks which exist only on the remote. + +## Workflow + +After the user starts Garbage Collection V3, LiveSync: + +1. completes a one-shot bidirectional CouchDB synchronisation; +2. reads the accepted-device list and current progress recorded on the remote; +3. warns when an accepted device has no current information or device progress differs, then requires explicit confirmation; +4. computes the chunks reachable from the local PouchDB revision tree; +5. creates a logical deletion for each locally present chunk which is not reachable; +6. completes a push-only replication so that those deletions reach CouchDB; +7. requests CouchDB compaction and waits for it for up to two minutes; and +8. clears the local chunk caches. + +If the initial synchronisation, device inspection, or confirmation fails, the workflow stops before collection. If push-only replication fails, the local logical deletions have already been created, but remote compaction is not started; synchronise again before retrying or using another device. A compaction failure is reported separately. + +## Reachability rules + +A chunk remains reachable when it is referenced by any of the following: + +- the current database winner for a file; +- any other live conflict revision for that file; +- an available revision on either side of a live conflict which is required to describe the divergence; or +- the nearest available revision shared by both live conflict branches. + +Chunk identifiers are content-derived and shared between files. Reachability is therefore collected into one set across the database. A chunk used by two or more current files remains protected even when one file is updated or deleted. + +An ordinary superseded linear revision does not protect its former chunks. Once no current file or live conflict branch references a chunk, it can be collected. After a conflict is resolved, chunks unique to the discarded branch and to no-longer-needed merge ancestry can also become eligible. + +## Consequences + +Garbage Collection deliberately trades historical recoverability for storage. A metadata revision may remain in the revision tree after a chunk which only that superseded revision used has been collected, so that historical body can become unreadable. Remote compaction can then discard old CouchDB revision bodies. Tombstones and retained metadata also consume storage, so the operation does not promise the smallest possible database. + +Writing the same bytes again produces the same content-derived chunk identifier. If that chunk was collected previously, the normal chunk-writing path creates a new live revision for it, and ordinary replication can transfer it again. This does not recover an older file revision automatically; it only makes the newly written content available. + +Garbage Collection does not reconstruct a chunk which is already missing, determine whether an unreadable revision is important, or repair a damaged local database. Use **Verify and repair all files**, another healthy replica, or a backup for those cases. Use **Overwrite Server Data with This Device's Files** only when a chosen Vault is authoritative and a deliberate remote rebuild is required. + +## Verification + +Commonlib tests use real in-memory PouchDB revision trees to verify: + +- collection eligibility after a normal file update; +- protection of chunks shared by multiple current files; +- protection of all live conflict branches and their nearest available shared ancestor; +- eligibility of losing-branch and ancestor-only chunks after conflict resolution; +- propagation of chunk deletion to another PouchDB database; and +- recreation and propagation when the same content is written again. + +Self-hosted LiveSync tests verify that Garbage Collection V3 uses Commonlib's revision-aware result, deletes only unreachable chunks, performs the initial bidirectional and final push-only replications in order, and requests remote compaction. CouchDB's own compaction implementation remains an external database boundary. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index ee85985e..82931923 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -129,7 +129,7 @@ Browser security errors, particularly CORS failures, may reach the plug-in only LiveSync stores file metadata, chunks, revision history, conflicts, deletions, and tombstones. Deleting or shortening a file therefore does not immediately remove every object which once represented it. -Garbage Collection can remove unreferenced chunks, but it is appropriate only when the Vault and local database are healthy and all relevant devices have synchronised. Tombstones and retained revisions are not free, so Garbage Collection does not guarantee a minimal database. +Garbage Collection V3 can remove unreferenced chunks from a healthy CouchDB setup, but it is appropriate only when the Vault and local database are healthy and all relevant devices have synchronised. Current files and live conflict branches protect their required chunks; an ordinary superseded revision does not. Tombstones and retained metadata are not free, so Garbage Collection does not guarantee a minimal database. Review the [Garbage Collection V3 specification](specs_garbage_collection.md) before using it. `Overwrite Server Data with This Device's Files` is a separate rebuild operation and is the more certain way to reconstruct a central remote from a chosen authoritative Vault. It is also destructive and may discard changes which exist only on another device. Review [Recovery and flag files](recovery.md#garbage-collection-is-not-rebuild) before choosing between them. diff --git a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts index 37a9c7e5..8c2bbd81 100644 --- a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts +++ b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts @@ -5,7 +5,6 @@ import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, REMOTE_COUCHDB, - REMOTE_P2P, type DocumentID, type EntryDoc, type EntryLeaf, @@ -53,8 +52,7 @@ export class LocalDatabaseMaintenance extends LiveSyncCommands { name: "Garbage Collection V3 (advanced, beta)", icon: "trash-2", checkCallback: (checking) => { - const isApplicableRemote = - this.settings.remoteType === REMOTE_COUCHDB || this.settings.remoteType === REMOTE_P2P; + const isApplicableRemote = this.settings.remoteType === REMOTE_COUCHDB; if (!this.settings.useEdgeCaseMode || !this._isDatabaseReady() || !isApplicableRemote) { return false; } @@ -464,7 +462,7 @@ Note: **Make sure to synchronise all devices before deletion.** const confirmMessage = `This function deletes unused chunks from the device. If there are differences between devices, some chunks may be missing when resolving conflicts. Be sure to synchronise before executing. -However, if you have deleted them, you may be able to recover them by performing Hatch -> Recreate missing chunks for all files. +If chunks used by current Vault files are deleted, Hatch -> Recreate chunks for current Vault files can recreate them only from files currently present in the Vault. It cannot recover unreadable historical or conflict content. Are you ready to delete unused chunks?`; @@ -926,41 +924,22 @@ This may indicate that some devices have not completed synchronisation, which co const gcStartTime = Date.now(); // Perform Garbage Collection (new implementation). const localDatabase = this.localDatabase.localDatabase; - const usedChunks = new Set(); - const allChunks = new Map(); - - const IDs = this.localDatabase.findEntryNames("", "", {}); - let i = 0; - const doc_count = (await localDatabase.info()).doc_count; - for await (const id of IDs) { - const doc = await this.localDatabase.getRaw(id as DocumentID); - i++; - if (i % 100 == 0) { - this._notice(`Garbage Collection: Scanned ${i} / ~${doc_count} `, "gc-scanning"); - } - if (!doc) continue; - if ("children" in doc) { - const children = (doc.children || []) as DocumentID[]; - for (const chunkId of children) { - usedChunks.add(chunkId); - } - } else if (doc.type === EntryTypes.CHUNK) { - allChunks.set(doc._id, doc._rev); - } - } + // Use the revision-aware reachability scan. Reading only winning revisions + // would make chunks used exclusively by live conflict branches look unused. + const { used: usedChunks, existing: allChunks } = await this.localDatabase.allChunks(); this._notice( `Garbage Collection: Scanning completed. Total chunks: ${allChunks.size}, Used chunks: ${usedChunks.size}`, "gc-scanning" ); - const unusedChunks = [...allChunks.keys()].filter((e) => !usedChunks.has(e)); + const unusedChunks = [...allChunks.entries()].filter(([chunkId]) => !usedChunks.has(chunkId)); this._notice(`Garbage Collection: Found ${unusedChunks.length} unused chunks to delete.`, "gc-scanning"); const deleteChunkDocs = unusedChunks.map( - (chunkId) => + ([chunkId, chunk]) => ({ - _id: chunkId, + _id: chunkId as DocumentID, _deleted: true, - _rev: allChunks.get(chunkId), + _rev: chunk._rev, }) as EntryLeaf ); const response = await localDatabase.bulkDocs(deleteChunkDocs); diff --git a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts index 19be24ee..55878d03 100644 --- a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts +++ b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts @@ -24,7 +24,12 @@ vi.mock("@/common/events", () => ({ onEvent: vi.fn(), }, })); -import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { + DEFAULT_SETTINGS, + REMOTE_COUCHDB, + REMOTE_MINIO, + REMOTE_P2P, +} from "@vrtmrz/livesync-commonlib/compat/common/types"; import { LocalDatabaseMaintenance } from "./CmdLocalDatabaseMainte"; import { ensureLocalDatabaseMaintenancePrerequisites } from "./maintenancePrerequisites"; @@ -87,6 +92,9 @@ describe("LocalDatabaseMaintenance prerequisites", () => { settings.useEdgeCaseMode = true; expect(garbageCollect?.checkCallback?.(true)).toBe(true); + settings.remoteType = REMOTE_P2P; + expect(garbageCollect?.checkCallback?.(true)).toBe(false); + settings.remoteType = REMOTE_MINIO; expect(garbageCollect?.checkCallback?.(true)).toBe(false); }); @@ -176,4 +184,129 @@ describe("LocalDatabaseMaintenance prerequisites", () => { expect(askSelectStringDialogue).not.toHaveBeenCalled(); expect(applyPartial).not.toHaveBeenCalled(); }); + + it("describes the current chunk-recreation action without promising historical recovery", async () => { + const maintenance = Object.create(LocalDatabaseMaintenance.prototype) as LocalDatabaseMaintenance; + const askSelectStringDialogue = vi.fn().mockResolvedValue("Cancel"); + Object.assign(maintenance, { + core: { + confirm: { + askSelectStringDialogue, + }, + }, + _log: vi.fn(), + }); + vi.spyOn(maintenance, "ensureAvailable").mockResolvedValue(true); + vi.spyOn(maintenance, "trackChanges").mockResolvedValue(undefined); + + await maintenance.performGC(); + + const message = vi.mocked(askSelectStringDialogue).mock.calls[0]?.[0] as string; + expect(message).toContain("Hatch -> Recreate chunks for current Vault files"); + expect(message).toContain("only from files currently present in the Vault"); + expect(message).not.toContain("Recreate missing chunks for all files"); + }); +}); + +describe("LocalDatabaseMaintenance Garbage Collection V3", () => { + it("keeps chunks referenced by a live conflict revision and deletes only unreachable chunks", async () => { + const maintenance = Object.create(LocalDatabaseMaintenance.prototype) as LocalDatabaseMaintenance; + const pushModes: string[] = []; + const deletedChunks: Array<{ _id: string; _rev?: string; _deleted?: boolean }> = []; + const allChunks = vi.fn(async () => ({ + used: new Set(["h:winner", "h:conflict"]), + existing: new Map([ + ["h:winner", { _id: "h:winner", _rev: "1-winner", type: "leaf", data: "winner" }], + ["h:conflict", { _id: "h:conflict", _rev: "1-conflict", type: "leaf", data: "conflict" }], + ["h:obsolete", { _id: "h:obsolete", _rev: "1-obsolete", type: "leaf", data: "obsolete" }], + ]), + })); + const rawDocuments = new Map([ + [ + "note.md", + { + _id: "note.md", + _rev: "2-winner", + _conflicts: ["2-conflict"], + type: "plain", + children: ["h:winner"], + }, + ], + ["h:winner", { _id: "h:winner", _rev: "1-winner", type: "leaf", data: "winner" }], + ["h:conflict", { _id: "h:conflict", _rev: "1-conflict", type: "leaf", data: "conflict" }], + ["h:obsolete", { _id: "h:obsolete", _rev: "1-obsolete", type: "leaf", data: "obsolete" }], + ]); + const findEntryNames = vi.fn(async function* () { + yield* rawDocuments.keys(); + }); + const getRaw = vi.fn(async (id: string) => rawDocuments.get(id)); + const localDatabase = { + allChunks, + localDatabase: { + info: vi.fn(async () => ({ doc_count: rawDocuments.size })), + bulkDocs: vi.fn(async (docs: Array<{ _id: string; _rev?: string; _deleted?: boolean }>) => { + deletedChunks.push(...docs); + return docs.map(({ _id }) => ({ ok: true, id: _id, rev: "2-deleted" })); + }), + }, + findEntryNames, + getRaw, + }; + const replicator = { + openOneShotReplication: vi.fn( + async ( + _settings: typeof DEFAULT_SETTINGS, + _showResult: boolean, + _ignoreCleanLock: boolean, + mode: string + ) => { + pushModes.push(mode); + return true; + } + ), + getConnectedDeviceList: vi.fn(async () => ({ + accepted_nodes: ["device-a"], + node_info: { + "device-a": { + progress: "10-local", + device_name: "Device A", + app_version: "1.12.7", + plugin_version: "1.0.0-beta.0", + }, + }, + })), + }; + Object.assign(maintenance, { + core: { + settings: { + ...DEFAULT_SETTINGS, + remoteType: REMOTE_COUCHDB, + }, + replicator, + confirm: { + askSelectStringDialogue: vi.fn(async () => "Proceed Garbage Collection"), + }, + }, + localDatabase, + _notice: vi.fn(), + }); + vi.spyOn(maintenance, "ensureAvailable").mockResolvedValue(true); + vi.spyOn(maintenance, "compactDatabase").mockResolvedValue(undefined); + vi.spyOn(maintenance, "clearHash").mockImplementation(() => undefined); + + await maintenance.gcv3(); + + expect(allChunks).toHaveBeenCalledOnce(); + expect(findEntryNames).not.toHaveBeenCalled(); + expect(getRaw).not.toHaveBeenCalled(); + expect(deletedChunks).toEqual([ + { + _id: "h:obsolete", + _rev: "1-obsolete", + _deleted: true, + }, + ]); + expect(pushModes).toEqual(["sync", "pushOnly"]); + expect(maintenance.compactDatabase).toHaveBeenCalledOnce(); + }); }); diff --git a/src/modules/features/SettingDialogue/PaneMaintenance.ts b/src/modules/features/SettingDialogue/PaneMaintenance.ts index 30a59323..47fd1424 100644 --- a/src/modules/features/SettingDialogue/PaneMaintenance.ts +++ b/src/modules/features/SettingDialogue/PaneMaintenance.ts @@ -187,7 +187,7 @@ export function paneMaintenance( ) .addOnUpdate(this.onlyOnMinIO); }); - void addPanel(paneEl, "Garbage Collection V3 (Beta)", (e) => e, this.onlyOnP2POrCouchDB).then((paneEl) => { + void addPanel(paneEl, "Garbage Collection V3 (Beta)", (e) => e, this.onlyOnCouchDB).then((paneEl) => { new Setting(paneEl) .setName("Perform Garbage Collection") .setDesc("Perform Garbage Collection to remove unused chunks and reduce database size.") diff --git a/updates.md b/updates.md index bacce5b9..7f1c10c2 100644 --- a/updates.md +++ b/updates.md @@ -25,12 +25,13 @@ Earlier releases remain available in the 0.25 release history and the legacy rel ### Fixed - An unreadable conflict revision is no longer deleted automatically merely because its chunks are unavailable on the current device. +- Garbage Collection V3 now protects chunks required by every live conflict branch and the available revision ancestry needed to review and merge conflicts, instead of considering only the database winner. The action is offered only for CouchDB because P2P has no central database to compact and does not provide the device inventory required by the workflow. - Choosing **Apply settings to this device, and fetch again** for a compatible configuration mismatch now applies the remote settings before Fetch, instead of updating the remote database with this device's settings. - Accepted settings which control how new chunks are created now take effect before synchronisation is retried, rather than leaving the previous hash or splitter active until restart. ### Testing -- Added regressions for revision repair, P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, and mobile dialogues. +- Added regressions for revision repair, P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, mobile dialogues, conflict-aware chunk reachability, shared chunks, collection propagation, and content-addressed chunk recreation. ## 1.0.0-beta.2 From 0a7a3b1635c04973089924acd15a0c22ef3ece41 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 24 Jul 2026 17:53:22 +0000 Subject: [PATCH 094/117] Harden Garbage Collection V3 validation --- docs/specs_garbage_collection.md | 4 +- .../CmdLocalDatabaseMainte.ts | 13 ++- .../CmdLocalDatabaseMainte.unit.spec.ts | 109 ++++++++++++++++++ updates.md | 4 +- 4 files changed, 122 insertions(+), 8 deletions(-) diff --git a/docs/specs_garbage_collection.md b/docs/specs_garbage_collection.md index c10647b5..40f00c82 100644 --- a/docs/specs_garbage_collection.md +++ b/docs/specs_garbage_collection.md @@ -19,14 +19,14 @@ After the user starts Garbage Collection V3, LiveSync: 1. completes a one-shot bidirectional CouchDB synchronisation; 2. reads the accepted-device list and current progress recorded on the remote; -3. warns when an accepted device has no current information or device progress differs, then requires explicit confirmation; +3. requires parseable progress information, warns when an accepted device has no current information or device progress differs, then requires explicit confirmation; 4. computes the chunks reachable from the local PouchDB revision tree; 5. creates a logical deletion for each locally present chunk which is not reachable; 6. completes a push-only replication so that those deletions reach CouchDB; 7. requests CouchDB compaction and waits for it for up to two minutes; and 8. clears the local chunk caches. -If the initial synchronisation, device inspection, or confirmation fails, the workflow stops before collection. If push-only replication fails, the local logical deletions have already been created, but remote compaction is not started; synchronise again before retrying or using another device. A compaction failure is reported separately. +If the initial synchronisation, device inspection, or confirmation fails, the workflow stops before collection. Missing or invalid device progress is treated as a failed inspection rather than offered as a confirmation override. If push-only replication fails, the local logical deletions have already been created, but remote compaction is not started; synchronise again before retrying or using another device. A compaction failure or completion timeout is reported separately and is not also reported as a successful completion. ## Reachability rules diff --git a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts index 8c2bbd81..4c25400d 100644 --- a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts +++ b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts @@ -760,7 +760,7 @@ Success: ${successCount}, Errored: ${errored}`; timeout -= 2000; if (timeout <= 0) { this._notice("Compaction on remote database timed out.", "gc-compact"); - break; + return; } } else { break; @@ -883,9 +883,14 @@ It is preferable to update all devices if possible. If you have any devices that } //2. Check whether the progress values in NodeData are roughly the same (only the numerical part is needed). - const progressValues = Object.values(node_info) - .map((e) => e.progress.split("-")[0]) - .map((e) => parseInt(e)); + const progressValues = Object.values(node_info).map((entry) => { + const progress = typeof entry.progress === "string" ? entry.progress.split("-")[0] : ""; + return /^\d+$/u.test(progress) ? Number(progress) : Number.NaN; + }); + if (progressValues.length === 0 || progressValues.some((progress) => !Number.isSafeInteger(progress))) { + this._notice("No connected device information found. Cancelling Garbage Collection."); + return; + } const maxProgress = Math.max(...progressValues); const minProgress = Math.min(...progressValues); const progressDifference = maxProgress - minProgress; diff --git a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts index 55878d03..3ecc4180 100644 --- a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts +++ b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts @@ -9,6 +9,13 @@ vi.mock("octagonal-wheels/concurrency/lock_v2", () => ({ vi.mock("octagonal-wheels/collection", () => ({ arrayToChunkedArray: vi.fn((values: unknown[]) => [values]), })); +vi.mock("@vrtmrz/livesync-commonlib/compat/common/utils", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + delay: vi.fn(async () => undefined), + }; +}); vi.mock("@/features/LiveSyncCommands", () => ({ LiveSyncCommands: class LiveSyncCommands { core!: { settings: unknown }; @@ -209,6 +216,108 @@ describe("LocalDatabaseMaintenance prerequisites", () => { }); describe("LocalDatabaseMaintenance Garbage Collection V3", () => { + it("does not report remote compaction as successful after its completion wait times out", async () => { + const maintenance = Object.create(LocalDatabaseMaintenance.prototype) as LocalDatabaseMaintenance; + const notice = vi.fn(); + const remoteDatabase = { + compact: vi.fn(async () => ({ ok: true })), + info: vi.fn(async () => ({ compact_running: true })), + }; + Object.assign(maintenance, { + core: { + replicator: { + connectRemoteCouchDBWithSetting: vi.fn(async () => ({ db: remoteDatabase })), + }, + settings: { + ...DEFAULT_SETTINGS, + remoteType: REMOTE_COUCHDB, + }, + }, + _notice: notice, + }); + + await maintenance.compactDatabase(); + + expect(notice).toHaveBeenCalledWith("Compaction on remote database timed out.", "gc-compact"); + expect(notice).not.toHaveBeenCalledWith( + "Compaction on remote database completed successfully.", + "gc-compact" + ); + }); + + it.each([ + ["no device progress entries", {}], + [ + "an unparseable device progress entry", + { + "device-a": { + progress: "", + device_name: "Device A", + app_version: "1.12.7", + plugin_version: "1.0.0-beta.0", + }, + }, + ], + [ + "a missing device progress entry", + { + "device-a": { + device_name: "Device A", + app_version: "1.12.7", + plugin_version: "1.0.0-beta.0", + }, + }, + ], + ] as const)("cancels before collection when the milestone has %s", async (_case, nodeInfo) => { + const maintenance = Object.create(LocalDatabaseMaintenance.prototype) as LocalDatabaseMaintenance; + const pushModes: string[] = []; + const allChunks = vi.fn(async () => ({ + used: new Set(), + existing: new Map(), + })); + const replicator = { + openOneShotReplication: vi.fn(async (...args: unknown[]) => { + pushModes.push(String(args[3])); + return true; + }), + getConnectedDeviceList: vi.fn(async () => ({ + accepted_nodes: Object.keys(nodeInfo), + node_info: nodeInfo, + })), + }; + const notice = vi.fn(); + Object.assign(maintenance, { + core: { + settings: { + ...DEFAULT_SETTINGS, + remoteType: REMOTE_COUCHDB, + }, + replicator, + confirm: { + askSelectStringDialogue: vi.fn(async () => "Proceed Garbage Collection"), + }, + }, + localDatabase: { + allChunks, + localDatabase: { + bulkDocs: vi.fn(async () => []), + }, + }, + _notice: notice, + }); + vi.spyOn(maintenance, "ensureAvailable").mockResolvedValue(true); + vi.spyOn(maintenance, "compactDatabase").mockResolvedValue(undefined); + vi.spyOn(maintenance, "clearHash").mockImplementation(() => undefined); + + await maintenance.gcv3(); + + expect(allChunks).not.toHaveBeenCalled(); + expect(pushModes).toEqual(["sync"]); + expect(notice).toHaveBeenCalledWith( + "No connected device information found. Cancelling Garbage Collection." + ); + }); + it("keeps chunks referenced by a live conflict revision and deletes only unreachable chunks", async () => { const maintenance = Object.create(LocalDatabaseMaintenance.prototype) as LocalDatabaseMaintenance; const pushModes: string[] = []; diff --git a/updates.md b/updates.md index 7f1c10c2..2989226e 100644 --- a/updates.md +++ b/updates.md @@ -25,13 +25,13 @@ Earlier releases remain available in the 0.25 release history and the legacy rel ### Fixed - An unreadable conflict revision is no longer deleted automatically merely because its chunks are unavailable on the current device. -- Garbage Collection V3 now protects chunks required by every live conflict branch and the available revision ancestry needed to review and merge conflicts, instead of considering only the database winner. The action is offered only for CouchDB because P2P has no central database to compact and does not provide the device inventory required by the workflow. +- Garbage Collection V3 now protects chunks required by every live conflict branch and the available revision ancestry needed to review and merge conflicts, instead of considering only the database winner. The action is offered only for CouchDB because P2P has no central database to compact and does not provide the device inventory required by the workflow. Collection now stops when device progress cannot be verified, and a compaction timeout is no longer followed by a contradictory success message. - Choosing **Apply settings to this device, and fetch again** for a compatible configuration mismatch now applies the remote settings before Fetch, instead of updating the remote database with this device's settings. - Accepted settings which control how new chunks are created now take effect before synchronisation is retried, rather than leaving the previous hash or splitter active until restart. ### Testing -- Added regressions for revision repair, P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, mobile dialogues, conflict-aware chunk reachability, shared chunks, collection propagation, and content-addressed chunk recreation. +- Added regressions for revision repair, P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, mobile dialogues, conflict-aware chunk reachability, device-progress safeguards, compaction timeouts, shared chunks, collection propagation, and content-addressed chunk recreation. ## 1.0.0-beta.2 From c8162fd0cd869aae8f75533bc12ecfa25d037c79 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 24 Jul 2026 17:53:55 +0000 Subject: [PATCH 095/117] Make dialogue prose selectable --- .../services/LiveSyncUI/DialogHost.svelte | 2 + styles.css | 5 ++ test/e2e-obsidian/scripts/dialog-mounts.ts | 74 ++++++++++++++++--- updates.md | 3 +- 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src/modules/services/LiveSyncUI/DialogHost.svelte b/src/modules/services/LiveSyncUI/DialogHost.svelte index 111dd4ae..6265c7bf 100644 --- a/src/modules/services/LiveSyncUI/DialogHost.svelte +++ b/src/modules/services/LiveSyncUI/DialogHost.svelte @@ -68,6 +68,8 @@ display: flex; flex-direction: column; padding-bottom: var(--keyboard-height, 0px); + user-select: text; + -webkit-user-select: text; } .dialog-host :global(button) { diff --git a/styles.css b/styles.css index 3d63babd..ab7ca645 100644 --- a/styles.css +++ b/styles.css @@ -12,6 +12,11 @@ background-color: var(--text-muted); } +.vpk-action-dialog__message { + user-select: text; + -webkit-user-select: text; +} + .conflict-dev-name { display: inline-block; min-width: 5em; diff --git a/test/e2e-obsidian/scripts/dialog-mounts.ts b/test/e2e-obsidian/scripts/dialog-mounts.ts index f2109068..99d565de 100644 --- a/test/e2e-obsidian/scripts/dialog-mounts.ts +++ b/test/e2e-obsidian/scripts/dialog-mounts.ts @@ -194,6 +194,28 @@ async function verifyRemoteSizeNoticeAndDialogue(): Promise<{ .filter({ hasText: "Synchronisation paused for compatibility review" }), }); await compatibilityReview.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const message = compatibilityReview.locator(".vpk-action-dialog__message"); + const textSelection = await message.evaluate((element) => { + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(element); + selection?.removeAllRanges(); + selection?.addRange(range); + const selectedText = selection?.toString() ?? ""; + selection?.removeAllRanges(); + return { + selectedText, + userSelect: getComputedStyle(element).userSelect, + }; + }); + if ( + textSelection.userSelect !== "text" || + !textSelection.selectedText.includes("Remote synchronisation is paused on this device") + ) { + throw new Error( + `Expected the action dialogue message to be selectable, received user-select=${textSelection.userSelect} and selected text '${textSelection.selectedText}'.` + ); + } const actions = compatibilityReview.locator(".vpk-action-dialog__actions--vertical"); await actions.waitFor({ state: "visible", timeout: uiTimeoutMs }); const flexDirection = await actions.evaluate((element) => getComputedStyle(element).flexDirection); @@ -286,12 +308,32 @@ async function verifyRemoteSelectionDialogue(mode: DialogueMode): Promise { + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(element); + selection?.removeAllRanges(); + selection?.addRange(range); + const selectedText = selection?.toString() ?? ""; + selection?.removeAllRanges(); + return { + selectedText, + userSelect: getComputedStyle(element).userSelect, + }; + }); + if ( + textSelection.userSelect !== "text" || + !textSelection.selectedText.includes("signalling relay is required for peer discovery") + ) { + throw new Error( + `Expected Svelte dialogue prose to be selectable, received user-select=${textSelection.userSelect} and selected text '${textSelection.selectedText}'.` + ); + } await modal .getByRole("button", { name: "No, please take me back" }) .waitFor({ state: "visible", timeout: uiTimeoutMs }); @@ -831,9 +873,13 @@ async function verifyHatchSurfacesAndSafeActions(): Promise { } }; }, repairRunStateKey); - await liveSyncSettings.getByRole("button", { name: "Recreate all", exact: true }).click({ - timeout: uiTimeoutMs, - }); + await page + .locator(".sls-setting:visible") + .last() + .getByRole("button", { name: "Recreate current chunks", exact: true }) + .click({ + timeout: uiTimeoutMs, + }); await page.waitForFunction( (stateKey) => (globalThis as unknown as Record)[stateKey]?.done === true, @@ -848,9 +894,13 @@ async function verifyHatchSurfacesAndSafeActions(): Promise { throw new Error(`Recreate missing chunks failed: ${repairState.error}`); } - await liveSyncSettings.getByRole("button", { name: "Verify all", exact: true }).click({ - timeout: uiTimeoutMs, - }); + await page + .locator(".sls-setting:visible") + .last() + .getByRole("button", { name: "Verify all", exact: true }) + .click({ + timeout: uiTimeoutMs, + }); await page .locator(".notice") .filter({ hasText: /^done$/u }) diff --git a/updates.md b/updates.md index 2989226e..7d0ee73a 100644 --- a/updates.md +++ b/updates.md @@ -21,6 +21,7 @@ Earlier releases remain available in the 0.25 release history and the legacy rel - First-device P2P setup now accepts a successfully opened signalling room without requiring another peer to be online. Additional-device Fetch still requires selecting a source peer and completing `P2P Rebuild`. - Manual CouchDB setup now distinguishes creating a first database from connecting an additional device to an existing one. Settings mode can save an unverified profile explicitly, while onboarding requires a successful connection, and each proposed server-configuration fix requires separate confirmation. - Differences limited to the chunk hash algorithm, chunk size, or splitter version are now aligned automatically by default. Existing content remains readable, while an explicit opt-out and any difference which also involves an incompatible setting retain manual review. +- Text in setup and review dialogues can now be selected for copying or translation. ### Fixed @@ -31,7 +32,7 @@ Earlier releases remain available in the 0.25 release history and the legacy rel ### Testing -- Added regressions for revision repair, P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, mobile dialogues, conflict-aware chunk reachability, device-progress safeguards, compaction timeouts, shared chunks, collection propagation, and content-addressed chunk recreation. +- Added regressions for revision repair, P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, selectable and mobile dialogues, conflict-aware chunk reachability, device-progress safeguards, compaction timeouts, shared chunks, collection propagation, and content-addressed chunk recreation. ## 1.0.0-beta.2 From c24fc1dc82e0fe1bb74e5438e2a694c34818da08 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 24 Jul 2026 19:06:53 +0000 Subject: [PATCH 096/117] Update Commonlib to 0.1.0-rc.12 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 531210de..e23e436a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@smithy/querystring-builder": "^4.2.9", "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", - "@vrtmrz/livesync-commonlib": "0.1.0-rc.11", + "@vrtmrz/livesync-commonlib": "0.1.0-rc.12", "@vrtmrz/obsidian-plugin-kit": "0.1.2", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", @@ -4764,9 +4764,9 @@ } }, "node_modules/@vrtmrz/livesync-commonlib": { - "version": "0.1.0-rc.11", - "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.0-rc.11.tgz", - "integrity": "sha512-o811duZFajxDFI6cy7zwtXPg6lihx5e1MH6Xse1sx4HseYurbaDf9DtZJdAtfkjTxl5wOZRSnVMoMLn+I/xD+g==", + "version": "0.1.0-rc.12", + "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.0-rc.12.tgz", + "integrity": "sha512-pMiOL4x5pDKCYOwtH3nG+9Ra1sLjowItBUrYoHpvcrweV4NSN0OkAEMk1vxBQlKMmKtnrAdy0WuQvmsdLOWHqA==", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", diff --git a/package.json b/package.json index ad11c68b..cfcaff98 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,7 @@ "@smithy/querystring-builder": "^4.2.9", "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", - "@vrtmrz/livesync-commonlib": "0.1.0-rc.11", + "@vrtmrz/livesync-commonlib": "0.1.0-rc.12", "@vrtmrz/obsidian-plugin-kit": "0.1.2", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", From 0d68b6530fcc4cb7bff75521b5eba7b2970586cf Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sat, 25 Jul 2026 03:48:51 +0000 Subject: [PATCH 097/117] Test Garbage Collection V3 against CouchDB --- docs/specs_garbage_collection.md | 4 +- ...CmdLocalDatabaseMainte.integration.spec.ts | 257 ++++++++++++++++++ 2 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.integration.spec.ts diff --git a/docs/specs_garbage_collection.md b/docs/specs_garbage_collection.md index 40f00c82..8a75f975 100644 --- a/docs/specs_garbage_collection.md +++ b/docs/specs_garbage_collection.md @@ -60,4 +60,6 @@ Commonlib tests use real in-memory PouchDB revision trees to verify: - propagation of chunk deletion to another PouchDB database; and - recreation and propagation when the same content is written again. -Self-hosted LiveSync tests verify that Garbage Collection V3 uses Commonlib's revision-aware result, deletes only unreachable chunks, performs the initial bidirectional and final push-only replications in order, and requests remote compaction. CouchDB's own compaction implementation remains an external database boundary. +Self-hosted LiveSync unit tests verify that Garbage Collection V3 uses Commonlib's revision-aware result, deletes only unreachable chunks, performs the initial bidirectional and final push-only replications in order, and requests remote compaction. + +A disposable real-CouchDB integration test verifies logical deletion on the server, retention of shared and conflict chunks, compaction completion, replication after collection, and recreation of a content-addressed chunk. CouchDB's choice and timing of physical byte reclamation remain an external database boundary, so the test does not assert a particular reduction in database size. diff --git a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.integration.spec.ts b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.integration.spec.ts new file mode 100644 index 00000000..84f94ed2 --- /dev/null +++ b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.integration.spec.ts @@ -0,0 +1,257 @@ +import { describe, expect, it, vi } from "vitest"; +import PouchDB from "pouchdb-core"; +import HttpPouch from "pouchdb-adapter-http"; +import MemoryAdapter from "pouchdb-adapter-memory"; +import replication from "pouchdb-replication"; + +vi.mock("@/features/LiveSyncCommands", () => ({ + LiveSyncCommands: class LiveSyncCommands { + core!: { settings: unknown }; + get settings() { + return this.core.settings; + } + }, +})); +vi.mock("@/common/events", () => ({ + EVENT_ANALYSE_DB_USAGE: "analyse", + EVENT_REQUEST_PERFORM_GC_V3: "gc", + eventHub: { + onEvent: vi.fn(), + }, +})); + +import { + DEFAULT_SETTINGS, + REMOTE_COUCHDB, + type DocumentID, + type EntryDoc, +} from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { LiveSyncLocalDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/LiveSyncLocalDB"; +import { LocalDatabaseMaintenance } from "./CmdLocalDatabaseMainte"; + +PouchDB.plugin(HttpPouch).plugin(MemoryAdapter).plugin(replication); + +type FixtureContent = { + type: "leaf" | "plain"; + data?: string; + path?: string; + children?: string[]; + ctime?: number; + mtime?: number; + size?: number; + eden?: Record; +}; + +type FixtureDocument = PouchDB.Core.PutDocument & { + _id: DocumentID; + _revisions?: { + start: number; + ids: string[]; + }; +}; + +function chunk(id: string, data = id): FixtureDocument { + return { + _id: id as DocumentID, + type: "leaf", + data, + }; +} + +function revision(id: string, rev: string, history: string[], children: string[]): FixtureDocument { + return { + _id: id as DocumentID, + _rev: rev, + _revisions: { + start: Number(rev.split("-")[0]), + ids: history, + }, + type: "plain", + path: id, + children, + ctime: 1, + mtime: 1, + size: children.length, + eden: {}, + } as unknown as FixtureDocument; +} + +function liveSyncDatabaseFor(database: PouchDB.Database): LiveSyncLocalDB { + const subject = Object.create(LiveSyncLocalDB.prototype) as LiveSyncLocalDB; + Object.assign(subject, { + localDatabase: database as unknown as PouchDB.Database, + }); + return subject; +} + +function requiredEnvironment(name: "hostname" | "username" | "password"): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Missing required integration-test environment variable: ${name}`); + } + return value; +} + +describe("LocalDatabaseMaintenance Garbage Collection V3 with CouchDB", () => { + it("propagates collection safely, completes compaction, and permits content-addressed chunk recreation", async () => { + const databaseName = `livesync-gcv3-${crypto.randomUUID()}`; + const local = new PouchDB(`${databaseName}-source`, { adapter: "memory" }); + const replica = new PouchDB(`${databaseName}-replica`, { adapter: "memory" }); + const remote = new PouchDB( + `${requiredEnvironment("hostname").replace(/\/+$/u, "")}/${databaseName}`, + { + adapter: "http", + auth: { + username: requiredEnvironment("username"), + password: requiredEnvironment("password"), + }, + } + ); + + try { + await remote.info(); + await local.bulkDocs([ + chunk("h:obsolete"), + chunk("h:current"), + chunk("h:shared"), + chunk("h:base"), + chunk("h:left"), + chunk("h:right"), + ]); + + const firstRevision = revision("first.md", "1-first", ["first"], ["h:obsolete"]); + delete firstRevision._rev; + delete firstRevision._revisions; + await local.put(firstRevision); + await local.put({ + ...(await local.get("first.md")), + children: ["h:current"], + }); + + for (const id of ["second.md", "third.md"]) { + const sharedRevision = revision(id, `1-${id}`, [id], ["h:shared"]); + delete sharedRevision._rev; + delete sharedRevision._revisions; + await local.put(sharedRevision); + } + + await local.bulkDocs( + [ + revision("conflicted.md", "1-base", ["base"], ["h:base"]), + revision("conflicted.md", "2-left", ["left", "base"], ["h:left"]), + revision("conflicted.md", "2-right", ["right", "base"], ["h:right"]), + ], + { new_edits: false } + ); + + const liveSyncDatabase = liveSyncDatabaseFor(local); + const replicationModes: string[] = []; + const replicator = { + openOneShotReplication: vi.fn( + async ( + _settings: typeof DEFAULT_SETTINGS, + _showResult: boolean, + _ignoreCleanLock: boolean, + mode: string + ) => { + replicationModes.push(mode); + if (mode === "sync") { + await local.sync(remote); + } else if (mode === "pushOnly") { + await local.replicate.to(remote); + } else { + throw new Error(`Unexpected replication mode: ${mode}`); + } + return true; + } + ), + getConnectedDeviceList: vi.fn(() => + Promise.resolve({ + accepted_nodes: ["integration-device"], + node_info: { + "integration-device": { + progress: "10-local", + device_name: "Integration device", + app_version: "1.12.7", + plugin_version: "1.0.0-beta.0", + }, + }, + }) + ), + connectRemoteCouchDBWithSetting: vi.fn(() => Promise.resolve({ db: remote })), + }; + const notice = vi.fn(); + const maintenance = Object.create(LocalDatabaseMaintenance.prototype) as LocalDatabaseMaintenance; + Object.assign(maintenance, { + core: { + settings: { + ...DEFAULT_SETTINGS, + remoteType: REMOTE_COUCHDB, + }, + replicator, + confirm: { + askSelectStringDialogue: vi.fn(() => Promise.resolve("Proceed Garbage Collection")), + }, + }, + localDatabase: liveSyncDatabase, + _notice: notice, + }); + vi.spyOn(maintenance, "ensureAvailable").mockResolvedValue(true); + const clearHash = vi.spyOn(maintenance, "clearHash").mockImplementation(() => undefined); + + await maintenance.gcv3(); + + expect(replicationModes).toEqual(["sync", "pushOnly"]); + expect(clearHash).toHaveBeenCalledOnce(); + expect(notice).toHaveBeenCalledWith("Compaction on remote database completed successfully.", "gc-compact"); + expect(notice).not.toHaveBeenCalledWith("Compaction on remote database timed out.", "gc-compact"); + expect(notice).not.toHaveBeenCalledWith("Compaction on remote database failed.", "gc-compact"); + + const obsoleteRow = (await remote.allDocs({ keys: ["h:obsolete"] })).rows[0]; + expect(obsoleteRow).toMatchObject({ + id: "h:obsolete", + value: { + deleted: true, + }, + }); + + for (const retainedChunk of ["h:current", "h:shared", "h:base", "h:left", "h:right"]) { + await expect(remote.get(retainedChunk)).resolves.toMatchObject({ + _id: retainedChunk, + type: "leaf", + }); + } + await expect(remote.get("second.md")).resolves.toMatchObject({ children: ["h:shared"] }); + await expect(remote.get("third.md")).resolves.toMatchObject({ children: ["h:shared"] }); + await expect(remote.get("conflicted.md", { conflicts: true })).resolves.toMatchObject({ + _conflicts: [expect.any(String)], + }); + + await remote.replicate.to(replica); + await expect(replica.get("h:obsolete")).rejects.toMatchObject({ status: 404 }); + for (const retainedChunk of ["h:current", "h:shared", "h:base", "h:left", "h:right"]) { + await expect(replica.get(retainedChunk)).resolves.toMatchObject({ + _id: retainedChunk, + type: "leaf", + }); + } + + await local.put(chunk("h:obsolete", "recreated")); + await local.replicate.to(remote); + await remote.replicate.to(replica); + + await expect(remote.get("h:obsolete")).resolves.toMatchObject({ + _id: "h:obsolete", + type: "leaf", + data: "recreated", + }); + await expect(replica.get("h:obsolete")).resolves.toMatchObject({ + _id: "h:obsolete", + type: "leaf", + data: "recreated", + }); + } finally { + await Promise.all([local.destroy(), replica.destroy(), remote.destroy()]); + } + }, 30_000); +}); From a7bc337fd1dc59b185fd268e9d531a1cec3ef916 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sat, 25 Jul 2026 03:49:18 +0000 Subject: [PATCH 098/117] Test Security Seed refresh in Real Obsidian --- package.json | 1 + test/e2e-obsidian/README.md | 13 +- test/e2e-obsidian/runner/couchdb.ts | 48 +- .../runner/liveSyncWorkflow.test.ts | 23 + test/e2e-obsidian/runner/liveSyncWorkflow.ts | 57 +- test/e2e-obsidian/runner/securitySeed.test.ts | 63 ++ test/e2e-obsidian/runner/securitySeed.ts | 77 ++ test/e2e-obsidian/scripts/run-focused.ts | 1 + .../scripts/security-seed-reconnect.ts | 745 ++++++++++++++++++ 9 files changed, 1005 insertions(+), 23 deletions(-) create mode 100644 test/e2e-obsidian/runner/securitySeed.test.ts create mode 100644 test/e2e-obsidian/runner/securitySeed.ts create mode 100644 test/e2e-obsidian/scripts/security-seed-reconnect.ts diff --git a/package.json b/package.json index cfcaff98..927ee1db 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "test:e2e:obsidian:startup-scan": "tsx test/e2e-obsidian/scripts/startup-scan.ts", "test:e2e:obsidian:setup-uri-workflow": "tsx test/e2e-obsidian/scripts/setup-uri-workflow.ts", "test:e2e:obsidian:two-vault-sync": "tsx test/e2e-obsidian/scripts/two-vault-sync.ts", + "test:e2e:obsidian:security-seed-reconnect": "tsx test/e2e-obsidian/scripts/security-seed-reconnect.ts", "test:e2e:obsidian:hidden-file-snippet-sync": "tsx test/e2e-obsidian/scripts/hidden-file-snippet-sync.ts", "test:e2e:obsidian:customisation-sync": "tsx test/e2e-obsidian/scripts/customisation-sync.ts", "test:e2e:obsidian:setting-markdown-export": "tsx test/e2e-obsidian/scripts/setting-markdown-export.ts", diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index f06c7b57..73b17e1e 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -55,6 +55,7 @@ After changing plug-in source, use the focused wrapper rather than invoking a sc ```bash npm run test:e2e:obsidian:focused -- settings-ui npm run test:e2e:obsidian:focused -- two-vault-sync +npm run test:e2e:obsidian:focused -- security-seed-reconnect ``` The wrapper accepts only maintained real-Obsidian scenario names; run it with `--help` for the current list. It deliberately does not manage CouchDB, Object Storage, or the P2P signalling relay. Start the required fixture first, or use the complete service-managed suite. @@ -101,7 +102,7 @@ The same workflow checks the two remote-activity status boundaries. It first hol `test:e2e:obsidian:couchdb-manual-setup-workflow` follows the visible onboarding path for the first device when no Setup URI is available. It enters end-to-end encryption and CouchDB details, runs the read-only `Check server requirements` step, requires the prepared fixture to pass without applying a server fix, and lets the onboarding connection test create the named database. After Rebuild completes on the first device, it creates an ordinary note, asks that working device to generate a Setup URI for a second device, completes Fetch there, and verifies a bidirectional note round-trip. The workflow captures each decision point and the expanded server-check result; password controls remain visually masked. -If this status workflow fails while Obsidian is running, it writes a full-page screenshot and a JSON snapshot of the status text and counters under `/tmp/obsidian-livesync-e2e`. The dialogue-mount workflow leaves desktop and mobile screenshots for both representative Svelte routes, and the Hidden File Sync workflow captures the successfully displayed JSON Resolve dialogue before selecting an option. The suite therefore records representative evidence without capturing every interaction. Set `E2E_OBSIDIAN_DIAGNOSTICS_DIR` to use another directory. +If this status workflow fails while Obsidian is running, it writes a full-page screenshot and a JSON snapshot of the status text and counters under `/tmp/obsidian-livesync-e2e`. The dialogue-mount workflow leaves desktop and mobile screenshots for both representative Svelte routes, the Hidden File Sync workflow captures the successfully displayed JSON Resolve dialogue before selecting an option, and the Security Seed reconnect workflow captures each significant application state. The suite therefore records representative evidence without capturing every interaction. Set `E2E_OBSIDIAN_DIAGNOSTICS_DIR` to use another directory. The two-Vault workflow performs the missing-marker review once for each isolated Vault. Later process launches reuse the same profile-backed acknowledgement, rather than seeding a replacement or repeatedly applying a decision for the first device. The Hidden File Sync scenario is narrower: it starts from an explicitly acknowledged marker because it tests consumer-owned hidden-file behaviour, JSON resolution, target filtering, and grouped mobile Notices rather than duplicating the compatibility workflow. After `app.emulateMobile(true)`, its fixture operations use the active DevTools renderer because Obsidian can remove desktop-only CLI commands in mobile mode. @@ -134,6 +135,12 @@ LIVESYNC_CLI_COMMAND="docker run --rm --network host --user $(id -u):$(id -g) -- `test:e2e:obsidian:two-vault-sync` runs a two-vault note synchronisation workflow. It verifies note creation, update, ordinary rename, a case-only file name change within the same directory, deletion, and a separate encrypted round-trip with Path Obfuscation enabled. Its target-filter scenario confirms that one Vault receives and checkpoints a remote document without reflecting it, restarts with the same profile and filter, and then reflects the stored document after the filter is broadened through the settings service. Directory case changes deliberately remain outside this scenario because they require directory-aware rename handling. The optional Markdown conflict check can be enabled with `E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT=true`. It creates divergent revisions in two separate Vaults, performs a conservative merge on one Vault, edits that result again, and requires the other Vault to replace its known deleted losing revision without recreating the conflict. The separate `E2E_OBSIDIAN_INCLUDE_CONFLICT_OPERATIONS=true` check keeps four conflicts active while one Vault edits, deletes, performs a case-only rename, and performs a cross-path rename. It asserts that each operation extends the revision displayed on that device, replicates the exact resulting revision tree, and preserves the other live branch. During focused development, `E2E_OBSIDIAN_ONLY_CONFLICT_OPERATIONS=true` runs that self-contained scope without the ordinary, target-filter, or encrypted scenarios. Both conflict checks remain outside the default local suite. +`test:e2e:obsidian:security-seed-reconnect` is a focused CouchDB release-acceptance workflow. Device A first recognises an initial remote Security Seed, stops automatic replication while remaining open, and creates an unsent note. The runner replaces only the Security Seed in the managed remote synchronisation-parameter fixture. Device A must retain its deliberately stale cached value until the next one-shot synchronisation, refresh it before sending, and upload an HKDF-encrypted payload which uses the replacement value. A fresh device B must decrypt that note and send an encrypted note back; the original device A then receives the return journey with its Vault and isolated profile preserved. Desktop Obsidian may enforce a single application instance, so the two device sessions run sequentially after the same-process stale-cache assertion has completed. + +The workflow creates a random dedicated database, records only SHA-256 Seed fingerprints, and never writes a Seed, passphrase, or CouchDB credentials to its result. It also requires the remote Seed and all other synchronisation parameters to remain unchanged after the replacement revision, rejects HKDF and Seed errors from either session, writes `security-seed-reconnect-result.json`, and verifies that every Obsidian process, temporary Vault, isolated profile, and database has been removed. The result file and stage screenshots are retained in `E2E_OBSIDIAN_DIAGNOSTICS_DIR`; the screenshots show ordinary Vault content, not settings or secrets. The strict cleanup workflow rejects `E2E_OBSIDIAN_KEEP_VAULT` and `E2E_OBSIDIAN_KEEP_COUCHDB`. + +This proves in real Obsidian the plug-in behaviour shared by supported platforms, including the encrypted bidirectional round-trip and protection against a stale client restoring the old remote Seed. It does not verify iPadOS-specific background or reconnect lifecycle behaviour, and it does not count as Android device evidence. The workflow remains outside `test:e2e:obsidian:local-suite` because it is a focused release-acceptance check. + `test:e2e:obsidian:conflict-dialog-policy` creates three real local revision leaves without a remote service and opens the pairwise merge dialogue in Obsidian. It verifies the three-version count, requires the four decision buttons to be stacked vertically, concatenates the displayed pair as a child of the displayed winner, confirms that the untouched leaf remains as one conflict, postpones that remaining pair, restarts the same isolated Vault and profile, and confirms that only the two live versions are reconstructed. It also verifies that an ordinary repeated conflict check does not reopen a postponed dialogue, that **Resolve if conflicted.** explicitly reopens it, and that the active editor retains the appropriate unresolved-conflict warning. The scenario then invokes the same Commonlib consumer boundary used for an incoming replicated document and checks that a postponed warning disappears, an open stale dialogue closes, and the conflict-processing queue completes even when the dialogue closes immediately. This isolates the Obsidian UI contract from transport and second-device setup. The fixture owns one temporary Vault and profile, and the session runner stops Obsidian before removing them. `test:e2e:obsidian:revision-repair` creates two live revisions in a temporary real Obsidian Vault, removes a chunk used only by the non-winning revision, and proves that automatic conflict checking does not discard the unreadable branch. **Verify and repair all files** must show the winner and conflict separately, identify the exact unreadable revision and missing chunk, and leave the revision tree unchanged when reading is retried. The scenario then verifies both the cancellation path and the explicit confirmation path for discarding that exact unreadable live revision, requires the winner to remain unchanged, and captures the repair card. It uses no remote service; a retry is therefore expected to remain unreadable unless the chunk is already available locally. @@ -204,12 +211,14 @@ Useful environment variables: - `LIVESYNC_CLI_COMMAND`: optional LiveSync CLI executable and prefix arguments used by the CLI-to-Obsidian compatibility check. The default is the locally built CLI. - `E2E_LIVESYNC_SOURCE_ARTIFACT_ROOT`: optional cache directory containing the exact pinned 0.25.83 plug-in artefacts. Cached files are always checksum-verified. - `E2E_LIVESYNC_TARGET_ARTIFACT_ROOT`: directory containing the built 1.0 target `main.js`, `manifest.json`, and `styles.css`; default is the repository root. +- `E2E_OBSIDIAN_ARTIFACT_ROOT`: directory containing the plug-in artefact installed by a direct scenario invocation; default is the repository root. +- `E2E_OBSIDIAN_ARTIFACT_REVISION`: exact source commit recorded by the Security Seed reconnect result when `E2E_OBSIDIAN_ARTIFACT_ROOT` is a downloaded artefact rather than a Git worktree. - `E2E_OBSIDIAN_FILE_TIMEOUT_MS`: timeout for waiting until a note created through Obsidian's vault API is reflected to disk. - `E2E_OBSIDIAN_CORE_READY_TIMEOUT_MS`: timeout for waiting until Self-hosted LiveSync reports that its core lifecycle and local database are ready. - `E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS`: timeout for waiting until a file appears in Self-hosted LiveSync's local database. - `E2E_OBSIDIAN_COUCHDB_TIMEOUT_MS`: timeout for waiting until CouchDB contains uploaded E2E documents. - `E2E_OBSIDIAN_REMOTE_ACTIVITY_TIMEOUT_MS`: timeout for an observed remote activity to enter or leave its status boundary; default is 30 seconds. -- `E2E_OBSIDIAN_DIAGNOSTICS_DIR`: directory for screenshots and status snapshots captured after a remote-activity failure; default is `/tmp/obsidian-livesync-e2e`. +- `E2E_OBSIDIAN_DIAGNOSTICS_DIR`: directory for screenshots and status snapshots, including the Security Seed reconnect stages; default is `/tmp/obsidian-livesync-e2e`. - `E2E_OBSIDIAN_OBJECT_STORAGE_TIMEOUT_MS`: timeout for waiting until Object Storage contains uploaded E2E objects. - `E2E_OBSIDIAN_KEEP_COUCHDB=true`: keep the temporary CouchDB database for inspection. - `E2E_OBSIDIAN_KEEP_OBJECT_STORAGE=true`: keep the temporary Object Storage prefix for inspection. diff --git a/test/e2e-obsidian/runner/couchdb.ts b/test/e2e-obsidian/runner/couchdb.ts index 11672549..1ed61f88 100644 --- a/test/e2e-obsidian/runner/couchdb.ts +++ b/test/e2e-obsidian/runner/couchdb.ts @@ -42,6 +42,12 @@ export type CouchDbDatabaseInfo = { update_seq: number | string; }; +export type CouchDbPutResponse = { + ok: boolean; + id: string; + rev: string; +}; + function parseEnvFile(content: string): Record { const entries = content .split(/\r?\n/u) @@ -79,6 +85,14 @@ function databaseUrl(config: Pick, dbName: string, suffix return `${config.uri.replace(/\/+$/u, "")}/${encodeURIComponent(dbName)}${suffix}`; } +function documentSuffix(documentId: string): string { + const localPrefix = "_local/"; + if (documentId.startsWith(localPrefix)) { + return `/_local/${encodeURIComponent(documentId.slice(localPrefix.length))}`; + } + return `/${encodeURIComponent(documentId)}`; +} + async function couchDbRequest( config: Pick, path: string, @@ -146,8 +160,8 @@ export async function putCouchDbDocument( config: CouchDbConfig, dbName: string, document: CouchDbDocument -): Promise { - const response = await fetch(databaseUrl(config, dbName, `/${encodeURIComponent(document._id)}`), { +): Promise { + const response = await fetch(databaseUrl(config, dbName, documentSuffix(document._id)), { method: "PUT", headers: { authorization: authHeader(config), @@ -160,6 +174,23 @@ export async function putCouchDbDocument( `Failed to write CouchDB document ${document._id}. HTTP ${response.status}: ${await response.text()}` ); } + return (await response.json()) as CouchDbPutResponse; +} + +export async function fetchCouchDbDocument( + config: CouchDbConfig, + dbName: string, + documentId: string +): Promise { + const response = await fetch(databaseUrl(config, dbName, documentSuffix(documentId)), { + headers: { authorization: authHeader(config) }, + }); + if (!response.ok) { + throw new Error( + `Failed to read CouchDB document ${documentId}. HTTP ${response.status}: ${await response.text()}` + ); + } + return (await response.json()) as CouchDbDocument; } export async function deleteCouchDbDatabase(config: CouchDbConfig, dbName: string): Promise { @@ -174,6 +205,19 @@ export async function deleteCouchDbDatabase(config: CouchDbConfig, dbName: strin } } +export async function couchDbDatabaseExists(config: CouchDbConfig, dbName: string): Promise { + const response = await fetch(databaseUrl(config, dbName), { + headers: { authorization: authHeader(config) }, + }); + if (response.status === 404) { + return false; + } + if (!response.ok) { + throw new Error(`Failed to inspect CouchDB ${dbName}. HTTP ${response.status}: ${await response.text()}`); + } + return true; +} + export async function fetchAllCouchDbDocs(config: CouchDbConfig, dbName: string): Promise { const response = await fetch(databaseUrl(config, dbName, "/_all_docs?include_docs=true"), { headers: { authorization: authHeader(config) }, diff --git a/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts b/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts index 17c4dea6..c85d0163 100644 --- a/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts +++ b/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts @@ -10,6 +10,7 @@ vi.mock("./cli.ts", () => ({ evalObsidianJson })); import { assertE2eCompatibilityMarker, createE2eCouchDbPluginData, + waitForLiveSyncCoreReady, type CompatibilityMarkerState, } from "./liveSyncWorkflow.ts"; @@ -54,3 +55,25 @@ describe("configured CouchDB fixture", () => { expect(pluginData.activeConfigurationId).toBe(Object.keys(remoteConfigurations ?? {})[0]); }); }); + +describe("Real Obsidian core readiness", () => { + it("retries while the plug-in core is temporarily unavailable during reload", async () => { + evalObsidianJson.mockReset(); + evalObsidianJson + .mockRejectedValueOnce(new Error("Cannot read properties of undefined (reading 'core')")) + .mockResolvedValueOnce({ + databaseReady: true, + appReady: true, + configured: true, + remoteType: "", + settingVersion: 10, + suspended: false, + }); + + await expect(waitForLiveSyncCoreReady("obsidian-cli", {}, 1000)).resolves.toMatchObject({ + databaseReady: true, + appReady: true, + }); + expect(evalObsidianJson).toHaveBeenCalledTimes(2); + }); +}); diff --git a/test/e2e-obsidian/runner/liveSyncWorkflow.ts b/test/e2e-obsidian/runner/liveSyncWorkflow.ts index 0827c269..ca10fd62 100644 --- a/test/e2e-obsidian/runner/liveSyncWorkflow.ts +++ b/test/e2e-obsidian/runner/liveSyncWorkflow.ts @@ -353,31 +353,50 @@ export async function waitForLiveSyncCoreReady( ): Promise { const deadline = Date.now() + timeoutMs; let lastReadiness: CoreReadiness | undefined; + let lastError: unknown; while (Date.now() < deadline) { - lastReadiness = await evalObsidianJson( - cliBinary, - [ - "(async()=>{", - "const core=app.plugins.plugins['obsidian-livesync'].core;", - "const settings=core.services.setting.currentSettings();", - "return JSON.stringify({", - "databaseReady:core.services.database.isDatabaseReady(),", - "appReady:core.services.appLifecycle.isReady(),", - "configured:settings?.isConfigured===true,", - "remoteType:settings?.remoteType??'',", - "settingVersion:settings?.settingVersion,", - "suspended:core.services.appLifecycle.isSuspended(),", - "});", - "})()", - ].join(""), - env - ); + try { + lastReadiness = await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync']?.core;", + "if(!core) return JSON.stringify({databaseReady:false,appReady:false});", + "const settings=core.services.setting.currentSettings();", + "return JSON.stringify({", + "databaseReady:core.services.database.isDatabaseReady(),", + "appReady:core.services.appLifecycle.isReady(),", + "configured:settings?.isConfigured===true,", + "remoteType:settings?.remoteType??'',", + "settingVersion:settings?.settingVersion,", + "suspended:core.services.appLifecycle.isSuspended(),", + "});", + "})()", + ].join(""), + env + ); + lastError = undefined; + } catch (error) { + // Obsidian reloads the renderer while enabling the plug-in. During + // that short window the CLI can reach the Vault before the plug-in + // catalogue has exposed its core. This is a readiness state, not a + // failed scenario, so retain the error for the eventual timeout. + lastError = error; + await new Promise((resolve) => setTimeout(resolve, 500)); + continue; + } if (lastReadiness.databaseReady && lastReadiness.appReady) { return lastReadiness; } await new Promise((resolve) => setTimeout(resolve, 500)); } - throw new Error(`Timed out waiting for Self-hosted LiveSync core readiness: ${JSON.stringify(lastReadiness)}`); + const errorSuffix = + lastError === undefined + ? "" + : ` Last error: ${lastError instanceof Error ? lastError.message : String(lastError)}`; + throw new Error( + `Timed out waiting for Self-hosted LiveSync core readiness: ${JSON.stringify(lastReadiness)}${errorSuffix}` + ); } /** diff --git a/test/e2e-obsidian/runner/securitySeed.test.ts b/test/e2e-obsidian/runner/securitySeed.test.ts new file mode 100644 index 00000000..02966683 --- /dev/null +++ b/test/e2e-obsidian/runner/securitySeed.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { + SECURITY_SEED_DOCUMENT_ID, + changedSynchronisationParameterFields, + fingerprintSecuritySeed, + replaceSecuritySeed, + requireSecuritySeedDocument, + snapshotSecuritySeedDocument, +} from "./securitySeed.ts"; + +const seedA = Buffer.alloc(32, 1).toString("base64"); +const seedB = Buffer.alloc(32, 2).toString("base64"); + +describe("Security Seed E2E evidence", () => { + it("reports stable, non-secret fingerprints", () => { + expect(fingerprintSecuritySeed(seedA)).toMatch(/^sha256:[0-9a-f]{16}$/u); + expect(fingerprintSecuritySeed(seedA)).toBe(fingerprintSecuritySeed(seedA)); + expect(fingerprintSecuritySeed(seedA)).not.toBe(fingerprintSecuritySeed(seedB)); + }); + + it("redacts the Seed from the machine-readable document snapshot", () => { + const document = requireSecuritySeedDocument({ + _id: SECURITY_SEED_DOCUMENT_ID, + _rev: "0-1", + type: "syncinfo", + protocolVersion: 2, + pbkdf2salt: seedA, + }); + + const snapshot = snapshotSecuritySeedDocument(document); + + expect(snapshot).toEqual({ + id: SECURITY_SEED_DOCUMENT_ID, + revision: "0-1", + fingerprint: fingerprintSecuritySeed(seedA), + fields: { + type: "syncinfo", + protocolVersion: 2, + }, + }); + expect(JSON.stringify(snapshot)).not.toContain(seedA); + }); + + it("replaces only the Seed and identifies later synchronisation-parameter changes", () => { + const before = requireSecuritySeedDocument({ + _id: SECURITY_SEED_DOCUMENT_ID, + _rev: "0-1", + type: "syncinfo", + protocolVersion: 2, + pbkdf2salt: seedA, + }); + const replaced = replaceSecuritySeed(before, seedB); + const laterRevision = { + ...replaced, + _rev: "0-3", + }; + + expect(before.pbkdf2salt).toBe(seedA); + expect(replaced.pbkdf2salt).toBe(seedB); + expect(changedSynchronisationParameterFields(before, replaced)).toEqual(["pbkdf2salt"]); + expect(changedSynchronisationParameterFields(replaced, laterRevision)).toEqual([]); + }); +}); diff --git a/test/e2e-obsidian/runner/securitySeed.ts b/test/e2e-obsidian/runner/securitySeed.ts new file mode 100644 index 00000000..5851b078 --- /dev/null +++ b/test/e2e-obsidian/runner/securitySeed.ts @@ -0,0 +1,77 @@ +import { createHash, randomBytes } from "node:crypto"; +import type { CouchDbDocument } from "./couchdb.ts"; + +export const SECURITY_SEED_DOCUMENT_ID = "_local/obsidian_livesync_sync_parameters"; + +export type SecuritySeedDocument = CouchDbDocument & { + _id: typeof SECURITY_SEED_DOCUMENT_ID; + _rev: string; + pbkdf2salt: string; +}; + +export type SecuritySeedDocumentSnapshot = { + id: string; + revision: string; + fingerprint: string; + fields: Record; +}; + +function decodeSecuritySeed(seed: string): Buffer { + const bytes = Buffer.from(seed, "base64"); + if (seed.length === 0 || bytes.length === 0) { + throw new Error("The Security Seed is empty or is not valid base64."); + } + return bytes; +} + +export function createSecuritySeed(): string { + return randomBytes(32).toString("base64"); +} + +export function fingerprintSecuritySeed(seed: string): string { + const bytes = Uint8Array.from(decodeSecuritySeed(seed)); + return `sha256:${createHash("sha256").update(bytes).digest("hex").slice(0, 16)}`; +} + +export function requireSecuritySeedDocument(document: CouchDbDocument): SecuritySeedDocument { + if (document._id !== SECURITY_SEED_DOCUMENT_ID) { + throw new Error(`Unexpected synchronisation-parameter document: ${document._id}`); + } + if (typeof document._rev !== "string" || document._rev.length === 0) { + throw new Error("The synchronisation-parameter document does not have a revision."); + } + if (typeof document.pbkdf2salt !== "string") { + throw new Error("The synchronisation-parameter document does not have a Security Seed."); + } + decodeSecuritySeed(document.pbkdf2salt); + return document as SecuritySeedDocument; +} + +export function replaceSecuritySeed(document: SecuritySeedDocument, replacementSeed: string): SecuritySeedDocument { + decodeSecuritySeed(replacementSeed); + return { + ...document, + pbkdf2salt: replacementSeed, + }; +} + +export function snapshotSecuritySeedDocument(document: SecuritySeedDocument): SecuritySeedDocumentSnapshot { + const { _id, _rev, pbkdf2salt, ...fields } = document; + return { + id: _id, + revision: _rev, + fingerprint: fingerprintSecuritySeed(pbkdf2salt), + fields, + }; +} + +export function changedSynchronisationParameterFields( + before: SecuritySeedDocument, + after: SecuritySeedDocument +): string[] { + const ignoredFields = new Set(["_rev"]); + return [...new Set([...Object.keys(before), ...Object.keys(after)])] + .filter((key) => !ignoredFields.has(key)) + .filter((key) => JSON.stringify(before[key]) !== JSON.stringify(after[key])) + .sort(); +} diff --git a/test/e2e-obsidian/scripts/run-focused.ts b/test/e2e-obsidian/scripts/run-focused.ts index 7f3274d1..a46bc515 100644 --- a/test/e2e-obsidian/scripts/run-focused.ts +++ b/test/e2e-obsidian/scripts/run-focused.ts @@ -22,6 +22,7 @@ const focusedScenarios = new Set([ "startup-scan", "setup-uri-workflow", "two-vault-sync", + "security-seed-reconnect", "hidden-file-snippet-sync", "customisation-sync", "setting-markdown-export", diff --git a/test/e2e-obsidian/scripts/security-seed-reconnect.ts b/test/e2e-obsidian/scripts/security-seed-reconnect.ts new file mode 100644 index 00000000..e0027a23 --- /dev/null +++ b/test/e2e-obsidian/scripts/security-seed-reconnect.ts @@ -0,0 +1,745 @@ +import { execFileSync } from "node:child_process"; +import { createHash, randomUUID } from "node:crypto"; +import { access, mkdir, readFile, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { evalObsidianJson } from "../runner/cli.ts"; +import { + assertCouchDbReachable, + couchDbDatabaseExists, + createCouchDbDatabase, + deleteCouchDbDatabase, + fetchAllCouchDbDocs, + fetchCouchDbDocument, + loadCouchDbConfig, + makeUniqueDatabaseName, + putCouchDbDocument, + waitForCouchDbDocs, + type CouchDbConfig, + type CouchDbDocument, +} from "../runner/couchdb.ts"; +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { + createE2eCouchDbPluginData, + createE2eObsidianDeviceLocalState, + prepareRemote, + pushLocalChanges, + waitForLiveSyncCoreReady, + waitForLocalDatabaseEntry, + type LocalDatabaseEntry, +} from "../runner/liveSyncWorkflow.ts"; +import { + SECURITY_SEED_DOCUMENT_ID, + changedSynchronisationParameterFields, + createSecuritySeed, + fingerprintSecuritySeed, + replaceSecuritySeed, + requireSecuritySeedDocument, + snapshotSecuritySeedDocument, + type SecuritySeedDocument, + type SecuritySeedDocumentSnapshot, +} from "../runner/securitySeed.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { captureObsidianPage } from "../runner/ui.ts"; +import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts"; + +process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "30000"; +process.env.E2E_OBSIDIAN_COUCHDB_TIMEOUT_MS ??= "20000"; +process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ??= "15000"; + +const outboundPath = "E2E/security-seed/device-a.md"; +const returnPath = "E2E/security-seed/device-b.md"; +const hkdfErrorMessages = [ + "Encryption with HKDF failed", + "Decryption with HKDF failed", + "Failed to initialise the encryption key", + "Failed to obtain PBKDF2 salt", +] as const; + +type RunnerContext = { + binary: string; + cliBinary: string; + artifactRoot: string; + couchDb: CouchDbConfig; + dbName: string; + activeSessions: Set; + allSessions: ObsidianLiveSyncSession[]; + screenshots: string[]; +}; + +type DeviceLabel = "device-a" | "device-a-return" | "device-b"; + +type SourceEvidence = { + exactCommit: string; + revisionSource: "git-worktree" | "provided-artifact"; + workingTreeClean: boolean | null; + pluginVersion: string; + pluginArtifactSha256: string; +}; + +type ReplicationSettingsState = { + liveSync: boolean; + syncOnStart: boolean; + syncOnSave: boolean; + periodicReplication: boolean; + syncOnFileOpen: boolean; + syncOnEditorSave: boolean; +}; + +type SessionHealth = { + matchingErrorMessages: string[]; +}; + +type ScenarioEvidence = { + source: SourceEvidence; + securitySeed: { + initial: SecuritySeedDocumentSnapshot; + replacement: SecuritySeedDocumentSnapshot; + final: SecuritySeedDocumentSnapshot; + cachedBeforeReplacement: string; + cachedAfterRemoteReplacement: string; + cachedAfterReplication: string; + replacementChangedFields: string[]; + finalChangedFields: string[]; + }; + synchronisation: { + deviceAToDeviceB: boolean; + deviceBToDeviceA: boolean; + deviceAEncryptedPayload: boolean; + deviceBEncryptedPayload: boolean; + }; + health: { + deviceA: SessionHealth; + deviceB: SessionHealth; + }; + screenshots: string[]; +}; + +type TeardownEvidence = { + sessionsStopped: boolean; + vaultRemoved: boolean; + profileRemoved: boolean; + databaseRemoved: boolean; + remainingTrackedSessions: number; +}; + +class MultipleErrors extends Error { + readonly errors: unknown[]; + + constructor(message: string, errors: unknown[]) { + super(message); + this.name = "MultipleErrors"; + this.errors = errors; + } +} + +function assertEqual(actual: unknown, expected: unknown, message: string): void { + if (actual !== expected) { + throw new Error(`${message}\nExpected: ${String(expected)}\nActual: ${String(actual)}`); + } +} + +function inspectGitRevision( + artifactRoot: string +): Pick { + try { + const exactCommit = execFileSync("git", ["-C", artifactRoot, "rev-parse", "HEAD"], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + const status = execFileSync("git", ["-C", artifactRoot, "status", "--porcelain"], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + return { + exactCommit, + revisionSource: "git-worktree", + workingTreeClean: status.length === 0, + }; + } catch { + const exactCommit = process.env.E2E_OBSIDIAN_ARTIFACT_REVISION?.trim(); + if (!exactCommit) { + throw new Error( + "E2E_OBSIDIAN_ARTIFACT_REVISION is required when the plug-in artefact is not in a Git worktree." + ); + } + return { + exactCommit, + revisionSource: "provided-artifact", + workingTreeClean: null, + }; + } +} + +async function inspectSourceEvidence(artifactRoot: string): Promise { + const manifest = JSON.parse(await readFile(join(artifactRoot, "manifest.json"), "utf-8")) as { + version?: unknown; + }; + if (typeof manifest.version !== "string" || manifest.version.length === 0) { + throw new Error("The plug-in manifest does not have a version."); + } + const mainJs = await readFile(join(artifactRoot, "main.js")); + return { + ...inspectGitRevision(artifactRoot), + pluginVersion: manifest.version, + pluginArtifactSha256: createHash("sha256").update(Uint8Array.from(mainJs)).digest("hex"), + }; +} + +function e2eeSettings(passphrase: string): Record { + return { + encrypt: true, + passphrase, + usePathObfuscation: true, + E2EEAlgorithm: "v2", + }; +} + +async function captureStage(context: RunnerContext, session: ObsidianLiveSyncSession, filename: string): Promise { + const screenshot = await captureObsidianPage(session.remoteDebuggingPort, filename, async () => undefined); + context.screenshots.push(screenshot); + console.log(`Security Seed E2E screenshot: ${screenshot}`); +} + +async function startConfiguredSession( + context: RunnerContext, + vault: TemporaryVault, + passphrase: string, + deviceLabel: DeviceLabel +): Promise { + const couchDbSettings = { + uri: context.couchDb.uri, + username: context.couchDb.username, + password: context.couchDb.password, + dbName: context.dbName, + }; + const overrides = e2eeSettings(passphrase); + const session = await startObsidianLiveSyncSession({ + binary: context.binary, + cliBinary: context.cliBinary, + artifactRoot: context.artifactRoot, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: createE2eCouchDbPluginData(couchDbSettings, overrides), + localStorageEntries: createE2eObsidianDeviceLocalState(vault.name), + }); + context.activeSessions.add(session); + context.allSessions.push(session); + try { + await captureStage(context, session, `security-seed-${deviceLabel}-startup.png`); + await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv); + await prepareRemote(context.cliBinary, session.cliEnv); + await captureStage(context, session, `security-seed-${deviceLabel}-configured.png`); + return session; + } catch (error) { + await captureStage(context, session, `security-seed-${deviceLabel}-setup-failure.png`).catch(() => undefined); + await stopTrackedSession(context, session); + throw error; + } +} + +async function stopTrackedSession(context: RunnerContext, session: ObsidianLiveSyncSession): Promise { + if (!context.activeSessions.has(session)) { + return; + } + await session.app.stop(); + context.activeSessions.delete(session); +} + +async function stopTrackedSessions(context: RunnerContext): Promise { + const errors: unknown[] = []; + for (const session of [...context.activeSessions]) { + try { + await stopTrackedSession(context, session); + } catch (error) { + errors.push(error); + } + } + if (errors.length > 0) { + throw new MultipleErrors("Could not stop every Real Obsidian session.", errors); + } +} + +async function pauseAutomaticReplication(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + const state = await evalObsidianJson( + cliBinary, + [ + "(()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "core.services.replicator.getActiveReplicator()?.closeReplication();", + "const settings=core.services.setting.currentSettings();", + "return JSON.stringify({", + "liveSync:Boolean(settings.liveSync),", + "syncOnStart:Boolean(settings.syncOnStart),", + "syncOnSave:Boolean(settings.syncOnSave),", + "periodicReplication:Boolean(settings.periodicReplication),", + "syncOnFileOpen:Boolean(settings.syncOnFileOpen),", + "syncOnEditorSave:Boolean(settings.syncOnEditorSave),", + "});", + "})()", + ].join(""), + env + ); + for (const [name, enabled] of Object.entries(state)) { + if (enabled) { + throw new Error(`Automatic replication remained enabled through ${name}.`); + } + } + return state; +} + +async function cachedSecuritySeedFingerprint(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + const result = await evalObsidianJson<{ fingerprint: string }>( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const settings=core.services.setting.currentSettings();", + "const replicator=core.services.replicator.getActiveReplicator();", + "const seed=await replicator.getReplicationPBKDF2Salt(settings,false);", + "const digest=await crypto.subtle.digest('SHA-256',seed);", + "const fingerprint='sha256:'+Array.from(new Uint8Array(digest))", + ".map((value)=>value.toString(16).padStart(2,'0')).join('').slice(0,16);", + "return JSON.stringify({fingerprint});", + "})()", + ].join(""), + env + ); + return result.fingerprint; +} + +async function writeNoteViaObsidian( + cliBinary: string, + env: NodeJS.ProcessEnv, + path: string, + content: string +): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const content=${JSON.stringify(content)};`, + "const folder=path.split('/').slice(0,-1).join('/');", + "if(folder&&!(await app.vault.adapter.exists(folder))) await app.vault.createFolder(folder);", + "const existing=app.vault.getAbstractFileByPath(path);", + "if(existing) await app.vault.modify(existing,content);", + "else await app.vault.create(path,content);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + env + ); +} + +async function openNoteViaObsidian(cliBinary: string, env: NodeJS.ProcessEnv, path: string): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + "const file=app.vault.getAbstractFileByPath(path);", + "if(!file) throw new Error(`Could not find note to open: ${path}`);", + "await app.workspace.getLeaf(false).openFile(file);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + env + ); +} + +async function pathExists(path: string): Promise { + try { + await access(path); + return true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return false; + } + throw error; + } +} + +async function waitForPathContent( + vaultPath: string, + path: string, + expected: string, + timeoutMs = Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ?? 15000) +): Promise { + const fullPath = join(vaultPath, path); + const deadline = Date.now() + timeoutMs; + let lastContent = ""; + while (Date.now() < deadline) { + if (await pathExists(fullPath)) { + lastContent = await readFile(fullPath, "utf-8"); + if (lastContent === expected) { + return; + } + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`Timed out waiting for ${path}. Last content:\n${lastContent}`); +} + +function remoteContainsEntry(documents: CouchDbDocument[], entry: LocalDatabaseEntry): boolean { + const ids = new Set(documents.map((document) => document._id)); + return ids.has(entry.id) && entry.children.every((childId) => ids.has(childId)); +} + +async function assertEntryNotRemote(context: RunnerContext, entry: LocalDatabaseEntry): Promise { + const response = await fetchAllCouchDbDocs(context.couchDb, context.dbName); + const documents = response.rows.flatMap((row) => (row.doc ? [row.doc] : [])); + if (remoteContainsEntry(documents, entry)) { + throw new Error("The pending device-A document reached CouchDB before the Security Seed replacement."); + } +} + +async function waitForEncryptedRemoteEntry(context: RunnerContext, entry: LocalDatabaseEntry): Promise { + const documents = await waitForCouchDbDocs(context.couchDb, context.dbName, (docs) => + remoteContainsEntry(docs, entry) + ); + const byId = new Map(documents.map((document) => [document._id, document])); + const encrypted = entry.children.every((childId) => { + const data = byId.get(childId)?.data; + return typeof data === "string" && data.startsWith("%="); + }); + if (!encrypted) { + throw new Error("A replicated chunk did not use the expected HKDF-encrypted payload format."); + } + return true; +} + +async function inspectSessionHealth(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const patterns=${JSON.stringify(hkdfErrorMessages)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "await core.services.API.showWindow('log-log');", + "const sleep=(ms)=>new Promise((resolve)=>setTimeout(resolve,ms));", + "let text='';", + "for(let i=0;i<20;i++){", + "text=Array.from(document.querySelectorAll('.logpane .log pre'))", + ".map((element)=>element.textContent??'').join('\\n');", + "if(text.length>0) break;", + "await sleep(50);", + "}", + "const unresolved=JSON.stringify((await core.services.appLifecycle.getUnresolvedMessages()).flat());", + "const matchingErrorMessages=patterns.filter((pattern)=>text.includes(pattern)||unresolved.includes(pattern));", + "for(const leaf of app.workspace.getLeavesOfType('log-log')) leaf.detach();", + "return JSON.stringify({matchingErrorMessages});", + "})()", + ].join(""), + env + ); +} + +async function fetchSecuritySeedDocument(context: RunnerContext): Promise { + return requireSecuritySeedDocument( + await fetchCouchDbDocument(context.couchDb, context.dbName, SECURITY_SEED_DOCUMENT_ID) + ); +} + +async function replaceRemoteSecuritySeed( + context: RunnerContext, + before: SecuritySeedDocument, + replacementSeed: string +): Promise { + const replacement = replaceSecuritySeed(before, replacementSeed); + const putResult = await putCouchDbDocument(context.couchDb, context.dbName, replacement); + const after = await fetchSecuritySeedDocument(context); + assertEqual(after._rev, putResult.rev, "The replacement Security Seed revision was not stored."); + assertEqual( + fingerprintSecuritySeed(after.pbkdf2salt), + fingerprintSecuritySeed(replacementSeed), + "The replacement Security Seed was not stored." + ); + const changedFields = changedSynchronisationParameterFields(before, after); + assertEqual( + JSON.stringify(changedFields), + JSON.stringify(["pbkdf2salt"]), + "Replacing the remote Security Seed changed another synchronisation parameter." + ); + return after; +} + +async function runScenario( + context: RunnerContext, + vaultA: TemporaryVault, + vaultB: TemporaryVault +): Promise { + const passphrase = `security-seed-e2e-${randomUUID()}`; + const source = await inspectSourceEvidence(context.artifactRoot); + let sessionA = await startConfiguredSession(context, vaultA, passphrase, "device-a"); + + await pushLocalChanges(context.cliBinary, sessionA.cliEnv); + await captureStage(context, sessionA, "security-seed-device-a-initial-sync.png"); + const initialDocument = await fetchSecuritySeedDocument(context); + const initial = snapshotSecuritySeedDocument(initialDocument); + const cachedBeforeReplacement = await cachedSecuritySeedFingerprint(context.cliBinary, sessionA.cliEnv); + assertEqual( + cachedBeforeReplacement, + initial.fingerprint, + "Device A did not cache the initial remote Security Seed." + ); + + await pauseAutomaticReplication(context.cliBinary, sessionA.cliEnv); + const outboundContent = `Encrypted from device A: ${randomUUID()}\n`; + await writeNoteViaObsidian(context.cliBinary, sessionA.cliEnv, outboundPath, outboundContent); + const outboundEntry = await waitForLocalDatabaseEntry(context.cliBinary, sessionA.cliEnv, outboundPath); + await assertEntryNotRemote(context, outboundEntry); + + const replacementSeed = createSecuritySeed(); + const replacementDocument = await replaceRemoteSecuritySeed(context, initialDocument, replacementSeed); + const replacement = snapshotSecuritySeedDocument(replacementDocument); + await openNoteViaObsidian(context.cliBinary, sessionA.cliEnv, outboundPath); + await captureStage(context, sessionA, "security-seed-device-a-replacement-pending.png"); + const cachedAfterRemoteReplacement = await cachedSecuritySeedFingerprint(context.cliBinary, sessionA.cliEnv); + assertEqual( + cachedAfterRemoteReplacement, + initial.fingerprint, + "Device A did not retain the deliberately stale Security Seed before replication." + ); + assertEqual( + replacement.fingerprint, + fingerprintSecuritySeed(replacementSeed), + "The runner did not install the intended replacement Security Seed." + ); + + await pushLocalChanges(context.cliBinary, sessionA.cliEnv); + const cachedAfterReplication = await cachedSecuritySeedFingerprint(context.cliBinary, sessionA.cliEnv); + assertEqual( + cachedAfterReplication, + replacement.fingerprint, + "Device A did not refresh the Security Seed before replication." + ); + const deviceAEncryptedPayload = await waitForEncryptedRemoteEntry(context, outboundEntry); + await captureStage(context, sessionA, "security-seed-device-a-refreshed-sync.png"); + const deviceAHealthBeforeRestart = await inspectSessionHealth(context.cliBinary, sessionA.cliEnv); + await stopTrackedSession(context, sessionA); + + const sessionB = await startConfiguredSession(context, vaultB, passphrase, "device-b"); + await pushLocalChanges(context.cliBinary, sessionB.cliEnv); + await waitForPathContent(vaultB.path, outboundPath, outboundContent); + await openNoteViaObsidian(context.cliBinary, sessionB.cliEnv, outboundPath); + await captureStage(context, sessionB, "security-seed-device-b-received.png"); + + await pauseAutomaticReplication(context.cliBinary, sessionB.cliEnv); + const returnContent = `Encrypted from device B: ${randomUUID()}\n`; + await writeNoteViaObsidian(context.cliBinary, sessionB.cliEnv, returnPath, returnContent); + const returnEntry = await waitForLocalDatabaseEntry(context.cliBinary, sessionB.cliEnv, returnPath); + await pushLocalChanges(context.cliBinary, sessionB.cliEnv); + const deviceBEncryptedPayload = await waitForEncryptedRemoteEntry(context, returnEntry); + const deviceBHealth = await inspectSessionHealth(context.cliBinary, sessionB.cliEnv); + await stopTrackedSession(context, sessionB); + + sessionA = await startConfiguredSession(context, vaultA, passphrase, "device-a-return"); + await pushLocalChanges(context.cliBinary, sessionA.cliEnv); + await waitForPathContent(vaultA.path, returnPath, returnContent); + await openNoteViaObsidian(context.cliBinary, sessionA.cliEnv, returnPath); + await captureStage(context, sessionA, "security-seed-device-a-return-received.png"); + + const finalDocument = await fetchSecuritySeedDocument(context); + const final = snapshotSecuritySeedDocument(finalDocument); + assertEqual( + final.fingerprint, + replacement.fingerprint, + "A client rolled the remote Security Seed back after reconnecting." + ); + const finalChangedFields = changedSynchronisationParameterFields(replacementDocument, finalDocument); + if (finalChangedFields.length > 0) { + throw new Error( + `A client rewrote unexpected synchronisation-parameter fields: ${finalChangedFields.join(", ")}` + ); + } + + const deviceAHealthAfterRestart = await inspectSessionHealth(context.cliBinary, sessionA.cliEnv); + const deviceAHealth = { + matchingErrorMessages: [ + ...new Set([ + ...deviceAHealthBeforeRestart.matchingErrorMessages, + ...deviceAHealthAfterRestart.matchingErrorMessages, + ]), + ], + }; + if (deviceAHealth.matchingErrorMessages.length > 0 || deviceBHealth.matchingErrorMessages.length > 0) { + throw new Error( + `HKDF or Security Seed errors were logged: ${JSON.stringify({ + deviceA: deviceAHealth.matchingErrorMessages, + deviceB: deviceBHealth.matchingErrorMessages, + })}` + ); + } + + return { + source, + securitySeed: { + initial, + replacement, + final, + cachedBeforeReplacement, + cachedAfterRemoteReplacement, + cachedAfterReplication, + replacementChangedFields: changedSynchronisationParameterFields(initialDocument, replacementDocument), + finalChangedFields, + }, + synchronisation: { + deviceAToDeviceB: true, + deviceBToDeviceA: true, + deviceAEncryptedPayload, + deviceBEncryptedPayload, + }, + health: { + deviceA: deviceAHealth, + deviceB: deviceBHealth, + }, + screenshots: [...context.screenshots], + }; +} + +async function cleanupResources( + context: RunnerContext, + vaults: TemporaryVault[], + databaseCreated: boolean +): Promise { + const errors: unknown[] = []; + try { + await stopTrackedSessions(context); + } catch (error) { + errors.push(error); + } + for (const vault of vaults) { + try { + await vault.dispose(); + } catch (error) { + errors.push(error); + } + } + if (databaseCreated) { + try { + await deleteCouchDbDatabase(context.couchDb, context.dbName); + } catch (error) { + errors.push(error); + } + } + + const sessionsStopped = context.allSessions.every( + (session) => session.app.process.exitCode !== null || session.app.process.signalCode !== null + ); + const vaultRemoved = (await Promise.all(vaults.map(async (vault) => !(await pathExists(vault.path))))).every( + Boolean + ); + const profileRemoved = (await Promise.all(vaults.map(async (vault) => !(await pathExists(vault.statePath))))).every( + Boolean + ); + let databaseRemoved = !databaseCreated; + if (databaseCreated) { + try { + databaseRemoved = !(await couchDbDatabaseExists(context.couchDb, context.dbName)); + } catch (error) { + errors.push(error); + } + } + const evidence = { + sessionsStopped, + vaultRemoved, + profileRemoved, + databaseRemoved, + remainingTrackedSessions: context.activeSessions.size, + }; + if (!sessionsStopped || !vaultRemoved || !profileRemoved || !databaseRemoved || context.activeSessions.size > 0) { + errors.push(new Error(`Security Seed E2E teardown was incomplete: ${JSON.stringify(evidence)}`)); + } + if (errors.length > 0) { + throw Object.assign(new MultipleErrors("Security Seed E2E teardown failed.", errors), { + evidence, + }); + } + return evidence; +} + +async function writeResult(result: unknown): Promise { + const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e"; + const resultPath = join(outputDirectory, "security-seed-reconnect-result.json"); + await mkdir(outputDirectory, { recursive: true }); + await writeFile(resultPath, `${JSON.stringify(result, null, 2)}\n`, "utf-8"); + return resultPath; +} + +async function main(): Promise { + if (process.env.E2E_OBSIDIAN_KEEP_VAULT === "true" || process.env.E2E_OBSIDIAN_KEEP_COUCHDB === "true") { + throw new Error("The Security Seed reconnect scenario requires strict Vault, profile, and database cleanup."); + } + + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) { + throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + } + const artifactRoot = resolve(process.env.E2E_OBSIDIAN_ARTIFACT_ROOT ?? process.cwd()); + const couchDb = await loadCouchDbConfig(); + const dbName = makeUniqueDatabaseName(couchDb.dbPrefix, "security-seed-reconnect"); + const context: RunnerContext = { + binary, + cliBinary: cli.binary, + artifactRoot, + couchDb, + dbName, + activeSessions: new Set(), + allSessions: [], + screenshots: [], + }; + const vaults: TemporaryVault[] = []; + let databaseCreated = false; + let evidence: ScenarioEvidence | undefined; + let scenarioError: unknown; + let teardown: TeardownEvidence | undefined; + let teardownError: unknown; + + try { + await assertCouchDbReachable(couchDb); + await createCouchDbDatabase(couchDb, dbName); + databaseCreated = true; + vaults.push(await createTemporaryVault("obsidian-livesync-security-seed-a-")); + vaults.push(await createTemporaryVault("obsidian-livesync-security-seed-b-")); + evidence = await runScenario(context, vaults[0], vaults[1]); + } catch (error) { + scenarioError = error; + } finally { + try { + teardown = await cleanupResources(context, vaults, databaseCreated); + } catch (error) { + teardownError = error; + } + } + + if (scenarioError !== undefined || teardownError !== undefined) { + const errors = [scenarioError, teardownError].filter((error) => error !== undefined); + if (errors.length === 1) { + throw errors[0]; + } + throw new MultipleErrors("Security Seed reconnect scenario and teardown both failed.", errors); + } + if (!evidence || !teardown) { + throw new Error("Security Seed reconnect evidence was not produced."); + } + + const result = { + scenario: "security-seed-reconnect", + ...evidence, + teardown, + limitations: { + platformCommonRealObsidian: true, + iPadOsBackgroundReconnect: false, + androidDeviceLifecycle: false, + }, + }; + const resultPath = await writeResult(result); + console.log(`Security Seed E2E result: ${resultPath}`); + console.log(JSON.stringify(result, null, 2)); +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exitCode = 1; +}); From 99eb9cd46f44d350d083de90815e6624ea06e402 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sat, 25 Jul 2026 03:49:39 +0000 Subject: [PATCH 099/117] Record beta.3 and current validation --- updates.md | 28 ++++++++++++++++++++++------ versions.json | 3 ++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/updates.md b/updates.md index 7d0ee73a..6c612c98 100644 --- a/updates.md +++ b/updates.md @@ -16,23 +16,39 @@ Earlier releases remain available in the 0.25 release history and the legacy rel - **Verify and repair all files** now reports the database winner, every conflict revision, missing chunks, and unavailable shared ancestors separately. It can retry an exact revision without changing the tree, while discarding an unreadable live revision requires explicit confirmation. - Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands keep their identifiers, so hotkeys already assigned to them continue to work. The onboarding wizard can be reopened from **Self-hosted LiveSync settings** → **Setup**. -- Enabling Hidden File Sync now opens one progress Notice before its setting is saved and reuses that Notice throughout the initial file scan, instead of stacking separate phase and restart Notices. -- P2P is now presented only after it has been configured: its status pane no longer opens at start-up, its ribbon icon remains hidden for CouchDB-only Vaults, and the retired P2P pane command has been removed. The current pane distinguishes announcing changes, following a peer, and persistent per-device actions. Setup and guidance now distinguish the required signalling relay from optional TURN, and describe the public signalling relay's privacy and availability limits. -- First-device P2P setup now accepts a successfully opened signalling room without requiring another peer to be online. Additional-device Fetch still requires selecting a source peer and completing `P2P Rebuild`. -- Manual CouchDB setup now distinguishes creating a first database from connecting an additional device to an existing one. Settings mode can save an unverified profile explicitly, while onboarding requires a successful connection, and each proposed server-configuration fix requires separate confirmation. -- Differences limited to the chunk hash algorithm, chunk size, or splitter version are now aligned automatically by default. Existing content remains readable, while an explicit opt-out and any difference which also involves an incompatible setting retain manual review. - Text in setup and review dialogues can now be selected for copying or translation. ### Fixed - An unreadable conflict revision is no longer deleted automatically merely because its chunks are unavailable on the current device. - Garbage Collection V3 now protects chunks required by every live conflict branch and the available revision ancestry needed to review and merge conflicts, instead of considering only the database winner. The action is offered only for CouchDB because P2P has no central database to compact and does not provide the device inventory required by the workflow. Collection now stops when device progress cannot be verified, and a compaction timeout is no longer followed by a contradictory success message. + +### Testing + +- Added regressions for revision repair, command availability, selectable dialogues, conflict-aware chunk reachability, device-progress safeguards, and compaction timeouts. +- Added a real CouchDB integration test for logical chunk deletion, shared and conflict chunk retention, compaction completion, downstream replication, and content-addressed chunk recreation. +- Added a real Obsidian encrypted reconnect scenario which replaces the remote Security Seed while one client retains the previous value, verifies that synchronisation refreshes it without restoring the old value, and proves a bidirectional encrypted round-trip. + +## 1.0.0-beta.3 + +24th July, 2026 + +### Improved + +- Enabling Hidden File Sync now opens one progress Notice before its setting is saved and reuses that Notice throughout the initial file scan, instead of stacking separate phase and restart Notices. +- P2P is now presented only after it has been configured: its status pane no longer opens at start-up, its ribbon icon remains hidden for CouchDB-only Vaults, and the retired P2P pane command has been removed. The current pane distinguishes announcing changes, following a peer, and persistent per-device actions. Setup and guidance now distinguish the required signalling relay from optional TURN, and describe the public signalling relay's privacy and availability limits. +- First-device P2P setup now accepts a successfully opened signalling room without requiring another peer to be online. Additional-device Fetch still requires selecting a source peer and completing `P2P Rebuild`. +- Manual CouchDB setup now distinguishes creating a first database from connecting an additional device to an existing one. Settings mode can save an unverified profile explicitly, while onboarding requires a successful connection, and each proposed server-configuration fix requires separate confirmation. +- Differences limited to the chunk hash algorithm, chunk size, or splitter version are now aligned automatically by default. Existing content remains readable, while an explicit opt-out and any difference which also involves an incompatible setting retain manual review. + +### Fixed + - Choosing **Apply settings to this device, and fetch again** for a compatible configuration mismatch now applies the remote settings before Fetch, instead of updating the remote database with this device's settings. - Accepted settings which control how new chunks are created now take effect before synchronisation is retried, rather than leaving the previous hash or splitter active until restart. ### Testing -- Added regressions for revision repair, P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, selectable and mobile dialogues, conflict-aware chunk reachability, device-progress safeguards, compaction timeouts, shared chunks, collection propagation, and content-addressed chunk recreation. +- Added regressions for P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, and mobile dialogues. ## 1.0.0-beta.2 diff --git a/versions.json b/versions.json index a0f3dfb2..a4585f26 100644 --- a/versions.json +++ b/versions.json @@ -8,5 +8,6 @@ "0.25.83": "1.7.2", "1.0.0-beta.0": "1.7.2", "1.0.0-beta.1": "1.7.2", - "1.0.0-beta.2": "1.7.2" + "1.0.0-beta.2": "1.7.2", + "1.0.0-beta.3": "1.7.2" } From bea0e68091a7bd4481829763a66e9b152d711c13 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sat, 25 Jul 2026 04:34:53 +0000 Subject: [PATCH 100/117] Keep translation details out of startup path --- .../onLayoutReady/enablei18n.ts | 83 ++++++++++--- .../onLayoutReady/enablei18n.unit.spec.ts | 110 ++++++++++++++++++ test/e2e-obsidian/README.md | 18 +++ updates.md | 1 + 4 files changed, 198 insertions(+), 14 deletions(-) create mode 100644 src/serviceFeatures/onLayoutReady/enablei18n.unit.spec.ts diff --git a/src/serviceFeatures/onLayoutReady/enablei18n.ts b/src/serviceFeatures/onLayoutReady/enablei18n.ts index 83bdb9bc..5a83eafb 100644 --- a/src/serviceFeatures/onLayoutReady/enablei18n.ts +++ b/src/serviceFeatures/onLayoutReady/enablei18n.ts @@ -1,4 +1,4 @@ -import { getLanguage, requireApiVersion } from "@/deps"; +import { getLanguage, Notice, requireApiVersion } from "@/deps"; import { createServiceFeature } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@/common/rosetta"; import { $msg, __onMissingTranslation, setLang } from "@/common/translation"; @@ -15,7 +15,40 @@ function tryGetLanguage(onError: (error: unknown) => void) { return "en"; } -export const enableI18nFeature = createServiceFeature(async ({ services: { setting, API } }) => { +class ObsidianLanguageAppliedNotice { + private reminder: Notice | undefined; + + show(openDetails: () => void): void { + this.clear(); + let reminderAnchor: HTMLAnchorElement | undefined; + const appliedMessage = + $msg("dialog.yourLanguageAvailable") + .split(/\r?\n\s*\r?\n/u, 1)[0] + ?.trim() ?? $msg("Display Language"); + const fragment = createFragment((documentFragment) => { + documentFragment.createSpan({ + text: `${appliedMessage} `, + }); + documentFragment.createEl("a", { text: $msg("Open the dialog") }, (anchor) => { + reminderAnchor = anchor; + anchor.addEventListener("click", (event) => { + event.preventDefault(); + this.clear(); + openDetails(); + }); + }); + }); + this.reminder = new Notice(fragment, 0); + reminderAnchor?.closest(".notice")?.classList.add("livesync-language-applied-notice"); + } + + clear(): void { + this.reminder?.hide(); + this.reminder = undefined; + } +} + +export const enableI18nFeature = createServiceFeature(async ({ services: { setting, API, appLifecycle } }) => { // Clear missing translation handler to avoid unnecessary warnings. __onMissingTranslation(() => {}); let isChanged = false; @@ -36,26 +69,48 @@ export const enableI18nFeature = createServiceFeature(async ({ services: { setti // settings.displayLanguage = obsidianLanguage as I18N_LANGS; await setting.applyPartial({ displayLanguage: obsidianLanguage as I18N_LANGS }); isChanged = true; - setLang(settings.displayLanguage); + setLang(obsidianLanguage as I18N_LANGS); } else if (settings.displayLanguage == "") { // settings.displayLanguage = "def"; await setting.applyPartial({ displayLanguage: "def" }); - setLang(settings.displayLanguage); + setLang("def"); await setting.saveSettingData(); } } if (isChanged) { - const revert = $msg("dialog.yourLanguageAvailable.btnRevertToDefault"); - if ( - (await API.confirm.askSelectStringDialogue($msg(`dialog.yourLanguageAvailable`), ["OK", revert], { - defaultAction: "OK", - title: $msg(`dialog.yourLanguageAvailable.Title`), - })) == revert - ) { - await setting.applyPartial({ displayLanguage: "def" }); - setLang(settings.displayLanguage); - } await setting.saveSettingData(); + const reminder = new ObsidianLanguageAppliedNotice(); + appLifecycle.onUnload.addHandler(() => { + reminder.clear(); + return Promise.resolve(true); + }); + reminder.show(() => { + void (async () => { + try { + const revert = $msg("dialog.yourLanguageAvailable.btnRevertToDefault"); + if ( + (await API.confirm.askSelectStringDialogue( + $msg(`dialog.yourLanguageAvailable`), + ["OK", revert], + { + defaultAction: "OK", + title: $msg("Display Language"), + } + )) == revert + ) { + await setting.applyPartial({ displayLanguage: "def" }); + setLang("def"); + await setting.saveSettingData(); + } + } catch (error) { + API.addLog( + `Failed to open translation details: ${String(error)}`, + LOG_LEVEL_VERBOSE, + "i18n-language" + ); + } + })(); + }); } return true; }); diff --git a/src/serviceFeatures/onLayoutReady/enablei18n.unit.spec.ts b/src/serviceFeatures/onLayoutReady/enablei18n.unit.spec.ts new file mode 100644 index 00000000..8f88eb62 --- /dev/null +++ b/src/serviceFeatures/onLayoutReady/enablei18n.unit.spec.ts @@ -0,0 +1,110 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const noticeState = vi.hoisted(() => ({ + instances: [] as Array<{ hide: ReturnType; duration: number }>, + spanTexts: [] as string[], +})); + +vi.mock("@/deps", () => ({ + getLanguage: () => "ja", + requireApiVersion: () => true, + Notice: class { + hide = vi.fn(); + + constructor(_fragment: unknown, duration: number) { + noticeState.instances.push({ hide: this.hide, duration }); + } + }, +})); + +vi.mock("@/common/translation", () => ({ + $msg: (key: string) => + ({ + "dialog.yourLanguageAvailable": "Translation has been applied.\n\nMore details.", + "dialog.yourLanguageAvailable.btnRevertToDefault": "Keep Default", + "dialog.yourLanguageAvailable.Title": "Translation is available!", + "Display Language": "Display language", + "Open the dialog": "Open the dialogue", + })[key] ?? key, + __onMissingTranslation: vi.fn(), + setLang: vi.fn(), +})); + +import { enableI18nFeature } from "./enablei18n.ts"; + +describe("automatic display language", () => { + let clickDetails: ((event: { preventDefault(): void }) => void) | undefined; + + beforeEach(() => { + noticeState.instances.length = 0; + noticeState.spanTexts.length = 0; + clickDetails = undefined; + vi.stubGlobal("createFragment", (build: (fragment: unknown) => void) => { + const anchor = { + addEventListener: (_event: string, listener: (event: { preventDefault(): void }) => void) => { + clickDetails = listener; + }, + closest: () => ({ classList: { add: vi.fn() } }), + }; + const fragment = { + createSpan: ({ text }: { text: string }) => noticeState.spanTexts.push(text), + createEl: (_tag: string, _options: unknown, configure: (element: typeof anchor) => void) => { + configure(anchor); + return anchor; + }, + }; + build(fragment); + return fragment; + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it("lets start-up continue and opens translation details only from a persistent Notice", async () => { + const settings = { displayLanguage: "" }; + const applyPartial = vi.fn(async (partial: Partial) => Object.assign(settings, partial)); + const saveSettingData = vi.fn().mockResolvedValue(undefined); + const askSelectStringDialogue = vi.fn().mockResolvedValue("Keep Default"); + const unloadHandlers: Array<() => Promise> = []; + const host = { + services: { + setting: { + currentSettings: () => settings, + applyPartial, + saveSettingData, + }, + API: { + addLog: vi.fn(), + confirm: { askSelectStringDialogue }, + }, + appLifecycle: { + onUnload: { + addHandler: (handler: () => Promise) => unloadHandlers.push(handler), + }, + }, + }, + }; + + await expect(enableI18nFeature(host as never)).resolves.toBe(true); + + expect(settings.displayLanguage).toBe("ja"); + expect(saveSettingData).toHaveBeenCalledOnce(); + expect(askSelectStringDialogue).not.toHaveBeenCalled(); + expect(noticeState.instances).toHaveLength(1); + expect(noticeState.instances[0]?.duration).toBe(0); + expect(noticeState.spanTexts).toEqual(["Translation has been applied. "]); + expect(clickDetails).toBeTypeOf("function"); + + clickDetails?.({ preventDefault: vi.fn() }); + await vi.waitFor(() => expect(askSelectStringDialogue).toHaveBeenCalledOnce()); + expect(askSelectStringDialogue.mock.calls[0]?.[2]).toMatchObject({ title: "Display language" }); + await vi.waitFor(() => expect(settings.displayLanguage).toBe("def")); + expect(saveSettingData).toHaveBeenCalledTimes(2); + + await expect(unloadHandlers[0]?.()).resolves.toBe(true); + expect(noticeState.instances[0]?.hide).toHaveBeenCalled(); + }); +}); diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index 73b17e1e..f7a452f8 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -30,6 +30,24 @@ On macOS, `@vrtmrz/obsidian-test-session` keeps the generated Vault and profile Multi-session workflows must keep each started Obsidian session tracked until its stop operation completes. If a scenario throws, teardown stops every active session before disposing its temporary Vault and profile, so a failed CLI or synchronisation operation cannot leave Obsidian using directories which have already been removed. +## Observing and diagnosing a scenario + +Use externally visible behaviour as the pass condition: Vault files, remote-service state, revision data, or visible Obsidian UI. A log line can explain a failure, but should not replace an assertion about the resulting behaviour. + +The maintained runner provides several complementary observation paths: + +- `evalObsidianJson()` and `obsidian-cli eval` can read a small, explicitly selected piece of LiveSync or Obsidian state. +- `withObsidianPage()` can inspect the active renderer, invoke a registered command, or interact with visible UI through CDP. `captureObsidianPage()`, `captureObsidianDialogue()`, and `captureObsidianElement()` retain screenshots; the capture helpers also write a full-page `.failure.png` before rethrowing a UI assertion failure. +- `session.app.output()` returns the standard output and standard error captured from the isolated Obsidian process. This is especially useful when the renderer or CLI becomes unreachable. +- **Show log** (`obsidian-livesync:view-log`) exposes the recent LiveSync log, while **Copy full report to clipboard** (`obsidian-livesync:dump-debug-info`) opens the generated diagnostic report. `dialog-mounts.ts` verifies both surfaces, and focused scenarios may inspect the log pane and `appLifecycle.getUnresolvedMessages()` for a bounded set of expected errors. +- Renderer `console` messages and uncaught page errors are not retained automatically. A focused investigation can attach `page.on("console", ...)` and `page.on("pageerror", ...)` while it owns a `withObsidianPage()` callback. That observer ends when the callback closes its CDP connection, so use it around the action under investigation rather than treating it as a session-wide audit trail. + +If a scenario times out or appears to do nothing, capture the visible page before teardown, then record a bounded state snapshot and the relevant tail of the LiveSync log, unresolved messages, and process output. If an unexplained Notice appears, retain a screenshot while it is still visible before opening or dismissing it, then use the log or full report to identify its source. A Notice alone is not enough evidence for its cause. + +Set `showVerboseLog: true` only in isolated plug-in data when a focused investigation needs it. Keep captured output short and redact it before retaining or sharing it: logs and reports can contain Vault paths, document names, endpoints, credentials, Setup URIs, passphrases, or Security Seed material. Do not collect verbose logs from an ordinary user Vault. + +Collect evidence before cleanup, and keep process, Vault, profile, and remote-fixture cleanup in `finally`. After `app.emulateMobile(true)`, use the active CDP renderer for fixture operations because Obsidian may remove desktop-only CLI commands. Visually inspect screenshots before copying selected images into user documentation; a passing locator assertion does not establish that a dialogue is readable or unobstructed. + ## Local Setup Set `OBSIDIAN_BINARY` when Obsidian is not installed in a standard location. Set `OBSIDIAN_CLI` as well when its companion executable is outside the built-in discovery paths. diff --git a/updates.md b/updates.md index 6c612c98..002bc6a8 100644 --- a/updates.md +++ b/updates.md @@ -17,6 +17,7 @@ Earlier releases remain available in the 0.25 release history and the legacy rel - **Verify and repair all files** now reports the database winner, every conflict revision, missing chunks, and unavailable shared ancestors separately. It can retry an exact revision without changing the tree, while discarding an unreadable live revision requires explicit confirmation. - Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands keep their identifiers, so hotkeys already assigned to them continue to work. The onboarding wizard can be reopened from **Self-hosted LiveSync settings** → **Setup**. - Text in setup and review dialogues can now be selected for copying or translation. +- When LiveSync adopts an available interface translation on first start-up, it now continues initialisation and leaves a persistent Notice from which the translation details can be opened, instead of waiting for an unsolicited dialogue. ### Fixed From 12fc43a69c94f5d42c8008baa5c773e47e3d04bf Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sat, 25 Jul 2026 09:01:25 +0000 Subject: [PATCH 101/117] Improve recovery diagnostics and actions --- docs/specs_conflict_resolution.md | 19 +- docs/troubleshooting.md | 8 +- .../messages/LiveSyncProvisionalMessages.ts | 52 +- .../HiddenFileSync/CmdHiddenFileSync.ts | 125 +++- .../CmdHiddenFileSync.unit.spec.ts | 200 +++++- .../ConflictResolveModal.ts | 75 +- .../ConflictResolveModal.unit.spec.ts | 61 ++ .../features/SettingDialogue/PaneHatch.ts | 669 ++++++++++++++---- src/serviceFeatures/fileRepair.ts | 31 +- src/serviceFeatures/fileRepair.unit.spec.ts | 56 ++ src/serviceFeatures/fileRepairPresentation.ts | 144 ++++ .../fileRepairPresentation.unit.spec.ts | 230 ++++++ styles.css | 55 +- test/e2e-obsidian/README.md | 2 +- test/e2e-obsidian/scripts/revision-repair.ts | 494 +++++++++++-- updates.md | 2 +- 16 files changed, 1980 insertions(+), 243 deletions(-) create mode 100644 src/serviceFeatures/fileRepairPresentation.ts create mode 100644 src/serviceFeatures/fileRepairPresentation.unit.spec.ts diff --git a/docs/specs_conflict_resolution.md b/docs/specs_conflict_resolution.md index f74c137d..25dfd012 100644 --- a/docs/specs_conflict_resolution.md +++ b/docs/specs_conflict_resolution.md @@ -50,11 +50,24 @@ The compatibility implementation currently selects the newer modification time f A document revision can remain in the PouchDB tree while one or more chunks needed to reconstruct its content are unavailable. Missing content is not evidence that the revision is obsolete. LiveSync therefore leaves an unreadable winner or conflict revision in the tree instead of deleting it during automatic conflict processing. -**Hatch** → **Verify and repair all files** inspects the current winner, every current conflict revision, and the nearest shared ancestor for each conflict. It reports exact revision identifiers and local chunk availability separately: +**Hatch** → **Verify and repair all files** inspects the current winner, every current conflict revision, and the nearest shared ancestor for each conflict. A logical-deletion winner and an absent Vault file already agree, so that state is not reported unless another live branch still requires attention. When the Vault already matches the winner but conflict branches remain, the card shows the compact status `✅ Vault matches winner · ⚠️ Conflicts: N`; matching the winner does not mean that the conflict has been resolved. +Each reported live revision has a compact **…** menu. The available actions depend on the exact revision and current Vault state: + +- **Compare with Vault** opens the existing difference dialogue in read-only mode for differing text files. +- **Apply this revision to Vault** writes the selected readable revision, even when it is not the database winner. Replacing an existing file requires confirmation. +- **Mark this revision as the Vault version** is offered when the bytes already match. It records exact device-local provenance without creating a child revision, and refuses the operation if the file changed after inspection. +- **Store Vault file as a child of this revision** preserves the current Vault bytes on the explicitly selected live branch. +- **Apply logical deletion to Vault** removes an existing Vault file after confirmation. An absent file needs no retained deletion provenance. - **Retry reading revision** attempts the configured chunk-retrieval path again. It does not change the revision tree. -- **Discard unreadable revision** is available only for a current winner or conflict revision which remains unreadable when the action is performed. It requires confirmation and creates a logical deletion for that exact revision. -- A shared ancestor is informational. An ancestor which is no longer a live revision cannot be discarded independently through this workflow. If its body is unavailable, conservative three-way merge remains disabled, although readable live revisions can still be selected manually. +- **Discard this branch** is available for each exact live revision while at least one other live branch remains. It requires confirmation and creates a logical deletion on only the selected branch without changing the current Vault file. +- **Discard unreadable revision** remains available as a recovery action when an unreadable revision is the only live leaf. It requires confirmation because no other database branch remains. + +Every mutating action rechecks that the selected revision is still a current live leaf. If another operation resolved or replaced it, the action fails and the card is refreshed instead of extending an obsolete branch. + +The card uses compact, mobile-friendly diagnostic rows with an emoji and a text label. `🧩 Missing chunks: N` identifies an unreadable revision. In the database row, `Δsize` is decoded size minus recorded size; in the Vault row, `Δsize vs DB` is Vault size minus decoded database size. `Δtime` is Vault modification time minus database modification time. The ordinary two-second comparison window still labels which side is newer. These values help diagnose a mismatch; path, size, and modification time do not prove revision identity or decide which content should win. + +A shared ancestor is informational. An ancestor which is no longer a live revision cannot be discarded independently through this workflow. If its body is unavailable, conservative three-way merge remains disabled, although readable live revisions can still be selected manually. Logical deletion does not recreate missing bytes, purge the document history, or prove that the deleted version was unimportant. Another replica or backup may still contain the missing chunks. Recover from that source before discarding a revision whenever possible. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 82931923..95434b6c 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -57,10 +57,12 @@ If the log reports missing chunks or a size mismatch: 2. restart Obsidian once to rule out an interrupted fetch; 3. synchronise a device or restore a backup which still has the correct content; 4. on that healthy device, run `Recreate chunks for current Vault files`, then synchronise; -5. run `Verify and repair all files` from `Hatch`; review the winner, every conflict revision, and any unavailable shared ancestor separately; and -6. use `Discard unreadable revision` only after confirming that the exact revision is no longer recoverable or wanted. +5. run `Verify and repair all files` from `Hatch`; review the winner, every conflict revision, and any unavailable shared ancestor separately; use each revision's **…** menu to compare readable text, apply that exact revision to the Vault, store the Vault file as its child, or record an exact byte-for-byte match; and +6. use `Discard this branch` only after confirming that the exact live branch is no longer wanted. Use the separate `Discard unreadable revision` recovery action only when an unreadable revision is the sole live leaf. -`Retry reading revision` does not change the revision tree. `Discard unreadable revision` creates a logical deletion for one current winner or conflict revision after rechecking it. It does not purge history or reconstruct missing content. An unavailable non-live ancestor cannot be deleted through this workflow; it disables conservative three-way merge but does not prevent explicit selection between readable live revisions. +The repair card uses compact diagnostic rows which remain readable in a narrow mobile settings pane. `🧩 Missing chunks: N` marks an unreadable revision. In the database row, `Δsize` means decoded size minus recorded size; `Δsize vs DB` means Vault size minus decoded database size; and `Δtime` means Vault modification time minus database modification time. These are diagnostic values, not a rule for deciding which revision is correct. `✅ Vault matches winner · ⚠️ Conflicts: N` means that the current Vault bytes agree with the database winner while other live branches still need a decision. Every mutating action rechecks that its selected revision is still live. Applying a logical deletion to an existing Vault file requires confirmation; a logical-deletion winner with no Vault file already agrees and is omitted. + +`Retry reading revision` does not change the revision tree. `Discard this branch` creates a logical deletion on one exact live revision while another live branch remains and leaves the current Vault file unchanged. If the discarded revision was recorded as the Vault's exact source, that stale device-local provenance is removed. `Discard unreadable revision` provides the corresponding explicit escape hatch for a sole unreadable live leaf. Neither action purges history or reconstructs missing content. An unavailable non-live ancestor cannot be deleted through this workflow; it disables conservative three-way merge but does not prevent explicit selection between readable live revisions. `Recreate chunks for current Vault files` uses current Vault content. It cannot recreate unique bytes which exist only in an unreadable historical or conflict revision. diff --git a/src/common/messages/LiveSyncProvisionalMessages.ts b/src/common/messages/LiveSyncProvisionalMessages.ts index 4618edc9..6353e8d8 100644 --- a/src/common/messages/LiveSyncProvisionalMessages.ts +++ b/src/common/messages/LiveSyncProvisionalMessages.ts @@ -74,21 +74,51 @@ export const liveSyncProvisionalEnglishMessages = { "Database information for ${FILE}": "Database information for ${FILE}", "All revisions and chunk availability below are a snapshot of this device's local database; the remote is not queried. Review the Vault-relative path, document identifier, content-derived chunk identifiers, and metadata before sharing this report. File contents are omitted.": "All revisions and chunk availability below are a snapshot of this device's local database; the remote is not queried. Review the Vault-relative path, document identifier, content-derived chunk identifiers, and metadata before sharing this report. File contents are omitted.", - "Vault file: modified ${TIME}, size ${SIZE}": "Vault file: modified ${TIME}, size ${SIZE}", - "Vault file: missing": "Vault file: missing", - "Local database document: missing": "Local database document: missing", + "📁 Vault: ${SIZE} B · ${TIME}": "📁 Vault: ${SIZE} B · ${TIME}", + "📁 Vault: missing": "📁 Vault: missing", + "🗄️ Local DB: missing": "🗄️ Local DB: missing", + "Vault and database revision": "Vault and database revision", + "Vault file": "Vault file", + "Database revision": "Database revision", + "Vault file is newer": "Vault file is newer", + "Database revision is newer": "Database revision is newer", + "Within the two-second comparison window": "Within the two-second comparison window", + "Timestamp comparison unavailable": "Timestamp comparison unavailable", "${ROLE}: ${REVISION}": "${ROLE}: ${REVISION}", "Winner revision": "Winner revision", "Conflict revision": "Conflict revision", "Unknown revision": "Unknown revision", - "Logical deletion": "Logical deletion", + "🗑️ Logical deletion": "🗑️ Logical deletion", "Readable on this device; recorded size ${RECORDED}, decoded size ${ACTUAL}": "Readable on this device; recorded size ${RECORDED}, decoded size ${ACTUAL}", - "Unreadable on this device; ${COUNT} referenced chunks are missing or deleted": - "Unreadable on this device; ${COUNT} referenced chunks are missing or deleted", - "Matches the current Vault file": "Matches the current Vault file", - "Differs from the current Vault file": "Differs from the current Vault file", + "🧩 Missing chunks: ${COUNT}": "🧩 Missing chunks: ${COUNT}", + "📦 DB: recorded ${RECORDED} B · decoded ${DECODED} B · Δsize ${DIFFERENCE} B": + "📦 DB: recorded ${RECORDED} B · decoded ${DECODED} B · Δsize ${DIFFERENCE} B", + "📦 DB: recorded ${RECORDED} B · decoded unavailable": + "📦 DB: recorded ${RECORDED} B · decoded unavailable", + "📁 Vault: ${VAULT} B · Δsize vs DB ${DIFFERENCE} B": + "📁 Vault: ${VAULT} B · Δsize vs DB ${DIFFERENCE} B", + "🕒 DB ${DATABASE_TIME} · Vault ${VAULT_TIME} · Δtime ${DIFFERENCE} ms (${RELATION})": + "🕒 DB ${DATABASE_TIME} · Vault ${VAULT_TIME} · Δtime ${DIFFERENCE} ms (${RELATION})", + "✅ Matches Vault": "✅ Matches Vault", + "⚠️ Differs from Vault": "⚠️ Differs from Vault", + "✅ Vault matches winner": "✅ Vault matches winner", + "⚠️ Conflicts: ${COUNT}": "⚠️ Conflicts: ${COUNT}", + "Compare with Vault": "Compare with Vault", + "Apply this revision to Vault": "Apply this revision to Vault", + "Apply database revision ${REVISION} to ${FILE}? The current Vault file will be overwritten.": + "Apply database revision ${REVISION} to ${FILE}? The current Vault file will be overwritten.", + "Apply database revision to Vault": "Apply database revision to Vault", + "Mark this revision as the Vault version": "Mark this revision as the Vault version", + "Store Vault file as a child of this revision": "Store Vault file as a child of this revision", + "Apply logical deletion to Vault": "Apply logical deletion to Vault", + "Apply logical deletion ${REVISION} to ${FILE}? The current Vault file will be removed.": + "Apply logical deletion ${REVISION} to ${FILE}? The current Vault file will be removed.", "Retry reading revision": "Retry reading revision", + "Discard this branch": "Discard this branch", + "Discard branch": "Discard branch", + "Discard database branch ${REVISION} of ${FILE}? This creates a logical deletion for that exact live branch. The current Vault file will not be changed.": + "Discard database branch ${REVISION} of ${FILE}? This creates a logical deletion for that exact live branch. The current Vault file will not be changed.", "Discard unreadable revision": "Discard unreadable revision", "Discard database revision ${REVISION} of ${FILE}? This creates a logical deletion for that exact live revision. Missing content cannot be recovered by this action.": "Discard database revision ${REVISION} of ${FILE}? This creates a logical deletion for that exact live revision. Missing content cannot be recovered by this action.", @@ -97,9 +127,11 @@ export const liveSyncProvisionalEnglishMessages = { "Shared ancestor ${REVISION} is not readable on this device. Automatic three-way merging may be unavailable, but the live revisions remain available for explicit review.", "No shared ancestor is available for this conflict. The live revisions remain available for explicit review.": "No shared ancestor is available for this conflict. The live revisions remain available for explicit review.", + "More actions for revision ${REVISION}": "More actions for revision ${REVISION}", + "More actions for ${FILE}": "More actions for ${FILE}", "Show revision history": "Show revision history", - "Use Vault file in local database": "Use Vault file in local database", - "Restore database winner to Vault": "Restore database winner to Vault", + "Store Vault file as a new local database document": + "Store Vault file as a new local database document", "Copy database information": "Copy database information", "Recreate chunks for current Vault files": "Recreate chunks for current Vault files", "Recreate chunks from the files currently present in this Vault. This cannot reconstruct unavailable historical or conflict content.": diff --git a/src/features/HiddenFileSync/CmdHiddenFileSync.ts b/src/features/HiddenFileSync/CmdHiddenFileSync.ts index 92826376..ca21b320 100644 --- a/src/features/HiddenFileSync/CmdHiddenFileSync.ts +++ b/src/features/HiddenFileSync/CmdHiddenFileSync.ts @@ -1530,6 +1530,29 @@ Offline Changed files: ${files.length}`; } } + private async getLiveInternalRevision( + prefixedFileName: FilePathWithPrefix, + revision: string + ): Promise { + const [selected, current, conflicts] = await Promise.all([ + this.core.databaseFileAccess.fetchEntryMeta(prefixedFileName, revision, true), + this.core.databaseFileAccess.fetchEntryMeta(prefixedFileName, undefined, true), + this.core.databaseFileAccess.getConflictedRevs(prefixedFileName), + ]); + const liveRevisions = new Set([ + ...(current && current._rev ? [current._rev] : []), + ...conflicts, + ]); + if (!selected || selected._rev !== revision || !liveRevisions.has(revision)) { + this._log( + `Could not use hidden-file revision ${revision} of ${stripAllPrefixes(prefixedFileName)}; the selected revision is no longer live`, + LOG_LEVEL_NOTICE + ); + return false; + } + return selected; + } + async storeInternalFileToDatabase(file: InternalFileInfo | UXFileInfo, forceWrite = false) { const storeFilePath = stripAllPrefixes(file.path); const storageFilePath = file.path; @@ -1581,6 +1604,79 @@ Offline Changed files: ${files.length}`; }); } + async storeInternalFileToDatabaseWithBaseRevision( + file: InternalFileInfo | UXFileInfo, + baseRevision: string, + createIfDifferent = true + ): Promise { + const storeFilePath = stripAllPrefixes(file.path); + const storageFilePath = file.path; + if (await this.services.vault.isIgnoredByIgnoreFile(storageFilePath)) { + return false; + } + const prefixedFileName = addPrefix(storeFilePath, ICHeader); + + return await serialized("file-" + prefixedFileName, async () => { + try { + const baseData = await this.getLiveInternalRevision(prefixedFileName, baseRevision); + if (baseData === false) { + return false; + } + const fileInfo = "stat" in file && "body" in file ? file : await this.loadFileWithInfo(storeFilePath); + if (fileInfo.deleted) { + throw new Error(`Hidden file:${storeFilePath} is deleted. This should not be occurred.`); + } + if (!baseData.deleted && !baseData._deleted) { + const loadedBase = await this.core.databaseFileAccess.fetchEntryFromMeta(baseData, true, true); + if (loadedBase && (await isDocContentSame(readAsBlob(loadedBase), fileInfo.body))) { + this.updateLastProcessed(storeFilePath, baseData, fileInfo.stat); + return true; + } + } + if (!createIfDifferent) { + this._log( + `Could not mark hidden file ${storeFilePath} as revision ${baseRevision}; the storage content differs`, + LOG_LEVEL_NOTICE + ); + return false; + } + + const storedRevision = await this.core.databaseFileAccess.storeWithBaseRevision( + { + ...fileInfo, + path: storeFilePath, + name: fileInfo.name || storeFilePath.split("/").pop() || "", + isInternal: true, + }, + baseRevision, + true + ); + if (storedRevision === false) { + return false; + } + this.updateLastProcessed( + storeFilePath, + { + ...baseData, + _rev: storedRevision, + path: prefixedFileName, + ctime: fileInfo.stat.ctime, + mtime: fileInfo.stat.mtime, + size: fileInfo.stat.size, + deleted: false, + }, + fileInfo.stat + ); + this._log(`STORAGE --> DB:${storageFilePath}: (hidden, selected branch) Done`); + return true; + } catch (ex) { + this._log(`STORAGE --> DB:${storageFilePath}: (hidden, selected branch) Failed`); + this._log(ex, LOG_LEVEL_VERBOSE); + return false; + } + }); + } + async deleteInternalFileOnDatabase(filenameSrc: FilePath, forceWrite = false) { const storeFilePath = filenameSrc; const storageFilePath = filenameSrc; @@ -1644,7 +1740,8 @@ Offline Changed files: ${files.length}`; metaEntry?: MetaEntry | LoadedEntry, preventDoubleProcess = true, onlyNew = false, - includeDeletion = true + includeDeletion = true, + requiredLiveRevision?: string ) { const prefixedFileName = addPrefix(storageFilePath, ICHeader); if (await this.services.vault.isIgnoredByIgnoreFile(storageFilePath)) { @@ -1653,9 +1750,11 @@ Offline Changed files: ${files.length}`; return await serialized("file-" + prefixedFileName, async () => { try { // Check conflicted status - const metaOnDB = metaEntry - ? metaEntry - : await this.localDatabase.getDBEntryMeta(prefixedFileName, { conflicts: true }, true); + const metaOnDB = requiredLiveRevision + ? await this.getLiveInternalRevision(prefixedFileName, requiredLiveRevision) + : metaEntry + ? metaEntry + : await this.localDatabase.getDBEntryMeta(prefixedFileName, { conflicts: true }, true); if (metaOnDB === false) throw new Error(`File not found on database.:${storageFilePath}`); // Prevent overwrite for Prevent overwriting while some conflicted revision exists. if (metaOnDB?._conflicts?.length) { @@ -1729,6 +1828,24 @@ Offline Changed files: ${files.length}`; }); } + async extractInternalFileRevisionFromDatabase( + storageFilePath: FilePath, + revision: string, + force = false + ): Promise { + return Boolean( + await this.extractInternalFileFromDatabase( + storageFilePath, + force, + undefined, + true, + false, + true, + revision + ) + ); + } + async __checkIsNeedToWriteFile(storageFilePath: FilePath, content: string | ArrayBuffer): Promise { try { const storageContent = await this.core.storageAccess.readHiddenFileAuto(storageFilePath); diff --git a/src/features/HiddenFileSync/CmdHiddenFileSync.unit.spec.ts b/src/features/HiddenFileSync/CmdHiddenFileSync.unit.spec.ts index 7e7685d2..ad664a0f 100644 --- a/src/features/HiddenFileSync/CmdHiddenFileSync.unit.spec.ts +++ b/src/features/HiddenFileSync/CmdHiddenFileSync.unit.spec.ts @@ -1,5 +1,12 @@ import { describe, expect, it, vi } from "vitest"; -import { LOG_LEVEL_NOTICE } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { + type DocumentID, + LOG_LEVEL_NOTICE, + type FilePath, + type FilePathWithPrefix, + type MetaEntry, + type UXFileInfo, +} from "@vrtmrz/livesync-commonlib/compat/common/types"; vi.mock("@/deps.ts", () => ({})); vi.mock("@/features/HiddenFileCommon/JsonResolveModal.ts", () => ({ @@ -27,6 +34,70 @@ vi.mock("./configureHiddenFileSyncMode.ts", () => ({ import { HiddenFileSync } from "./CmdHiddenFileSync.ts"; import { configureHiddenFileSyncMode } from "./configureHiddenFileSyncMode.ts"; +function createHiddenRevisionOperation() { + const path = ".obsidian/plugins/example/data.json" as FilePath; + const file = { + path, + name: "data.json", + isInternal: true, + body: new Blob(["{\"value\":\"vault\"}"]), + stat: { + ctime: 1, + mtime: 2, + size: 17, + type: "file", + }, + } as UXFileInfo; + const selected = { + _id: "i:example" as DocumentID, + _rev: "2-selected", + path: `i:${path}` as FilePathWithPrefix, + ctime: 1, + mtime: 2, + size: 17, + type: "plain", + datatype: "plain", + children: [], + eden: {}, + deleted: false, + } as MetaEntry; + const winner = { + ...selected, + _rev: "3-winner", + } as MetaEntry; + const databaseFileAccess = { + fetchEntryMeta: vi.fn( + async (_path: unknown, revision?: string) => + revision === selected._rev ? selected : winner + ), + getConflictedRevs: vi.fn(async () => [selected._rev]), + fetchEntryFromMeta: vi.fn(async () => ({ ...selected, data: "{\"value\":\"database\"}" })), + storeWithBaseRevision: vi.fn(async () => "3-vault-child"), + }; + const hiddenFileSync = Object.create(HiddenFileSync.prototype) as HiddenFileSync; + Object.assign(hiddenFileSync, { + core: { + services: { + vault: { + isIgnoredByIgnoreFile: vi.fn(async () => false), + }, + }, + databaseFileAccess, + }, + loadFileWithInfo: vi.fn(async () => file), + updateLastProcessed: vi.fn(), + _log: vi.fn(), + }); + return { + hiddenFileSync, + path, + file, + selected, + winner, + databaseFileAccess, + }; +} + describe("HiddenFileSync configuration-change notices", () => { it("shows manual Hidden File Sync commands only when the feature, Advanced mode, and runtime are ready", () => { const commands: Array<{ @@ -270,3 +341,130 @@ describe("HiddenFileSync configuration-change notices", () => { expect(progress.done).toHaveBeenCalledWith("Failed"); }); }); + +describe("HiddenFileSync exact revision repair operations", () => { + it("stores the current hidden Vault file as a child of the selected live revision", async () => { + const { + hiddenFileSync, + file, + selected, + databaseFileAccess, + } = createHiddenRevisionOperation(); + + await expect( + hiddenFileSync.storeInternalFileToDatabaseWithBaseRevision(file, selected._rev!) + ).resolves.toBe(true); + + expect(databaseFileAccess.storeWithBaseRevision).toHaveBeenCalledWith( + expect.objectContaining({ + path: file.path, + body: file.body, + isInternal: true, + }), + selected._rev, + true + ); + expect(hiddenFileSync.updateLastProcessed).toHaveBeenCalledWith( + file.path, + expect.objectContaining({ _rev: "3-vault-child" }), + file.stat + ); + }); + + it("refuses to extend a hidden-file revision which is no longer live", async () => { + const { + hiddenFileSync, + file, + selected, + databaseFileAccess, + } = createHiddenRevisionOperation(); + databaseFileAccess.getConflictedRevs.mockResolvedValue([]); + + await expect( + hiddenFileSync.storeInternalFileToDatabaseWithBaseRevision(file, selected._rev!) + ).resolves.toBe(false); + + expect(databaseFileAccess.storeWithBaseRevision).not.toHaveBeenCalled(); + expect(hiddenFileSync.updateLastProcessed).not.toHaveBeenCalled(); + }); + + it("does not create a hidden-file child when asked only to mark a revision which differs from the Vault", async () => { + const { + hiddenFileSync, + file, + selected, + databaseFileAccess, + } = createHiddenRevisionOperation(); + + await expect( + hiddenFileSync.storeInternalFileToDatabaseWithBaseRevision( + file, + selected._rev!, + false + ) + ).resolves.toBe(false); + + expect(databaseFileAccess.storeWithBaseRevision).not.toHaveBeenCalled(); + expect(hiddenFileSync.updateLastProcessed).not.toHaveBeenCalled(); + }); + + it("marks a matching hidden-file revision without creating a child", async () => { + const { + hiddenFileSync, + file, + selected, + databaseFileAccess, + } = createHiddenRevisionOperation(); + databaseFileAccess.fetchEntryFromMeta.mockResolvedValue({ + ...selected, + data: "{\"value\":\"vault\"}", + }); + + await expect( + hiddenFileSync.storeInternalFileToDatabaseWithBaseRevision( + file, + selected._rev!, + false + ) + ).resolves.toBe(true); + + expect(databaseFileAccess.storeWithBaseRevision).not.toHaveBeenCalled(); + expect(hiddenFileSync.updateLastProcessed).toHaveBeenCalledWith( + file.path, + selected, + file.stat + ); + }); + + it("applies the selected live hidden-file revision through the existing extraction path", async () => { + const { + hiddenFileSync, + path, + selected, + } = createHiddenRevisionOperation(); + const extract = vi.fn(async () => true); + hiddenFileSync.extractInternalFileFromDatabase = extract; + + await expect( + hiddenFileSync.extractInternalFileRevisionFromDatabase(path, selected._rev!, true) + ).resolves.toBe(true); + + expect(extract).toHaveBeenCalledWith(path, true, undefined, true, false, true, selected._rev); + }); + + it("does not apply a hidden-file revision which ceased to be live", async () => { + const { + hiddenFileSync, + path, + selected, + databaseFileAccess, + } = createHiddenRevisionOperation(); + databaseFileAccess.getConflictedRevs.mockResolvedValue([]); + + await expect( + hiddenFileSync.extractInternalFileRevisionFromDatabase(path, selected._rev!, true) + ).resolves.toBe(false); + + expect(databaseFileAccess.fetchEntryFromMeta).not.toHaveBeenCalled(); + }); +}); diff --git a/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts b/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts index 8e53f4a9..838f7be1 100644 --- a/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts +++ b/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts @@ -13,6 +13,13 @@ export const POSTPONED = Symbol("postponed"); export type MergeDialogResult = typeof CANCELLED | typeof POSTPONED | typeof LEAVE_TO_SUBSEQUENT | string; +export type ConflictResolveModalOptions = { + readOnly?: boolean; + title?: string; + localName?: string; + remoteName?: string; +}; + export class ConflictResolveModal extends Modal { result: diff_result; filename: FilePathWithPrefix; @@ -25,6 +32,7 @@ export class ConflictResolveModal extends Modal { title: string = "Conflicting changes"; pluginPickMode: boolean = false; + readOnly: boolean = false; localName: string = "Base"; remoteName: string = "Conflicted"; offEvent?: ReturnType; @@ -37,16 +45,22 @@ export class ConflictResolveModal extends Modal { filename: FilePathWithPrefix, diff: diff_result, pluginPickMode?: boolean, - remoteName?: string + remoteName?: string, + options?: ConflictResolveModalOptions ) { super(app); this.result = diff; this.filename = filename; this.pluginPickMode = pluginPickMode || false; + this.readOnly = options?.readOnly ?? false; if (this.pluginPickMode) { this.title = "Pick a version"; this.remoteName = `${remoteName || "Remote"}`; this.localName = "Local"; + } else if (this.readOnly) { + this.title = options?.title ?? "Vault and database revision"; + this.localName = options?.localName ?? "Vault file"; + this.remoteName = options?.remoteName ?? "Database revision"; } } @@ -101,16 +115,18 @@ export class ConflictResolveModal extends Modal { if (this.offEvent) { this.offEvent(); } - // Cancel an older dialogue for this path before subscribing this - // instance. Emitting after subscription would close the replacement - // itself; the instance-owned result promise then completes the older - // caller even when it only begins waiting after this event. - eventHub.emitEvent(EVENT_CONFLICT_CANCELLED, this.filename); - this.offEvent = eventHub.onEvent(EVENT_CONFLICT_CANCELLED, (path) => { - if (path === this.filename) { - this.sendResponse(CANCELLED); - } - }); + if (!this.readOnly) { + // Cancel an older dialogue for this path before subscribing this + // instance. Emitting after subscription would close the replacement + // itself; the instance-owned result promise then completes the older + // caller even when it only begins waiting after this event. + eventHub.emitEvent(EVENT_CONFLICT_CANCELLED, this.filename); + this.offEvent = eventHub.onEvent(EVENT_CONFLICT_CANCELLED, (path) => { + if (path === this.filename) { + this.sendResponse(CANCELLED); + } + }); + } this.titleEl.setText(this.title); contentEl.empty(); const diffOptionsRow = contentEl.createDiv(""); @@ -159,24 +175,31 @@ export class ConflictResolveModal extends Modal { this.appendVersionInfo(div2, "deleted", this.localName, date1); this.appendVersionInfo(div2, "added", this.remoteName, date2); const actionContainer = contentEl.createDiv("conflict-action-container"); - actionContainer.createEl("button", { text: `Use ${this.localName}` }, (e) => { - e.addClass("conflict-action-button"); - e.addEventListener("click", () => this.sendResponse(this.result.right.rev)); - }); - actionContainer.createEl("button", { text: `Use ${this.remoteName}` }, (e) => { - e.addClass("conflict-action-button"); - e.addEventListener("click", () => this.sendResponse(this.result.left.rev)); - }); - if (!this.pluginPickMode) { - actionContainer.createEl("button", { text: "Concat both" }, (e) => { + if (this.readOnly) { + actionContainer.createEl("button", { text: "Close" }, (e) => { e.addClass("conflict-action-button"); - e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT)); + e.addEventListener("click", () => this.sendResponse(CANCELLED)); + }); + } else { + actionContainer.createEl("button", { text: `Use ${this.localName}` }, (e) => { + e.addClass("conflict-action-button"); + e.addEventListener("click", () => this.sendResponse(this.result.right.rev)); + }); + actionContainer.createEl("button", { text: `Use ${this.remoteName}` }, (e) => { + e.addClass("conflict-action-button"); + e.addEventListener("click", () => this.sendResponse(this.result.left.rev)); + }); + if (!this.pluginPickMode) { + actionContainer.createEl("button", { text: "Concat both" }, (e) => { + e.addClass("conflict-action-button"); + e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT)); + }); + } + actionContainer.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) => { + e.addClass("conflict-action-button"); + e.addEventListener("click", () => this.sendResponse(this.pluginPickMode ? CANCELLED : POSTPONED)); }); } - actionContainer.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) => { - e.addClass("conflict-action-button"); - e.addEventListener("click", () => this.sendResponse(this.pluginPickMode ? CANCELLED : POSTPONED)); - }); if (diffLength > 100 * 1024) { this.diffView.empty(); this.diffView.setText("(Too large diff to display)"); diff --git a/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.unit.spec.ts b/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.unit.spec.ts index aaf154d7..ded31a0a 100644 --- a/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.unit.spec.ts +++ b/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.unit.spec.ts @@ -5,6 +5,8 @@ import { CANCELLED, type diff_result, type FilePathWithPrefix } from "@vrtmrz/li vi.mock("@/deps.ts", () => ({ App: class App {}, Modal: class Modal { + createdButtons: string[] = []; + private createElement(): Record { const element: Record = { addClass: vi.fn(), @@ -22,6 +24,14 @@ vi.mock("@/deps.ts", () => ({ }; element.createDiv = vi.fn(() => this.createElement()); element.createEl = vi.fn((_tag: string, _options?: unknown, callback?: (child: unknown) => void) => { + if ( + _tag === "button" && + typeof _options === "object" && + _options !== null && + "text" in _options + ) { + this.createdButtons.push(String((_options as { text: unknown }).text)); + } const child = this.createElement(); callback?.(child); return child; @@ -82,4 +92,55 @@ describe("ConflictResolveModal result lifecycle", () => { expect(previousResult).toBe(CANCELLED); expect(replacementState).toBe("still-open"); }); + + it("renders a read-only comparison with no resolution actions", () => { + const ReadOnlyModal = ConflictResolveModal as unknown as new ( + ...args: unknown[] + ) => ConflictResolveModal & { createdButtons: string[] }; + const modal = new ReadOnlyModal( + {}, + "repair-preview.md", + conflict, + false, + undefined, + { + readOnly: true, + title: "Vault and database revision", + localName: "Vault file", + remoteName: "Database revision", + } + ); + + modal.onOpen(); + + expect(modal.createdButtons).toContain("Close"); + expect(modal.createdButtons).not.toContain("Use Vault file"); + expect(modal.createdButtons).not.toContain("Use Database revision"); + expect(modal.createdButtons).not.toContain("Concat both"); + expect(modal.createdButtons).not.toContain("Not now"); + modal.close(); + }); + + it("does not cancel an active conflict dialogue when a read-only comparison opens for the same file", async () => { + const filename = "repair-alongside-conflict.md" as FilePathWithPrefix; + const previous = new ConflictResolveModal({} as never, filename, conflict); + const ReadOnlyModal = ConflictResolveModal as unknown as new ( + ...args: unknown[] + ) => ConflictResolveModal; + const comparison = new ReadOnlyModal({}, filename, conflict, false, undefined, { + readOnly: true, + }); + previous.onOpen(); + + comparison.onOpen(); + const previousState = await Promise.race([ + previous.waitForResult(), + new Promise<"still-open">((resolve) => setTimeout(() => resolve("still-open"), 25)), + ]); + + previous.sendResponse(CANCELLED); + comparison.close(); + + expect(previousState).toBe("still-open"); + }); }); diff --git a/src/modules/features/SettingDialogue/PaneHatch.ts b/src/modules/features/SettingDialogue/PaneHatch.ts index 4cf67e40..63b5c79f 100644 --- a/src/modules/features/SettingDialogue/PaneHatch.ts +++ b/src/modules/features/SettingDialogue/PaneHatch.ts @@ -3,13 +3,14 @@ import { type DocumentID, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, - type MetaEntry, type FilePath, type EntryDoc, + type diff_result, } from "@vrtmrz/livesync-commonlib/compat/common/types"; -import { readAsBlob } from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import { createBlob, readAsBlob } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger"; import { shouldBeIgnored } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path"; +import { Menu, diff_match_patch } from "@/deps.ts"; import { $msg } from "@/common/translation"; import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; @@ -20,7 +21,6 @@ import { EVENT_REQUEST_RUN_FIX_INCOMPLETE, eventHub, } from "@/common/events.ts"; -import { ICHeader } 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"; @@ -33,11 +33,17 @@ import { retryReadFileDatabaseRevision, } from "@/serviceFeatures/fileDatabaseInfo.ts"; import { + discardLiveBranch, discardUnreadableLiveRevision, inspectFileRepair, type FileRepairInspection, type FileRepairRevision, } from "@/serviceFeatures/fileRepair.ts"; +import { + getFileRepairRevisionActions, + getFileRepairRevisionComparison, +} from "@/serviceFeatures/fileRepairPresentation.ts"; +import { ConflictResolveModal } from "@/modules/features/InteractiveConflictResolving/ConflictResolveModal.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"); @@ -123,57 +129,195 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, void addPanel(paneEl, "Recovery and Repair").then((paneEl) => { const resultArea = paneEl.createDiv({ text: "", cls: "sls-repair-results" }); - const addActionButton = ( + type RepairMenuAction = { + title: string; + run: () => Promise | void; + warning?: boolean; + }; + const addActionMenu = ( parent: HTMLElement, - text: string, - action: (button: HTMLButtonElement) => Promise | void, - warning = false + label: string, + actions: RepairMenuAction[] ) => { - this.createEl(parent, "button", { text }, (button) => { - if (warning) { - button.addClass("mod-warning"); - } - button.onClickEvent(async () => { - button.disabled = true; - try { - await action(button); - } finally { - if (button.isConnected) { - button.disabled = false; - } + if (actions.length === 0) { + return; + } + this.createEl(parent, "button", { text: "…", cls: "sls-repair-action-menu" }, (button) => { + button.setAttr("aria-label", label); + button.setAttr("title", label); + button.onClickEvent(() => { + const menu = new Menu(); + for (const action of actions) { + menu.addItem((item) => { + item.setTitle(action.title); + if (action.warning) { + item.setWarning(true); + } + item.onClick(() => { + button.disabled = true; + void Promise.resolve() + .then(() => action.run()) + .catch((error) => { + Logger(error, LOG_LEVEL_VERBOSE); + Logger( + `Repair action '${action.title}' failed`, + LOG_LEVEL_NOTICE + ); + }) + .finally(() => { + if (button.isConnected) { + button.disabled = false; + } + }); + }); + }); } + const rect = button.getBoundingClientRect(); + menu.showAtPosition({ x: rect.left, y: rect.bottom }); }); }); }; + const findHiddenFile = async (path: string) => { + const addOn = this.core.getAddOn(HiddenFileSync.name); + if (!addOn) { + return false; + } + const file = (await addOn.scanInternalFiles()).find((entry) => entry.path === path); + if (!file) { + Logger(`Failed to find the file in the internal files: ${path}`, LOG_LEVEL_NOTICE); + return false; + } + return { addOn, file }; + }; const storeStorageInDatabase = async (path: string): Promise => { if (path.startsWith(".")) { - const addOn = this.core.getAddOn(HiddenFileSync.name); - if (!addOn) { - return false; - } - const file = (await addOn.scanInternalFiles()).find((entry) => entry.path === path); - if (!file) { - Logger(`Failed to find the file in the internal files: ${path}`, LOG_LEVEL_NOTICE); - return false; - } - return Boolean(await addOn.storeInternalFileToDatabase(file, true)); + const hidden = await findHiddenFile(path); + return hidden + ? Boolean(await hidden.addOn.storeInternalFileToDatabase(hidden.file, true)) + : false; } return Boolean(await this.core.fileHandler.storeFileToDB(path as FilePath, true)); }; - const applyWinnerToStorage = async ( + const storeStorageOnRevision = async ( path: string, - revision: FileRepairRevision + revision: string, + createIfDifferent = true ): Promise => { - if (revision.loadedEntry === false) { - return false; - } - if (revision.loadedEntry.path.startsWith(ICHeader)) { - const addOn = this.core.getAddOn(HiddenFileSync.name); - return addOn - ? Boolean(await addOn.extractInternalFileFromDatabase(path as FilePath, true)) + if (path.startsWith(".")) { + const hidden = await findHiddenFile(path); + return hidden + ? Boolean( + await hidden.addOn.storeInternalFileToDatabaseWithBaseRevision( + hidden.file, + revision, + createIfDifferent + ) + ) : false; } - return Boolean(await this.core.fileHandler.dbToStorage(revision.loadedEntry as MetaEntry, null, true)); + return Boolean( + await this.core.fileHandler.storeFileToDBWithBaseRevision( + path as FilePath, + revision, + createIfDifferent + ) + ); + }; + const applyRevisionToStorage = async ( + path: string, + revision: string, + force: boolean + ): Promise => { + if (path.startsWith(".")) { + const addOn = this.core.getAddOn(HiddenFileSync.name); + return addOn + ? Boolean( + await addOn.extractInternalFileRevisionFromDatabase( + path as FilePath, + revision, + force + ) + ) + : false; + } + return Boolean( + await this.core.fileHandler.dbToStorageWithSpecificRev( + path as FilePath, + revision, + force + ) + ); + }; + const openRevisionComparison = async ( + path: string, + selectedRevision: string + ): Promise => { + const latest = await inspectFileRepair(this.core, path); + const revision = latest.revisions.find( + ({ metadata }) => metadata.revision === selectedRevision + ); + if ( + !latest.information.storage.exists || + !revision || + revision.loadedEntry === false + ) { + Logger( + `Could not compare ${path} revision ${selectedRevision}; the Vault file or selected live revision is no longer readable`, + LOG_LEVEL_NOTICE + ); + return false; + } + const vaultText = await createBlob( + await this.core.storageAccess.readHiddenFileBinary(path) + ).text(); + const databaseText = await readAsBlob(revision.loadedEntry).text(); + const dmp = new diff_match_patch(); + const diff = dmp.diff_main(vaultText, databaseText); + dmp.diff_cleanupSemantic(diff); + const result: diff_result = { + left: { + rev: "vault", + data: vaultText, + ctime: latest.information.storage.ctime ?? 0, + mtime: latest.information.storage.mtime ?? 0, + }, + right: { + rev: selectedRevision, + data: databaseText, + ctime: revision.metadata.ctime, + mtime: revision.metadata.mtime, + }, + diff, + }; + new ConflictResolveModal( + this.app, + path as FilePathWithPrefix, + result, + false, + undefined, + { + readOnly: true, + title: $msg("Vault and database revision"), + localName: $msg("Vault file"), + remoteName: $msg("Database revision"), + } + ).open(); + return true; + }; + const formatSigned = (value: number) => `${value >= 0 ? "+" : ""}${value}`; + const timestampRelationLabel = ( + relation: ReturnType["timestampRelation"] + ) => { + switch (relation) { + case "vault-newer": + return $msg("Vault file is newer"); + case "database-newer": + return $msg("Database revision is newer"); + case "same-window": + return $msg("Within the two-second comparison window"); + default: + return $msg("Timestamp comparison unavailable"); + } }; const addRepairResult = (inspection: FileRepairInspection) => { const { information, revisions } = inspection; @@ -188,40 +332,128 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, Logger(`Verification no longer reports a problem for ${path}`, LOG_LEVEL_NOTICE); } }; + const runMutation = async ( + description: string, + mutation: () => Promise + ) => { + try { + const succeeded = await mutation(); + if (!succeeded) { + Logger(`${description} failed: ${path}`, LOG_LEVEL_NOTICE); + } + } finally { + await refresh(); + } + }; + const discardLiveBranchAction = (revision: string): RepairMenuAction => ({ + title: $msg("Discard this branch"), + warning: true, + run: async () => { + const confirmed = + (await this.core.confirm.askYesNoDialog( + $msg( + "Discard database branch ${REVISION} of ${FILE}? This creates a logical deletion for that exact live branch. The current Vault file will not be changed.", + { + REVISION: revision, + FILE: path, + } + ), + { + title: $msg("Discard branch"), + defaultOption: "No", + } + )) === "yes"; + if (!confirmed) { + return; + } + const result = await discardLiveBranch(this.core, path, revision); + Logger( + `Discard database branch ${revision} of ${path}: ${result}`, + result === "discarded" ? LOG_LEVEL_NOTICE : LOG_LEVEL_VERBOSE + ); + await refresh(); + }, + }); - this.createEl(card, "h6", { text: path }); + const fileHeader = this.createEl(card, "div", { cls: "sls-repair-header" }); + this.createEl(fileHeader, "h6", { text: path }); + const fileMenuHost = this.createEl(fileHeader, "div"); if (information.storage.exists) { this.createEl(card, "div", { - text: $msg("Vault file: modified ${TIME}, size ${SIZE}", { + text: $msg("📁 Vault: ${SIZE} B · ${TIME}", { TIME: new Date(information.storage.mtime ?? 0).toLocaleString(), SIZE: `${information.storage.size ?? 0}`, }), + cls: "sls-repair-metric", }); } else { - this.createEl(card, "div", { text: $msg("Vault file: missing") }); + this.createEl(card, "div", { + text: $msg("📁 Vault: missing"), + cls: "sls-repair-metric", + }); } if (!information.database.exists) { - this.createEl(card, "div", { text: $msg("Local database document: missing") }); + this.createEl(card, "div", { + text: $msg("🗄️ Local DB: missing"), + cls: "sls-repair-metric", + }); + } + if (information.database.conflictCount > 0) { + const winner = revisions.find(({ role }) => role === "winner"); + const vaultMatchesWinner = + winner !== undefined && + (winner.metadata.deleted + ? !information.storage.exists + : information.storage.exists && + winner.contentMatchesStorage === true); + const status = this.createEl(card, "div", { cls: "sls-repair-status" }); + if (vaultMatchesWinner) { + this.createEl(status, "span", { + text: $msg("✅ Vault matches winner"), + cls: "sls-repair-status-ok", + }); + } + this.createEl(status, "span", { + text: $msg("⚠️ Conflicts: ${COUNT}", { + COUNT: `${information.database.conflictCount}`, + }), + cls: "sls-repair-status-warning", + }); } const addRevision = (revision: FileRepairRevision) => { const { metadata } = revision; const revisionEl = this.createEl(card, "div", { cls: "sls-repair-revision" }); - this.createEl(revisionEl, "div", { + const revisionHeader = this.createEl(revisionEl, "div", { + cls: "sls-repair-header", + }); + this.createEl(revisionHeader, "div", { text: $msg("${ROLE}: ${REVISION}", { ROLE: revision.role === "winner" ? $msg("Winner revision") : $msg("Conflict revision"), REVISION: metadata.revision ?? $msg("Unknown revision"), }), cls: "sls-repair-revision-title", }); + const revisionMenuHost = this.createEl(revisionHeader, "div"); + const comparison = getFileRepairRevisionComparison(inspection, revision); if (metadata.deleted) { - this.createEl(revisionEl, "div", { text: $msg("Logical deletion") }); + this.createEl(revisionEl, "div", { + text: $msg("🗑️ Logical deletion"), + cls: "sls-repair-metric", + }); } else if (revision.contentReadable) { this.createEl(revisionEl, "div", { - text: $msg("Readable on this device; recorded size ${RECORDED}, decoded size ${ACTUAL}", { - RECORDED: `${metadata.recordedSize}`, - ACTUAL: `${revision.loadedEntry === false ? 0 : readAsBlob(revision.loadedEntry).size}`, - }), + text: $msg( + "📦 DB: recorded ${RECORDED} B · decoded ${DECODED} B · Δsize ${DIFFERENCE} B", + { + RECORDED: `${comparison.recordedSize}`, + DECODED: `${comparison.decodedSize ?? 0}`, + DIFFERENCE: formatSigned( + comparison.recordedToDecodedSizeDifference ?? 0 + ), + } + ), + cls: "sls-repair-metric", }); } else { const missing = metadata.chunks.filter( @@ -229,10 +461,16 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, !embedded && localDatabaseState !== "available" ); this.createEl(revisionEl, "div", { - text: $msg("Unreadable on this device; ${COUNT} referenced chunks are missing or deleted", { + text: $msg("🧩 Missing chunks: ${COUNT}", { COUNT: `${missing.length}`, }), - cls: "mod-warning", + cls: "sls-repair-metric mod-warning", + }); + this.createEl(revisionEl, "div", { + text: $msg("📦 DB: recorded ${RECORDED} B · decoded unavailable", { + RECORDED: `${comparison.recordedSize}`, + }), + cls: "sls-repair-metric", }); if (missing.length > 0) { this.createEl(revisionEl, "code", { @@ -243,28 +481,196 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, }); } } + if ( + comparison.vaultSize !== null && + comparison.databaseToVaultSizeDifference !== null + ) { + this.createEl(revisionEl, "div", { + text: $msg("📁 Vault: ${VAULT} B · Δsize vs DB ${DIFFERENCE} B", { + VAULT: `${comparison.vaultSize}`, + DIFFERENCE: formatSigned( + comparison.databaseToVaultSizeDifference + ), + }), + cls: "sls-repair-metric", + }); + } + if ( + comparison.vaultMtime !== null && + comparison.timestampDifferenceMs !== null + ) { + this.createEl(revisionEl, "div", { + text: $msg( + "🕒 DB ${DATABASE_TIME} · Vault ${VAULT_TIME} · Δtime ${DIFFERENCE} ms (${RELATION})", + { + DATABASE_TIME: new Date( + comparison.databaseMtime + ).toLocaleString(), + VAULT_TIME: new Date( + comparison.vaultMtime + ).toLocaleString(), + DIFFERENCE: formatSigned( + comparison.timestampDifferenceMs + ), + RELATION: timestampRelationLabel( + comparison.timestampRelation + ), + } + ), + cls: "sls-repair-metric", + }); + } if (revision.contentMatchesStorage === true) { - this.createEl(revisionEl, "div", { text: $msg("Matches the current Vault file") }); + this.createEl(revisionEl, "div", { + text: $msg("✅ Matches Vault"), + cls: "sls-repair-metric", + }); } else if (revision.contentMatchesStorage === false) { - this.createEl(revisionEl, "div", { text: $msg("Differs from the current Vault file") }); + this.createEl(revisionEl, "div", { + text: $msg("⚠️ Differs from Vault"), + cls: "sls-repair-metric mod-warning", + }); } - if (!metadata.deleted && !revision.contentReadable && metadata.revision) { - const actions = this.createEl(revisionEl, "div", { cls: "sls-repair-actions" }); - addActionButton(actions, $msg("Retry reading revision"), async () => { - const loaded = await retryReadFileDatabaseRevision(this.core, path, metadata.revision!); - Logger( - loaded - ? `Revision ${metadata.revision} of ${path} is readable after retry` - : `Revision ${metadata.revision} of ${path} remains unreadable`, - LOG_LEVEL_NOTICE - ); - await refresh(); + const policy = getFileRepairRevisionActions(inspection, revision); + const revisionActions: RepairMenuAction[] = []; + if (metadata.revision && policy.compareWithVault) { + revisionActions.push({ + title: $msg("Compare with Vault"), + run: async () => { + await openRevisionComparison(path, metadata.revision!); + }, }); - addActionButton( - actions, - $msg("Discard unreadable revision"), - async () => { + } + if (metadata.revision && policy.applyRevisionToVault) { + revisionActions.push({ + title: $msg("Apply this revision to Vault"), + run: async () => { + if (await this.core.storageAccess.isExistsIncludeHidden(path)) { + const confirmed = + (await this.core.confirm.askYesNoDialog( + $msg( + "Apply database revision ${REVISION} to ${FILE}? The current Vault file will be overwritten.", + { + REVISION: metadata.revision!, + FILE: path, + } + ), + { + title: $msg("Apply database revision to Vault"), + defaultOption: "No", + } + )) === "yes"; + if (!confirmed) { + return; + } + } + await runMutation( + `Apply database revision ${metadata.revision} to the Vault`, + () => + applyRevisionToStorage( + path, + metadata.revision!, + true + ) + ); + }, + }); + } + if (metadata.revision && policy.markAsVaultRevision) { + revisionActions.push({ + title: $msg("Mark this revision as the Vault version"), + run: async () => { + await runMutation( + `Mark database revision ${metadata.revision} as the Vault version`, + () => + storeStorageOnRevision( + path, + metadata.revision!, + false + ) + ); + }, + }); + } + if (metadata.revision && policy.storeVaultOnBranch) { + revisionActions.push({ + title: $msg("Store Vault file as a child of this revision"), + run: async () => { + await runMutation( + `Store the Vault file on database revision ${metadata.revision}`, + () => + storeStorageOnRevision( + path, + metadata.revision! + ) + ); + }, + }); + } + if (metadata.revision && policy.applyLogicalDeletionToVault) { + revisionActions.push({ + title: $msg("Apply logical deletion to Vault"), + warning: true, + run: async () => { + if (await this.core.storageAccess.isExistsIncludeHidden(path)) { + const confirmed = + (await this.core.confirm.askYesNoDialog( + $msg( + "Apply logical deletion ${REVISION} to ${FILE}? The current Vault file will be removed.", + { + REVISION: metadata.revision!, + FILE: path, + } + ), + { + title: $msg("Apply logical deletion to Vault"), + defaultOption: "No", + } + )) === "yes"; + if (!confirmed) { + return; + } + } + await runMutation( + `Apply logical deletion ${metadata.revision} to the Vault`, + () => + applyRevisionToStorage( + path, + metadata.revision!, + true + ) + ); + }, + }); + } + if (metadata.revision && policy.retryRevision) { + revisionActions.push({ + title: $msg("Retry reading revision"), + run: async () => { + const loaded = await retryReadFileDatabaseRevision( + this.core, + path, + metadata.revision! + ); + Logger( + loaded + ? `Revision ${metadata.revision} of ${path} is readable after retry` + : `Revision ${metadata.revision} of ${path} remains unreadable`, + LOG_LEVEL_NOTICE + ); + await refresh(); + }, + }); + } + if (metadata.revision && policy.discardBranch) { + revisionActions.push(discardLiveBranchAction(metadata.revision)); + } + if (metadata.revision && policy.discardRevision) { + revisionActions.push({ + title: $msg("Discard unreadable revision"), + warning: true, + run: async () => { const confirmed = (await this.core.confirm.askYesNoDialog( $msg( @@ -293,55 +699,54 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, ); await refresh(); }, - true - ); + }); } + addActionMenu( + revisionMenuHost, + $msg("More actions for revision ${REVISION}", { + REVISION: metadata.revision ?? $msg("Unknown revision"), + }), + revisionActions + ); }; revisions.forEach(addRevision); for (const revision of information.database.unavailableConflictRevisions) { const revisionEl = this.createEl(card, "div", { cls: "sls-repair-revision" }); - this.createEl(revisionEl, "div", { + const revisionHeader = this.createEl(revisionEl, "div", { + cls: "sls-repair-header", + }); + this.createEl(revisionHeader, "div", { text: $msg("${ROLE}: ${REVISION}", { ROLE: $msg("Conflict revision"), REVISION: revision, }), cls: "sls-repair-revision-title", }); + const revisionMenuHost = this.createEl(revisionHeader, "div"); this.createEl(revisionEl, "div", { text: $msg("Revision metadata is unavailable on this device"), cls: "mod-warning", }); - const actions = this.createEl(revisionEl, "div", { cls: "sls-repair-actions" }); - addActionButton(actions, $msg("Retry reading revision"), async () => { - await retryReadFileDatabaseRevision(this.core, path, revision); - await refresh(); - }); - addActionButton( - actions, - $msg("Discard unreadable revision"), - async () => { - const confirmed = - (await this.core.confirm.askYesNoDialog( - $msg( - "Discard database revision ${REVISION} of ${FILE}? This creates a logical deletion for that exact live revision. Missing content cannot be recovered by this action.", - { - REVISION: revision, - FILE: path, - } - ), - { - title: $msg("Discard unreadable revision"), - defaultOption: "No", - } - )) === "yes"; - if (!confirmed) { - return; - } - await discardUnreadableLiveRevision(this.core, path, revision); - await refresh(); - }, - true + addActionMenu( + revisionMenuHost, + $msg("More actions for revision ${REVISION}", { + REVISION: revision, + }), + [ + { + title: $msg("Retry reading revision"), + run: async () => { + await retryReadFileDatabaseRevision( + this.core, + path, + revision + ); + await refresh(); + }, + }, + discardLiveBranchAction(revision), + ] ); } @@ -365,45 +770,41 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, } const winner = revisions.find(({ role }) => role === "winner"); - const actions = this.createEl(card, "div", { cls: "sls-repair-actions" }); - if (winner?.loadedEntry && information.storage.exists) { + const fileActions: RepairMenuAction[] = []; + if (winner?.loadedEntry) { const winnerEntry = winner.loadedEntry; - addActionButton(actions, $msg("Show revision history"), () => { - eventHub.emitEvent(EVENT_REQUEST_SHOW_HISTORY, { - file: path as FilePathWithPrefix, - fileOnDB: winnerEntry, - }); + fileActions.push({ + title: $msg("Show revision history"), + run: () => { + eventHub.emitEvent(EVENT_REQUEST_SHOW_HISTORY, { + file: path as FilePathWithPrefix, + fileOnDB: winnerEntry, + }); + }, }); } - if ( - information.storage.exists && - information.database.conflictCount === 0 && - (!winner || winner.contentReadable) - ) { - addActionButton(actions, $msg("Use Vault file in local database"), async () => { - if (!(await storeStorageInDatabase(path))) { - Logger(`Failed to store the Vault file in the local database: ${path}`, LOG_LEVEL_NOTICE); - return; - } - await refresh(); + if (information.storage.exists && !information.database.exists) { + fileActions.push({ + title: $msg("Store Vault file as a new local database document"), + run: async () => { + await runMutation( + "Store the Vault file as a new local database document", + () => storeStorageInDatabase(path) + ); + }, }); } - if ( - !information.storage.exists && - information.database.conflictCount === 0 && - winner?.loadedEntry - ) { - addActionButton(actions, $msg("Restore database winner to Vault"), async () => { - if (!(await applyWinnerToStorage(path, winner))) { - Logger(`Failed to restore the database winner to the Vault: ${path}`, LOG_LEVEL_NOTICE); - return; - } - await refresh(); - }); - } - addActionButton(actions, $msg("Copy database information"), async () => { - await copyFileDatabaseInfo(this.core, path); + fileActions.push({ + title: $msg("Copy database information"), + run: async () => { + await copyFileDatabaseInfo(this.core, path); + }, }); + addActionMenu( + fileMenuHost, + $msg("More actions for ${FILE}", { FILE: path }), + fileActions + ); }; new Setting(paneEl) diff --git a/src/serviceFeatures/fileRepair.ts b/src/serviceFeatures/fileRepair.ts index f5cef48b..64172843 100644 --- a/src/serviceFeatures/fileRepair.ts +++ b/src/serviceFeatures/fileRepair.ts @@ -35,6 +35,8 @@ export type DiscardUnreadableRevisionResult = | "no-longer-live" | "revision-is-readable"; +export type DiscardLiveBranchResult = "discarded" | "failed" | "no-longer-live" | "only-live-revision"; + export async function inspectFileRepair(core: FileRepairCore, path: string): Promise { const information = await inspectFileDatabaseInfo(core, path); const storageContent = information.storage.exists @@ -62,12 +64,12 @@ export async function inspectFileRepair(core: FileRepairCore, path: string): Pro } const winner = revisions.find(({ role }) => role === "winner"); + const winnerRepresentsStoredFile = winner !== undefined && !winner.metadata.deleted; const databaseAndStorageDiffer = - information.storage.exists !== information.database.exists || + information.storage.exists !== winnerRepresentsStoredFile || (information.storage.exists && - winner !== undefined && - (winner.metadata.deleted || winner.contentMatchesStorage === false)) || - (!information.storage.exists && winner !== undefined && !winner.metadata.deleted); + winnerRepresentsStoredFile && + winner.contentMatchesStorage === false); const unreadableLiveRevision = information.database.unavailableConflictRevisions.length > 0 || revisions.some(({ contentReadable }) => !contentReadable); @@ -107,3 +109,24 @@ export async function discardUnreadableLiveRevision( const deleted = await core.fileHandler.deleteRevisionFromDB(latest.databasePath, revision); return deleted ? "discarded" : "failed"; } + +export async function discardLiveBranch( + core: FileRepairCore, + path: string, + revision: string +): Promise { + const latest = await inspectFileDatabaseInfo(core, path); + const liveRevisions = [ + latest.database.currentRevision, + ...latest.database.conflictRevisions, + ].filter((candidate): candidate is string => candidate !== null); + if (!liveRevisions.includes(revision)) { + return "no-longer-live"; + } + if (liveRevisions.length < 2) { + return "only-live-revision"; + } + + const deleted = await core.fileHandler.deleteRevisionFromDB(latest.databasePath, revision); + return deleted ? "discarded" : "failed"; +} diff --git a/src/serviceFeatures/fileRepair.unit.spec.ts b/src/serviceFeatures/fileRepair.unit.spec.ts index 9d066877..af12a34b 100644 --- a/src/serviceFeatures/fileRepair.unit.spec.ts +++ b/src/serviceFeatures/fileRepair.unit.spec.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { + discardLiveBranch, discardUnreadableLiveRevision, inspectFileRepair, } from "./fileRepair"; @@ -16,6 +17,7 @@ function createCore() { size: 7, type: "plain", children: ["h:current"], + deleted: false, eden: {}, }; const conflict = { @@ -118,6 +120,29 @@ describe("file repair inspection", () => { expect(inspection.requiresAttention).toBe(true); }); + it("omits a logical deletion which already matches an absent Vault file", async () => { + const { core, current } = createCore(); + current.deleted = true; + current._conflicts = []; + current.children = []; + core.storageAccess.isExistsIncludeHidden.mockResolvedValue(false); + core.storageAccess.statHidden.mockResolvedValue(null as never); + + const inspection = await inspectFileRepair(core as never, "note.md"); + + expect(inspection.revisions).toEqual([ + expect.objectContaining({ + role: "winner", + contentReadable: true, + metadata: expect.objectContaining({ + deleted: true, + revision: "3-current", + }), + }), + ]); + expect(inspection.requiresAttention).toBe(false); + }); + it("rechecks liveness and readability before discarding an exact revision", async () => { const { core, deleteRevisionFromDB } = createCore(); @@ -169,4 +194,35 @@ describe("file repair inspection", () => { expect(deleteRevisionFromDB).not.toHaveBeenCalled(); }); + + it("discards an exact readable winner while another live branch remains", async () => { + const { core, deleteRevisionFromDB } = createCore(); + + await expect( + discardLiveBranch(core as never, "note.md", "3-current") + ).resolves.toBe("discarded"); + + expect(deleteRevisionFromDB).toHaveBeenCalledWith("note.md", "3-current"); + }); + + it("refuses to discard the only live branch", async () => { + const { core, current, deleteRevisionFromDB } = createCore(); + current._conflicts = []; + + await expect( + discardLiveBranch(core as never, "note.md", "3-current") + ).resolves.toBe("only-live-revision"); + + expect(deleteRevisionFromDB).not.toHaveBeenCalled(); + }); + + it("refuses to discard a branch which is no longer live", async () => { + const { core, deleteRevisionFromDB } = createCore(); + + await expect( + discardLiveBranch(core as never, "note.md", "1-stale") + ).resolves.toBe("no-longer-live"); + + expect(deleteRevisionFromDB).not.toHaveBeenCalled(); + }); }); diff --git a/src/serviceFeatures/fileRepairPresentation.ts b/src/serviceFeatures/fileRepairPresentation.ts new file mode 100644 index 00000000..4906f4cb --- /dev/null +++ b/src/serviceFeatures/fileRepairPresentation.ts @@ -0,0 +1,144 @@ +import { + BASE_IS_NEW, + EVEN, + TARGET_IS_NEW, +} from "@vrtmrz/livesync-commonlib/compat/common/models/shared.const.symbols"; +import { + compareMTime, + readAsBlob, +} from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import { isPlainText } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path"; +import type { + FileRepairInspection, + FileRepairRevision, +} from "./fileRepair"; + +export type FileRepairRevisionActions = { + compareWithVault: boolean; + applyRevisionToVault: boolean; + markAsVaultRevision: boolean; + storeVaultOnBranch: boolean; + applyLogicalDeletionToVault: boolean; + retryRevision: boolean; + discardBranch: boolean; + discardRevision: boolean; +}; + +export type FileRepairTimestampRelation = + | "vault-newer" + | "database-newer" + | "same-window" + | "unavailable"; + +export type FileRepairRevisionComparison = { + recordedSize: number; + decodedSize: number | null; + recordedToDecodedSizeDifference: number | null; + vaultSize: number | null; + databaseToVaultSizeDifference: number | null; + databaseMtime: number; + vaultMtime: number | null; + timestampDifferenceMs: number | null; + timestampRelation: FileRepairTimestampRelation; +}; + +export function getFileRepairRevisionActions( + inspection: FileRepairInspection, + revision: FileRepairRevision +): FileRepairRevisionActions { + const storageExists = inspection.information.storage.exists; + const hasRevision = revision.metadata.revision !== null; + const readableFileRevision = + !revision.metadata.deleted && + revision.contentReadable && + revision.loadedEntry !== false; + const matchesVault = storageExists && revision.contentMatchesStorage === true; + const hasConflictBranches = inspection.information.database.conflictCount > 0; + + return { + compareWithVault: + readableFileRevision && + storageExists && + revision.contentMatchesStorage === false && + isPlainText(inspection.information.path), + applyRevisionToVault: + hasRevision && + readableFileRevision && + (!storageExists || revision.contentMatchesStorage !== true), + markAsVaultRevision: + hasRevision && + readableFileRevision && + matchesVault, + storeVaultOnBranch: + hasRevision && + storageExists && + revision.contentMatchesStorage !== true, + applyLogicalDeletionToVault: + hasRevision && + revision.metadata.deleted && + storageExists, + retryRevision: + hasRevision && + !revision.metadata.deleted && + !revision.contentReadable, + discardBranch: hasRevision && hasConflictBranches, + discardRevision: + hasRevision && + !hasConflictBranches && + !revision.metadata.deleted && + !revision.contentReadable, + }; +} + +export function getFileRepairRevisionComparison( + inspection: FileRepairInspection, + revision: FileRepairRevision +): FileRepairRevisionComparison { + const decodedSize = + revision.loadedEntry === false + ? null + : readAsBlob(revision.loadedEntry).size; + const vaultSize = + inspection.information.storage.exists + ? (inspection.information.storage.size ?? null) + : null; + const databaseMtime = revision.metadata.mtime; + const vaultMtime = + inspection.information.storage.exists + ? (inspection.information.storage.mtime ?? null) + : null; + const timestampDifferenceMs = + databaseMtime > 0 && vaultMtime !== null && vaultMtime > 0 + ? vaultMtime - databaseMtime + : null; + let timestampRelation: FileRepairTimestampRelation = "unavailable"; + if (timestampDifferenceMs !== null) { + const comparison = compareMTime(vaultMtime!, databaseMtime); + timestampRelation = + comparison === EVEN + ? "same-window" + : comparison === BASE_IS_NEW + ? "vault-newer" + : comparison === TARGET_IS_NEW + ? "database-newer" + : "unavailable"; + } + + return { + recordedSize: revision.metadata.recordedSize, + decodedSize, + recordedToDecodedSizeDifference: + decodedSize === null + ? null + : decodedSize - revision.metadata.recordedSize, + vaultSize, + databaseToVaultSizeDifference: + decodedSize === null || vaultSize === null + ? null + : vaultSize - decodedSize, + databaseMtime, + vaultMtime, + timestampDifferenceMs, + timestampRelation, + }; +} diff --git a/src/serviceFeatures/fileRepairPresentation.unit.spec.ts b/src/serviceFeatures/fileRepairPresentation.unit.spec.ts new file mode 100644 index 00000000..1f5f968c --- /dev/null +++ b/src/serviceFeatures/fileRepairPresentation.unit.spec.ts @@ -0,0 +1,230 @@ +import { describe, expect, it } from "vitest"; +import type { FilePathWithPrefix } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { FileRepairInspection, FileRepairRevision } from "./fileRepair"; +import { + getFileRepairRevisionActions, + getFileRepairRevisionComparison, +} from "./fileRepairPresentation"; + +function createInspection( + revision: Partial = {}, + storage: { exists: boolean; size?: number; mtime?: number } = { + exists: true, + size: 12, + mtime: 5_500, + } +): { inspection: FileRepairInspection; revision: FileRepairRevision } { + const completeRevision = { + role: "conflict", + metadata: { + documentId: "f:note", + revision: "2-conflict", + current: false, + deleted: false, + storageType: "plain", + storageLayout: "chunked", + ctime: 1, + mtime: 2_000, + recordedSize: 9, + revisionHistory: [], + chunkReferences: 0, + uniqueChunkReferences: 0, + embeddedChunkReferences: 0, + locallyStoredChunkReferences: 0, + contentAvailableLocally: true, + chunks: [], + }, + contentReadable: true, + contentMatchesStorage: false, + loadedEntry: { + _id: "f:note", + _rev: "2-conflict", + path: "note.md", + ctime: 1, + mtime: 2_000, + size: 9, + type: "plain", + datatype: "plain", + children: [], + eden: {}, + data: "content", + }, + ...revision, + } as FileRepairRevision; + const inspection = { + information: { + path: "note.md", + databasePath: "note.md" as FilePathWithPrefix, + storage, + database: { + source: "local database on this device", + remoteQueried: false, + exists: true, + currentRevision: "3-winner", + conflictCount: 1, + conflictRevisions: ["2-conflict"], + unavailableConflictRevisions: [], + revisions: [], + mergeBases: [], + }, + }, + revisions: [completeRevision], + requiresAttention: true, + } satisfies FileRepairInspection; + return { inspection, revision: completeRevision }; +} + +describe("file repair presentation", () => { + it("offers both reconciliation directions for a readable differing revision", () => { + const { inspection, revision } = createInspection(); + + expect(getFileRepairRevisionActions(inspection, revision)).toEqual({ + compareWithVault: true, + applyRevisionToVault: true, + markAsVaultRevision: false, + storeVaultOnBranch: true, + applyLogicalDeletionToVault: false, + retryRevision: false, + discardRevision: false, + discardBranch: true, + }); + }); + + it("marks an exact matching revision without creating another child", () => { + const { inspection, revision } = createInspection({ + contentMatchesStorage: true, + }); + + expect(getFileRepairRevisionActions(inspection, revision)).toMatchObject({ + compareWithVault: false, + applyRevisionToVault: false, + markAsVaultRevision: true, + storeVaultOnBranch: false, + discardBranch: true, + }); + }); + + it("does not offer a text comparison for a binary file", () => { + const { inspection, revision } = createInspection(); + inspection.information.path = "image.png"; + + expect(getFileRepairRevisionActions(inspection, revision)).toMatchObject({ + compareWithVault: false, + applyRevisionToVault: true, + storeVaultOnBranch: true, + }); + }); + + it("offers explicit deletion or branch extension for a logical deletion", () => { + const { inspection, revision } = createInspection({ + metadata: { + ...createInspection().revision.metadata, + deleted: true, + }, + contentReadable: true, + contentMatchesStorage: null, + loadedEntry: false, + }); + + expect(getFileRepairRevisionActions(inspection, revision)).toMatchObject({ + applyRevisionToVault: false, + storeVaultOnBranch: true, + applyLogicalDeletionToVault: true, + retryRevision: false, + discardRevision: false, + discardBranch: true, + }); + }); + + it("offers retry, discard, and branch extension for an unreadable live revision", () => { + const { inspection, revision } = createInspection({ + contentReadable: false, + contentMatchesStorage: null, + loadedEntry: false, + }); + + expect(getFileRepairRevisionActions(inspection, revision)).toMatchObject({ + compareWithVault: false, + applyRevisionToVault: false, + markAsVaultRevision: false, + storeVaultOnBranch: true, + retryRevision: true, + discardRevision: false, + discardBranch: true, + }); + }); + + it("keeps the existing unreadable-leaf escape hatch when there is no conflict branch", () => { + const { inspection, revision } = createInspection({ + role: "winner", + contentReadable: false, + contentMatchesStorage: null, + loadedEntry: false, + }); + inspection.information.database.conflictCount = 0; + inspection.information.database.conflictRevisions = []; + inspection.information.database.currentRevision = revision.metadata.revision; + + expect(getFileRepairRevisionActions(inspection, revision)).toMatchObject({ + discardRevision: true, + discardBranch: false, + }); + }); + + it("does not offer a storage action for a matching absent logical deletion", () => { + const { inspection, revision } = createInspection( + { + metadata: { + ...createInspection().revision.metadata, + deleted: true, + }, + contentReadable: true, + contentMatchesStorage: null, + loadedEntry: false, + }, + { exists: false } + ); + + expect(getFileRepairRevisionActions(inspection, revision)).toMatchObject({ + applyLogicalDeletionToVault: false, + storeVaultOnBranch: false, + }); + }); + + it("reports recorded, decoded, Vault-size, and timestamp differences", () => { + const { inspection, revision } = createInspection(); + + expect(getFileRepairRevisionComparison(inspection, revision)).toEqual({ + recordedSize: 9, + decodedSize: 7, + recordedToDecodedSizeDifference: -2, + vaultSize: 12, + databaseToVaultSizeDifference: 5, + databaseMtime: 2_000, + vaultMtime: 5_500, + timestampDifferenceMs: 3_500, + timestampRelation: "vault-newer", + }); + }); + + it("uses the same two-second timestamp comparison window as synchronisation", () => { + const { inspection, revision } = createInspection( + { + metadata: { + ...createInspection().revision.metadata, + mtime: 3_001, + }, + }, + { + exists: true, + size: 12, + mtime: 3_999, + } + ); + + expect(getFileRepairRevisionComparison(inspection, revision)).toMatchObject({ + timestampDifferenceMs: 998, + timestampRelation: "same-window", + }); + }); +}); diff --git a/styles.css b/styles.css index ab7ca645..8c9ec551 100644 --- a/styles.css +++ b/styles.css @@ -612,6 +612,54 @@ body.is-mobile .livesync-compatibility-review-notice { background: var(--background-secondary); } +.sls-repair-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--size-4-2); + min-width: 0; +} + +.sls-repair-header > :first-child { + flex: 1 1 auto; + min-width: 0; +} + +.sls-repair-header h6 { + margin: 0; + overflow-wrap: anywhere; +} + +.sls-repair-status { + display: flex; + flex-wrap: wrap; + gap: var(--size-4-2); + margin-top: var(--size-4-1); + font-size: var(--font-ui-smaller); +} + +.sls-repair-status-ok { + color: var(--text-success); +} + +.sls-repair-status-warning { + color: var(--text-warning); +} + +.sls-repair-metric { + margin-top: var(--size-4-1); + font-size: var(--font-ui-smaller); + line-height: var(--line-height-tight); + overflow-wrap: anywhere; +} + +.sls-repair-action-menu { + min-width: var(--clickable-icon-size); + width: var(--clickable-icon-size); + height: var(--clickable-icon-size); + padding: 0; +} + .sls-repair-revision { margin-top: var(--size-4-2); padding: var(--size-4-2); @@ -636,13 +684,6 @@ body.is-mobile .livesync-compatibility-review-notice { color: var(--text-warning); } -.sls-repair-actions { - display: flex; - flex-wrap: wrap; - gap: var(--size-4-2); - margin-top: var(--size-4-2); -} - /* Diff navigation */ .diff-options-row { display: flex; diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index f7a452f8..20717f69 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -161,7 +161,7 @@ This proves in real Obsidian the plug-in behaviour shared by supported platforms `test:e2e:obsidian:conflict-dialog-policy` creates three real local revision leaves without a remote service and opens the pairwise merge dialogue in Obsidian. It verifies the three-version count, requires the four decision buttons to be stacked vertically, concatenates the displayed pair as a child of the displayed winner, confirms that the untouched leaf remains as one conflict, postpones that remaining pair, restarts the same isolated Vault and profile, and confirms that only the two live versions are reconstructed. It also verifies that an ordinary repeated conflict check does not reopen a postponed dialogue, that **Resolve if conflicted.** explicitly reopens it, and that the active editor retains the appropriate unresolved-conflict warning. The scenario then invokes the same Commonlib consumer boundary used for an incoming replicated document and checks that a postponed warning disappears, an open stale dialogue closes, and the conflict-processing queue completes even when the dialogue closes immediately. This isolates the Obsidian UI contract from transport and second-device setup. The fixture owns one temporary Vault and profile, and the session runner stops Obsidian before removing them. -`test:e2e:obsidian:revision-repair` creates two live revisions in a temporary real Obsidian Vault, removes a chunk used only by the non-winning revision, and proves that automatic conflict checking does not discard the unreadable branch. **Verify and repair all files** must show the winner and conflict separately, identify the exact unreadable revision and missing chunk, and leave the revision tree unchanged when reading is retried. The scenario then verifies both the cancellation path and the explicit confirmation path for discarding that exact unreadable live revision, requires the winner to remain unchanged, and captures the repair card. It uses no remote service; a retry is therefore expected to remain unreadable unless the chunk is already available locally. +`test:e2e:obsidian:revision-repair` creates an ordinary healthy logical deletion and two conflicting live revisions in a temporary real Obsidian Vault, then removes a chunk used only by the non-winning revision. It proves that automatic conflict checking does not discard the unreadable branch, and that a healthy logical deletion with no Vault file is neither reported nor retained as Vault provenance. **Verify and repair all files** must show the winner and conflict separately, identify the exact unreadable revision and missing chunk, show the compact `Δsize` and `Δtime` diagnostics, and expose the appropriate `…` menu for each branch. The scenario opens the existing comparison dialogue in read-only mode, applies the readable winner to the Vault, shows the compact matching-winner and remaining-conflict status, records the exact winner as Vault provenance without creating a child, and confirms that retrying the unreadable branch leaves the revision tree unchanged. It then verifies both the cancellation path and the explicit confirmation path for discarding only that selected live branch, requires the winner and its Vault provenance to remain unchanged, and captures the repair card, a 360-pixel-wide reflow check, the matching-winner status, both revision menus, and the read-only comparison. The narrow capture checks responsive layout, not a mobile operating-system lifecycle. The scenario uses no remote service; a retry is therefore expected to remain unreadable unless the chunk is already available locally. `test:e2e:obsidian:hidden-file-snippet-sync` runs a two-vault hidden file round-trip. It verifies creation and deletion of a real `.obsidian/snippets/*.css` file, automatic JSON conflict merging for a hidden file with the merged result propagated by a second synchronisation, manual JSON Resolve dialogue application through Obsidian's UI, and per-device target patterns where one vault ignores a hidden file that the other vault synchronises. Initial enablement must open one user-visible progress Notice before the enabled setting is saved, then retain that Notice while its nested rebuild and scan phases continue in the ordinary log. The configured fixture starts with a current CouchDB remote profile, so migration from legacy remote settings remains the responsibility of the upgrade scenarios and cannot add unrelated Notices to this check. It also covers [issue #555](https://github.com/vrtmrz/obsidian-livesync/issues/555) by requiring several plug-in and settings changes to share one separate action Notice whose controls remain usable in mobile layouts; a manually dismissed group must not repeat its acknowledged rows when a later change arrives. diff --git a/test/e2e-obsidian/scripts/revision-repair.ts b/test/e2e-obsidian/scripts/revision-repair.ts index 405c20c9..7b5e642d 100644 --- a/test/e2e-obsidian/scripts/revision-repair.ts +++ b/test/e2e-obsidian/scripts/revision-repair.ts @@ -8,8 +8,10 @@ import { import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; import { captureObsidianElement, withObsidianPage } from "../runner/ui.ts"; import { createTemporaryVault } from "../runner/vault.ts"; +import type { Locator, Page } from "playwright"; const path = "revision-repair.md"; +const healthyDeletedPath = "healthy-logical-deletion.md"; const baseContent = "Revision repair\n\nShared base.\n"; const branchContents = [ `Revision repair\n\nLeft branch.\n${"L".repeat(4096)}\n`, @@ -28,6 +30,11 @@ type RevisionTree = { conflictRevisions: string[]; }; +type VaultWinnerState = { + matches: boolean; + winnerRevision: string; +}; + type ObsidianSettingsController = { open(): void; openTabById(tabId: string): void; @@ -56,6 +63,49 @@ async function createAndOpenBaseFile(cliBinary: string, env: NodeJS.ProcessEnv): ); } +async function createHealthyLogicalDeletion(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(healthyDeletedPath)};`, + `const content=${JSON.stringify(`Healthy logical deletion\n\n${"D".repeat(4096)}\n`)};`, + "let file=app.vault.getAbstractFileByPath(path);", + "if(!file) file=await app.vault.create(path,content);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + env + ); + await waitForLocalDatabaseEntry(cliBinary, env, healthyDeletedPath); + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(healthyDeletedPath)};`, + `const timeoutMs=${JSON.stringify(uiTimeoutMs)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const file=app.vault.getAbstractFileByPath(path);", + "if(!file) throw new Error(`Logical-deletion fixture is missing from the Vault: ${path}`);", + "await app.vault.delete(file);", + "const id=await core.services.path.path2id(path);", + "const deadline=Date.now()+timeoutMs;", + "const sleep=(ms)=>new Promise((resolve)=>setTimeout(resolve,ms));", + "while(Date.now()false);", + " if(!app.vault.getAbstractFileByPath(path)&&doc?.deleted&&(doc._conflicts??[]).length===0){", + " return JSON.stringify(doc._rev);", + " }", + " await sleep(250);", + "}", + "throw new Error(`Timed out waiting for a healthy logical deletion: ${path}`);", + "})()", + ].join(""), + env + ); +} + async function createBrokenConflict( cliBinary: string, env: NodeJS.ProcessEnv, @@ -128,6 +178,93 @@ async function readRevisionTree(cliBinary: string, env: NodeJS.ProcessEnv): Prom ); } +async function readVaultWinnerState(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const file=app.vault.getAbstractFileByPath(path);", + "if(!file) throw new Error(`Vault file is missing: ${path}`);", + "const entry=await core.localDatabase.getDBEntry(path,undefined,false,true,true);", + "if(!entry||!entry._rev) throw new Error(`Database winner is missing: ${path}`);", + "const vaultContent=await app.vault.read(file);", + "const data=Array.isArray(entry.data)?entry.data:[entry.data];", + "const databaseContent=await new Blob(data).text();", + "return JSON.stringify({", + " matches:vaultContent===databaseContent,", + " winnerRevision:entry._rev,", + "});", + "})()", + ].join(""), + env + ); +} + +async function readFileReflectionProvenance( + cliBinary: string, + env: NodeJS.ProcessEnv, + targetPath = path +): Promise<{ revision: string; observedStorageMtime?: number } | null> { + return await evalObsidianJson<{ revision: string; observedStorageMtime?: number } | null>( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(targetPath)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const store=core.services.keyValueDB.openSimpleStore('file-reflection-provenance-v1');", + "return JSON.stringify((await store.get(path))??null);", + "})()", + ].join(""), + env + ); +} + +function repairCard(settings: Locator): Locator { + return settings.locator(".sls-repair-result").filter({ hasText: path }); +} + +function revisionCard(settings: Locator, revision: string): Locator { + return repairCard(settings).locator(".sls-repair-revision").filter({ hasText: revision }); +} + +async function openRevisionActionMenu(page: Page, settings: Locator, revision: string): Promise { + await revisionCard(settings, revision) + .getByRole("button", { + name: `More actions for revision ${revision}`, + exact: true, + }) + .click({ timeout: uiTimeoutMs }); + const menu = page.locator(".menu:visible").last(); + await menu.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const box = await menu.boundingBox(); + const viewport = await page.evaluate(() => ({ + width: window.innerWidth, + height: window.innerHeight, + })); + if ( + box === null || + box.y < 0 || + box.y + box.height > viewport.height - 4 + ) { + throw new Error( + `Revision action menu is outside the viewport: ${JSON.stringify({ + box, + viewport, + })}` + ); + } + return menu; +} + +async function selectRevisionAction(page: Page, settings: Locator, revision: string, action: string): Promise { + const menu = await openRevisionActionMenu(page, settings, revision); + const item = menu.getByText(action, { exact: true }); + await item.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await item.click({ timeout: uiTimeoutMs }); +} + async function requestConflictCheck(cliBinary: string, env: NodeJS.ProcessEnv): Promise { await evalObsidianJson( cliBinary, @@ -187,7 +324,22 @@ async function main(): Promise { await waitForLiveSyncCoreReady(cliBinary, session.cliEnv); await createAndOpenBaseFile(cliBinary, session.cliEnv); const base = await waitForLocalDatabaseEntry(cliBinary, session.cliEnv, path); + const healthyDeletionRevision = await createHealthyLogicalDeletion(cliBinary, session.cliEnv); const fixture = await createBrokenConflict(cliBinary, session.cliEnv, base.rev); + const healthyDeletionProvenance = await readFileReflectionProvenance( + cliBinary, + session.cliEnv, + healthyDeletedPath + ); + if (healthyDeletionProvenance !== null) { + throw new Error( + `A healthy logical deletion retained Vault provenance indefinitely: ${JSON.stringify({ + healthyDeletedPath, + healthyDeletionRevision, + healthyDeletionProvenance, + })}` + ); + } await requestConflictCheck(cliBinary, session.cliEnv); const afterAutomaticCheck = await readRevisionTree(cliBinary, session.cliEnv); @@ -219,13 +371,17 @@ async function main(): Promise { await verifySetting.getByRole("button", { name: "Verify all", exact: true }).click({ timeout: uiTimeoutMs, }); - const card = settings.locator(".sls-repair-result").filter({ hasText: path }); + const card = repairCard(settings); await card.waitFor({ state: "visible", timeout: uiTimeoutMs }); - const brokenRevision = card - .locator(".sls-repair-revision") - .filter({ hasText: fixture.conflictRevision }); + if ((await settings.locator(".sls-repair-result").filter({ hasText: healthyDeletedPath }).count()) !== 0) { + throw new Error( + `Verify and Repair reported the healthy logical deletion ${healthyDeletedPath} (${healthyDeletionRevision}).` + ); + } + const winnerRevision = revisionCard(settings, fixture.winnerRevision); + const brokenRevision = revisionCard(settings, fixture.conflictRevision); await brokenRevision - .getByText(/Unreadable on this device/u) + .getByText(/🧩 Missing chunks: 1/u) .waitFor({ state: "visible", timeout: uiTimeoutMs }); await brokenRevision.getByText(fixture.missingChunkId, { exact: false }).waitFor({ state: "visible", @@ -234,43 +390,276 @@ async function main(): Promise { if ((await card.locator(".sls-repair-revision").count()) !== 2) { throw new Error("Verify and Repair did not render the winner and conflict revision separately."); } - - await brokenRevision.getByRole("button", { name: "Retry reading revision", exact: true }).click({ + for (const label of [ + /📦 DB: recorded/u, + /📁 Vault:/u, + /Δsize vs DB/u, + /🕒 DB /u, + /Δtime /u, + /⚠️ Differs from Vault/u, + ]) { + await winnerRevision.getByText(label).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + } + await brokenRevision.getByText(/decoded unavailable/u).waitFor({ + state: "visible", timeout: uiTimeoutMs, }); - await settings - .locator(".sls-repair-result") - .filter({ hasText: path }) - .locator(".sls-repair-revision") - .filter({ hasText: fixture.conflictRevision }) - .getByText(/Unreadable on this device/u) - .waitFor({ state: "visible", timeout: uiTimeoutMs }); + const winnerMenu = await openRevisionActionMenu(page, settings, fixture.winnerRevision); + for (const label of [ + "Compare with Vault", + "Apply this revision to Vault", + "Store Vault file as a child of this revision", + "Discard this branch", + ]) { + await winnerMenu.getByText(label, { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + } + if ( + (await winnerMenu + .getByText("Mark this revision as the Vault version", { + exact: true, + }) + .count()) !== 0 + ) { + throw new Error("A differing revision incorrectly offered to record an exact Vault match."); + } + await page.keyboard.press("Escape"); }); - const afterRetry = await readRevisionTree(cliBinary, session.cliEnv); - if (!afterRetry.conflictRevisions.includes(fixture.conflictRevision)) { - throw new Error(`Retry changed the revision tree: ${JSON.stringify(afterRetry)}`); - } - - const screenshot = await captureObsidianElement( + const repairCardScreenshot = await captureObsidianElement( session.remoteDebuggingPort, "revision-repair-unreadable-conflict.png", (page) => page.locator(".sls-repair-result").filter({ hasText: path }) ); + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const card = page.locator(".sls-repair-result").filter({ hasText: path }); + await card.evaluate((element) => { + const htmlElement = element as HTMLElement; + htmlElement.dataset.e2eOriginalStyle = htmlElement.getAttribute("style") ?? ""; + htmlElement.style.width = "360px"; + htmlElement.style.maxWidth = "100%"; + }); + const dimensions = await card.evaluate((element) => ({ + clientWidth: element.clientWidth, + scrollWidth: element.scrollWidth, + })); + if (dimensions.scrollWidth > dimensions.clientWidth + 1) { + throw new Error( + `Revision repair card overflowed at mobile width: ${JSON.stringify(dimensions)}` + ); + } + }); + const mobileWidthScreenshot = await captureObsidianElement( + session.remoteDebuggingPort, + "revision-repair-mobile-width.png", + (page) => page.locator(".sls-repair-result").filter({ hasText: path }) + ); + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const card = page.locator(".sls-repair-result").filter({ hasText: path }); + await card.evaluate((element) => { + const htmlElement = element as HTMLElement; + const originalStyle = htmlElement.dataset.e2eOriginalStyle ?? ""; + if (originalStyle.length > 0) { + htmlElement.setAttribute("style", originalStyle); + } else { + htmlElement.removeAttribute("style"); + } + delete htmlElement.dataset.e2eOriginalStyle; + }); + }); await withObsidianPage(session.remoteDebuggingPort, async (page) => { const settings = page.locator(".sls-setting"); - const brokenRevision = () => - settings - .locator(".sls-repair-result") - .filter({ hasText: path }) - .locator(".sls-repair-revision") - .filter({ hasText: fixture.conflictRevision }); - await brokenRevision() - .getByRole("button", { name: "Discard unreadable revision", exact: true }) + await openRevisionActionMenu(page, settings, fixture.winnerRevision); + }); + const readableMenuScreenshot = await captureObsidianElement( + session.remoteDebuggingPort, + "revision-repair-readable-actions.png", + (page) => page.locator(".menu:visible").last() + ); + + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + await page.keyboard.press("Escape"); + const settings = page.locator(".sls-setting"); + await selectRevisionAction(page, settings, fixture.winnerRevision, "Compare with Vault"); + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Vault and database revision", + }), + }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal.getByText(path, { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await modal.getByText(/Vault file:/u).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await modal.getByText(/Database revision:/u).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + const actions = modal.locator(".conflict-action-container"); + await actions.getByRole("button", { name: "Close", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + for (const action of ["Use Vault file", "Use Database revision", "Concat both", "Not now"]) { + if ((await actions.getByRole("button", { name: action, exact: true }).count()) !== 0) { + throw new Error(`Read-only comparison exposed the resolution action '${action}'.`); + } + } + }); + const comparisonScreenshot = await captureObsidianElement( + session.remoteDebuggingPort, + "revision-repair-read-only-comparison.png", + (page) => + page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Vault and database revision", + }), + }) + ); + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Vault and database revision", + }), + }); + await modal + .locator(".conflict-action-container") + .getByRole("button", { name: "Close", exact: true }) .click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + const beforeApply = await readRevisionTree(cliBinary, session.cliEnv); + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const settings = page.locator(".sls-setting"); + await selectRevisionAction(page, settings, fixture.winnerRevision, "Apply this revision to Vault"); const confirmation = page.locator(".modal-container").filter({ - has: page.locator(".modal-title").filter({ hasText: "Discard unreadable revision" }), + has: page.locator(".modal-title").filter({ + hasText: "Apply database revision to Vault", + }), + }); + await confirmation.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await confirmation.getByRole("button", { name: "Yes", exact: true }).click({ timeout: uiTimeoutMs }); + await revisionCard(settings, fixture.winnerRevision) + .getByText("✅ Matches Vault", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + const status = repairCard(settings).locator(".sls-repair-status"); + await status + .getByText("✅ Vault matches winner", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await status + .getByText("⚠️ Conflicts: 1", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + const matchedWinnerWithConflictScreenshot = await captureObsidianElement( + session.remoteDebuggingPort, + "revision-repair-winner-match-with-conflict.png", + (page) => page.locator(".sls-repair-result").filter({ hasText: path }) + ); + const afterApply = await readRevisionTree(cliBinary, session.cliEnv); + if (JSON.stringify(afterApply) !== JSON.stringify(beforeApply)) { + throw new Error( + `Applying a live revision to the Vault changed the revision tree: ${JSON.stringify({ + beforeApply, + afterApply, + })}` + ); + } + const vaultWinner = await readVaultWinnerState(cliBinary, session.cliEnv); + const appliedProvenance = await readFileReflectionProvenance(cliBinary, session.cliEnv); + if ( + !vaultWinner.matches || + vaultWinner.winnerRevision !== fixture.winnerRevision || + appliedProvenance?.revision !== fixture.winnerRevision + ) { + throw new Error( + `Applying the winner did not preserve exact Vault provenance: ${JSON.stringify({ + vaultWinner, + appliedProvenance, + fixture, + })}` + ); + } + + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const settings = page.locator(".sls-setting"); + const menu = await openRevisionActionMenu(page, settings, fixture.winnerRevision); + await menu + .getByText("Mark this revision as the Vault version", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await menu + .getByText("Discard this branch", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await menu + .getByText("Mark this revision as the Vault version", { exact: true }) + .click({ timeout: uiTimeoutMs }); + await revisionCard(settings, fixture.winnerRevision) + .getByText("✅ Matches Vault", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + const afterExactMark = await readRevisionTree(cliBinary, session.cliEnv); + const markedProvenance = await readFileReflectionProvenance(cliBinary, session.cliEnv); + if ( + JSON.stringify(afterExactMark) !== JSON.stringify(beforeApply) || + markedProvenance?.revision !== fixture.winnerRevision + ) { + throw new Error( + `Recording an exact Vault match changed the tree or lost provenance: ${JSON.stringify({ + beforeApply, + afterExactMark, + markedProvenance, + })}` + ); + } + + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const settings = page.locator(".sls-setting"); + const menu = await openRevisionActionMenu(page, settings, fixture.conflictRevision); + for (const label of [ + "Store Vault file as a child of this revision", + "Retry reading revision", + "Discard this branch", + ]) { + await menu.getByText(label, { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + } + }); + const unreadableMenuScreenshot = await captureObsidianElement( + session.remoteDebuggingPort, + "revision-repair-unreadable-actions-context.png", + (page) => page.locator("body") + ); + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + await page.keyboard.press("Escape"); + const settings = page.locator(".sls-setting"); + await selectRevisionAction(page, settings, fixture.conflictRevision, "Retry reading revision"); + await revisionCard(settings, fixture.conflictRevision) + .getByText(/🧩 Missing chunks:/u) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + + const afterRetry = await readRevisionTree(cliBinary, session.cliEnv); + if (JSON.stringify(afterRetry) !== JSON.stringify(beforeApply)) { + throw new Error(`Retry changed the revision tree: ${JSON.stringify(afterRetry)}`); + } + + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const settings = page.locator(".sls-setting"); + await selectRevisionAction(page, settings, fixture.conflictRevision, "Discard this branch"); + const confirmation = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Discard branch" }), }); await confirmation.waitFor({ state: "visible", timeout: uiTimeoutMs }); await confirmation.getByRole("button", { name: "No", exact: true }).click({ timeout: uiTimeoutMs }); @@ -278,36 +667,23 @@ async function main(): Promise { }); const afterCancellation = await readRevisionTree(cliBinary, session.cliEnv); - if (!afterCancellation.conflictRevisions.includes(fixture.conflictRevision)) { + if (JSON.stringify(afterCancellation) !== JSON.stringify(beforeApply)) { throw new Error(`Cancelling discard changed the revision tree: ${JSON.stringify(afterCancellation)}`); } await withObsidianPage(session.remoteDebuggingPort, async (page) => { const settings = page.locator(".sls-setting"); - const brokenRevision = settings - .locator(".sls-repair-result") - .filter({ hasText: path }) - .locator(".sls-repair-revision") - .filter({ hasText: fixture.conflictRevision }); - await brokenRevision - .getByRole("button", { name: "Discard unreadable revision", exact: true }) - .click({ timeout: uiTimeoutMs }); + await selectRevisionAction(page, settings, fixture.conflictRevision, "Discard this branch"); const confirmation = page.locator(".modal-container").filter({ - has: page.locator(".modal-title").filter({ hasText: "Discard unreadable revision" }), + has: page.locator(".modal-title").filter({ hasText: "Discard branch" }), }); await confirmation.waitFor({ state: "visible", timeout: uiTimeoutMs }); await confirmation.getByRole("button", { name: "Yes", exact: true }).click({ timeout: uiTimeoutMs }); - await settings - .locator(".sls-repair-revision") - .filter({ hasText: fixture.conflictRevision }) - .waitFor({ state: "hidden", timeout: uiTimeoutMs }); + await repairCard(settings).waitFor({ state: "hidden", timeout: uiTimeoutMs }); }); const afterDiscard = await readRevisionTree(cliBinary, session.cliEnv); - if ( - afterDiscard.winnerRevision !== fixture.winnerRevision || - afterDiscard.conflictRevisions.length !== 0 - ) { + if (afterDiscard.winnerRevision !== fixture.winnerRevision || afterDiscard.conflictRevisions.length !== 0) { throw new Error( `Explicit discard did not remove only the selected unreadable revision: ${JSON.stringify({ fixture, @@ -315,11 +691,31 @@ async function main(): Promise { })}` ); } + const finalVaultWinner = await readVaultWinnerState(cliBinary, session.cliEnv); + const finalProvenance = await readFileReflectionProvenance(cliBinary, session.cliEnv); + if ( + !finalVaultWinner.matches || + finalVaultWinner.winnerRevision !== fixture.winnerRevision || + finalProvenance?.revision !== fixture.winnerRevision + ) { + throw new Error( + `Discarding the unreadable branch disturbed the healthy Vault reflection: ${JSON.stringify({ + finalVaultWinner, + finalProvenance, + fixture, + })}` + ); + } console.log( - "Real Obsidian kept an unreadable conflict revision through automatic checking and retry, rendered every live revision separately, required confirmation, and discarded only the selected revision." + "Real Obsidian omitted a healthy logical deletion; rendered each live revision with compact actions and diagnostics; showed that the Vault matched the winner while one conflict remained; compared and applied an exact readable revision without changing the tree; preserved Vault provenance; kept an unreadable branch through automatic checking, retry, and cancelled discard; and discarded only the selected branch after confirmation." ); - console.log(`Repair screenshot: ${screenshot}`); + console.log(`Repair card screenshot: ${repairCardScreenshot}`); + console.log(`Mobile-width repair card screenshot: ${mobileWidthScreenshot}`); + console.log(`Readable revision actions screenshot: ${readableMenuScreenshot}`); + console.log(`Read-only comparison screenshot: ${comparisonScreenshot}`); + console.log(`Matching winner with conflict screenshot: ${matchedWinnerWithConflictScreenshot}`); + console.log(`Unreadable revision actions screenshot: ${unreadableMenuScreenshot}`); } finally { if (session) { await session.app.stop(); diff --git a/updates.md b/updates.md index 002bc6a8..048686cc 100644 --- a/updates.md +++ b/updates.md @@ -14,7 +14,7 @@ Earlier releases remain available in the 0.25 release history and the legacy rel ### Improved -- **Verify and repair all files** now reports the database winner, every conflict revision, missing chunks, and unavailable shared ancestors separately. It can retry an exact revision without changing the tree, while discarding an unreadable live revision requires explicit confirmation. +- **Verify and repair all files** now reports the database winner, every conflict revision, missing chunks, unavailable shared ancestors, and file-information differences separately. Compact, mobile-friendly indicators show missing chunks, `Δsize`, `Δtime`, a Vault which matches the winner, and conflict branches which still remain. Each live revision has an **…** menu for read-only text comparison, exact revision-to-Vault reflection, recording an exact match, storing the Vault file as a child of that branch, or explicitly discarding only that branch while another live branch remains. A logical deletion which already matches an absent Vault file is no longer reported. Retrying a revision does not change the tree, and discarding the sole unreadable live revision remains an explicitly confirmed recovery action. - Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands keep their identifiers, so hotkeys already assigned to them continue to work. The onboarding wizard can be reopened from **Self-hosted LiveSync settings** → **Setup**. - Text in setup and review dialogues can now be selected for copying or translation. - When LiveSync adopts an available interface translation on first start-up, it now continues initialisation and leaves a persistent Notice from which the translation details can be opened, instead of waiting for an unsolicited dialogue. From 127d460e18915533a1bd4685de0e5ee1d35dddb2 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sat, 25 Jul 2026 11:18:10 +0000 Subject: [PATCH 102/117] Improve conflict inspection and recovery workflow --- docs/recovery.md | 22 +++++++ docs/settings.md | 12 ++-- docs/specs_conflict_resolution.md | 4 +- docs/specs_garbage_collection.md | 2 +- docs/terms.md | 2 +- docs/troubleshooting.md | 2 +- .../messages/LiveSyncProvisionalMessages.ts | 9 +-- src/deps.ts | 1 + .../features/SettingDialogue/PaneHatch.ts | 66 +++++++++---------- styles.css | 8 +++ test/e2e-obsidian/README.md | 2 +- test/e2e-obsidian/scripts/dialog-mounts.ts | 27 +++++++- test/e2e-obsidian/scripts/revision-repair.ts | 23 ++++--- updates.md | 2 +- 14 files changed, 120 insertions(+), 62 deletions(-) diff --git a/docs/recovery.md b/docs/recovery.md index 1d98ce04..2a76b141 100644 --- a/docs/recovery.md +++ b/docs/recovery.md @@ -32,6 +32,28 @@ While suspended: The flag deliberately enables file logging, which may affect performance. Remove it after the emergency has been understood. +## Recover a conflicted or mismatched file + +Use this workflow when one file, or a small number of known files, has conflicts, missing chunks, or a difference between the current Vault file and the local LiveSync database. The inspection is device-local: it does not query a remote database or prove that another device has the same chunks. + +The `Hatch` recovery controls are ordered by escalation. Running **Recreate chunks for current Vault files** again with unchanged chunk settings and file contents produces the same chunks, and does not alter the revision tree. **Inspect conflicts and file/database differences** then provides actions for exact revisions. **Resolve All conflicted files by the newer one** is last because it applies a modification-time policy in bulk and logically deletes every other live version. + +1. Stop editing the affected file, pause replication on the participating devices, and keep a separate copy of every readable version. +2. If another device or backup has the intended content, preserve that copy before changing any revision. +3. If the current Vault file is readable, select **Recreate current chunks**. This can restore only chunks derived from the current Vault contents; it cannot reconstruct unique bytes from an unavailable historical or conflict revision. +4. Select **Inspect conflicts and file/database differences** → **Scan all files**. +5. Review the database winner, every conflict revision, and any unavailable shared ancestor separately. Revision identifiers, `Δsize`, `Δtime`, and chunk availability are diagnostic evidence; they do not decide which content is correct. +6. Use the wrench menu on the exact revision: + - **Compare with Vault** opens a read-only comparison for readable text. + - **Apply this revision to Vault** replaces the Vault file with that readable database revision. + - **Mark this revision as the Vault version** records an exact byte-for-byte match without creating a child revision. + - **Store Vault file as a child of this revision** preserves the current Vault bytes on that selected branch. + - **Retry reading revision** retries configured chunk retrieval without changing the revision tree. + - **Apply logical deletion to Vault**, **Discard this branch**, and **Discard unreadable revision** are destructive decisions. Use them only after preserving every version which may still be needed. +7. Synchronise the healthy source if chunks were restored, scan again, and confirm that the expected conflict or difference has disappeared before resuming ordinary editing. + +An absent Vault file and a logical-deletion winner already agree and do not require a repair card unless another live branch remains. If the scan reports many unrelated files, or the local database itself is incomplete or corrupt, stop the per-file workflow and use [Reset synchronisation on this device](#reset-synchronisation-on-this-device) from a trusted remote. If the central remote must instead be reconstructed from an authoritative Vault, use [Overwrite server data with this device's files](#overwrite-server-data-with-this-devices-files). + ## Reset synchronisation on this device Use this when the remote copy is trusted but this device's local LiveSync database is incomplete, corrupt, or no longer aligned with it. diff --git a/docs/settings.md b/docs/settings.md index 08e545a4..0f0003de 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -731,15 +731,15 @@ Stop reflecting database changes to storage files. Recreate chunks from files currently present in the Vault. This can repair missing chunks for those exact current contents after they have been confirmed as authoritative. It cannot reconstruct unavailable historical or conflict content. -#### Resolve All conflicted files by the newer one - -After confirmation, resolve every conflict by modification time. This logically deletes every version except the newest one. It is a destructive policy choice and cannot recover content which is already unavailable. - -#### Verify and repair all files +#### Inspect conflicts and file/database differences Compare each Vault file with every current live revision in the local database. Each winner and conflict revision is shown separately with its exact revision identifier, local chunk availability, and relationship to the current Vault file. Unavailable shared ancestors are reported separately because they prevent conservative three-way merging but are not live revisions which can be discarded. -`Retry reading revision` retries the configured chunk-retrieval path without changing the revision tree. `Discard unreadable revision` is offered only for an exact current live revision which remains unreadable; after confirmation, it creates a logical deletion for that revision. Prefer recovery from another replica or backup before discarding it. +Select **Scan all files** to run the inspection. Each reported file and live revision has a wrench menu for read-only comparison, applying an exact database revision to the Vault, recording an exact byte match, preserving the Vault file as a child of a selected branch, retrying chunk retrieval, or explicitly discarding a branch. Destructive actions require confirmation. Follow [Recover a conflicted or mismatched file](recovery.md#recover-a-conflicted-or-mismatched-file) before changing revision history. + +#### Resolve All conflicted files by the newer one + +After confirmation, resolve every conflict by modification time. This logically deletes every version except the newest one. It is a destructive policy choice and cannot recover content which is already unavailable. #### Check and convert non-path-obfuscated files diff --git a/docs/specs_conflict_resolution.md b/docs/specs_conflict_resolution.md index 25dfd012..e476f75c 100644 --- a/docs/specs_conflict_resolution.md +++ b/docs/specs_conflict_resolution.md @@ -50,9 +50,9 @@ The compatibility implementation currently selects the newer modification time f A document revision can remain in the PouchDB tree while one or more chunks needed to reconstruct its content are unavailable. Missing content is not evidence that the revision is obsolete. LiveSync therefore leaves an unreadable winner or conflict revision in the tree instead of deleting it during automatic conflict processing. -**Hatch** → **Verify and repair all files** inspects the current winner, every current conflict revision, and the nearest shared ancestor for each conflict. A logical-deletion winner and an absent Vault file already agree, so that state is not reported unless another live branch still requires attention. When the Vault already matches the winner but conflict branches remain, the card shows the compact status `✅ Vault matches winner · ⚠️ Conflicts: N`; matching the winner does not mean that the conflict has been resolved. +**Hatch** → **Inspect conflicts and file/database differences** inspects the current winner, every current conflict revision, and the nearest shared ancestor for each conflict. A logical-deletion winner and an absent Vault file already agree, so that state is not reported unless another live branch still requires attention. When the Vault already matches the winner but conflict branches remain, the card shows the compact status `✅ Vault matches winner · ⚠️ Conflicts: N`; matching the winner does not mean that the conflict has been resolved. -Each reported live revision has a compact **…** menu. The available actions depend on the exact revision and current Vault state: +Each reported live revision has a compact wrench menu. The available actions depend on the exact revision and current Vault state: - **Compare with Vault** opens the existing difference dialogue in read-only mode for differing text files. - **Apply this revision to Vault** writes the selected readable revision, even when it is not the database winner. Replacing an existing file requires confirmation. diff --git a/docs/specs_garbage_collection.md b/docs/specs_garbage_collection.md index 8a75f975..67ca4a30 100644 --- a/docs/specs_garbage_collection.md +++ b/docs/specs_garbage_collection.md @@ -47,7 +47,7 @@ Garbage Collection deliberately trades historical recoverability for storage. A Writing the same bytes again produces the same content-derived chunk identifier. If that chunk was collected previously, the normal chunk-writing path creates a new live revision for it, and ordinary replication can transfer it again. This does not recover an older file revision automatically; it only makes the newly written content available. -Garbage Collection does not reconstruct a chunk which is already missing, determine whether an unreadable revision is important, or repair a damaged local database. Use **Verify and repair all files**, another healthy replica, or a backup for those cases. Use **Overwrite Server Data with This Device's Files** only when a chosen Vault is authoritative and a deliberate remote rebuild is required. +Garbage Collection does not reconstruct a chunk which is already missing, determine whether an unreadable revision is important, or repair a damaged local database. Use **Inspect conflicts and file/database differences**, another healthy replica, or a backup for those cases. Use **Overwrite Server Data with This Device's Files** only when a chosen Vault is authoritative and a deliberate remote rebuild is required. ## Verification diff --git a/docs/terms.md b/docs/terms.md index 721e2c44..98cdfcbe 100644 --- a/docs/terms.md +++ b/docs/terms.md @@ -27,7 +27,7 @@ All guidelines and conventions listed below are disclosed and maintained solely - Boot-up sequence (boot-sequence) - The initialisation process of the plug-in when Obsidian starts. It starts with the loading of the plug-in, setting up core services, loading saved settings, and opening the local database. Once the layout is ready, the plug-in checks for the presence of flag files, runs configuration diagnostics, connects to the remote database, and begins file watching. The sequence finishes once the plug-in is fully ready and operational. - Broken files (Size mismatch) - - A state where a file's metadata and the actual content stored in its chunks do not match, causing file retrieval or synchronisation failures. These mismatches can be detected and resolved by running validation tools such as `Verify and repair all files` on the Hatch pane. + - A state where a file's metadata and the actual content stored in its chunks do not match, causing file retrieval or synchronisation failures. These mismatches can be inspected with `Inspect conflicts and file/database differences` on the Hatch pane, then handled one exact revision at a time. - Chunk / Chunks - Divided units of data stored in the database or object storage to facilitate efficient synchronisation. - Compaction diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 95434b6c..0b7c283e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -57,7 +57,7 @@ If the log reports missing chunks or a size mismatch: 2. restart Obsidian once to rule out an interrupted fetch; 3. synchronise a device or restore a backup which still has the correct content; 4. on that healthy device, run `Recreate chunks for current Vault files`, then synchronise; -5. run `Verify and repair all files` from `Hatch`; review the winner, every conflict revision, and any unavailable shared ancestor separately; use each revision's **…** menu to compare readable text, apply that exact revision to the Vault, store the Vault file as its child, or record an exact byte-for-byte match; and +5. follow [Recover a conflicted or mismatched file](recovery.md#recover-a-conflicted-or-mismatched-file); run `Inspect conflicts and file/database differences` from `Hatch`, then use each revision's wrench menu to review and act on that exact branch; and 6. use `Discard this branch` only after confirming that the exact live branch is no longer wanted. Use the separate `Discard unreadable revision` recovery action only when an unreadable revision is the sole live leaf. The repair card uses compact diagnostic rows which remain readable in a narrow mobile settings pane. `🧩 Missing chunks: N` marks an unreadable revision. In the database row, `Δsize` means decoded size minus recorded size; `Δsize vs DB` means Vault size minus decoded database size; and `Δtime` means Vault modification time minus database modification time. These are diagnostic values, not a rule for deciding which revision is correct. `✅ Vault matches winner · ⚠️ Conflicts: N` means that the current Vault bytes agree with the database winner while other live branches still need a decision. Every mutating action rechecks that its selected revision is still live. Applying a logical deletion to an existing Vault file requires confirmation; a logical-deletion winner with no Vault file already agrees and is omitted. diff --git a/src/common/messages/LiveSyncProvisionalMessages.ts b/src/common/messages/LiveSyncProvisionalMessages.ts index 6353e8d8..a9a652fc 100644 --- a/src/common/messages/LiveSyncProvisionalMessages.ts +++ b/src/common/messages/LiveSyncProvisionalMessages.ts @@ -140,10 +140,11 @@ export const liveSyncProvisionalEnglishMessages = { "Resolve every conflict by modification time? This logically deletes every version except the newest one and cannot recover content which is already unavailable.": "Resolve every conflict by modification time? This logically deletes every version except the newest one and cannot recover content which is already unavailable.", "Resolve all conflicts by the newest version": "Resolve all conflicts by the newest version", - "Verify and repair all files": "Verify and repair all files", - "Compare each Vault file with every live local-database revision. Unreadable conflict versions remain visible until you retry or explicitly discard an exact revision.": - "Compare each Vault file with every live local-database revision. Unreadable conflict versions remain visible until you retry or explicitly discard an exact revision.", - "Verify all": "Verify all", + "Inspect conflicts and file/database differences": + "Inspect conflicts and file/database differences", + "Scan every Vault file and live local-database revision for conflicts, missing chunks, and differences. Each result provides actions for the exact revision.": + "Scan every Vault file and live local-database revision for conflicts, missing chunks, and differences. Each result provides actions for the exact revision.", + "Scan all files": "Scan all files", "Connection settings": "Connection settings", "Saved connections": "Saved connections", } as const; diff --git a/src/deps.ts b/src/deps.ts index 92405d0c..0e4eb819 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -26,6 +26,7 @@ export { WorkspaceLeaf, Menu, request, + setIcon, getLanguage, requireApiVersion, ButtonComponent, diff --git a/src/modules/features/SettingDialogue/PaneHatch.ts b/src/modules/features/SettingDialogue/PaneHatch.ts index 63b5c79f..628ae9f6 100644 --- a/src/modules/features/SettingDialogue/PaneHatch.ts +++ b/src/modules/features/SettingDialogue/PaneHatch.ts @@ -10,7 +10,7 @@ import { import { createBlob, readAsBlob } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger"; import { shouldBeIgnored } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path"; -import { Menu, diff_match_patch } from "@/deps.ts"; +import { Menu, diff_match_patch, setIcon } from "@/deps.ts"; import { $msg } from "@/common/translation"; import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; @@ -142,7 +142,8 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, if (actions.length === 0) { return; } - this.createEl(parent, "button", { text: "…", cls: "sls-repair-action-menu" }, (button) => { + this.createEl(parent, "button", { cls: "sls-repair-action-menu" }, (button) => { + setIcon(button, "wrench"); button.setAttr("aria-label", label); button.setAttr("title", label); button.onClickEvent(() => { @@ -823,47 +824,20 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, }) ); new Setting(paneEl) - .setName("Resolve All conflicted files by the newer one") - .setDesc( - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one." - ) - .addButton((button) => - button - .setButtonText("Resolve All") - .setCta() - .onClick(async () => { - const confirmed = - (await this.core.confirm.askYesNoDialog( - $msg( - "Resolve every conflict by modification time? This logically deletes every version except the newest one and cannot recover content which is already unavailable." - ), - { - title: $msg("Resolve all conflicts by the newest version"), - defaultOption: "No", - } - )) === "yes"; - if (!confirmed) { - return; - } - await this.services.conflict.resolveAllConflictedFilesByNewerOnes(); - }) - ); - - new Setting(paneEl) - .setName($msg("Verify and repair all files")) + .setName($msg("Inspect conflicts and file/database differences")) .setDesc( $msg( - "Compare each Vault file with every live local-database revision. Unreadable conflict versions remain visible until you retry or explicitly discard an exact revision." + "Scan every Vault file and live local-database revision for conflicts, missing chunks, and differences. Each result provides actions for the exact revision." ) ) .addButton((button) => button - .setButtonText($msg("Verify all")) + .setButtonText($msg("Scan all files")) .setDisabled(false) .setCta() .onClick(async () => { resultArea.replaceChildren(); - Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify"); + Logger("Start inspecting file/database state", LOG_LEVEL_NOTICE, "verify"); this.core.localDatabase.clearCaches(); const allPaths = await collectFileDatabaseInfoPaths(this.core); let i = 0; @@ -920,6 +894,32 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, // Logger(`${i}/${files.length}\n`, LOG_LEVEL_NOTICE, "verify-processed"); }) ); + new Setting(paneEl) + .setName("Resolve All conflicted files by the newer one") + .setDesc( + "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one." + ) + .addButton((button) => + button + .setButtonText("Resolve All") + .setCta() + .onClick(async () => { + const confirmed = + (await this.core.confirm.askYesNoDialog( + $msg( + "Resolve every conflict by modification time? This logically deletes every version except the newest one and cannot recover content which is already unavailable." + ), + { + title: $msg("Resolve all conflicts by the newest version"), + defaultOption: "No", + } + )) === "yes"; + if (!confirmed) { + return; + } + await this.services.conflict.resolveAllConflictedFilesByNewerOnes(); + }) + ); new Setting(paneEl) .setName("Check and convert non-path-obfuscated files") .setDesc("") diff --git a/styles.css b/styles.css index 8c9ec551..7815af18 100644 --- a/styles.css +++ b/styles.css @@ -654,12 +654,20 @@ body.is-mobile .livesync-compatibility-review-notice { } .sls-repair-action-menu { + display: inline-flex; + align-items: center; + justify-content: center; min-width: var(--clickable-icon-size); width: var(--clickable-icon-size); height: var(--clickable-icon-size); padding: 0; } +.sls-repair-action-menu .svg-icon { + width: 18px; + height: 18px; +} + .sls-repair-revision { margin-top: var(--size-4-2); padding: var(--size-4-2); diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index 20717f69..2b43797b 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -161,7 +161,7 @@ This proves in real Obsidian the plug-in behaviour shared by supported platforms `test:e2e:obsidian:conflict-dialog-policy` creates three real local revision leaves without a remote service and opens the pairwise merge dialogue in Obsidian. It verifies the three-version count, requires the four decision buttons to be stacked vertically, concatenates the displayed pair as a child of the displayed winner, confirms that the untouched leaf remains as one conflict, postpones that remaining pair, restarts the same isolated Vault and profile, and confirms that only the two live versions are reconstructed. It also verifies that an ordinary repeated conflict check does not reopen a postponed dialogue, that **Resolve if conflicted.** explicitly reopens it, and that the active editor retains the appropriate unresolved-conflict warning. The scenario then invokes the same Commonlib consumer boundary used for an incoming replicated document and checks that a postponed warning disappears, an open stale dialogue closes, and the conflict-processing queue completes even when the dialogue closes immediately. This isolates the Obsidian UI contract from transport and second-device setup. The fixture owns one temporary Vault and profile, and the session runner stops Obsidian before removing them. -`test:e2e:obsidian:revision-repair` creates an ordinary healthy logical deletion and two conflicting live revisions in a temporary real Obsidian Vault, then removes a chunk used only by the non-winning revision. It proves that automatic conflict checking does not discard the unreadable branch, and that a healthy logical deletion with no Vault file is neither reported nor retained as Vault provenance. **Verify and repair all files** must show the winner and conflict separately, identify the exact unreadable revision and missing chunk, show the compact `Δsize` and `Δtime` diagnostics, and expose the appropriate `…` menu for each branch. The scenario opens the existing comparison dialogue in read-only mode, applies the readable winner to the Vault, shows the compact matching-winner and remaining-conflict status, records the exact winner as Vault provenance without creating a child, and confirms that retrying the unreadable branch leaves the revision tree unchanged. It then verifies both the cancellation path and the explicit confirmation path for discarding only that selected live branch, requires the winner and its Vault provenance to remain unchanged, and captures the repair card, a 360-pixel-wide reflow check, the matching-winner status, both revision menus, and the read-only comparison. The narrow capture checks responsive layout, not a mobile operating-system lifecycle. The scenario uses no remote service; a retry is therefore expected to remain unreadable unless the chunk is already available locally. +`test:e2e:obsidian:revision-repair` creates an ordinary healthy logical deletion and two conflicting live revisions in a temporary real Obsidian Vault, then removes a chunk used only by the non-winning revision. It proves that automatic conflict checking does not discard the unreadable branch, and that a healthy logical deletion with no Vault file is neither reported nor retained as Vault provenance. **Inspect conflicts and file/database differences** must show the winner and conflict separately, identify the exact unreadable revision and missing chunk, show the compact `Δsize` and `Δtime` diagnostics, and expose a wrench menu with the appropriate actions for each branch. The scenario opens the existing comparison dialogue in read-only mode, applies the readable winner to the Vault, shows the compact matching-winner and remaining-conflict status, records the exact winner as Vault provenance without creating a child, and confirms that retrying the unreadable branch leaves the revision tree unchanged. It then verifies both the cancellation path and the explicit confirmation path for discarding only that selected live branch, requires the winner and its Vault provenance to remain unchanged, and captures the repair card, a 360-pixel-wide reflow check, the matching-winner status, both revision menus, and the read-only comparison. The narrow capture checks responsive layout, not a mobile operating-system lifecycle. The scenario uses no remote service; a retry is therefore expected to remain unreadable unless the chunk is already available locally. `test:e2e:obsidian:hidden-file-snippet-sync` runs a two-vault hidden file round-trip. It verifies creation and deletion of a real `.obsidian/snippets/*.css` file, automatic JSON conflict merging for a hidden file with the merged result propagated by a second synchronisation, manual JSON Resolve dialogue application through Obsidian's UI, and per-device target patterns where one vault ignores a hidden file that the other vault synchronises. Initial enablement must open one user-visible progress Notice before the enabled setting is saved, then retain that Notice while its nested rebuild and scan phases continue in the ordinary log. The configured fixture starts with a current CouchDB remote profile, so migration from legacy remote settings remains the responsibility of the upgrade scenarios and cannot add unrelated Notices to this check. It also covers [issue #555](https://github.com/vrtmrz/obsidian-livesync/issues/555) by requiring several plug-in and settings changes to share one separate action Notice whose controls remain usable in mobile layouts; a manually dismissed group must not repeat its acknowledged rows when a later change arrives. diff --git a/test/e2e-obsidian/scripts/dialog-mounts.ts b/test/e2e-obsidian/scripts/dialog-mounts.ts index 99d565de..e8caf9e2 100644 --- a/test/e2e-obsidian/scripts/dialog-mounts.ts +++ b/test/e2e-obsidian/scripts/dialog-mounts.ts @@ -765,18 +765,39 @@ async function verifyHatchSurfacesAndSafeActions(): Promise { for (const label of [ "Write logs into the file", "Recreate chunks for current Vault files", - "Verify and repair all files", + "Inspect conflicts and file/database differences", + "Resolve All conflicted files by the newer one", ]) { await liveSyncSettings.locator(".setting-item-name", { hasText: label }).waitFor({ state: "visible", timeout: uiTimeoutMs, }); } + const settingNames = await liveSyncSettings.locator(".setting-item-name").allTextContents(); + const recreateIndex = settingNames.findIndex((name) => + name.includes("Recreate chunks for current Vault files") + ); + const inspectIndex = settingNames.findIndex((name) => + name.includes("Inspect conflicts and file/database differences") + ); + const resolveIndex = settingNames.findIndex((name) => + name.includes("Resolve All conflicted files by the newer one") + ); + if ( + recreateIndex === -1 || + inspectIndex === -1 || + resolveIndex === -1 || + !(recreateIndex < inspectIndex && inspectIndex < resolveIndex) + ) { + throw new Error( + "Recovery actions are not ordered from chunk recreation through inspection to bulk conflict resolution" + ); + } await liveSyncSettings.getByRole("button", { name: "Recreate current chunks", exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs, }); - await liveSyncSettings.getByRole("button", { name: "Verify all", exact: true }).waitFor({ + await liveSyncSettings.getByRole("button", { name: "Scan all files", exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs, }); @@ -897,7 +918,7 @@ async function verifyHatchSurfacesAndSafeActions(): Promise { await page .locator(".sls-setting:visible") .last() - .getByRole("button", { name: "Verify all", exact: true }) + .getByRole("button", { name: "Scan all files", exact: true }) .click({ timeout: uiTimeoutMs, }); diff --git a/test/e2e-obsidian/scripts/revision-repair.ts b/test/e2e-obsidian/scripts/revision-repair.ts index 7b5e642d..c6e851ca 100644 --- a/test/e2e-obsidian/scripts/revision-repair.ts +++ b/test/e2e-obsidian/scripts/revision-repair.ts @@ -230,12 +230,15 @@ function revisionCard(settings: Locator, revision: string): Locator { } async function openRevisionActionMenu(page: Page, settings: Locator, revision: string): Promise { - await revisionCard(settings, revision) - .getByRole("button", { - name: `More actions for revision ${revision}`, - exact: true, - }) - .click({ timeout: uiTimeoutMs }); + const actionButton = revisionCard(settings, revision).getByRole("button", { + name: `More actions for revision ${revision}`, + exact: true, + }); + await actionButton.locator("svg.lucide-wrench").waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await actionButton.click({ timeout: uiTimeoutMs }); const menu = page.locator(".menu:visible").last(); await menu.waitFor({ state: "visible", timeout: uiTimeoutMs }); const box = await menu.boundingBox(); @@ -366,16 +369,18 @@ async function main(): Promise { await settings.waitFor({ state: "visible", timeout: uiTimeoutMs }); await settings.locator('.sls-setting-menu-btn[title="Hatch"]').click({ timeout: uiTimeoutMs }); const verifySetting = settings.locator(".setting-item").filter({ - has: page.getByText("Verify and repair all files", { exact: true }), + has: page.getByText("Inspect conflicts and file/database differences", { + exact: true, + }), }); - await verifySetting.getByRole("button", { name: "Verify all", exact: true }).click({ + await verifySetting.getByRole("button", { name: "Scan all files", exact: true }).click({ timeout: uiTimeoutMs, }); const card = repairCard(settings); await card.waitFor({ state: "visible", timeout: uiTimeoutMs }); if ((await settings.locator(".sls-repair-result").filter({ hasText: healthyDeletedPath }).count()) !== 0) { throw new Error( - `Verify and Repair reported the healthy logical deletion ${healthyDeletedPath} (${healthyDeletionRevision}).` + `File/database inspection reported the healthy logical deletion ${healthyDeletedPath} (${healthyDeletionRevision}).` ); } const winnerRevision = revisionCard(settings, fixture.winnerRevision); diff --git a/updates.md b/updates.md index 048686cc..54d99fb5 100644 --- a/updates.md +++ b/updates.md @@ -14,7 +14,7 @@ Earlier releases remain available in the 0.25 release history and the legacy rel ### Improved -- **Verify and repair all files** now reports the database winner, every conflict revision, missing chunks, unavailable shared ancestors, and file-information differences separately. Compact, mobile-friendly indicators show missing chunks, `Δsize`, `Δtime`, a Vault which matches the winner, and conflict branches which still remain. Each live revision has an **…** menu for read-only text comparison, exact revision-to-Vault reflection, recording an exact match, storing the Vault file as a child of that branch, or explicitly discarding only that branch while another live branch remains. A logical deletion which already matches an absent Vault file is no longer reported. Retrying a revision does not change the tree, and discarding the sole unreadable live revision remains an explicitly confirmed recovery action. +- **Inspect conflicts and file/database differences** now reports the database winner, every conflict revision, missing chunks, unavailable shared ancestors, and file-information differences separately. Compact, mobile-friendly indicators show missing chunks, `Δsize`, `Δtime`, a Vault which matches the winner, and conflict branches which still remain. Each live revision has a wrench menu for read-only text comparison, exact revision-to-Vault reflection, recording an exact match, storing the Vault file as a child of that branch, or explicitly discarding only that branch while another live branch remains. A logical deletion which already matches an absent Vault file is no longer reported. Retrying a revision does not change the tree, and discarding the sole unreadable live revision remains an explicitly confirmed recovery action. - Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands keep their identifiers, so hotkeys already assigned to them continue to work. The onboarding wizard can be reopened from **Self-hosted LiveSync settings** → **Setup**. - Text in setup and review dialogues can now be selected for copying or translation. - When LiveSync adopts an available interface translation on first start-up, it now continues initialisation and leaves a persistent Notice from which the translation details can be opened, instead of waiting for an unsolicited dialogue. From ad762d4ddfae966a49be040ebdcba6d733eec72b Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 26 Jul 2026 08:05:39 +0000 Subject: [PATCH 103/117] Update Commonlib and Obsidian test-session packages --- package-lock.json | 24 ++++++++++++------------ package.json | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index e23e436a..f30c0c01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@smithy/querystring-builder": "^4.2.9", "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", - "@vrtmrz/livesync-commonlib": "0.1.0-rc.12", + "@vrtmrz/livesync-commonlib": "0.1.0-rc.14", "@vrtmrz/obsidian-plugin-kit": "0.1.2", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", @@ -56,7 +56,7 @@ "@types/transform-pouch": "^1.0.6", "@typescript-eslint/parser": "8.56.1", "@vitest/coverage-v8": "^4.1.8", - "@vrtmrz/obsidian-test-session": "0.2.5", + "@vrtmrz/obsidian-test-session": "0.2.6", "dotenv-cli": "^11.0.0", "esbuild": "0.28.1", "esbuild-plugin-inline-worker": "^0.1.1", @@ -4764,9 +4764,9 @@ } }, "node_modules/@vrtmrz/livesync-commonlib": { - "version": "0.1.0-rc.12", - "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.0-rc.12.tgz", - "integrity": "sha512-pMiOL4x5pDKCYOwtH3nG+9Ra1sLjowItBUrYoHpvcrweV4NSN0OkAEMk1vxBQlKMmKtnrAdy0WuQvmsdLOWHqA==", + "version": "0.1.0-rc.14", + "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.0-rc.14.tgz", + "integrity": "sha512-5aEy0x/aGNJjNdQYpQQ3VK386qlXZNInxq8pcsjf+NTV/X8jBQ+Q+omiRjYqn6BpS30xVWWeAZ/vdTeaiLsDVw==", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", @@ -4839,9 +4839,9 @@ } }, "node_modules/@vrtmrz/obsidian-test-session": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-test-session/-/obsidian-test-session-0.2.5.tgz", - "integrity": "sha512-ZsI+Yx3z6IEFfh5Ey5mEUBNI0SUD6oDhP7D9LSZVEqPmdJz2JMiag7d8u/p1i6KpsoI2HbDvtw4RHpB+BQReAw==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-test-session/-/obsidian-test-session-0.2.6.tgz", + "integrity": "sha512-7xDTmTW3igBxwQGgbbpoRZU0JvOBpGgdRT4qF2WVrKz03OtKMJdkfJILY04/HZ+n4tvn9Ze8phJBr0dx3wewcQ==", "dev": true, "license": "MIT", "engines": { @@ -5975,15 +5975,15 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/braces": { diff --git a/package.json b/package.json index 927ee1db..5de42ca4 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "@types/transform-pouch": "^1.0.6", "@typescript-eslint/parser": "8.56.1", "@vitest/coverage-v8": "^4.1.8", - "@vrtmrz/obsidian-test-session": "0.2.5", + "@vrtmrz/obsidian-test-session": "0.2.6", "dotenv-cli": "^11.0.0", "esbuild": "0.28.1", "esbuild-plugin-inline-worker": "^0.1.1", @@ -165,7 +165,7 @@ "@smithy/querystring-builder": "^4.2.9", "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", - "@vrtmrz/livesync-commonlib": "0.1.0-rc.12", + "@vrtmrz/livesync-commonlib": "0.1.0-rc.14", "@vrtmrz/obsidian-plugin-kit": "0.1.2", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", From 4194a0067ce7eb0e29bdde0086f9f45790310873 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 26 Jul 2026 08:06:08 +0000 Subject: [PATCH 104/117] Stabilise configured Real Obsidian workflows --- .../runner/liveSyncWorkflow.test.ts | 16 ++ test/e2e-obsidian/runner/liveSyncWorkflow.ts | 11 +- test/e2e-obsidian/runner/securitySeed.ts | 11 ++ .../scripts/cli-to-obsidian-sync.ts | 35 ++-- test/e2e-obsidian/scripts/minio-upload.ts | 24 +++ test/e2e-obsidian/scripts/review-harness.ts | 158 +++++++++++++++++- .../scripts/security-seed-reconnect.ts | 23 +++ 7 files changed, 256 insertions(+), 22 deletions(-) diff --git a/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts b/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts index c85d0163..85f64421 100644 --- a/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts +++ b/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts @@ -10,6 +10,7 @@ vi.mock("./cli.ts", () => ({ evalObsidianJson })); import { assertE2eCompatibilityMarker, createE2eCouchDbPluginData, + prepareRemote, waitForLiveSyncCoreReady, type CompatibilityMarkerState, } from "./liveSyncWorkflow.ts"; @@ -77,3 +78,18 @@ describe("Real Obsidian core readiness", () => { expect(evalObsidianJson).toHaveBeenCalledTimes(2); }); }); + +describe("remote fixture preparation", () => { + it("waits for the remote Security Seed after resolving a new remote", async () => { + evalObsidianJson.mockReset(); + evalObsidianJson.mockResolvedValueOnce({ status: "resolved", securitySeedReady: true }); + + await prepareRemote("obsidian-cli", {}); + + const evaluatedCode = String(evalObsidianJson.mock.calls[0]?.[1] ?? ""); + expect(evaluatedCode.indexOf("markRemoteResolved")).toBeLessThan( + evaluatedCode.indexOf("ensurePBKDF2Salt") + ); + expect(evaluatedCode).toContain("Timed out preparing the remote Security Seed"); + }); +}); diff --git a/test/e2e-obsidian/runner/liveSyncWorkflow.ts b/test/e2e-obsidian/runner/liveSyncWorkflow.ts index ca10fd62..896fb058 100644 --- a/test/e2e-obsidian/runner/liveSyncWorkflow.ts +++ b/test/e2e-obsidian/runner/liveSyncWorkflow.ts @@ -462,6 +462,7 @@ export function assertObsidianServiceContextContract(result: ObsidianServiceCont } export async function prepareRemote(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + const timeoutMs = Number(process.env.E2E_OBSIDIAN_REMOTE_PREPARE_TIMEOUT_MS ?? 20000); await evalObsidianJson( cliBinary, [ @@ -471,8 +472,16 @@ export async function prepareRemote(cliBinary: string, env: NodeJS.ProcessEnv): "const replicator=core.services.replicator.getActiveReplicator();", "await replicator.tryCreateRemoteDatabase(settings);", "await replicator.markRemoteResolved(settings);", + `const deadline=Date.now()+${JSON.stringify(timeoutMs)};`, + "let securitySeedReady=false;", + "do{", + "securitySeedReady=await replicator.ensurePBKDF2Salt(settings,false,false);", + "if(securitySeedReady) break;", + "await new Promise((resolve)=>setTimeout(resolve,250));", + "}while(Date.now() { cliBinary: obsidianCli.binary, vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: createE2eCouchDbPluginData( + { + uri: couchDb.uri, + username: couchDb.username, + password: couchDb.password, + dbName, + }, + { + encrypt: true, + passphrase: e2eePassphrase, + usePathObfuscation: true, + E2EEAlgorithm: "v2", + } + ), + localStorageEntries: createE2eObsidianDeviceLocalState(vault.name), }); await waitForLiveSyncCoreReady(obsidianCli.binary, session.cliEnv); - await configureCouchDb( - obsidianCli.binary, - session.cliEnv, - { - uri: couchDb.uri, - username: couchDb.username, - password: couchDb.password, - dbName, - }, - { - encrypt: true, - passphrase: e2eePassphrase, - usePathObfuscation: true, - E2EEAlgorithm: "v2", - } - ); - await waitForLiveSyncCoreReady(obsidianCli.binary, session.cliEnv); await prepareRemote(obsidianCli.binary, session.cliEnv); await pushLocalChanges(obsidianCli.binary, session.cliEnv); diff --git a/test/e2e-obsidian/scripts/minio-upload.ts b/test/e2e-obsidian/scripts/minio-upload.ts index 6af8b42a..b0899f12 100644 --- a/test/e2e-obsidian/scripts/minio-upload.ts +++ b/test/e2e-obsidian/scripts/minio-upload.ts @@ -1,8 +1,27 @@ +/** + * Verifies one complete Object Storage upload from a real Obsidian Vault, + * through LiveSync's local database and Journal Sync, to an S3-compatible + * service observed independently through the AWS SDK. + * + * The isolated Vault starts with Object Storage settings and the device-local + * compatibility acknowledgement already in place. Unconfigured start-up is + * intentionally inert and belongs to the onboarding scenario; compatibility + * review and visible setup have their own dedicated workflows. Supplying those + * prerequisites here keeps this scenario focused on the upload boundary. + * + * Note creation, local-database observation, one-shot synchronisation, request + * accounting, remote-object inspection, and prefix cleanup remain in one + * scenario so that a pass proves the same payload crossed every boundary. + * Separate successes would not prove that those observations belonged to the + * same upload. + */ import { evalObsidianJson } from "../runner/cli.ts"; import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; import { assertEqual, configureObjectStorage, + createE2eObjectStoragePluginData, + createE2eObsidianDeviceLocalState, prepareRemote, pushLocalChanges, waitForLiveSyncCoreReady, @@ -100,6 +119,11 @@ async function main(): Promise { cliBinary: cli.binary, vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: createE2eObjectStoragePluginData({ + ...objectStorage, + bucketPrefix, + }), + localStorageEntries: createE2eObsidianDeviceLocalState(vault.name), }); await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); diff --git a/test/e2e-obsidian/scripts/review-harness.ts b/test/e2e-obsidian/scripts/review-harness.ts index e080a45a..3c7fb96f 100644 --- a/test/e2e-obsidian/scripts/review-harness.ts +++ b/test/e2e-obsidian/scripts/review-harness.ts @@ -1,3 +1,5 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; import { assertLocatorHasMinimumTouchTarget, assertLocatorWithinSafeArea, @@ -6,11 +8,17 @@ import { import { CURRENT_SETTING_VERSION } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const"; import { REVIEW_HARNESS_STATE_KEY } from "../../../src/features/ReviewHarness/reviewHarnessController.ts"; import { REVIEW_HARNESS_FIXTURE_ROOT } from "../../../src/features/ReviewHarness/reviewHarnessVaultFixture.ts"; +import { evalObsidianJson } from "../runner/cli.ts"; import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; import { waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts"; import { iPhoneSafeArea, setObsidianMobileTestMode } from "../runner/mobileUi.ts"; import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; -import { captureObsidianDialogue, obsidianRemoteDebuggingPort, withObsidianPage } from "../runner/ui.ts"; +import { + captureObsidianDialogue, + captureObsidianPage, + obsidianRemoteDebuggingPort, + withObsidianPage, +} from "../runner/ui.ts"; import { createTemporaryVault } from "../runner/vault.ts"; const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_REVIEW_HARNESS_TIMEOUT_MS ?? 15000); @@ -26,6 +34,135 @@ type ReviewHarnessTestGlobal = typeof globalThis & { reviewHarnessCopiedReport?: string; }; +type ReviewHarnessReadinessSnapshot = { + coreAvailable: boolean; + databaseReady?: boolean; + appReady?: boolean; + configured?: boolean; + remoteType?: string; + settingVersion?: number; + suspended?: boolean; + unresolvedMessages: string[]; +}; + +const sensitiveDiagnosticLine = + /security seed|passphrase|password|credential|secret|access.?key|jwt.?key|authori[sz]ation|obsidian:\/\/setuplivesync|sls\+/iu; +const interruptedStartupMessages = [ + "No replicator has been activated or has not been initialised yet.", + "Self-hosted LiveSync cannot be initialised, exiting loading.", +]; + +function redactDiagnosticLine(line: string): string { + if (sensitiveDiagnosticLine.test(line)) return "[REDACTED SENSITIVE LOG LINE]"; + return line.replace(/\bhttps?:\/\/[^/\s:@]+:[^@\s/]+@/giu, "https://[REDACTED]@"); +} + +async function assertNoInterruptedStartupNotice(stage: string): Promise { + const notices = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.waitForTimeout(1500); + return await page.locator(".notice").allTextContents(); + }); + const interrupted = notices.filter((notice) => + interruptedStartupMessages.some((message) => notice.includes(message)) + ); + if (interrupted.length > 0) { + throw new Error(`LiveSync emitted an interrupted-startup Notice during ${stage}: ${interrupted.join(" | ")}`); + } + console.log(`No interrupted-startup Notice observed during ${stage}.`); +} + +async function captureReadinessFailure( + cliBinary: string, + session: ObsidianLiveSyncSession, + readinessError: unknown +): Promise { + const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e"; + await mkdir(outputDirectory, { recursive: true }); + + const captureErrors: string[] = []; + let screenshotPath: string | undefined; + try { + screenshotPath = await captureObsidianPage( + obsidianRemoteDebuggingPort(), + "review-harness-core-not-ready.png", + async () => undefined + ); + } catch (error) { + captureErrors.push(`screenshot: ${error instanceof Error ? error.message : String(error)}`); + } + + let readiness: ReviewHarnessReadinessSnapshot | undefined; + try { + readiness = await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync']?.core;", + "if(!core)return JSON.stringify({coreAvailable:false,unresolvedMessages:[]});", + "const settings=core.services.setting.currentSettings();", + "let unresolvedMessages=[];", + "try{", + "unresolvedMessages=(await core.services.appLifecycle.getUnresolvedMessages()).flat()", + ".filter((message)=>message!==undefined&&message!==null)", + ".map((message)=>String(message)).slice(-50);", + "}catch(error){unresolvedMessages=[`Could not inspect unresolved messages: ${String(error)}`];}", + "return JSON.stringify({", + "coreAvailable:true,", + "databaseReady:core.services.database.isDatabaseReady(),", + "appReady:core.services.appLifecycle.isReady(),", + "configured:settings?.isConfigured===true,", + "remoteType:settings?.remoteType??'',", + "settingVersion:settings?.settingVersion,", + "suspended:core.services.appLifecycle.isSuspended(),", + "unresolvedMessages,", + "});", + "})()", + ].join(""), + session.cliEnv + ); + readiness.unresolvedMessages = readiness.unresolvedMessages.map(redactDiagnosticLine); + } catch (error) { + captureErrors.push(`readiness snapshot: ${error instanceof Error ? error.message : String(error)}`); + } + + let recentLog: string[] = []; + try { + recentLog = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const opened = await page.evaluate( + (commandId) => + (globalThis as ReviewHarnessTestGlobal).app?.commands?.executeCommandById(commandId) === true, + "obsidian-livesync:view-log" + ); + if (!opened) throw new Error("The Show log command was not registered."); + const logPane = page.locator(".logpane"); + await logPane.waitFor({ state: "visible", timeout: 5000 }); + return (await logPane.locator(".log pre").allTextContents()).slice(-80).map(redactDiagnosticLine); + }); + } catch (error) { + captureErrors.push(`recent log: ${error instanceof Error ? error.message : String(error)}`); + } + + const resultPath = join(outputDirectory, "review-harness-core-not-ready.json"); + await writeFile( + resultPath, + `${JSON.stringify( + { + capturedAt: new Date().toISOString(), + failure: readinessError instanceof Error ? readinessError.message : String(readinessError), + screenshotPath, + readiness, + recentLog, + captureErrors, + }, + null, + 2 + )}\n`, + "utf8" + ); + if (screenshotPath) console.error(`Review Harness core readiness screenshot: ${screenshotPath}`); + console.error(`Review Harness core readiness diagnostics: ${resultPath}`); +} + async function openHarness(): Promise { const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { return await page.evaluate( @@ -203,6 +340,8 @@ async function copyAndReadReport(): Promise { async function verifyMobileHarness(): Promise { await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), true, uiTimeoutMs); await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const harness = page.locator('[data-testid="review-harness"]'); + if (await harness.isVisible()) return; await page.evaluate(async (viewType) => { const plugin = (globalThis as ReviewHarnessTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; if (typeof plugin !== "object" || plugin === null || !("core" in plugin)) { @@ -252,7 +391,7 @@ async function main(): Promise { vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), pluginData: { - doctorProcessedVersion: "0.25.27", + doctorProcessedVersion: "1.0.0", settingVersion: CURRENT_SETTING_VERSION, isConfigured: true, additionalSuffixOfDatabaseName: "", @@ -269,7 +408,20 @@ async function main(): Promise { periodicReplication: true, }, }); - await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); + await assertNoInterruptedStartupNotice("plug-in session start"); + try { + await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); + } catch (error) { + await captureReadinessFailure(cli.binary, session, error).catch((diagnosticError: unknown) => { + console.error( + `Could not capture Review Harness readiness diagnostics: ${ + diagnosticError instanceof Error ? diagnosticError.message : String(diagnosticError) + }` + ); + }); + throw error; + } + await assertNoInterruptedStartupNotice("core readiness"); await keepCompatibilityPaused(); await openHarness(); await waitForHarness(); diff --git a/test/e2e-obsidian/scripts/security-seed-reconnect.ts b/test/e2e-obsidian/scripts/security-seed-reconnect.ts index e0027a23..d75ffc21 100644 --- a/test/e2e-obsidian/scripts/security-seed-reconnect.ts +++ b/test/e2e-obsidian/scripts/security-seed-reconnect.ts @@ -1,3 +1,26 @@ +/** + * Provides release evidence for the Security Seed refresh behaviour shared by + * supported platforms in real Obsidian. It verifies that an already-open + * device keeps its deliberately stale cached Seed until replication, refreshes + * from the managed CouchDB fixture before encrypting, and never restores the + * old Seed to the remote synchronisation-parameter document. + * + * The scenario uses isolated Vaults, profiles, and a random database because + * settings, the local database, the renderer process, and CouchDB must all + * participate in the result. Device A is restarted with the same Vault and + * profile, while device B is fresh. The devices run sequentially after the + * same-process stale-cache assertion because desktop Obsidian may enforce a + * single application instance; running them concurrently would test launcher + * behaviour rather than LiveSync's shared plug-in implementation. + * + * Seed replacement, A-to-B decryption, B-to-A return synchronisation, final + * remote-document comparison, error-log inspection, screenshots, and strict + * teardown remain one scenario. Together they prove that the same replacement + * Seed was used across the complete encrypted round trip and was not later + * rolled back. Independent passing checks would not establish that continuity. + * The result records fingerprints only and does not claim to cover an + * iPadOS-specific background or reconnect lifecycle. + */ import { execFileSync } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; import { access, mkdir, readFile, writeFile } from "node:fs/promises"; From 51a749099d9d5c0fb53bd1dc4447a725f9825314 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 26 Jul 2026 08:06:48 +0000 Subject: [PATCH 105/117] Verify P2P pane in a fresh mobile session --- .../services/ObsidianAPIService.unit.spec.ts | 67 +++++ test/e2e-obsidian/README.md | 16 +- test/e2e-obsidian/runner/mobileUi.ts | 76 +++++- test/e2e-obsidian/runner/session.test.ts | 32 +++ test/e2e-obsidian/runner/session.ts | 11 +- test/e2e-obsidian/scripts/p2p-pane.ts | 254 +++++++++++++++--- 6 files changed, 404 insertions(+), 52 deletions(-) create mode 100644 src/modules/services/ObsidianAPIService.unit.spec.ts diff --git a/src/modules/services/ObsidianAPIService.unit.spec.ts b/src/modules/services/ObsidianAPIService.unit.spec.ts new file mode 100644 index 00000000..6523bfaf --- /dev/null +++ b/src/modules/services/ObsidianAPIService.unit.spec.ts @@ -0,0 +1,67 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + platform: { + isMobile: false, + }, +})); + +vi.mock("@/deps.ts", () => ({ + Platform: mocks.platform, + requestUrl: vi.fn(), +})); + +vi.mock("@/deps", () => ({ + Platform: mocks.platform, + requestUrl: vi.fn(), +})); + +vi.mock("@/modules/essentialObsidian/APILib/ObsHttpHandler", () => ({ + ObsHttpHandler: class {}, +})); + +vi.mock("./ObsidianConfirm", () => ({ + ObsidianConfirm: class {}, +})); + +import { ObsidianAPIService } from "./ObsidianAPIService"; +import type { ObsidianServiceContext } from "./ObsidianServiceContext"; + +function createService(workspace: Record, isMobile = false): ObsidianAPIService { + return new ObsidianAPIService({ + app: { workspace, isMobile }, + } as unknown as ObsidianServiceContext); +} + +beforeEach(() => { + mocks.platform.isMobile = false; + vi.clearAllMocks(); +}); + +describe("ObsidianAPIService.showWindowOnRight", () => { + it("keeps the status view in the right leaf on mobile", async () => { + mocks.platform.isMobile = true; + const rightLeaf = { + setViewState: vi.fn().mockResolvedValue(undefined), + }; + const workspace = { + getLeavesOfType: vi.fn(() => []), + getLeaf: vi.fn(), + getRightLeaf: vi.fn(() => rightLeaf), + revealLeaf: vi.fn().mockResolvedValue(undefined), + }; + const service = createService(workspace, true); + + expect(service.isMobile()).toBe(true); + await service.showWindowOnRight("p2p-status"); + + expect(workspace.getLeavesOfType).toHaveBeenCalledWith("p2p-status"); + expect(workspace.getRightLeaf).toHaveBeenCalledWith(false); + expect(workspace.getLeaf).not.toHaveBeenCalled(); + expect(rightLeaf.setViewState).toHaveBeenCalledWith({ + type: "p2p-status", + active: false, + }); + expect(workspace.revealLeaf).toHaveBeenCalledWith(rightLeaf); + }); +}); diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index 2b43797b..ea57edab 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -7,12 +7,12 @@ The generic application discovery, isolated-vault, plug-in installation, process The current smoke runner verifies the launch path and the loaded plug-in's Service Context composition: 1. create a temporary vault, -2. install the built Self-hosted LiveSync plug-in artifacts, +2. install the built Self-hosted LiveSync plug-in artefacts, 3. launch real Obsidian, 4. open the temporary vault through `obsidian-cli`, -5. enable Obsidian community plug-ins for the temporary app profile, -6. reload Self-hosted LiveSync through `obsidian-cli`, -7. verify through `obsidian-cli eval` that the plug-in is loaded, +5. prepare the isolated Vault trust state and handle any Obsidian trust prompt, +6. preserve natural plug-in loading, or complete requested pre-load work before loading the plug-in once in controlled start-up, +7. verify through the active renderer that the plug-in is loaded, 8. observe event and translation results from the actual `ObsidianServiceContext`, 9. verify that the Service Hub and every exposed service retain that exact Context, 10. optionally drive a real vault or CouchDB workflow through Obsidian's own API, and @@ -24,6 +24,8 @@ Obsidian 1.12 stores the global community plug-in switch outside `.obsidian/comm Future workflows should use `startObsidianLiveSyncSession()` from `runner/session.ts` rather than repeating the launch and plug-in readiness sequence. Add generic Obsidian bootstrap improvements to Fancy Kit; keep LiveSync behaviour and scenario helpers here. +When a LiveSync-owned scenario must establish application state before the plug-in's first load, pass an instance-scoped `lifecycle.beforePluginStart` callback through that wrapper. For example, the P2P pane scenario calls `setObsidianMobileTestModeBeforePluginStart()` there so LiveSync observes the mobile application state while registering its command and view. Mobile emulation reopens Obsidian's workspace layout; this helper waits for both the `is-mobile` body state and `workspace.layoutReady` before controlled loading continues. The shared package owns the controlled start-up order and guarantees that the plug-in loads once; the LiveSync scenario owns the resulting command, workspace placement, and visible UI assertions. Changing the state only after loading the plug-in is not evidence of its mobile start-up behaviour. + Each test vault uses an isolated Obsidian profile. The runner creates temporary directories for `HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`, `XDG_DATA_HOME`, and Electron `--user-data-dir`, writes the vault registry into those directories, pre-seeds the temporary Chromium local storage so community plug-ins are trusted for that generated vault ID, and passes the same environment to `obsidian-cli`. This is intended to keep real Obsidian E2E runs separate from a developer's daily Obsidian profile and vault registry. On macOS, `@vrtmrz/obsidian-test-session` keeps the generated Vault and profile below `/tmp` so Obsidian's Unix-domain CLI socket remains below the platform path limit. It also gives only the isolated Obsidian process Chromium's mock-keychain flag, preventing the empty test HOME from opening a blocking login-keychain dialogue. LiveSync's deterministic fixture selects the built-in default language so a host-language translation prompt cannot pause plug-in readiness. The case-only rename check enumerates the parent directory and compares exact spellings because an old-path lookup still resolves the renamed file on the default case-insensitive macOS filesystem. @@ -110,7 +112,7 @@ The mobile pass uses Obsidian's `app.emulateMobile(true)`, a 390 by 844 CSS-pixe `test:e2e:obsidian:review-harness` exercises only the boundaries owned by the opt-in maintainer Harness. It retains a real compatibility pause, uses the fixed Harness restart action to persist a device-local continuation and reload Obsidian, and requires the Harness to delete that state before reopening. It also runs the bounded local observations, confirms the dedicated Vault fixture root is removed, captures the copied privacy-bounded Markdown report, and checks the Harness layout and touch targets in mobile test mode. Compatibility explanation and persistence details remain owned by `settings-ui`, real P2P transfer remains owned by the dedicated P2P suites, and general Vault reflection remains owned by `vault-reflection`; the Harness test does not duplicate those workflows. -`test:e2e:obsidian:p2p-pane` starts one unconfigured CouchDB-only session and one configured P2P session. It proves that the command remains registered while the retired command, automatic pane, and unconfigured ribbon entry are absent. For the configured profile, it verifies that the ribbon and current status command reach the pane without opening it at start-up, checks its connection control and horizontal layout, and captures unobstructed desktop and mobile screenshots. It deliberately uses no relay or peer: replacement of the active replicator is covered by focused unit tests, the Deno and Compose CLI P2P lifecycle suite covers the headless transport, and `p2p-setup-uri-workflow` owns the visible transfer path between two real Obsidian sessions. +`test:e2e:obsidian:p2p-pane` starts one configured CouchDB-only session with no P2P profile and separate configured P2P sessions for desktop and mobile. It proves that the command remains registered while the retired command, automatic pane, and ribbon entry without a P2P configuration are absent. For the configured P2P profiles, it verifies that the desktop ribbon is available, the current status command reaches the pane without it opening at start-up, checks its connection control and horizontal layout, and captures unobstructed desktop and mobile screenshots. The mobile session uses a fresh Vault, profile, and Obsidian process, enters `app.emulateMobile(true)` through `lifecycle.beforePluginStart`, and requires the P2P view to belong to the right drawer rather than inheriting desktop workspace state. It deliberately uses no relay or peer: replacement of the active replicator is covered by focused unit tests, the Deno and Compose CLI P2P lifecycle suite covers the headless transport, and `p2p-setup-uri-workflow` owns the visible transfer path between two real Obsidian sessions. `test:e2e:obsidian:local-suite` builds the plug-in and, unless `LIVESYNC_CLI_COMMAND` selects an external CLI, the local LiveSync CLI. It then runs discovery, smoke, the onboarding invitation, Svelte dialogue mounting, revision repair, settings UI, the Review Harness, the P2P status pane, Vault reflection, CouchDB upload and manual setup, CLI-to-Obsidian synchronisation, Object Storage upload and Setup URI round-trip, P2P Setup URI round-trip, startup scan, provisioned CouchDB Setup URI, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export in sequence. Start the local CouchDB, MinIO, and P2P relay fixtures before running it, or use `test:e2e:obsidian:local-suite:services` to let the wrapper stop leftover fixtures, start fresh fixtures, and stop them again after the run. @@ -126,6 +128,8 @@ The two-Vault workflow performs the missing-marker review once for each isolated `test:e2e:obsidian:cli-to-obsidian-sync` is the cross-runtime compatibility check for the official LiveSync CLI and the real Obsidian plug-in. Build the plug-in first, and build the local CLI too when no external CLI command is selected. The script uses E2EE, Path Obfuscation, and the current preferred chunk settings to create and synchronise a note through the CLI, starts real Obsidian with an isolated Vault and profile, synchronises the same CouchDB database, and verifies that the plug-in materialises identical note content. This covers the boundary that CLI-only and plug-in-only round trips do not exercise. +The isolated Obsidian session starts with its CouchDB settings and device-local compatibility acknowledgement already in place. This keeps the scenario focused on cross-runtime data compatibility; unconfigured start-up and visible CouchDB onboarding are covered by their dedicated workflows. + By default, the compatibility check runs `node src/apps/cli/dist/index.cjs`. Set `LIVESYNC_CLI_COMMAND` to test another CLI build or distribution. The value may be a quoted command line or a JSON array of executable and prefix arguments; the scenario arguments are appended without going through a shell. For example, to test an executable on `PATH`: @@ -141,7 +145,7 @@ LIVESYNC_CLI_COMMAND="docker run --rm --network host --user $(id -u):$(id -g) -- npm run test:e2e:obsidian:cli-to-obsidian-sync ``` -`test:e2e:obsidian:minio-upload` reuses the Object Storage variables from `.test.env` or the process environment. It expects a reachable S3-compatible service, configures Self-hosted LiveSync for Object Storage through `obsidian-cli eval`, creates a note in real Obsidian, runs one-shot Journal Sync, and verifies through the AWS SDK that objects were written under a unique bucket prefix. Adapter tests separately observe an in-progress SDK command, while this real-runtime workflow verifies the resulting request counters advance and rebalance. +`test:e2e:obsidian:minio-upload` reuses the Object Storage variables from `.test.env` or the process environment. It expects a reachable S3-compatible service and starts with isolated Object Storage settings and the device-local compatibility acknowledgement already in place, keeping the scenario focused on upload rather than unconfigured start-up or setup. It confirms those settings through `obsidian-cli eval`, creates a note in real Obsidian, runs one-shot Journal Sync, and verifies through the AWS SDK that objects were written under a unique bucket prefix. Adapter tests separately observe an in-progress SDK command, while this real-runtime workflow verifies the resulting request counters advance and rebalance. `test:e2e:obsidian:object-storage-setup-uri-workflow` uses the public Commonlib-backed tool to generate the initial Setup URI for a unique MinIO prefix, completes visible initialisation on the first device, and then asks that working real Obsidian device to create a new Setup URI through the registered command. A second real Obsidian device imports only the device-generated URI. The workflow verifies A-to-B and B-to-A notes, captures the documented onboarding choices, and removes the Object Storage prefix only after both sessions have stopped. diff --git a/test/e2e-obsidian/runner/mobileUi.ts b/test/e2e-obsidian/runner/mobileUi.ts index 530f008c..d492092d 100644 --- a/test/e2e-obsidian/runner/mobileUi.ts +++ b/test/e2e-obsidian/runner/mobileUi.ts @@ -1,3 +1,5 @@ +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; import { assertLocatorHasMinimumTouchTarget, assertLocatorWithinSafeArea, @@ -12,13 +14,20 @@ export const desktopViewport = { width: 1024, height: 768 } as const; export const iPhoneSafeArea = { top: 47, right: 0, bottom: 34, left: 0 } as const; type ObsidianTestApp = { + isMobile?: boolean; emulateMobile?: (mobile: boolean) => void; plugins?: { plugins: Record }; + workspace?: { layoutReady?: boolean }; }; type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp }; -export async function setObsidianMobileTestMode(port: number, enabled: boolean, timeoutMs: number): Promise { +async function applyObsidianMobileTestMode( + port: number, + enabled: boolean, + timeoutMs: number, + waitForLiveSync: boolean +): Promise { await withObsidianPage(port, async (page) => { await page.setViewportSize(enabled ? mobileViewport : desktopViewport); await page.evaluate((nextEnabled) => { @@ -28,17 +37,49 @@ export async function setObsidianMobileTestMode(port: number, enabled: boolean, } obsidianApp.emulateMobile(nextEnabled); }, enabled); - await page.waitForFunction( - (nextEnabled) => { + // Obsidian reopens its workspace layout when platform emulation + // changes. Loading a controlled plug-in before that transition has + // completed can leave the plug-in enabled but absent from the active + // renderer. + try { + await page.waitForFunction( + ({ nextEnabled, waitForLiveSync }) => { + const obsidianApp = (globalThis as ObsidianTestGlobal).app; + return ( + document.body.classList.contains("is-mobile") === nextEnabled && + obsidianApp?.workspace?.layoutReady === true && + (!waitForLiveSync || obsidianApp?.plugins?.plugins["obsidian-livesync"] !== undefined) + ); + }, + { nextEnabled: enabled, waitForLiveSync }, + { timeout: timeoutMs } + ); + } catch (error) { + const state = await page.evaluate(() => { const obsidianApp = (globalThis as ObsidianTestGlobal).app; - return ( - document.body.classList.contains("is-mobile") === nextEnabled && - obsidianApp?.plugins?.plugins["obsidian-livesync"] !== undefined - ); - }, - enabled, - { timeout: timeoutMs } - ); + return { + appIsMobile: obsidianApp?.isMobile ?? null, + bodyClasses: document.body.className, + documentReadyState: document.readyState, + liveSyncLoaded: obsidianApp?.plugins?.plugins["obsidian-livesync"] !== undefined, + viewport: { width: window.innerWidth, height: window.innerHeight }, + workspaceLayoutReady: obsidianApp?.workspace?.layoutReady ?? null, + }; + }); + const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e"; + await mkdir(outputDirectory, { recursive: true }); + const screenshotPath = join( + outputDirectory, + waitForLiveSync + ? "mobile-mode-transition.failure.png" + : "mobile-mode-before-plugin-start.failure.png" + ); + await page.screenshot({ path: screenshotPath, fullPage: true }); + const detail = error instanceof Error ? error.message : String(error); + throw new Error( + `Obsidian mobile-mode transition did not settle: ${JSON.stringify(state)}; screenshot=${screenshotPath}; cause=${detail}` + ); + } await page.evaluate( (safeArea) => { for (const edge of ["top", "right", "bottom", "left"] as const) { @@ -52,6 +93,19 @@ export async function setObsidianMobileTestMode(port: number, enabled: boolean, }); } +/** Enters mobile emulation before LiveSync's first load in a controlled session. */ +export async function setObsidianMobileTestModeBeforePluginStart( + port: number, + enabled: boolean, + timeoutMs: number +): Promise { + await applyObsidianMobileTestMode(port, enabled, timeoutMs, false); +} + +export async function setObsidianMobileTestMode(port: number, enabled: boolean, timeoutMs: number): Promise { + await applyObsidianMobileTestMode(port, enabled, timeoutMs, true); +} + export async function assertMobileDialogueLayout(page: Page, container: Locator, label: string): Promise { const dialogue = container.locator(".modal").last(); const closeButton = dialogue.locator(".modal-close-button"); diff --git a/test/e2e-obsidian/runner/session.test.ts b/test/e2e-obsidian/runner/session.test.ts index c959ffd3..8977cb96 100644 --- a/test/e2e-obsidian/runner/session.test.ts +++ b/test/e2e-obsidian/runner/session.test.ts @@ -52,4 +52,36 @@ describe("LiveSync real-Obsidian session", () => { }) ); }); + + it("forwards instance-scoped lifecycle hooks and the selected plug-in start mode", async () => { + const beforePluginStart = vi.fn(async () => undefined); + const vault = { + path: "/tmp/mobile-vault", + statePath: "/tmp/mobile-state", + name: "mobile-vault", + id: "mobile-vault-id", + homePath: "/tmp/mobile-state/home", + xdgConfigPath: "/tmp/mobile-state/xdg-config", + xdgCachePath: "/tmp/mobile-state/xdg-cache", + xdgDataPath: "/tmp/mobile-state/xdg-data", + userDataPath: "/tmp/mobile-state/user-data", + processMarker: "/tmp/mobile-state", + dispose: vi.fn(async () => undefined), + }; + + await startObsidianLiveSyncSession({ + binary: "/Applications/Obsidian", + cliBinary: "obsidian-cli", + vault, + pluginStartup: "controlled", + lifecycle: { beforePluginStart }, + }); + + expect(startObsidianPluginSession).toHaveBeenCalledWith( + expect.objectContaining({ + lifecycle: { beforePluginStart }, + pluginStartup: "controlled", + }) + ); + }); }); diff --git a/test/e2e-obsidian/runner/session.ts b/test/e2e-obsidian/runner/session.ts index 8f617587..8347f5e3 100644 --- a/test/e2e-obsidian/runner/session.ts +++ b/test/e2e-obsidian/runner/session.ts @@ -1,4 +1,9 @@ -import { startObsidianPluginSession, type ObsidianPluginSession } from "@vrtmrz/obsidian-test-session"; +import { + startObsidianPluginSession, + type ObsidianPluginSession, + type ObsidianPluginSessionLifecycle, + type ObsidianPluginStartupMode, +} from "@vrtmrz/obsidian-test-session"; import type { TemporaryVault } from "./vault.ts"; export type ObsidianLiveSyncSession = ObsidianPluginSession; @@ -11,6 +16,8 @@ export type StartObsidianLiveSyncSessionOptions = { startupGraceMs?: number; pluginData?: Record; localStorageEntries?: Readonly>; + pluginStartup?: ObsidianPluginStartupMode; + lifecycle?: ObsidianPluginSessionLifecycle; env?: NodeJS.ProcessEnv; }; @@ -26,6 +33,8 @@ export async function startObsidianLiveSyncSession( startupGraceMs: options.startupGraceMs, pluginData: options.pluginData, localStorageEntries: options.localStorageEntries, + pluginStartup: options.pluginStartup, + lifecycle: options.lifecycle, env: options.env, }); } diff --git a/test/e2e-obsidian/scripts/p2p-pane.ts b/test/e2e-obsidian/scripts/p2p-pane.ts index 8baf00e3..fc57b6c8 100644 --- a/test/e2e-obsidian/scripts/p2p-pane.ts +++ b/test/e2e-obsidian/scripts/p2p-pane.ts @@ -1,46 +1,181 @@ +/** + * Verifies the complete user-visible contract of the P2P status pane in real + * Obsidian: a configured CouchDB-only Vault with no P2P profile is not + * presented with P2P controls, while configured P2P devices can deliberately + * open the current pane in the appropriate workspace area. + * + * Desktop and mobile use separate Vaults, profiles, and Obsidian processes. + * Mobile mode is enabled before LiveSync's first load so that command and view + * registration observe the mobile application state, and no desktop workspace + * state can make a misplaced or restored pane appear correct. + * + * Command registration, automatic-opening policy, ribbon availability, + * workspace ownership, layout, and screenshots are kept in one scenario + * because together they describe one navigation path. Checking them in + * isolation could miss a pane which is registered correctly but opens in the + * wrong area, or one which is visible only because another session restored it. + */ import { assertLocatorWithinViewport, assertNoHorizontalOverflow } from "@vrtmrz/obsidian-test-session"; import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.type"; import { upsertRemoteConfigurationInPlace } from "@vrtmrz/livesync-commonlib/remote-configurations"; -import type { Page } from "playwright"; +import type { ConsoleMessage, Page } from "playwright"; import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; import { createE2eCouchDbPluginData, createE2eObsidianDeviceLocalState, waitForLiveSyncCoreReady, } from "../runner/liveSyncWorkflow.ts"; -import { setObsidianMobileTestMode } from "../runner/mobileUi.ts"; +import { setObsidianMobileTestModeBeforePluginStart } from "../runner/mobileUi.ts"; import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; import { captureObsidianPage, obsidianRemoteDebuggingPort, withObsidianPage } from "../runner/ui.ts"; import { createTemporaryVault } from "../runner/vault.ts"; const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_P2P_PANE_TIMEOUT_MS ?? 10000); +type ObsidianTestLeaf = { + containerEl?: HTMLElement; + view?: { getViewType?: () => string }; +}; + +type ObsidianTestWorkspace = { + activeLeaf?: ObsidianTestLeaf; + getLeavesOfType?: (type: string) => ObsidianTestLeaf[]; + getRightLeaf?: (split: boolean) => ObsidianTestLeaf | null; + rightSplit?: { containerEl?: HTMLElement }; +}; + type ObsidianTestApp = { commands?: { commands?: Record; executeCommandById(commandId: string): boolean; }; + isMobile?: boolean; + plugins?: { + plugins?: Record< + string, + { + core?: { + services?: { + API?: { + isMobile?: () => boolean; + }; + }; + }; + } + >; + }; + workspace?: ObsidianTestWorkspace; }; type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp }; -async function openP2PStatusPane(): Promise { - const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { - return await page.evaluate( - (commandId) => (globalThis as ObsidianTestGlobal).app?.commands?.executeCommandById(commandId) === true, - "obsidian-livesync:open-p2p-server-status" - ); +async function openP2PStatusPane(page: Page) { + return await page.evaluate((commandId) => { + const app = (globalThis as ObsidianTestGlobal).app; + const plugin = app?.plugins?.plugins?.["obsidian-livesync"]; + return { + opened: app?.commands?.executeCommandById(commandId) === true, + appIsMobile: app?.isMobile ?? null, + apiIsMobile: plugin?.core?.services?.API?.isMobile?.() ?? null, + bodyIsMobile: document.body.classList.contains("is-mobile"), + }; + }, "obsidian-livesync:open-p2p-server-status"); +} + +async function collectP2PWorkspaceState(page: Page) { + return await page.evaluate(() => { + const workspace = (globalThis as ObsidianTestGlobal).app?.workspace; + const activeLeaf = workspace?.activeLeaf; + const p2pLeaves = workspace?.getLeavesOfType?.("p2p-server-status") ?? []; + const rightLeaf = workspace?.getRightLeaf?.(false); + return { + bodyClasses: document.body.className, + activeLeaf: { + type: activeLeaf?.view?.getViewType?.() ?? null, + visible: activeLeaf?.containerEl?.checkVisibility?.() ?? null, + classes: activeLeaf?.containerEl?.className ?? null, + }, + p2pLeaves: p2pLeaves.map((leaf) => ({ + type: leaf.view?.getViewType?.() ?? null, + visible: leaf.containerEl?.checkVisibility?.() ?? null, + classes: leaf.containerEl?.className ?? null, + })), + rightLeaf: { + type: rightLeaf?.view?.getViewType?.() ?? null, + visible: rightLeaf?.containerEl?.checkVisibility?.() ?? null, + classes: rightLeaf?.containerEl?.className ?? null, + }, + visibleP2PContents: document.querySelectorAll( + ".workspace-leaf-content[data-type='p2p-server-status']:not(.is-hidden)" + ).length, + }; }); - if (!opened) { - throw new Error("The P2P status command was not registered or could not be executed."); +} + +async function assertMobileP2PPlacement(page: Page): Promise { + const placement = await page.evaluate(() => { + const workspace = (globalThis as ObsidianTestGlobal).app?.workspace; + const p2pLeaves = workspace?.getLeavesOfType?.("p2p-server-status") ?? []; + const rightSplit = workspace?.rightSplit?.containerEl; + const rightLeaf = workspace?.getRightLeaf?.(false); + return { + p2pLeafCount: p2pLeaves.length, + inRightSplit: p2pLeaves.some( + (leaf) => + (rightSplit?.contains(leaf.containerEl ?? null) ?? false) || + (leaf.containerEl?.closest(".mod-right-split, .workspace-drawer.mod-right") ?? null) !== null + ), + rightLeafType: rightLeaf?.view?.getViewType?.() ?? null, + p2pLeafClasses: p2pLeaves.map((leaf) => leaf.containerEl?.className ?? null), + rightSplitClasses: rightSplit?.className ?? null, + }; + }); + if (!placement.inRightSplit) { + throw new Error(`The mobile P2P status view was not opened in the right leaf: ${JSON.stringify(placement)}`); } } async function verifyP2PStatusPane(filename: string, mobile: boolean): Promise { - await openP2PStatusPane(); return await captureObsidianPage(obsidianRemoteDebuggingPort(), filename, async (page) => { + const runtimeErrors: string[] = []; + const onPageError = (error: Error) => runtimeErrors.push(`pageerror: ${error.message}`); + const onConsole = (message: ConsoleMessage) => { + if (message.type() === "error") runtimeErrors.push(`console: ${message.text()}`); + }; + page.on("pageerror", onPageError); + page.on("console", onConsole); + let dispatchState: Awaited> | undefined; const heading = page.getByRole("heading", { name: "Signalling Status" }).last(); - await heading.waitFor({ state: "visible", timeout: uiTimeoutMs }); + try { + dispatchState = await openP2PStatusPane(page); + if (!dispatchState.opened) { + throw new Error("The P2P status command was not registered or could not be executed."); + } + if ( + mobile && + (dispatchState.appIsMobile !== true || + dispatchState.apiIsMobile !== true || + dispatchState.bodyIsMobile !== true) + ) { + throw new Error( + `The mobile P2P command did not observe a fully mobile application state: ${JSON.stringify(dispatchState)}` + ); + } + await heading.waitFor({ state: "visible", timeout: uiTimeoutMs }); + } catch (error) { + const workspaceState = await collectP2PWorkspaceState(page); + console.error( + `P2P command state after failed open: ${JSON.stringify({ dispatchState, runtimeErrors })}` + ); + console.error(`P2P workspace state after failed open: ${JSON.stringify(workspaceState)}`); + throw error; + } finally { + page.off("pageerror", onPageError); + page.off("console", onConsole); + } + if (mobile) { + await assertMobileP2PPlacement(page); + } const pane = heading.locator( "xpath=ancestor::*[contains(concat(' ', normalize-space(@class), ' '), ' workspace-leaf-content ')][1]" ); @@ -51,7 +186,14 @@ async function verifyP2PStatusPane(filename: string, mobile: boolean): Promise { throw new Error("The retired P2P pane command is still exposed."); } if ((await page.locator(".workspace-leaf-content[data-type='p2p-server-status']:visible").count()) !== 0) { - throw new Error("The P2P status pane opened automatically for an unconfigured CouchDB user."); + throw new Error("The P2P status pane opened automatically for a CouchDB user without P2P configured."); } if ((await page.locator(".livesync-ribbon-p2p-server-status").count()) !== 0) { throw new Error("The P2P ribbon icon was shown without a P2P configuration."); @@ -104,12 +246,43 @@ async function assertConfiguredP2PUIIsAvailable(): Promise { }); } +async function assertConfiguredP2PCommandIsAvailable(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const state = await page.evaluate(() => { + const app = (globalThis as ObsidianTestGlobal).app; + const commands = app?.commands?.commands ?? {}; + return { + commandRegistered: commands["obsidian-livesync:open-p2p-server-status"] !== undefined, + openPaneCount: app?.workspace?.getLeavesOfType?.("p2p-server-status").length ?? 0, + }; + }); + if (!state.commandRegistered) { + throw new Error("The configured P2P status command was not registered in mobile mode."); + } + if (state.openPaneCount !== 0) { + throw new Error("The configured P2P status pane opened before the mobile user requested it."); + } + }); +} + async function dismissOpenNotices(page: Page): Promise { const deadline = Date.now() + uiTimeoutMs; let quietSince = Date.now(); while (Date.now() < deadline) { - const notices = page.locator(".notice:visible"); - if ((await notices.count()) === 0) { + const dismissed = await page.evaluate(() => { + const notices = (Array.from(document.querySelectorAll(".notice")) as HTMLElement[]).filter( + (notice) => notice.checkVisibility?.() ?? notice.offsetParent !== null + ); + for (const notice of notices) { + const closeButton = notice.querySelector(".notice-close-button") as HTMLElement | null; + // Obsidian 1.12 does not render a separate close control for + // every Notice; clicking the Notice itself is its standard + // dismiss action. + (closeButton ?? notice).click(); + } + return notices.length; + }); + if (dismissed === 0) { if (Date.now() - quietSince >= 500) { return; } @@ -117,14 +290,7 @@ async function dismissOpenNotices(page: Page): Promise { continue; } quietSince = Date.now(); - const closeButton = notices.first().locator(".notice-close-button"); - if ((await closeButton.count()) > 0) { - await closeButton.click({ force: true, timeout: uiTimeoutMs }); - } else { - // Obsidian 1.12 does not render a separate close control for every - // Notice; clicking the Notice itself is its standard dismiss action. - await notices.first().click({ force: true, position: { x: 2, y: 2 }, timeout: uiTimeoutMs }); - } + await page.waitForTimeout(50); } throw new Error("Transient Obsidian notices did not become quiet before the P2P status screenshot."); } @@ -169,7 +335,8 @@ async function withP2PSession( binary: string, cliBinary: string, pluginData: Record, - verify: () => Promise + verify: () => Promise, + options: { mobileBeforePluginStart?: boolean } = {} ): Promise { const vault = await createTemporaryVault(); let session: ObsidianLiveSyncSession | undefined; @@ -181,6 +348,17 @@ async function withP2PSession( startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), pluginData, localStorageEntries: createE2eObsidianDeviceLocalState(vault.name), + lifecycle: options.mobileBeforePluginStart + ? { + beforePluginStart: async ({ remoteDebuggingPort }) => { + await setObsidianMobileTestModeBeforePluginStart( + remoteDebuggingPort, + true, + uiTimeoutMs + ); + }, + } + : undefined, }); await waitForLiveSyncCoreReady(cliBinary, session.cliEnv); await verify(); @@ -210,17 +388,25 @@ async function main(): Promise { async () => { await assertConfiguredP2PUIIsAvailable(); const desktopScreenshot = await verifyP2PStatusPane("p2p-status-pane.png", false); - await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), true, uiTimeoutMs); - try { - const mobileScreenshot = await verifyP2PStatusPane("p2p-status-pane-mobile.png", true); - console.log( - `Configured P2P status UI remained opt-in and was reachable on desktop and mobile. Screenshots: ${desktopScreenshot}, ${mobileScreenshot}` - ); - } finally { - await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), false, uiTimeoutMs); - } + console.log( + `Configured P2P status UI remained opt-in and was reachable on desktop. Screenshot: ${desktopScreenshot}` + ); } ); + + await withP2PSession( + binary, + cli.binary, + createConfiguredP2PPluginData(), + async () => { + await assertConfiguredP2PCommandIsAvailable(); + const mobileScreenshot = await verifyP2PStatusPane("p2p-status-pane-mobile.png", true); + console.log( + `Configured P2P status UI remained opt-in and was reachable on mobile. Screenshot: ${mobileScreenshot}` + ); + }, + { mobileBeforePluginStart: true } + ); } main().catch((error: unknown) => { From 12061b7bf3d3a8c3cc723eb822dc3f12b21c61be Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 26 Jul 2026 08:28:44 +0000 Subject: [PATCH 106/117] Reconcile beta.4 metadata for the next preview --- updates.md | 18 +++++++++++++++++- versions.json | 3 ++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/updates.md b/updates.md index 54d99fb5..2ef4928e 100644 --- a/updates.md +++ b/updates.md @@ -14,7 +14,23 @@ Earlier releases remain available in the 0.25 release history and the legacy rel ### Improved -- **Inspect conflicts and file/database differences** now reports the database winner, every conflict revision, missing chunks, unavailable shared ancestors, and file-information differences separately. Compact, mobile-friendly indicators show missing chunks, `Δsize`, `Δtime`, a Vault which matches the winner, and conflict branches which still remain. Each live revision has a wrench menu for read-only text comparison, exact revision-to-Vault reflection, recording an exact match, storing the Vault file as a child of that branch, or explicitly discarding only that branch while another live branch remains. A logical deletion which already matches an absent Vault file is no longer reported. Retrying a revision does not change the tree, and discarding the sole unreadable live revision remains an explicitly confirmed recovery action. +- **Inspect conflicts and file/database differences** now compares the current Vault file with every live database revision. Compact, mobile-friendly indicators show missing chunks, `Δsize`, `Δtime`, whether the Vault matches the winner, and whether conflict branches remain. Each revision's wrench menu can compare readable text, reflect that exact revision to the Vault, record an exact byte match, store the Vault content as a child of that branch, or discard only that branch. + +### Fixed + +- A winning logical deletion is no longer reported as a missing Vault file when the file is already absent. + +### Testing + +- Strengthened Real Obsidian coverage for start-up with an existing configuration, Security Seed readiness, failure diagnostics, and P2P status pane placement in separate desktop and mobile sessions. + +## 1.0.0-beta.4 + +25th July, 2026 + +### Improved + +- **Verify and repair all files** now reports the database winner, every conflict revision, missing chunks, and unavailable shared ancestors separately. It can retry an exact revision without changing the tree, while discarding an unreadable live revision requires explicit confirmation. - Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands keep their identifiers, so hotkeys already assigned to them continue to work. The onboarding wizard can be reopened from **Self-hosted LiveSync settings** → **Setup**. - Text in setup and review dialogues can now be selected for copying or translation. - When LiveSync adopts an available interface translation on first start-up, it now continues initialisation and leaves a persistent Notice from which the translation details can be opened, instead of waiting for an unsolicited dialogue. diff --git a/versions.json b/versions.json index a4585f26..5f4652c9 100644 --- a/versions.json +++ b/versions.json @@ -9,5 +9,6 @@ "1.0.0-beta.0": "1.7.2", "1.0.0-beta.1": "1.7.2", "1.0.0-beta.2": "1.7.2", - "1.0.0-beta.3": "1.7.2" + "1.0.0-beta.3": "1.7.2", + "1.0.0-beta.4": "1.7.2" } From da4b188b38bbf8731da172cb53b3f343f7493414 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 26 Jul 2026 12:44:36 +0000 Subject: [PATCH 107/117] Clarify the file inspection action --- docs/recovery.md | 2 +- docs/settings.md | 2 +- src/common/messages/LiveSyncProvisionalMessages.ts | 2 +- src/modules/features/SettingDialogue/PaneHatch.ts | 2 +- test/e2e-obsidian/scripts/dialog-mounts.ts | 4 ++-- test/e2e-obsidian/scripts/revision-repair.ts | 2 +- updates.md | 3 +++ 7 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/recovery.md b/docs/recovery.md index 2a76b141..cb23d6fe 100644 --- a/docs/recovery.md +++ b/docs/recovery.md @@ -41,7 +41,7 @@ The `Hatch` recovery controls are ordered by escalation. Running **Recreate chun 1. Stop editing the affected file, pause replication on the participating devices, and keep a separate copy of every readable version. 2. If another device or backup has the intended content, preserve that copy before changing any revision. 3. If the current Vault file is readable, select **Recreate current chunks**. This can restore only chunks derived from the current Vault contents; it cannot reconstruct unique bytes from an unavailable historical or conflict revision. -4. Select **Inspect conflicts and file/database differences** → **Scan all files**. +4. Select **Inspect conflicts and file/database differences** → **Begin inspection**. 5. Review the database winner, every conflict revision, and any unavailable shared ancestor separately. Revision identifiers, `Δsize`, `Δtime`, and chunk availability are diagnostic evidence; they do not decide which content is correct. 6. Use the wrench menu on the exact revision: - **Compare with Vault** opens a read-only comparison for readable text. diff --git a/docs/settings.md b/docs/settings.md index 0f0003de..1ac07517 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -735,7 +735,7 @@ Recreate chunks from files currently present in the Vault. This can repair missi Compare each Vault file with every current live revision in the local database. Each winner and conflict revision is shown separately with its exact revision identifier, local chunk availability, and relationship to the current Vault file. Unavailable shared ancestors are reported separately because they prevent conservative three-way merging but are not live revisions which can be discarded. -Select **Scan all files** to run the inspection. Each reported file and live revision has a wrench menu for read-only comparison, applying an exact database revision to the Vault, recording an exact byte match, preserving the Vault file as a child of a selected branch, retrying chunk retrieval, or explicitly discarding a branch. Destructive actions require confirmation. Follow [Recover a conflicted or mismatched file](recovery.md#recover-a-conflicted-or-mismatched-file) before changing revision history. +Select **Begin inspection** to run the inspection. Each reported file and live revision has a wrench menu for read-only comparison, applying an exact database revision to the Vault, recording an exact byte match, preserving the Vault file as a child of a selected branch, retrying chunk retrieval, or explicitly discarding a branch. Destructive actions require confirmation. Follow [Recover a conflicted or mismatched file](recovery.md#recover-a-conflicted-or-mismatched-file) before changing revision history. #### Resolve All conflicted files by the newer one diff --git a/src/common/messages/LiveSyncProvisionalMessages.ts b/src/common/messages/LiveSyncProvisionalMessages.ts index a9a652fc..d9a4f5b2 100644 --- a/src/common/messages/LiveSyncProvisionalMessages.ts +++ b/src/common/messages/LiveSyncProvisionalMessages.ts @@ -144,7 +144,7 @@ export const liveSyncProvisionalEnglishMessages = { "Inspect conflicts and file/database differences", "Scan every Vault file and live local-database revision for conflicts, missing chunks, and differences. Each result provides actions for the exact revision.": "Scan every Vault file and live local-database revision for conflicts, missing chunks, and differences. Each result provides actions for the exact revision.", - "Scan all files": "Scan all files", + "Begin inspection": "Begin inspection", "Connection settings": "Connection settings", "Saved connections": "Saved connections", } as const; diff --git a/src/modules/features/SettingDialogue/PaneHatch.ts b/src/modules/features/SettingDialogue/PaneHatch.ts index 628ae9f6..45c51286 100644 --- a/src/modules/features/SettingDialogue/PaneHatch.ts +++ b/src/modules/features/SettingDialogue/PaneHatch.ts @@ -832,7 +832,7 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, ) .addButton((button) => button - .setButtonText($msg("Scan all files")) + .setButtonText($msg("Begin inspection")) .setDisabled(false) .setCta() .onClick(async () => { diff --git a/test/e2e-obsidian/scripts/dialog-mounts.ts b/test/e2e-obsidian/scripts/dialog-mounts.ts index e8caf9e2..86af117a 100644 --- a/test/e2e-obsidian/scripts/dialog-mounts.ts +++ b/test/e2e-obsidian/scripts/dialog-mounts.ts @@ -797,7 +797,7 @@ async function verifyHatchSurfacesAndSafeActions(): Promise { state: "visible", timeout: uiTimeoutMs, }); - await liveSyncSettings.getByRole("button", { name: "Scan all files", exact: true }).waitFor({ + await liveSyncSettings.getByRole("button", { name: "Begin inspection", exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs, }); @@ -918,7 +918,7 @@ async function verifyHatchSurfacesAndSafeActions(): Promise { await page .locator(".sls-setting:visible") .last() - .getByRole("button", { name: "Scan all files", exact: true }) + .getByRole("button", { name: "Begin inspection", exact: true }) .click({ timeout: uiTimeoutMs, }); diff --git a/test/e2e-obsidian/scripts/revision-repair.ts b/test/e2e-obsidian/scripts/revision-repair.ts index c6e851ca..8dbab659 100644 --- a/test/e2e-obsidian/scripts/revision-repair.ts +++ b/test/e2e-obsidian/scripts/revision-repair.ts @@ -373,7 +373,7 @@ async function main(): Promise { exact: true, }), }); - await verifySetting.getByRole("button", { name: "Scan all files", exact: true }).click({ + await verifySetting.getByRole("button", { name: "Begin inspection", exact: true }).click({ timeout: uiTimeoutMs, }); const card = repairCard(settings); diff --git a/updates.md b/updates.md index 2ef4928e..57ab3654 100644 --- a/updates.md +++ b/updates.md @@ -14,14 +14,17 @@ Earlier releases remain available in the 0.25 release history and the legacy rel ### Improved +- The Hatch action for **Inspect conflicts and file/database differences** is now labelled **Begin inspection** so that its purpose is clear without repeating the scope already stated by the setting. - **Inspect conflicts and file/database differences** now compares the current Vault file with every live database revision. Compact, mobile-friendly indicators show missing chunks, `Δsize`, `Δtime`, whether the Vault matches the winner, and whether conflict branches remain. Each revision's wrench menu can compare readable text, reflect that exact revision to the Vault, record an exact byte match, store the Vault content as a child of that branch, or discard only that branch. ### Fixed +- Start-up file scans now omit legacy LiveSync log files and flag files before comparing Vault and local-database state. Existing ignored database records remain untouched and no longer produce misleading restore or deletion warnings. - A winning logical deletion is no longer reported as a missing Vault file when the file is already absent. ### Testing +- Added Commonlib collection regressions for built-in ignored files, and updated the Real Obsidian inspection and revision-repair scenarios for the clearer action label. - Strengthened Real Obsidian coverage for start-up with an existing configuration, Security Seed readiness, failure diagnostics, and P2P status pane placement in separate desktop and mobile sessions. ## 1.0.0-beta.4 From 18e4d16c6fb0a22cbc88ac6a84ac0e3c55f154c8 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 26 Jul 2026 16:15:46 +0000 Subject: [PATCH 108/117] Use the stable Commonlib package --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f30c0c01..e6efdabf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@smithy/querystring-builder": "^4.2.9", "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", - "@vrtmrz/livesync-commonlib": "0.1.0-rc.14", + "@vrtmrz/livesync-commonlib": "0.1.0", "@vrtmrz/obsidian-plugin-kit": "0.1.2", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", @@ -4764,9 +4764,9 @@ } }, "node_modules/@vrtmrz/livesync-commonlib": { - "version": "0.1.0-rc.14", - "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.0-rc.14.tgz", - "integrity": "sha512-5aEy0x/aGNJjNdQYpQQ3VK386qlXZNInxq8pcsjf+NTV/X8jBQ+Q+omiRjYqn6BpS30xVWWeAZ/vdTeaiLsDVw==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.0.tgz", + "integrity": "sha512-rdzEubzLStioanE67pps2XSGZ2UGyMIyeEoKsdPztQLLW8wM05PWApOPbJujIydHcDr2hFanbDFe89Lk7Mn4XQ==", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", diff --git a/package.json b/package.json index 5de42ca4..a231db66 100644 --- a/package.json +++ b/package.json @@ -165,7 +165,7 @@ "@smithy/querystring-builder": "^4.2.9", "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", - "@vrtmrz/livesync-commonlib": "0.1.0-rc.14", + "@vrtmrz/livesync-commonlib": "0.1.0", "@vrtmrz/obsidian-plugin-kit": "0.1.2", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", From 4f01e7f687d4059690cdc93f5aecfd6c8e5dcbed Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 26 Jul 2026 16:16:26 +0000 Subject: [PATCH 109/117] Keep startup scan on a configured Vault --- test/e2e-obsidian/README.md | 2 +- test/e2e-obsidian/scripts/startup-scan.ts | 34 ++++++++++++++++------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index ea57edab..e8a749b8 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -151,7 +151,7 @@ LIVESYNC_CLI_COMMAND="docker run --rm --network host --user $(id -u):$(id -g) -- `test:e2e:obsidian:p2p-setup-uri-workflow` runs two concurrent isolated real Obsidian sessions against the local Compose Nostr relay fixture. The first device imports a generated initial Setup URI and completes its signalling test with zero peers, creates a Setup URI for the second device through the registered command, and remains online while the second device imports it. The second device must select the expected online source before Fetch can rebuild its local database. The workflow accepts each connection request visibly on the receiving device, verifies the initial A-to-B fetch, checks that the menu for the three persistent per-peer actions remains within the viewport, reconnects both P2P sessions in join order, and verifies the B-to-A return journey. Every started session remains tracked until teardown completes. -`test:e2e:obsidian:startup-scan` configures a temporary CouchDB database, stops Obsidian, writes a note directly into the vault, restarts Obsidian, and verifies from CouchDB that the boot-time scan picked up the offline file. +`test:e2e:obsidian:startup-scan` starts from a CouchDB fixture using current settings with its device-local compatibility marker already acknowledged, stops Obsidian, writes a note directly into the Vault, restarts the same isolated Vault and profile without rewriting its plug-in data, and verifies from CouchDB that the start-up scan picked up the offline file. Onboarding remains covered by `onboarding-invitation`; this scenario owns the ordinary configured restart and start-up scan. `test:e2e:obsidian:setup-uri-workflow` runs the repository's public Commonlib-backed CouchDB provisioning and Setup URI tools against the local CouchDB fixture. It configures a new, empty Vault in the first real Obsidian session through the visible onboarding wizard and uses Rebuild. After that device is working, it generates a new Setup URI through the registered command; the second real Obsidian Vault uses that URI for Fetch instead of reusing the initial Setup URI produced by the provisioning tool. The workflow verifies ordinary notes from the first device to the second and back again, independently enables Hidden File Sync on each device, and verifies a snippet. The retained Setup URI screenshots show only encrypted URIs and visually masked Setup URI passphrases; plaintext credentials are not captured. Files prefixed with `guide-` capture the relevant dialogue, settings panel, or workspace leaf without transient Notices. Public documentation copies selected images only after visual inspection; the E2E run does not overwrite repository documentation assets. diff --git a/test/e2e-obsidian/scripts/startup-scan.ts b/test/e2e-obsidian/scripts/startup-scan.ts index 9aaa4de0..432145a3 100644 --- a/test/e2e-obsidian/scripts/startup-scan.ts +++ b/test/e2e-obsidian/scripts/startup-scan.ts @@ -1,3 +1,13 @@ +/** + * Proves that a configured LiveSync Vault scans files created while Obsidian + * was stopped. The first launch receives a CouchDB profile using current + * settings and its acknowledged device-local compatibility marker before the + * plug-in loads. + * + * The second launch reuses the same Vault, profile, local database, and + * settings without rewriting plug-in data, so the assertion covers an + * ordinary configured restart rather than the separate onboarding flow. + */ import { mkdir, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { @@ -11,7 +21,8 @@ import { import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; import { assertEqual, - configureCouchDb, + createE2eCouchDbPluginData, + createE2eObsidianDeviceLocalState, prepareRemote, pushLocalChanges, waitForLiveSyncCoreReady, @@ -47,6 +58,12 @@ async function main(): Promise { const couchDb = await loadCouchDbConfig(); const dbName = makeUniqueDatabaseName(couchDb.dbPrefix, "startup-scan"); + const couchDbSettings = { + uri: couchDb.uri, + username: couchDb.username, + password: couchDb.password, + dbName, + }; const vault = await createTemporaryVault(); let session: ObsidianLiveSyncSession | undefined; @@ -63,15 +80,11 @@ async function main(): Promise { cliBinary: cli.binary, vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: createE2eCouchDbPluginData(couchDbSettings), + localStorageEntries: createE2eObsidianDeviceLocalState(vault.name), }); - await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); - const configured = await configureCouchDb(cli.binary, session.cliEnv, { - uri: couchDb.uri, - username: couchDb.username, - password: couchDb.password, - dbName, - }); - assertEqual(configured.isConfigured, true, "Self-hosted LiveSync was not configured."); + const initialReadiness = await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); + assertEqual(initialReadiness.configured, true, "Self-hosted LiveSync did not start configured."); await prepareRemote(cli.binary, session.cliEnv); await session.app.stop(); session = undefined; @@ -84,7 +97,8 @@ async function main(): Promise { vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), }); - await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); + const restartedReadiness = await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); + assertEqual(restartedReadiness.configured, true, "Self-hosted LiveSync lost its configuration on restart."); const localEntry = await waitForLocalDatabaseEntry(cli.binary, session.cliEnv, notePath); await pushLocalChanges(cli.binary, session.cliEnv); From 866575add55441f5cd17ecaa7bf9835f589b7a5a Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 26 Jul 2026 23:34:52 +0000 Subject: [PATCH 110/117] Correct staged release and merge gates --- .github/workflows/finalise-release.yml | 14 ++++- devs.md | 29 +++++----- utils/release-pr-body.mjs | 34 ++++++++---- utils/release-process.unit.spec.ts | 73 ++++++++++++++++---------- 4 files changed, 100 insertions(+), 50 deletions(-) diff --git a/.github/workflows/finalise-release.yml b/.github/workflows/finalise-release.yml index 2991d511..620c0915 100644 --- a/.github/workflows/finalise-release.yml +++ b/.github/workflows/finalise-release.yml @@ -62,6 +62,7 @@ jobs: VERSION: ${{ inputs.version }} EXPECTED_HEAD_SHA: ${{ inputs.expected_head_sha }} PRERELEASE: ${{ inputs.prerelease }} + PUBLISH_CLI: ${{ inputs.publish_cli }} run: | set -euo pipefail ACTUAL_HEAD_SHA="$(git rev-parse HEAD)" @@ -73,6 +74,10 @@ jobs: echo "Version ${VERSION} is a pre-release version, but prerelease was not enabled." >&2 exit 1 fi + if [[ "${VERSION}" != *-* && "${PRERELEASE}" == "true" && "${PUBLISH_CLI}" == "true" ]]; then + echo "A stable version staged as a pre-release must use publish_cli=false so that the CLI latest and major-minor image tags do not advance before BRAT validation." >&2 + exit 1 + fi node utils/release-notes.mjs validate "${VERSION}" - name: Ensure and push release tags @@ -120,8 +125,13 @@ jobs: echo "" echo "Dispatched the plug-in release workflow for \`${VERSION}\`. After approval for the release environment, it creates a draft GitHub Release." echo "" - if [[ "${PRERELEASE}" == "true" ]]; then - echo "Publish the draft as a pre-release, keep the release pull request in draft, and merge only after BRAT validation succeeds." + if [[ "${VERSION}" == *-* ]]; then + echo "Publish the draft as a pre-release without replacing the latest stable release." + echo "Keep the release pull request in draft and unmerged after BRAT validation; close it only through a separate maintainer action." + elif [[ "${PRERELEASE}" == "true" ]]; then + echo "Publish the draft initially as a pre-release without replacing the latest stable release." + echo "After BRAT validation, remove the pre-release designation and make this exact release the latest stable release before merging the release pull request." + echo "Create the stable CLI tag and publish its latest and major-minor image tags through a separate maintainer gate." else echo "Publish the draft as the latest stable release, keep the release pull request in draft, and merge only after BRAT validation succeeds." fi diff --git a/devs.md b/devs.md index a27bebc1..2951e8e5 100644 --- a/devs.md +++ b/devs.md @@ -244,7 +244,9 @@ export class ModuleExample extends AbstractObsidianModule { - Use SemVer beta identifiers such as `1.0.0-beta.0` for immutable integration previews. Increment the beta number when a published preview needs a correction. Reserve `1.0.0-rc.0` for the first feature- and contract-frozen release candidate. Historical `-patchedN` releases remain unchanged in the release history. - Publish a pre-release from an immutable reviewed tag, mark its GitHub Release as a pre-release, and do not replace the latest stable release. - A plug-in review release may omit the CLI image when the CLI artefact is not part of the required validation. When a pre-release CLI image is published, it receives immutable version and SHA-qualified tags only; it must not advance `latest` or a stable major-minor tag. -- Keep the release pull request in draft until the exact published plug-in has passed BRAT validation. If validation fails, prepare the next pre-release version rather than moving the existing tag. +- Keep a hyphenated pre-release's release pull request in draft and unmerged after BRAT validation. Reconcile the published version's metadata into its base branch through a reviewed metadata-only commit, then close the release pull request only through a separate maintainer action. +- Stage a stable version for BRAT by publishing its exact `x.y.z` tag initially as a GitHub pre-release with `prerelease=true` and `publish_cli=false`. The stable manifest version would otherwise make the CLI workflow advance `latest` and the major-minor image tag before validation. +- If validation fails, leave every published tag unchanged and prepare the next pre-release or patch version. ## Release Notes @@ -264,13 +266,15 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel - Do not tag the release branch when the PR is first created. Polish the release PR first, especially `updates.md`. - Once the release PR head is fixed, run the `Finalise Release Tags` workflow with its full head commit SHA. It validates the release branch, ensures that the plug-in tag points to that commit, optionally creates the corresponding CLI tag, and dispatches the plug-in release workflow. A CLI tag starts its own container workflow. The finalisation workflow can be retried when existing tags already point to the reviewed commit, but stops if a selected tag points elsewhere. - The plug-in publishing workflow is intentionally dispatch-only. Pushing a plug-in tag directly does not publish a GitHub Release; use `Finalise Release Tags`, or dispatch `Release Obsidian Plugin` explicitly for recovery or a pre-release. The CLI Docker workflow retains its documented branch, tag, and manual triggers. -- Approve the `Release Obsidian Plugin` workflow for the `release` environment, then inspect the generated draft GitHub Release. For a selected CLI publication, confirm the image tags appropriate to a stable or pre-release version. -- Publish a stable draft as the latest release, or publish a pre-release draft without replacing the latest stable release. In either case, keep the release PR in draft and leave its base branch unchanged until BRAT validation succeeds. Record that state in the PR. +- For a hyphenated pre-release, run finalisation with `prerelease=true`; CLI publication remains optional. For a stable version awaiting BRAT validation, use `prerelease=true` and `publish_cli=false`. +- Approve the `Release Obsidian Plugin` workflow for the `release` environment, then inspect the generated draft GitHub Release. When a hyphenated pre-release includes the CLI, confirm that it received only its immutable version and SHA-qualified image tags. +- Publish the draft as a GitHub pre-release without replacing the latest stable release. Keep its release pull request in draft and leave its base branch unchanged throughout BRAT validation. Record that state in the pull request. - Validate the published release through BRAT. Confirm start-up, ordinary bidirectional synchronisation, and any regression scenario relevant to the release. -- After BRAT validation succeeds, mark the release PR ready and merge it into the selected base branch with a merge commit. This keeps the tagged release commit in that branch's history. +- After a hyphenated pre-release passes, keep its release pull request unmerged. Add a reviewed metadata-only commit to the selected base branch which records the published version in `versions.json` and moves its exact tagged release notes out of `## Unreleased`, then close the release pull request only through a separate maintainer action. +- After a stable version passes, remove its GitHub pre-release designation and make that exact release the latest stable release. Create the stable CLI tag and publish its `latest` and major-minor image tags, if selected, through a separate maintainer gate. Only then mark the stable release pull request ready and merge it into the selected base branch with a merge commit. - If BRAT validation fails, keep the release PR in draft and do not move published tags. Before preparing the next version, add a reviewed metadata-only commit to the selected base branch which records the published version in `versions.json` and moves its exact tagged release notes out of `## Unreleased`. Keep only changes made after that tag under `## Unreleased`. Compare the historical section with `git show :updates.md`; do not merge the failed release PR or describe it as validated. The next release PR can then rotate only the correction notes while preserving the immutable release history. - Prepare and publish the next patch or pre-release version from that reconciled base. Leave the failed release PR draft until it is deliberately closed as superseded under a separate maintainer action. -- For a pre-release, set `prerelease=true` in `Finalise Release Tags`. A hyphenated version is rejected unless that input is enabled. +- A hyphenated version is rejected unless `prerelease=true`. A stable version staged with `prerelease=true` is rejected unless `publish_cli=false`. ### Release Cheat Sheet @@ -290,15 +294,16 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel - `version`: the same target version. - `release_branch`: leave blank unless the release branch used a custom name. - `expected_head_sha`: the full head commit SHA reviewed in the release PR. - - `prerelease`: enable for a version such as `1.0.0-rc.0`. - - `publish_cli`: disable when the reviewed release is plug-in-only. + - `prerelease`: enable for a version such as `1.0.0-rc.0`, and also when staging a stable version for BRAT. + - `publish_cli`: optional for a hyphenated pre-release, but disable it when staging a stable version. 5. Approve the `Release Obsidian Plugin` workflow for the `release` environment, then check the generated draft GitHub Release. -6. If CLI publication was selected, confirm that the CLI tag event published the expected image tags. -7. Publish the draft as a stable release or pre-release as selected, but keep the release PR in draft and leave its base branch unchanged. -8. Update the PR state message to describe the published release and state that merging remains on hold until BRAT validation is complete. +6. If a hyphenated pre-release includes the CLI, confirm that the CLI tag event published only immutable version and SHA-qualified image tags. +7. Publish the draft as a GitHub pre-release without replacing the latest stable release, but keep the release PR in draft and leave its base branch unchanged. +8. Update the PR state message to describe the published pre-release and state that merging remains on hold. 9. Validate the published release through BRAT, including start-up, ordinary bidirectional synchronisation, and any release-specific regression scenario. -10. After BRAT validation succeeds, mark the release PR ready and merge it into the selected base branch with a merge commit. -11. If validation fails, leave the PR in draft and do not move the published tags. Reconcile the published version's `updates.md` section and `versions.json` entry into the base branch as metadata only, then prepare the next patch or pre-release version from the remaining `## Unreleased` entries. +10. After a hyphenated pre-release passes, keep its release PR unmerged, reconcile its `versions.json` entry and exact release-note section into the selected base branch as metadata only, then close the PR through a separate maintainer action. +11. After a stable version passes, remove its pre-release designation, make the exact release the latest stable release, publish the stable CLI tags through a separate maintainer gate if selected, then mark the release PR ready and merge it into the selected base branch. +12. If validation fails, leave the PR in draft and do not move the published tags. Reconcile the published version's `updates.md` section and `versions.json` entry into the base branch as metadata only, then prepare the next patch or pre-release version from the remaining `## Unreleased` entries. ## Contribution Guidelines diff --git a/utils/release-pr-body.mjs b/utils/release-pr-body.mjs index 71a929d3..f48f159f 100644 --- a/utils/release-pr-body.mjs +++ b/utils/release-pr-body.mjs @@ -20,9 +20,10 @@ function inlineCode(value) { /** * Render the reader-facing checklist for a draft release pull request. * - * The version decides whether publication is stable or pre-release. The base - * branch is included explicitly because integration previews can target a - * reviewed integration branch rather than `main`. + * The version decides whether the release commit is an immutable SemVer + * pre-release or a stable version staged through a GitHub pre-release. The + * base branch is included explicitly because integration previews can target + * a reviewed integration branch rather than `main`. * * @param {string} version * @param {string} baseBranch @@ -39,13 +40,28 @@ export function renderReleasePrBody(version, baseBranch) { const isPrerelease = selectedVersion.includes("-"); const purpose = isPrerelease ? `an immutable pre-release for BRAT validation without replacing the latest stable release` - : `the next stable release`; + : `a stable version which will first be staged as a GitHub pre-release for BRAT validation`; const finaliseInstruction = isPrerelease ? "Run the finalise release workflow with this PR's fixed head SHA and `prerelease=true`" - : "Run the finalise release workflow with this PR's fixed head SHA and `prerelease=false`"; + : "Run the finalise release workflow with this PR's fixed head SHA, `prerelease=true`, and `publish_cli=false`"; const publicationInstruction = isPrerelease ? "Publish the GitHub Release as a pre-release without replacing the latest stable release, while keeping this pull request in draft" - : "Publish the GitHub Release as the latest stable release while keeping this pull request in draft"; + : "Publish the GitHub Release initially as a pre-release without replacing the latest stable release, while keeping this pull request in draft"; + const assetInstruction = isPrerelease + ? "Confirm the draft GitHub Release assets and the published CLI image, if selected" + : "Confirm the draft GitHub Release assets; keep stable CLI publication deferred until BRAT validation passes"; + const holdInstruction = isPrerelease + ? `Publishing and validating this pre-release does not unblock this pull request. Keep it in draft and unmerged, and leave ${baseBranchCode} unchanged.` + : `Publishing the GitHub pre-release does not unblock this pull request. Keep it in draft, and leave ${baseBranchCode} unchanged, until the exact published build has passed BRAT validation and has been promoted to the latest stable release.`; + const completionInstructions = isPrerelease + ? [ + "- [ ] Keep this pre-release pull request unmerged; close it only through a separate maintainer action", + ] + : [ + "- [ ] Remove the pre-release designation and make this exact release the latest stable release", + "- [ ] Create the stable CLI tag and publish its `latest` and major-minor image tags, if selected, through a separate maintainer gate", + `- [ ] Mark this pull request ready and merge it into ${baseBranchCode} with a merge commit`, + ]; return [ `This release pull request prepares Self-hosted LiveSync ${versionCode} from ${baseBranchCode} as ${purpose}.`, @@ -53,7 +69,7 @@ export function renderReleasePrBody(version, baseBranch) { "> [!IMPORTANT]", "> **Merge intentionally on hold**", ">", - `> Publishing the GitHub Release does not unblock this pull request. Keep this pull request in draft, and leave ${baseBranchCode} unchanged, until the exact published build has passed BRAT validation.`, + `> ${holdInstruction}`, "", "## Release checklist", "", @@ -62,10 +78,10 @@ export function renderReleasePrBody(version, baseBranch) { "- [ ] Confirm `manifest.json`, `versions.json`, workspace package versions, and the locked Commonlib package version", "- [ ] Confirm CI has passed", `- [ ] ${finaliseInstruction}`, - "- [ ] Confirm the draft GitHub Release assets and the published CLI image, if selected", + `- [ ] ${assetInstruction}`, `- [ ] ${publicationInstruction}`, "- [ ] Validate the exact published release with BRAT", - `- [ ] Mark this pull request ready and merge it into ${baseBranchCode} with a merge commit`, + ...completionInstructions, "", ].join("\n"); } diff --git a/utils/release-process.unit.spec.ts b/utils/release-process.unit.spec.ts index abdb7a05..62338eb0 100644 --- a/utils/release-process.unit.spec.ts +++ b/utils/release-process.unit.spec.ts @@ -4,10 +4,10 @@ import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; +import { renderReleasePrBody } from "./release-pr-body.mjs"; import { ensureTags } from "./release-tags.mjs"; const releaseNotesScript = fileURLToPath(new URL("./release-notes.mjs", import.meta.url)); -const releasePrBodyScript = fileURLToPath(new URL("./release-pr-body.mjs", import.meta.url)); const versionBumpScript = process.env.VERSION_BUMP_SCRIPT || fileURLToPath(new URL("../version-bump.mjs", import.meta.url)); const workspaceUpdateScript = fileURLToPath(new URL("../update-workspaces.mjs", import.meta.url)); @@ -160,13 +160,12 @@ describe("release notes", () => { describe("release workflow", () => { it("uses the locked Commonlib package instead of generated fallback declarations", () => { const workflow = readFileSync(prepareReleaseWorkflow, "utf8"); - const body = runNode(releasePrBodyScript, ["1.0.0-beta.0", "integration"], makeTemporaryDirectory()); + const body = renderReleasePrBody("1.0.0-beta.0", "integration"); expect(workflow).not.toContain("npm run build:lib:types"); expect(workflow).not.toMatch(/git add[^\n]*_types/); expect(workflow).toMatch(/git add[^\n]*package-lock\.json/); - expect(body.status, body.stderr).toBe(0); - expect(body.stdout).toContain("locked Commonlib package version"); + expect(body).toContain("locked Commonlib package version"); }); it("reruns the version lifecycle when the integration branch already selects the release version", () => { @@ -183,36 +182,56 @@ describe("release workflow", () => { expect(workflow).not.toContain("latest stable release"); }); - it("keeps the release PR in draft until BRAT validation", () => { - const prerelease = runNode( - releasePrBodyScript, - ["1.0.0-beta.0", "common-library-package-boundary"], - makeTemporaryDirectory() - ); + it("keeps an immutable pre-release out of its base branch after BRAT validation", () => { + const prerelease = renderReleasePrBody("1.0.0-rc.0", "common-library-package-boundary"); - expect(prerelease.status, prerelease.stderr).toBe(0); - expect(prerelease.stdout).toContain("Merge intentionally on hold"); - expect(prerelease.stdout).toContain("Self-hosted LiveSync `1.0.0-beta.0`"); - expect(prerelease.stdout).toContain("leave `common-library-package-boundary` unchanged"); - expect(prerelease.stdout).toContain("prerelease=true"); - expect(prerelease.stdout).toContain( + expect(prerelease).toContain("Merge intentionally on hold"); + expect(prerelease).toContain("Self-hosted LiveSync `1.0.0-rc.0`"); + expect(prerelease).toContain("leave `common-library-package-boundary` unchanged"); + expect(prerelease).toContain("prerelease=true"); + expect(prerelease).toContain( "Publish the GitHub Release as a pre-release without replacing the latest stable release" ); - expect(prerelease.stdout).toContain("Validate the exact published release with BRAT"); - expect(prerelease.stdout).toContain( - "Mark this pull request ready and merge it into `common-library-package-boundary` with a merge commit" - ); + expect(prerelease).toContain("Validate the exact published release with BRAT"); + expect(prerelease).toContain("Keep this pre-release pull request unmerged"); + expect(prerelease).toContain("close it only through a separate maintainer action"); + expect(prerelease).not.toContain("Mark this pull request ready and merge it"); }); - it("keeps stable release instructions distinct from pre-release instructions", () => { - const stable = runNode(releasePrBodyScript, ["1.0.0", "main"], makeTemporaryDirectory()); + it("publishes a stable version initially as a GitHub pre-release for BRAT validation", () => { + const stable = renderReleasePrBody("1.0.0", "main"); - expect(stable.status, stable.stderr).toBe(0); - expect(stable.stdout).toContain("prerelease=false"); - expect(stable.stdout).toContain( - "Publish the GitHub Release as the latest stable release while keeping this pull request in draft" + expect(stable).toContain("prerelease=true"); + expect(stable).toContain("publish_cli=false"); + expect(stable).toContain( + "Publish the GitHub Release initially as a pre-release without replacing the latest stable release" + ); + expect(stable).toContain( + "Remove the pre-release designation and make this exact release the latest stable release" + ); + expect(stable).toContain( + "Create the stable CLI tag and publish its `latest` and major-minor image tags, if selected, through a separate maintainer gate" + ); + expect(stable).toContain("Mark this pull request ready and merge it into `main` with a merge commit"); + expect(stable).not.toContain("prerelease=false"); + }); + + it("summarises immutable pre-releases separately from stable versions awaiting promotion", () => { + const workflow = readFileSync(finaliseReleaseWorkflow, "utf8"); + + expect(workflow).toContain('if [[ "${VERSION}" == *-* ]]; then'); + expect(workflow).toContain( + "Keep the release pull request in draft and unmerged after BRAT validation; close it only through a separate maintainer action." + ); + expect(workflow).toContain( + "After BRAT validation, remove the pre-release designation and make this exact release the latest stable release before merging the release pull request." + ); + expect(workflow).toContain( + 'if [[ "${VERSION}" != *-* && "${PRERELEASE}" == "true" && "${PUBLISH_CLI}" == "true" ]]; then' + ); + expect(workflow).toContain( + "A stable version staged as a pre-release must use publish_cli=false so that the CLI latest and major-minor image tags do not advance before BRAT validation." ); - expect(stable.stdout).not.toContain("as a pre-release without replacing"); }); it("dispatches the plug-in workflow and lets the CLI tag trigger its own workflow", () => { From e35583a7520a4de32f87a285281b0a85e3084c74 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 27 Jul 2026 00:52:15 +0000 Subject: [PATCH 111/117] Reconcile beta.5 metadata for release candidate --- updates.md | 18 ++++++++++++++++-- versions.json | 3 ++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/updates.md b/updates.md index 57ab3654..5384a33f 100644 --- a/updates.md +++ b/updates.md @@ -15,16 +15,30 @@ Earlier releases remain available in the 0.25 release history and the legacy rel ### Improved - The Hatch action for **Inspect conflicts and file/database differences** is now labelled **Begin inspection** so that its purpose is clear without repeating the scope already stated by the setting. -- **Inspect conflicts and file/database differences** now compares the current Vault file with every live database revision. Compact, mobile-friendly indicators show missing chunks, `Δsize`, `Δtime`, whether the Vault matches the winner, and whether conflict branches remain. Each revision's wrench menu can compare readable text, reflect that exact revision to the Vault, record an exact byte match, store the Vault content as a child of that branch, or discard only that branch. ### Fixed - Start-up file scans now omit legacy LiveSync log files and flag files before comparing Vault and local-database state. Existing ignored database records remain untouched and no longer produce misleading restore or deletion warnings. -- A winning logical deletion is no longer reported as a missing Vault file when the file is already absent. ### Testing - Added Commonlib collection regressions for built-in ignored files, and updated the Real Obsidian inspection and revision-repair scenarios for the clearer action label. + +## 1.0.0-beta.5 + +26th July, 2026 + +### Improved + +- **Inspect conflicts and file/database differences** now compares the current Vault file with the database winner and every live conflict revision. Compact, mobile-friendly indicators show missing chunks, `Δsize`, `Δtime`, whether the Vault matches the winner, and whether conflict branches remain. +- Each reported file and live revision now has a compact wrench menu. Its available actions can compare readable text, apply the selected revision to the Vault, record an exact byte match, store the Vault content as a child of the selected branch, retry retrieving missing chunks without changing the revision tree, or discard only the selected live branch after confirmation. + +### Fixed + +- A winning logical deletion is no longer reported as a missing Vault file when the file is already absent. + +### Testing + - Strengthened Real Obsidian coverage for start-up with an existing configuration, Security Seed readiness, failure diagnostics, and P2P status pane placement in separate desktop and mobile sessions. ## 1.0.0-beta.4 diff --git a/versions.json b/versions.json index 5f4652c9..f97f3468 100644 --- a/versions.json +++ b/versions.json @@ -10,5 +10,6 @@ "1.0.0-beta.1": "1.7.2", "1.0.0-beta.2": "1.7.2", "1.0.0-beta.3": "1.7.2", - "1.0.0-beta.4": "1.7.2" + "1.0.0-beta.4": "1.7.2", + "1.0.0-beta.5": "1.7.2" } From 8c6824b7f610f8fa77107d111cf28b128bdb3ebc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 01:36:33 +0000 Subject: [PATCH 112/117] Releasing 1.0.0-rc.0 --- manifest.json | 2 +- package-lock.json | 10 +++++----- package.json | 2 +- src/apps/cli/package.json | 2 +- src/apps/webapp/package.json | 2 +- src/apps/webpeer/package.json | 2 +- updates.md | 4 ++++ versions.json | 3 ++- 8 files changed, 16 insertions(+), 11 deletions(-) diff --git a/manifest.json b/manifest.json index 327fbad3..39908b15 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "1.0.0-beta.0", + "version": "1.0.0-rc.0", "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", diff --git a/package-lock.json b/package-lock.json index e6efdabf..aadd050b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "1.0.0-beta.0", + "version": "1.0.0-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "1.0.0-beta.0", + "version": "1.0.0-rc.0", "license": "MIT", "workspaces": [ "src/apps/cli", @@ -15913,7 +15913,7 @@ }, "src/apps/cli": { "name": "self-hosted-livesync-cli", - "version": "1.0.0-beta.0-cli", + "version": "1.0.0-rc.0-cli", "dependencies": { "chokidar": "^4.0.0", "minimatch": "^10.2.5", @@ -15938,7 +15938,7 @@ }, "src/apps/webapp": { "name": "livesync-webapp", - "version": "1.0.0-beta.0-webapp", + "version": "1.0.0-rc.0-webapp", "dependencies": { "octagonal-wheels": "^0.1.51" }, @@ -15950,7 +15950,7 @@ } }, "src/apps/webpeer": { - "version": "1.0.0-beta.0-webpeer", + "version": "1.0.0-rc.0-webpeer", "dependencies": { "octagonal-wheels": "^0.1.51" }, diff --git a/package.json b/package.json index a231db66..9e9da536 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "1.0.0-beta.0", + "version": "1.0.0-rc.0", "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", diff --git a/src/apps/cli/package.json b/src/apps/cli/package.json index 51e94d64..3be2ca45 100644 --- a/src/apps/cli/package.json +++ b/src/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "self-hosted-livesync-cli", "private": true, - "version": "1.0.0-beta.0-cli", + "version": "1.0.0-rc.0-cli", "main": "dist/index.cjs", "type": "module", "scripts": { diff --git a/src/apps/webapp/package.json b/src/apps/webapp/package.json index 3f64c34a..eac857df 100644 --- a/src/apps/webapp/package.json +++ b/src/apps/webapp/package.json @@ -1,7 +1,7 @@ { "name": "livesync-webapp", "private": true, - "version": "1.0.0-beta.0-webapp", + "version": "1.0.0-rc.0-webapp", "type": "module", "description": "Browser-based Self-hosted LiveSync using FileSystem API", "scripts": { diff --git a/src/apps/webpeer/package.json b/src/apps/webpeer/package.json index 16f9c47c..f49a942b 100644 --- a/src/apps/webpeer/package.json +++ b/src/apps/webpeer/package.json @@ -1,7 +1,7 @@ { "name": "webpeer", "private": true, - "version": "1.0.0-beta.0-webpeer", + "version": "1.0.0-rc.0-webpeer", "type": "module", "scripts": { "dev": "vite", diff --git a/updates.md b/updates.md index 5384a33f..d779029e 100644 --- a/updates.md +++ b/updates.md @@ -12,6 +12,10 @@ Earlier releases remain available in the 0.25 release history and the legacy rel ## Unreleased +## 1.0.0-rc.0 + +27th July, 2026 + ### Improved - The Hatch action for **Inspect conflicts and file/database differences** is now labelled **Begin inspection** so that its purpose is clear without repeating the scope already stated by the setting. diff --git a/versions.json b/versions.json index f97f3468..d176d6b5 100644 --- a/versions.json +++ b/versions.json @@ -11,5 +11,6 @@ "1.0.0-beta.2": "1.7.2", "1.0.0-beta.3": "1.7.2", "1.0.0-beta.4": "1.7.2", - "1.0.0-beta.5": "1.7.2" + "1.0.0-beta.5": "1.7.2", + "1.0.0-rc.0": "1.7.2" } From 3ce6ccf7a6f9b1231508c90ce13a23351fa8748a Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 27 Jul 2026 01:57:25 +0000 Subject: [PATCH 113/117] Polish 1.0.0-rc.0 release notes --- updates.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/updates.md b/updates.md index d779029e..a4b5de0c 100644 --- a/updates.md +++ b/updates.md @@ -16,17 +16,67 @@ Earlier releases remain available in the 0.25 release history and the legacy rel 27th July, 2026 -### Improved +The work towards 1.0 has become so substantial that I have written [an article about it](https://fancy-syncing.vrtmrz.net/blog/0036-livesync-1_0_0-en.html) (linked again here). -- The Hatch action for **Inspect conflicts and file/database differences** is now labelled **Begin inspection** so that its purpose is clear without repeating the scope already stated by the setting. +### Important -### Fixed +- This is the first 1.0 release candidate. It remains an opt-in pre-release for BRAT validation and does not replace the latest stable release. Update every participating device before resuming synchronisation, and continue to use a current backup while testing with an existing Vault. +- An upgraded, copied, or restored Vault may pause replication for an explicit compatibility review. Existing automatic synchronisation choices are preserved and resume only after the decision has been saved. -- Start-up file scans now omit legacy LiveSync log files and flag files before comparing Vault and local-database state. Existing ignored database records remain untouched and no longer produce misleading restore or deletion warnings. +### Changes consolidated from beta.0 through beta.5 + +#### Setup and compatibility + +- An unconfigured Vault now waits for the user to start setup. Onboarding is offered through a persistent Notice and remains available from **Self-hosted LiveSync settings** → **Setup**. +- Setup now creates named CouchDB, Object Storage, and P2P connections. Setup URIs preserve their connection names and selections, and reserve Fetch or Rebuild before the ordinary start-up scan begins. +- Existing Vaults retain their effective legacy settings, including the case-insensitive file-name fallback used when an older release had no explicit case setting. +- Manual CouchDB setup distinguishes creating the first database from connecting another device. Onboarding requires a successful connection, while Settings can explicitly save an unverified connection and offers each server-setting correction separately. +- Compatible differences limited to the chunk hash algorithm, chunk size, or splitter version are aligned automatically by default. Existing chunks remain readable, an explicit opt-out remains available, and differences involving incompatible settings still require review. + +#### Conflict handling and recovery + +- Automatic text and structured-data merge now uses the nearest revision actually shared by both branches. A resolution received from another device no longer recreates the same conflict merely because the Vault still contains the exact content of the removed branch. +- Edits, logical deletions, and renames made while a file remains conflicted extend the revision displayed on that device. When the relationship cannot be proved, LiveSync preserves the branches for review. +- **Not now** postpones repeated automatic merge dialogues while retaining the unresolved-conflict warning. Three or more live revisions are reviewed one reproducible pair at a time, completed pairs remain resolved across restart, and explicit commands can reopen a postponed conflict. +- **Inspect conflicts and file/database differences** compares the Vault with the database winner and every live conflict revision. Compact indicators show missing chunks, `Δsize`, `Δtime`, whether the Vault matches the winner, and whether conflicts remain. +- Each reported file and live revision has a compact wrench menu for comparison, applying an exact readable revision, recording an exact byte match, storing the Vault content as a child of a selected branch, retrying missing chunks without changing the tree, or explicitly discarding one selected live branch. +- Unreadable live revisions are preserved during automatic handling. An absent Vault file and a winning logical deletion are treated as agreement unless another live branch still requires attention. +- Garbage Collection V3 is limited to CouchDB and now protects every live conflict branch, required shared ancestry, and shared chunks. It stops when device progress cannot be verified and reports compaction failure without a contradictory success message. + +#### P2P and optional synchronisation features + +- P2P and Hidden File Sync remain supported opt-in features. Customisation Sync remains a supported Advanced workflow, while Data Compression remains available but disabled by default. +- P2P controls remain outside the ordinary CouchDB experience until P2P is configured. The current status pane distinguishes announcing changes, following a peer, and persistent per-device actions. +- First-device P2P setup can complete its signalling test without another peer online. Fetch on an additional device still requires an available source peer and a completed P2P Rebuild. +- P2P setup and guidance now distinguish the required signalling relay from optional TURN, describe the replaceable public relay's privacy and availability limits, and reliably close and recreate relay connections across settings changes and database resets. +- Enabling Hidden File Sync opens one progress Notice before saving the setting and reuses it until the initial scan has finished instead of stacking phase, reload, and restart messages. +- Broadening selectors, ignore rules, size or modification-time limits, or file-name case handling now rechecks previously received files without requiring another remote update. + +#### Interface and operations + +- Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands retain their identifiers so that existing hotkeys continue to work. +- Setup and review dialogue text can be selected for copying or translation. Applying an available interface translation no longer holds start-up behind an unsolicited dialogue; a persistent Notice opens the existing details on demand. +- Action buttons are arranged for narrow mobile screens, long dialogues keep their controls reachable, and persistent Notices no longer cover close controls. +- Remote-size warnings use persistent clickable Notices. Initial uploads and Rebuild no longer ask to send every chunk in advance; ordinary replication completes the transfer. +- Obsolete controls for the plug-in trash setting and fixed chunk revisions were removed. The Change Log remains available but no longer opens automatically or tracks an unread count. + +#### Other fixes and security + +- The optional Custom HTTP Handler used by Object Storage sends the correct byte range from binary request bodies and reports unsupported body types instead of silently sending an empty request. +- Fly.io setup generates CouchDB and Vault encryption secrets with cryptographically secure randomness. Dependency updates address excessive CPU use from crafted path patterns and `mailto:` links, and the CLI rejects detected path traversal and symbolic-link components before Vault operations. +- Self-hosted LiveSync now owns its translation catalogue. Commonlib supplies canonical English to other consumers, while translation contributions can be made in the main Self-hosted LiveSync repository. + +### Changes since beta.5 + +- The Hatch action for **Inspect conflicts and file/database differences** is now labelled **Begin inspection** so that its purpose is clear without repeating the setting name. +- Start-up and full-inspection scans now omit built-in legacy LiveSync log files and recovery flag files before comparing Vault and local-database state. Existing ignored database records remain untouched, and user-configured ignore behaviour is unchanged. ### Testing -- Added Commonlib collection regressions for built-in ignored files, and updated the Real Obsidian inspection and revision-repair scenarios for the clearer action label. +- Expanded automated Real Obsidian coverage for upgrades, two-device synchronisation, CouchDB, Object Storage, P2P, Hidden File Sync, mobile dialogues, conflict and revision recovery, failure diagnostics, and strict clean-up. +- Real CouchDB integration coverage verifies logical deletion, shared and conflict chunk retention, compaction, downstream replication, and recreation of content-addressed chunks. +- An encrypted Real Obsidian reconnect scenario replaces the remote Security Seed while one client retains the previous value, verifies that synchronisation adopts the replacement without restoring the old value, and proves a bidirectional encrypted round-trip. +- The beta series was exercised through BRAT on macOS, iOS, and Android, including upgrade from 0.25.83, bidirectional synchronisation, P2P setup, conflict handling, recovery controls, mobile layouts, and start-up with existing configurations. The exact RC artefact will be validated separately after publication. ## 1.0.0-beta.5 From d5a40a7e3d2bafd1da0c5df28d5eac0cb84f63b7 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 27 Jul 2026 03:37:46 +0000 Subject: [PATCH 114/117] release: prepare 1.0.0-rc.1 --- .github/workflows/finalise-release.yml | 9 ++++++- devs.md | 6 ++--- manifest.json | 2 +- package-lock.json | 10 ++++---- package.json | 2 +- src/apps/cli/Dockerfile | 6 ++--- src/apps/cli/docker-image.unit.spec.ts | 11 +++++++++ src/apps/cli/package.json | 2 +- .../cli/setup-uri-e2e-helper.unit.spec.ts | 11 +++++++++ src/apps/cli/test/test-setup-put-cat-linux.sh | 2 +- src/apps/webapp/package.json | 2 +- src/apps/webpeer/package.json | 2 +- updates.md | 24 +++++++++++++++++++ utils/release-process.unit.spec.ts | 11 ++++++--- versions.json | 3 ++- 15 files changed, 81 insertions(+), 22 deletions(-) create mode 100644 src/apps/cli/docker-image.unit.spec.ts create mode 100644 src/apps/cli/setup-uri-e2e-helper.unit.spec.ts diff --git a/.github/workflows/finalise-release.yml b/.github/workflows/finalise-release.yml index 620c0915..9451fc7e 100644 --- a/.github/workflows/finalise-release.yml +++ b/.github/workflows/finalise-release.yml @@ -101,8 +101,15 @@ jobs: GH_TOKEN: ${{ github.token }} VERSION: ${{ inputs.version }} PRERELEASE: ${{ inputs.prerelease }} + PUBLISH_CLI: ${{ inputs.publish_cli }} run: | set -euo pipefail + if [[ "${PUBLISH_CLI}" == "true" ]]; then + gh workflow run cli-docker.yml \ + --ref "${VERSION}-cli" \ + --field dry_run=false \ + --field force=false + fi gh workflow run release.yml \ --ref "${VERSION}" \ --field tag="${VERSION}" \ @@ -118,7 +125,7 @@ jobs: { echo "Ensured the plug-in tag \`${VERSION}\` points to the reviewed release commit." if [[ "${PUBLISH_CLI}" == "true" ]]; then - echo "The CLI tag \`${VERSION}-cli\` was also created; its tag event starts the container workflow." + echo "The CLI tag \`${VERSION}-cli\` was also created, and finalisation explicitly dispatched the CLI container workflow." else echo "CLI publication was omitted." fi diff --git a/devs.md b/devs.md index 2951e8e5..d80a0b1c 100644 --- a/devs.md +++ b/devs.md @@ -264,8 +264,8 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel - Run the `Prepare Release PR` workflow with the target version and selected base branch. It creates the release branch, updates versions, confirms that Commonlib is locked to an immutable package version, moves the `## Unreleased` notes to the target version, commits the release preparation, pushes the branch, and opens a draft release PR. The base branch may already select the target development version; the workflow still runs the version lifecycle so that release-only metadata such as `versions.json` is recorded in the release commit. - Do not tag the release branch when the PR is first created. Polish the release PR first, especially `updates.md`. -- Once the release PR head is fixed, run the `Finalise Release Tags` workflow with its full head commit SHA. It validates the release branch, ensures that the plug-in tag points to that commit, optionally creates the corresponding CLI tag, and dispatches the plug-in release workflow. A CLI tag starts its own container workflow. The finalisation workflow can be retried when existing tags already point to the reviewed commit, but stops if a selected tag points elsewhere. -- The plug-in publishing workflow is intentionally dispatch-only. Pushing a plug-in tag directly does not publish a GitHub Release; use `Finalise Release Tags`, or dispatch `Release Obsidian Plugin` explicitly for recovery or a pre-release. The CLI Docker workflow retains its documented branch, tag, and manual triggers. +- Once the release PR head is fixed, run the `Finalise Release Tags` workflow with its full head commit SHA. It validates the release branch, ensures that the plug-in tag points to that commit, optionally creates the corresponding CLI tag, and explicitly dispatches the selected plug-in and CLI release workflows. The finalisation workflow can be retried when existing tags already point to the reviewed commit, but stops if a selected tag points elsewhere. +- The plug-in publishing workflow is intentionally dispatch-only. Pushing a plug-in tag directly does not publish a GitHub Release; use `Finalise Release Tags`, or dispatch `Release Obsidian Plugin` explicitly for recovery or a pre-release. When CLI publication is selected, finalisation dispatches the CLI Docker workflow against the reviewed CLI tag instead of relying on a tag created by `GITHUB_TOKEN` to start another workflow. - For a hyphenated pre-release, run finalisation with `prerelease=true`; CLI publication remains optional. For a stable version awaiting BRAT validation, use `prerelease=true` and `publish_cli=false`. - Approve the `Release Obsidian Plugin` workflow for the `release` environment, then inspect the generated draft GitHub Release. When a hyphenated pre-release includes the CLI, confirm that it received only its immutable version and SHA-qualified image tags. - Publish the draft as a GitHub pre-release without replacing the latest stable release. Keep its release pull request in draft and leave its base branch unchanged throughout BRAT validation. Record that state in the pull request. @@ -297,7 +297,7 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel - `prerelease`: enable for a version such as `1.0.0-rc.0`, and also when staging a stable version for BRAT. - `publish_cli`: optional for a hyphenated pre-release, but disable it when staging a stable version. 5. Approve the `Release Obsidian Plugin` workflow for the `release` environment, then check the generated draft GitHub Release. -6. If a hyphenated pre-release includes the CLI, confirm that the CLI tag event published only immutable version and SHA-qualified image tags. +6. If a hyphenated pre-release includes the CLI, confirm that the explicitly dispatched CLI workflow published only immutable version and SHA-qualified image tags. 7. Publish the draft as a GitHub pre-release without replacing the latest stable release, but keep the release PR in draft and leave its base branch unchanged. 8. Update the PR state message to describe the published pre-release and state that merging remains on hold. 9. Validate the published release through BRAT, including start-up, ordinary bidirectional synchronisation, and any release-specific regression scenario. diff --git a/manifest.json b/manifest.json index 39908b15..def3b652 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "1.0.0-rc.0", + "version": "1.0.0-rc.1", "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", diff --git a/package-lock.json b/package-lock.json index aadd050b..bcfc013d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "1.0.0-rc.0", + "version": "1.0.0-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "1.0.0-rc.0", + "version": "1.0.0-rc.1", "license": "MIT", "workspaces": [ "src/apps/cli", @@ -15913,7 +15913,7 @@ }, "src/apps/cli": { "name": "self-hosted-livesync-cli", - "version": "1.0.0-rc.0-cli", + "version": "1.0.0-rc.1-cli", "dependencies": { "chokidar": "^4.0.0", "minimatch": "^10.2.5", @@ -15938,7 +15938,7 @@ }, "src/apps/webapp": { "name": "livesync-webapp", - "version": "1.0.0-rc.0-webapp", + "version": "1.0.0-rc.1-webapp", "dependencies": { "octagonal-wheels": "^0.1.51" }, @@ -15950,7 +15950,7 @@ } }, "src/apps/webpeer": { - "version": "1.0.0-rc.0-webpeer", + "version": "1.0.0-rc.1-webpeer", "dependencies": { "octagonal-wheels": "^0.1.51" }, diff --git a/package.json b/package.json index 9e9da536..d391fd91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "1.0.0-rc.0", + "version": "1.0.0-rc.1", "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", diff --git a/src/apps/cli/Dockerfile b/src/apps/cli/Dockerfile index bf6b9bf7..152710ae 100644 --- a/src/apps/cli/Dockerfile +++ b/src/apps/cli/Dockerfile @@ -101,9 +101,9 @@ COPY --from=runtime-deps /deps/node_modules ./node_modules # Copy the built CLI bundle from builder stage COPY --from=builder /build/src/apps/cli/dist ./dist -# Install entrypoint wrapper -COPY src/apps/cli/docker-entrypoint.sh /usr/local/bin/livesync-cli -RUN chmod +x /usr/local/bin/livesync-cli +# Install the entrypoint wrapper with a deterministic mode, regardless of +# source checkout permissions. +COPY --chmod=755 src/apps/cli/docker-entrypoint.sh /usr/local/bin/livesync-cli # Mount your vault / local database directory here VOLUME ["/data"] diff --git a/src/apps/cli/docker-image.unit.spec.ts b/src/apps/cli/docker-image.unit.spec.ts new file mode 100644 index 00000000..8834ebd6 --- /dev/null +++ b/src/apps/cli/docker-image.unit.spec.ts @@ -0,0 +1,11 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const dockerfile = readFileSync(new URL("./Dockerfile", import.meta.url), "utf8"); + +describe("CLI Docker image", () => { + it("sets a deterministic readable and executable entrypoint mode", () => { + expect(dockerfile).toContain("COPY --chmod=755 src/apps/cli/docker-entrypoint.sh /usr/local/bin/livesync-cli"); + expect(dockerfile).not.toContain("RUN chmod +x /usr/local/bin/livesync-cli"); + }); +}); diff --git a/src/apps/cli/package.json b/src/apps/cli/package.json index 3be2ca45..481ae59a 100644 --- a/src/apps/cli/package.json +++ b/src/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "self-hosted-livesync-cli", "private": true, - "version": "1.0.0-rc.0-cli", + "version": "1.0.0-rc.1-cli", "main": "dist/index.cjs", "type": "module", "scripts": { diff --git a/src/apps/cli/setup-uri-e2e-helper.unit.spec.ts b/src/apps/cli/setup-uri-e2e-helper.unit.spec.ts new file mode 100644 index 00000000..fc7d38b4 --- /dev/null +++ b/src/apps/cli/setup-uri-e2e-helper.unit.spec.ts @@ -0,0 +1,11 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const setupPutCatHelper = readFileSync(new URL("./test/test-setup-put-cat-linux.sh", import.meta.url), "utf8"); + +describe("CLI setup URI E2E helper", () => { + it("evaluates Commonlib package imports as ESM", () => { + expect(setupPutCatHelper).toContain("node --input-type=module -e"); + expect(setupPutCatHelper).not.toContain("npx tsx -e"); + }); +}); diff --git a/src/apps/cli/test/test-setup-put-cat-linux.sh b/src/apps/cli/test/test-setup-put-cat-linux.sh index 12c781a4..773833d9 100644 --- a/src/apps/cli/test/test-setup-put-cat-linux.sh +++ b/src/apps/cli/test/test-setup-put-cat-linux.sh @@ -27,7 +27,7 @@ cli_test_init_settings_file "$SETTINGS_FILE" echo "[INFO] creating setup URI from settings" SETUP_URI="$( - SETTINGS_FILE="$SETTINGS_FILE" SETUP_PASSPHRASE="$SETUP_PASSPHRASE" npx tsx -e ' + SETTINGS_FILE="$SETTINGS_FILE" SETUP_PASSPHRASE="$SETUP_PASSPHRASE" node --input-type=module -e ' import { fs } from "@vrtmrz/livesync-commonlib/node"; import { encodeSettingsToSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting"; (async () => { diff --git a/src/apps/webapp/package.json b/src/apps/webapp/package.json index eac857df..bd5cbcb1 100644 --- a/src/apps/webapp/package.json +++ b/src/apps/webapp/package.json @@ -1,7 +1,7 @@ { "name": "livesync-webapp", "private": true, - "version": "1.0.0-rc.0-webapp", + "version": "1.0.0-rc.1-webapp", "type": "module", "description": "Browser-based Self-hosted LiveSync using FileSystem API", "scripts": { diff --git a/src/apps/webpeer/package.json b/src/apps/webpeer/package.json index f49a942b..28a08597 100644 --- a/src/apps/webpeer/package.json +++ b/src/apps/webpeer/package.json @@ -1,7 +1,7 @@ { "name": "webpeer", "private": true, - "version": "1.0.0-rc.0-webpeer", + "version": "1.0.0-rc.1-webpeer", "type": "module", "scripts": { "dev": "vite", diff --git a/updates.md b/updates.md index a4b5de0c..0a7c5f95 100644 --- a/updates.md +++ b/updates.md @@ -12,6 +12,30 @@ Earlier releases remain available in the 0.25 release history and the legacy rel ## Unreleased +## 1.0.0-rc.1 + +27th July, 2026 + +The work towards 1.0 has become so substantial that I have written [an article about it](https://fancy-syncing.vrtmrz.net/blog/0036-livesync-1_0_0-en.html) (linked again here). + +### Important + +- This candidate retains the plug-in behaviour prepared for rc.0. The version was advanced because release tags are immutable; rc.0 was stopped during CLI validation before a plug-in release was published. +- This remains an opt-in pre-release for BRAT validation and does not replace the latest stable release. The exact rc.1 plug-in and CLI artefacts will be validated separately after publication. + +### CLI and release validation + +- CLI release validation now generates Setup URIs through the supported ESM package interface, allowing the Docker test to reach the CLI container instead of stopping during test preparation. +- The CLI Docker image now assigns its entrypoint permissions explicitly, so non-root execution does not depend on permissions inherited from the source checkout. +- Release finalisation now explicitly dispatches the CLI container workflow when CLI publication is selected, rather than relying on a workflow-created tag to start another workflow. +- Focused regression tests guard the ESM execution mode and deterministic container entrypoint permissions, while the existing release-workflow tests now require explicit CLI dispatch with non-dry-run, immutable-tag inputs. + +### Testing + +- The native CLI setup, put, cat, list, information, deletion, conflict-resolution, and revision-retrieval scenario completed with the packaged Commonlib dependency. +- The same scenario completed through the rebuilt non-root Docker image. +- The focused CLI and release-workflow unit tests passed after first demonstrating all three regressions against the unmodified implementation. + ## 1.0.0-rc.0 27th July, 2026 diff --git a/utils/release-process.unit.spec.ts b/utils/release-process.unit.spec.ts index 62338eb0..c9595e1b 100644 --- a/utils/release-process.unit.spec.ts +++ b/utils/release-process.unit.spec.ts @@ -234,16 +234,21 @@ describe("release workflow", () => { ); }); - it("dispatches the plug-in workflow and lets the CLI tag trigger its own workflow", () => { + it("dispatches the selected plug-in and CLI workflows explicitly", () => { const workflow = readFileSync(finaliseReleaseWorkflow, "utf8"); expect(workflow).toContain("actions: write"); expect(workflow).toContain('node utils/release-tags.mjs ensure "${VERSION}" "${EXPECTED_HEAD_SHA}"'); expect(workflow).toContain('git push --atomic origin "refs/tags/${VERSION}" "refs/tags/${VERSION}-cli"'); expect(workflow).not.toContain("Tag already exists"); + expect(workflow).toContain("PUBLISH_CLI: ${{ inputs.publish_cli }}"); + expect(workflow).toContain('if [[ "${PUBLISH_CLI}" == "true" ]]; then'); + expect(workflow).toContain("gh workflow run cli-docker.yml"); + expect(workflow).toContain('--ref "${VERSION}-cli"'); + expect(workflow).toContain("--field dry_run=false"); + expect(workflow).toContain("--field force=false"); expect(workflow).toContain("gh workflow run release.yml"); - expect(workflow).not.toContain("gh workflow run cli-docker.yml"); - expect(workflow).toContain("its tag event starts the container workflow"); + expect(workflow).toContain("explicitly dispatched the CLI container workflow"); }); it("publishes only by explicit dispatch and validates the selected release", () => { diff --git a/versions.json b/versions.json index d176d6b5..ac318380 100644 --- a/versions.json +++ b/versions.json @@ -12,5 +12,6 @@ "1.0.0-beta.3": "1.7.2", "1.0.0-beta.4": "1.7.2", "1.0.0-beta.5": "1.7.2", - "1.0.0-rc.0": "1.7.2" + "1.0.0-rc.0": "1.7.2", + "1.0.0-rc.1": "1.7.2" } From c3f2163204d6c3bac48e68a03d5c23ccc7db1130 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 27 Jul 2026 08:12:33 +0000 Subject: [PATCH 115/117] Release 1.0.0 --- docs/releases/1.0-previews.md | 161 ++++++++++++++++++++++ manifest.json | 2 +- package-lock.json | 10 +- package.json | 2 +- src/apps/cli/package.json | 2 +- src/apps/webapp/package.json | 2 +- src/apps/webpeer/package.json | 2 +- updates.md | 245 +++++++++------------------------- updates_old.md | 1 + versions.json | 5 +- 10 files changed, 234 insertions(+), 198 deletions(-) create mode 100644 docs/releases/1.0-previews.md diff --git a/docs/releases/1.0-previews.md b/docs/releases/1.0-previews.md new file mode 100644 index 00000000..d4940dc2 --- /dev/null +++ b/docs/releases/1.0-previews.md @@ -0,0 +1,161 @@ +# 1.0 preview release history + +This document records the opt-in beta and release-candidate builds published before 1.0.0. Most users upgrading from 0.25.83 only need the consolidated [1.0.0 release notes](../../updates.md). + +The prepared `1.0.0-rc.0` tag was not published as a plug-in release and is therefore omitted. + +## 1.0.0-rc.1 + +27th July, 2026 + +The work towards 1.0 has become so substantial that I have written [an article about it](https://fancy-syncing.vrtmrz.net/blog/0036-livesync-1_0_0-en.html) (linked again here). + +### Important + +- This candidate retains the plug-in behaviour prepared for rc.0. The version was advanced because release tags are immutable; rc.0 was stopped during CLI validation before a plug-in release was published. +- This remains an opt-in pre-release for BRAT validation and does not replace the latest stable release. The exact rc.1 plug-in and CLI artefacts will be validated separately after publication. + +### CLI and release validation + +- CLI release validation now generates Setup URIs through the supported ESM package interface, allowing the Docker test to reach the CLI container instead of stopping during test preparation. +- The CLI Docker image now assigns its entrypoint permissions explicitly, so non-root execution does not depend on permissions inherited from the source checkout. +- Release finalisation now explicitly dispatches the CLI container workflow when CLI publication is selected, rather than relying on a workflow-created tag to start another workflow. +- Focused regression tests guard the ESM execution mode and deterministic container entrypoint permissions, while the existing release-workflow tests now require explicit CLI dispatch with non-dry-run, immutable-tag inputs. + +### Testing + +- The native CLI setup, put, cat, list, information, deletion, conflict-resolution, and revision-retrieval scenario completed with the packaged Commonlib dependency. +- The same scenario completed through the rebuilt non-root Docker image. +- The focused CLI and release-workflow unit tests passed after first demonstrating all three regressions against the unmodified implementation. + +## 1.0.0-beta.5 + +26th July, 2026 + +### Improved + +- **Inspect conflicts and file/database differences** now compares the current Vault file with the database winner and every live conflict revision. Compact, mobile-friendly indicators show missing chunks, `Δsize`, `Δtime`, whether the Vault matches the winner, and whether conflict branches remain. +- Each reported file and live revision now has a compact wrench menu. Its available actions can compare readable text, apply the selected revision to the Vault, record an exact byte match, store the Vault content as a child of the selected branch, retry retrieving missing chunks without changing the revision tree, or discard only the selected live branch after confirmation. + +### Fixed + +- A winning logical deletion is no longer reported as a missing Vault file when the file is already absent. + +### Testing + +- Strengthened Real Obsidian coverage for start-up with an existing configuration, Security Seed readiness, failure diagnostics, and P2P status pane placement in separate desktop and mobile sessions. + +## 1.0.0-beta.4 + +25th July, 2026 + +### Improved + +- **Verify and repair all files** now reports the database winner, every conflict revision, missing chunks, and unavailable shared ancestors separately. It can retry an exact revision without changing the tree, while discarding an unreadable live revision requires explicit confirmation. +- Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands keep their identifiers, so hotkeys already assigned to them continue to work. The onboarding wizard can be reopened from **Self-hosted LiveSync settings** → **Setup**. +- Text in setup and review dialogues can now be selected for copying or translation. +- When LiveSync adopts an available interface translation on first start-up, it now continues initialisation and leaves a persistent Notice from which the translation details can be opened, instead of waiting for an unsolicited dialogue. + +### Fixed + +- An unreadable conflict revision is no longer deleted automatically merely because its chunks are unavailable on the current device. +- Garbage Collection V3 now protects chunks required by every live conflict branch and the available revision ancestry needed to review and merge conflicts, instead of considering only the database winner. The action is offered only for CouchDB because P2P has no central database to compact and does not provide the device inventory required by the workflow. Collection now stops when device progress cannot be verified, and a compaction timeout is no longer followed by a contradictory success message. + +### Testing + +- Added regressions for revision repair, command availability, selectable dialogues, conflict-aware chunk reachability, device-progress safeguards, and compaction timeouts. +- Added a real CouchDB integration test for logical chunk deletion, shared and conflict chunk retention, compaction completion, downstream replication, and content-addressed chunk recreation. +- Added a real Obsidian encrypted reconnect scenario which replaces the remote Security Seed while one client retains the previous value, verifies that synchronisation refreshes it without restoring the old value, and proves a bidirectional encrypted round-trip. + +## 1.0.0-beta.3 + +24th July, 2026 + +### Improved + +- Enabling Hidden File Sync now opens one progress Notice before its setting is saved and reuses that Notice throughout the initial file scan, instead of stacking separate phase and restart Notices. +- P2P is now presented only after it has been configured: its status pane no longer opens at start-up, its ribbon icon remains hidden for CouchDB-only Vaults, and the retired P2P pane command has been removed. The current pane distinguishes announcing changes, following a peer, and persistent per-device actions. Setup and guidance now distinguish the required signalling relay from optional TURN, and describe the public signalling relay's privacy and availability limits. +- First-device P2P setup now accepts a successfully opened signalling room without requiring another peer to be online. Additional-device Fetch still requires selecting a source peer and completing `P2P Rebuild`. +- Manual CouchDB setup now distinguishes creating a first database from connecting an additional device to an existing one. Settings mode can save an unverified profile explicitly, while onboarding requires a successful connection, and each proposed server-configuration fix requires separate confirmation. +- Differences limited to the chunk hash algorithm, chunk size, or splitter version are now aligned automatically by default. Existing content remains readable, while an explicit opt-out and any difference which also involves an incompatible setting retain manual review. + +### Fixed + +- Choosing **Apply settings to this device, and fetch again** for a compatible configuration mismatch now applies the remote settings before Fetch, instead of updating the remote database with this device's settings. +- Accepted settings which control how new chunks are created now take effect before synchronisation is retried, rather than leaving the previous hash or splitter active until restart. + +### Testing + +- Added regressions for P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, and mobile dialogues. + +## 1.0.0-beta.2 + +23rd July, 2026 + +### Improved + +- Choosing **Not now** on a merge conflict now postpones repeated dialogues for that conflict while the active file retains an unresolved-conflict warning. Three or more live versions show their current count and are reviewed one deterministic pair at a time; completed pairs remain resolved across restart. The existing conflict commands can reopen a postponed conflict explicitly, and a later conflict prompts again after the current one has been resolved. + +### Fixed + +- Answering or externally closing a merge dialogue immediately no longer leaves conflict processing waiting for a response which has already occurred. + +### Testing + +- Added revision-tree regressions and focused real-Obsidian scenarios for multiple-version review and restart between resolution stages. + +## 1.0.0-beta.1 + +22nd July, 2026 + +### Important + +- This corrected opt-in integration preview follows `1.0.0-beta.0` and does not replace the latest stable release. Update every participating device before resuming synchronisation, and continue to use a current backup while testing with an existing Vault. + +### Fixed + +- Conflict resolutions made on another device no longer recreate the same conflict when the receiving Vault still contains the exact content of the deleted losing revision. Automatic text and structured-data merge now uses the nearest revision actually shared by both branches instead of inferring ancestry from revision generation numbers. +- Edits, deletions, and renames made while a file is conflicted now extend the exact revision displayed on that device. If LiveSync cannot prove the displayed branch, it preserves the affected branches for review instead of silently applying the operation to the database winner. + +### Testing + +- Added revision-tree regressions and focused real-Obsidian scenarios for propagated resolutions and file operations performed while a conflict remains active. + +## 1.0.0-beta.0 + +22nd July, 2026 + +### Important + +- This is an opt-in 1.0 integration preview for BRAT and testing with existing Vaults. It does not replace the latest stable release. Use it with a current backup, and update every participating device before resuming synchronisation. +- An upgraded, copied, or restored Vault may pause replication for an explicit compatibility review. The review preserves the existing automatic synchronisation choices and resumes them only after the decision has been saved successfully. + +### Improved + +- An unconfigured installation now waits for you to start setup. A long-lived Notice offers the setup action, and **Open onboarding wizard** remains available from the command palette instead of the dialogue opening automatically. +- The setup wizard now creates named remote profiles for CouchDB, Object Storage, and P2P. Current Setup URIs preserve their profile names and selections, and the wizard reserves Rebuild or Fetch before the ordinary start-up scan begins. +- Peer-to-Peer Synchronisation (P2P) and Hidden File Sync are supported opt-in features. JWT authentication, ignore files, automatic newer-file conflict resolution, and Garbage Collection V3 remain previews. Customisation Sync remains a supported advanced workflow. +- Data Compression remains available after measurement showed a modest, workload-dependent reduction in stored and transferred chunk data. Its benefits, costs, and reason for remaining disabled by default in 1.0 are described in the Data Compression specification. +- Compatibility review now runs before Config Doctor without overlapping it. Existing Vaults retain their automatic synchronisation choices and explicit file-name case setting. For installations created by earlier releases, LiveSync preserves whether setup had been completed and saves a missing legacy case setting as case-insensitive. +- P2P connections now restart reliably after settings are reapplied or the local database is reset. Setup on an additional device asks you to select the source device once. Disconnecting leaves the LiveSync room and closes its signalling relay connections so that reconnecting can establish a new room. +- Action buttons are stacked vertically, long setup dialogues keep their controls reachable on mobile screens, and persistent Notices no longer cover close controls. Hidden File Sync reload and restart requests are grouped into one message, including the case reported in issue #555. +- Warnings about estimated remote storage size now appear as long-lived clickable Notices instead of timed dialogues. Initial uploads and Rebuild operations no longer prompt to send every chunk in advance; ordinary replication completes the transfer. +- Removed the obsolete **Use the trash bin** control and the setting for fixed chunk revisions. Remote deletion still follows Obsidian's preference, and chunk revisions remain content-derived. The Change Log remains available but no longer opens automatically or tracks unread versions. + +### Fixed + +- The optional Custom HTTP Handler used by Object Storage now sends the correct byte range from binary request bodies and reports unsupported body types instead of silently sending an empty request. +- When selectors, ignore files, size limits, modification-time limits, or file-name case settings are broadened, LiveSync now rechecks previously received files without requiring another remote update. +- P2P setup on the first device no longer displays reset or upload steps for a central database, and Config Doctor now offers its chunk size recommendation for CouchDB only when a CouchDB remote profile is selected. + +### Security + +- Fly.io setup now generates CouchDB and Vault encryption secrets with cryptographically secure randomness. Dependency updates prevent excessive CPU use from specially crafted path patterns and `mailto:` links. The CLI rejects path traversal and symbolic-link components detected before Vault operations. + +### Miscellaneous + +- Self-hosted LiveSync now owns its translation catalogue. Commonlib provides English messages to other applications, and translation contributions can be made directly to the Self-hosted LiveSync repository. + +### Testing + +- Expanded automated testing in Obsidian for upgrades, synchronisation between two devices, CouchDB, Object Storage, P2P, Hidden File Sync, mobile dialogues, and clean-up after failures. diff --git a/manifest.json b/manifest.json index def3b652..cf0ec3d1 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "1.0.0-rc.1", + "version": "1.0.0", "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", diff --git a/package-lock.json b/package-lock.json index bcfc013d..98b450ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "1.0.0-rc.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "1.0.0-rc.1", + "version": "1.0.0", "license": "MIT", "workspaces": [ "src/apps/cli", @@ -15913,7 +15913,7 @@ }, "src/apps/cli": { "name": "self-hosted-livesync-cli", - "version": "1.0.0-rc.1-cli", + "version": "1.0.0-cli", "dependencies": { "chokidar": "^4.0.0", "minimatch": "^10.2.5", @@ -15938,7 +15938,7 @@ }, "src/apps/webapp": { "name": "livesync-webapp", - "version": "1.0.0-rc.1-webapp", + "version": "1.0.0-webapp", "dependencies": { "octagonal-wheels": "^0.1.51" }, @@ -15950,7 +15950,7 @@ } }, "src/apps/webpeer": { - "version": "1.0.0-rc.1-webpeer", + "version": "1.0.0-webpeer", "dependencies": { "octagonal-wheels": "^0.1.51" }, diff --git a/package.json b/package.json index d391fd91..d2ab993c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "1.0.0-rc.1", + "version": "1.0.0", "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", diff --git a/src/apps/cli/package.json b/src/apps/cli/package.json index 481ae59a..44e66a6b 100644 --- a/src/apps/cli/package.json +++ b/src/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "self-hosted-livesync-cli", "private": true, - "version": "1.0.0-rc.1-cli", + "version": "1.0.0-cli", "main": "dist/index.cjs", "type": "module", "scripts": { diff --git a/src/apps/webapp/package.json b/src/apps/webapp/package.json index bd5cbcb1..dd98602f 100644 --- a/src/apps/webapp/package.json +++ b/src/apps/webapp/package.json @@ -1,7 +1,7 @@ { "name": "livesync-webapp", "private": true, - "version": "1.0.0-rc.1-webapp", + "version": "1.0.0-webapp", "type": "module", "description": "Browser-based Self-hosted LiveSync using FileSystem API", "scripts": { diff --git a/src/apps/webpeer/package.json b/src/apps/webpeer/package.json index 28a08597..7474c1cd 100644 --- a/src/apps/webpeer/package.json +++ b/src/apps/webpeer/package.json @@ -1,7 +1,7 @@ { "name": "webpeer", "private": true, - "version": "1.0.0-rc.1-webpeer", + "version": "1.0.0-webpeer", "type": "module", "scripts": { "dev": "vite", diff --git a/updates.md b/updates.md index 0a7c5f95..078e03fc 100644 --- a/updates.md +++ b/updates.md @@ -12,224 +12,99 @@ Earlier releases remain available in the 0.25 release history and the legacy rel ## Unreleased -## 1.0.0-rc.1 +## 1.0.0 27th July, 2026 The work towards 1.0 has become so substantial that I have written [an article about it](https://fancy-syncing.vrtmrz.net/blog/0036-livesync-1_0_0-en.html) (linked again here). -### Important +### Setup and compatibility -- This candidate retains the plug-in behaviour prepared for rc.0. The version was advanced because release tags are immutable; rc.0 was stopped during CLI validation before a plug-in release was published. -- This remains an opt-in pre-release for BRAT validation and does not replace the latest stable release. The exact rc.1 plug-in and CLI artefacts will be validated separately after publication. - -### CLI and release validation - -- CLI release validation now generates Setup URIs through the supported ESM package interface, allowing the Docker test to reach the CLI container instead of stopping during test preparation. -- The CLI Docker image now assigns its entrypoint permissions explicitly, so non-root execution does not depend on permissions inherited from the source checkout. -- Release finalisation now explicitly dispatches the CLI container workflow when CLI publication is selected, rather than relying on a workflow-created tag to start another workflow. -- Focused regression tests guard the ESM execution mode and deterministic container entrypoint permissions, while the existing release-workflow tests now require explicit CLI dispatch with non-dry-run, immutable-tag inputs. - -### Testing - -- The native CLI setup, put, cat, list, information, deletion, conflict-resolution, and revision-retrieval scenario completed with the packaged Commonlib dependency. -- The same scenario completed through the rebuilt non-root Docker image. -- The focused CLI and release-workflow unit tests passed after first demonstrating all three regressions against the unmodified implementation. - -## 1.0.0-rc.0 - -27th July, 2026 - -The work towards 1.0 has become so substantial that I have written [an article about it](https://fancy-syncing.vrtmrz.net/blog/0036-livesync-1_0_0-en.html) (linked again here). - -### Important - -- This is the first 1.0 release candidate. It remains an opt-in pre-release for BRAT validation and does not replace the latest stable release. Update every participating device before resuming synchronisation, and continue to use a current backup while testing with an existing Vault. -- An upgraded, copied, or restored Vault may pause replication for an explicit compatibility review. Existing automatic synchronisation choices are preserved and resume only after the decision has been saved. - -### Changes consolidated from beta.0 through beta.5 - -#### Setup and compatibility +#### Improved - An unconfigured Vault now waits for the user to start setup. Onboarding is offered through a persistent Notice and remains available from **Self-hosted LiveSync settings** → **Setup**. - Setup now creates named CouchDB, Object Storage, and P2P connections. Setup URIs preserve their connection names and selections, and reserve Fetch or Rebuild before the ordinary start-up scan begins. -- Existing Vaults retain their effective legacy settings, including the case-insensitive file-name fallback used when an older release had no explicit case setting. - Manual CouchDB setup distinguishes creating the first database from connecting another device. Onboarding requires a successful connection, while Settings can explicitly save an unverified connection and offers each server-setting correction separately. - Compatible differences limited to the chunk hash algorithm, chunk size, or splitter version are aligned automatically by default. Existing chunks remain readable, an explicit opt-out remains available, and differences involving incompatible settings still require review. -#### Conflict handling and recovery +#### Fixed + +- Existing Vaults retain their effective legacy settings, including the case-insensitive file-name fallback used when an older release had no explicit case setting. + +#### Security + +- Fly.io setup generates CouchDB and Vault encryption secrets with cryptographically secure randomness. +- Dependency updates address excessive CPU use from crafted path patterns and `mailto:` links. + +### Conflict handling and recovery + +#### Improved -- Automatic text and structured-data merge now uses the nearest revision actually shared by both branches. A resolution received from another device no longer recreates the same conflict merely because the Vault still contains the exact content of the removed branch. -- Edits, logical deletions, and renames made while a file remains conflicted extend the revision displayed on that device. When the relationship cannot be proved, LiveSync preserves the branches for review. - **Not now** postpones repeated automatic merge dialogues while retaining the unresolved-conflict warning. Three or more live revisions are reviewed one reproducible pair at a time, completed pairs remain resolved across restart, and explicit commands can reopen a postponed conflict. - **Inspect conflicts and file/database differences** compares the Vault with the database winner and every live conflict revision. Compact indicators show missing chunks, `Δsize`, `Δtime`, whether the Vault matches the winner, and whether conflicts remain. - Each reported file and live revision has a compact wrench menu for comparison, applying an exact readable revision, recording an exact byte match, storing the Vault content as a child of a selected branch, retrying missing chunks without changing the tree, or explicitly discarding one selected live branch. + +#### Fixed + +- Automatic text and structured-data merge now uses the nearest revision actually shared by both branches. A resolution received from another device no longer recreates the same conflict merely because the Vault still contains the exact content of the removed branch. +- Edits, logical deletions, and renames made while a file remains conflicted extend the revision displayed on that device. When the relationship cannot be proved, LiveSync preserves the branches for review. - Unreadable live revisions are preserved during automatic handling. An absent Vault file and a winning logical deletion are treated as agreement unless another live branch still requires attention. - Garbage Collection V3 is limited to CouchDB and now protects every live conflict branch, required shared ancestry, and shared chunks. It stops when device progress cannot be verified and reports compaction failure without a contradictory success message. -#### P2P and optional synchronisation features +### P2P and optional synchronisation features + +#### Improved - P2P and Hidden File Sync remain supported opt-in features. Customisation Sync remains a supported Advanced workflow, while Data Compression remains available but disabled by default. - P2P controls remain outside the ordinary CouchDB experience until P2P is configured. The current status pane distinguishes announcing changes, following a peer, and persistent per-device actions. -- First-device P2P setup can complete its signalling test without another peer online. Fetch on an additional device still requires an available source peer and a completed P2P Rebuild. -- P2P setup and guidance now distinguish the required signalling relay from optional TURN, describe the replaceable public relay's privacy and availability limits, and reliably close and recreate relay connections across settings changes and database resets. +- P2P setup and guidance now distinguish the required signalling relay from optional TURN and describe the replaceable public relay's privacy and availability limits. - Enabling Hidden File Sync opens one progress Notice before saving the setting and reuses it until the initial scan has finished instead of stacking phase, reload, and restart messages. -- Broadening selectors, ignore rules, size or modification-time limits, or file-name case handling now rechecks previously received files without requiring another remote update. -#### Interface and operations +#### Fixed + +- First-device P2P setup can complete its signalling test without another peer online. Fetch on an additional device still requires an available source peer and a completed P2P Rebuild. +- P2P relay connections now close and are recreated reliably after settings changes and database resets. + +### Interface, translation, and operations + +#### Improved - Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands retain their identifiers so that existing hotkeys continue to work. -- Setup and review dialogue text can be selected for copying or translation. Applying an available interface translation no longer holds start-up behind an unsolicited dialogue; a persistent Notice opens the existing details on demand. -- Action buttons are arranged for narrow mobile screens, long dialogues keep their controls reachable, and persistent Notices no longer cover close controls. +- Setup and review dialogue text can be selected for copying or translation. - Remote-size warnings use persistent clickable Notices. Initial uploads and Rebuild no longer ask to send every chunk in advance; ordinary replication completes the transfer. - Obsolete controls for the plug-in trash setting and fixed chunk revisions were removed. The Change Log remains available but no longer opens automatically or tracks an unread count. - -#### Other fixes and security - -- The optional Custom HTTP Handler used by Object Storage sends the correct byte range from binary request bodies and reports unsupported body types instead of silently sending an empty request. -- Fly.io setup generates CouchDB and Vault encryption secrets with cryptographically secure randomness. Dependency updates address excessive CPU use from crafted path patterns and `mailto:` links, and the CLI rejects detected path traversal and symbolic-link components before Vault operations. - Self-hosted LiveSync now owns its translation catalogue. Commonlib supplies canonical English to other consumers, while translation contributions can be made in the main Self-hosted LiveSync repository. -### Changes since beta.5 +#### Fixed -- The Hatch action for **Inspect conflicts and file/database differences** is now labelled **Begin inspection** so that its purpose is clear without repeating the setting name. -- Start-up and full-inspection scans now omit built-in legacy LiveSync log files and recovery flag files before comparing Vault and local-database state. Existing ignored database records remain untouched, and user-configured ignore behaviour is unchanged. +- Applying an available interface translation no longer holds start-up behind an unsolicited dialogue; a persistent Notice opens the existing details on demand. +- Action buttons are arranged for narrow mobile screens, long dialogues keep their controls reachable, and persistent Notices no longer cover close controls. -### Testing +### Storage and file selection + +#### Fixed + +- The optional Custom HTTP Handler used by Object Storage sends the correct byte range from binary request bodies and reports unsupported body types instead of silently sending an empty request. +- Broadening selectors, ignore rules, size or modification-time limits, or file-name case handling now rechecks previously received files without requiring another remote update. +- Start-up and full-inspection scans omit built-in legacy LiveSync log files and recovery flag files before comparing Vault and local-database state. Existing ignored database records remain untouched, and user-configured ignore behaviour is unchanged. + +### Command-line tool + +#### Fixed + +- CLI Setup URI validation now uses the supported Commonlib ESM package interface. +- The non-root Docker image no longer depends on permissions inherited from the source checkout. + +#### Security + +- The CLI rejects detected path traversal and symbolic-link components before Vault operations. + +### Validation + +#### Testing - Expanded automated Real Obsidian coverage for upgrades, two-device synchronisation, CouchDB, Object Storage, P2P, Hidden File Sync, mobile dialogues, conflict and revision recovery, failure diagnostics, and strict clean-up. - Real CouchDB integration coverage verifies logical deletion, shared and conflict chunk retention, compaction, downstream replication, and recreation of content-addressed chunks. - An encrypted Real Obsidian reconnect scenario replaces the remote Security Seed while one client retains the previous value, verifies that synchronisation adopts the replacement without restoring the old value, and proves a bidirectional encrypted round-trip. -- The beta series was exercised through BRAT on macOS, iOS, and Android, including upgrade from 0.25.83, bidirectional synchronisation, P2P setup, conflict handling, recovery controls, mobile layouts, and start-up with existing configurations. The exact RC artefact will be validated separately after publication. - -## 1.0.0-beta.5 - -26th July, 2026 - -### Improved - -- **Inspect conflicts and file/database differences** now compares the current Vault file with the database winner and every live conflict revision. Compact, mobile-friendly indicators show missing chunks, `Δsize`, `Δtime`, whether the Vault matches the winner, and whether conflict branches remain. -- Each reported file and live revision now has a compact wrench menu. Its available actions can compare readable text, apply the selected revision to the Vault, record an exact byte match, store the Vault content as a child of the selected branch, retry retrieving missing chunks without changing the revision tree, or discard only the selected live branch after confirmation. - -### Fixed - -- A winning logical deletion is no longer reported as a missing Vault file when the file is already absent. - -### Testing - -- Strengthened Real Obsidian coverage for start-up with an existing configuration, Security Seed readiness, failure diagnostics, and P2P status pane placement in separate desktop and mobile sessions. - -## 1.0.0-beta.4 - -25th July, 2026 - -### Improved - -- **Verify and repair all files** now reports the database winner, every conflict revision, missing chunks, and unavailable shared ancestors separately. It can retry an exact revision without changing the tree, while discarding an unreadable live revision requires explicit confirmation. -- Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands keep their identifiers, so hotkeys already assigned to them continue to work. The onboarding wizard can be reopened from **Self-hosted LiveSync settings** → **Setup**. -- Text in setup and review dialogues can now be selected for copying or translation. -- When LiveSync adopts an available interface translation on first start-up, it now continues initialisation and leaves a persistent Notice from which the translation details can be opened, instead of waiting for an unsolicited dialogue. - -### Fixed - -- An unreadable conflict revision is no longer deleted automatically merely because its chunks are unavailable on the current device. -- Garbage Collection V3 now protects chunks required by every live conflict branch and the available revision ancestry needed to review and merge conflicts, instead of considering only the database winner. The action is offered only for CouchDB because P2P has no central database to compact and does not provide the device inventory required by the workflow. Collection now stops when device progress cannot be verified, and a compaction timeout is no longer followed by a contradictory success message. - -### Testing - -- Added regressions for revision repair, command availability, selectable dialogues, conflict-aware chunk reachability, device-progress safeguards, and compaction timeouts. -- Added a real CouchDB integration test for logical chunk deletion, shared and conflict chunk retention, compaction completion, downstream replication, and content-addressed chunk recreation. -- Added a real Obsidian encrypted reconnect scenario which replaces the remote Security Seed while one client retains the previous value, verifies that synchronisation refreshes it without restoring the old value, and proves a bidirectional encrypted round-trip. - -## 1.0.0-beta.3 - -24th July, 2026 - -### Improved - -- Enabling Hidden File Sync now opens one progress Notice before its setting is saved and reuses that Notice throughout the initial file scan, instead of stacking separate phase and restart Notices. -- P2P is now presented only after it has been configured: its status pane no longer opens at start-up, its ribbon icon remains hidden for CouchDB-only Vaults, and the retired P2P pane command has been removed. The current pane distinguishes announcing changes, following a peer, and persistent per-device actions. Setup and guidance now distinguish the required signalling relay from optional TURN, and describe the public signalling relay's privacy and availability limits. -- First-device P2P setup now accepts a successfully opened signalling room without requiring another peer to be online. Additional-device Fetch still requires selecting a source peer and completing `P2P Rebuild`. -- Manual CouchDB setup now distinguishes creating a first database from connecting an additional device to an existing one. Settings mode can save an unverified profile explicitly, while onboarding requires a successful connection, and each proposed server-configuration fix requires separate confirmation. -- Differences limited to the chunk hash algorithm, chunk size, or splitter version are now aligned automatically by default. Existing content remains readable, while an explicit opt-out and any difference which also involves an incompatible setting retain manual review. - -### Fixed - -- Choosing **Apply settings to this device, and fetch again** for a compatible configuration mismatch now applies the remote settings before Fetch, instead of updating the remote database with this device's settings. -- Accepted settings which control how new chunks are created now take effect before synchronisation is retried, rather than leaving the previous hash or splitter active until restart. - -### Testing - -- Added regressions for P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, and mobile dialogues. - -## 1.0.0-beta.2 - -23rd July, 2026 - -### Improved - -- Choosing **Not now** on a merge conflict now postpones repeated dialogues for that conflict while the active file retains an unresolved-conflict warning. Three or more live versions show their current count and are reviewed one deterministic pair at a time; completed pairs remain resolved across restart. The existing conflict commands can reopen a postponed conflict explicitly, and a later conflict prompts again after the current one has been resolved. - -### Fixed - -- Answering or externally closing a merge dialogue immediately no longer leaves conflict processing waiting for a response which has already occurred. - -### Testing - -- Added revision-tree regressions and focused real-Obsidian scenarios for multiple-version review and restart between resolution stages. - -## 1.0.0-beta.1 - -22nd July, 2026 - -### Important - -- This corrected opt-in integration preview follows `1.0.0-beta.0` and does not replace the latest stable release. Update every participating device before resuming synchronisation, and continue to use a current backup while testing with an existing Vault. - -### Fixed - -- Conflict resolutions made on another device no longer recreate the same conflict when the receiving Vault still contains the exact content of the deleted losing revision. Automatic text and structured-data merge now uses the nearest revision actually shared by both branches instead of inferring ancestry from revision generation numbers. -- Edits, deletions, and renames made while a file is conflicted now extend the exact revision displayed on that device. If LiveSync cannot prove the displayed branch, it preserves the affected branches for review instead of silently applying the operation to the database winner. - -### Testing - -- Added revision-tree regressions and focused real-Obsidian scenarios for propagated resolutions and file operations performed while a conflict remains active. - -## 1.0.0-beta.0 - -22nd July, 2026 - -### Important - -- This is an opt-in 1.0 integration preview for BRAT and testing with existing Vaults. It does not replace the latest stable release. Use it with a current backup, and update every participating device before resuming synchronisation. -- An upgraded, copied, or restored Vault may pause replication for an explicit compatibility review. The review preserves the existing automatic synchronisation choices and resumes them only after the decision has been saved successfully. - -### Improved - -- An unconfigured installation now waits for you to start setup. A long-lived Notice offers the setup action, and **Open onboarding wizard** remains available from the command palette instead of the dialogue opening automatically. -- The setup wizard now creates named remote profiles for CouchDB, Object Storage, and P2P. Current Setup URIs preserve their profile names and selections, and the wizard reserves Rebuild or Fetch before the ordinary start-up scan begins. -- Peer-to-Peer Synchronisation (P2P) and Hidden File Sync are supported opt-in features. JWT authentication, ignore files, automatic newer-file conflict resolution, and Garbage Collection V3 remain previews. Customisation Sync remains a supported advanced workflow. -- Data Compression remains available after measurement showed a modest, workload-dependent reduction in stored and transferred chunk data. Its benefits, costs, and reason for remaining disabled by default in 1.0 are described in the Data Compression specification. -- Compatibility review now runs before Config Doctor without overlapping it. Existing Vaults retain their automatic synchronisation choices and explicit file-name case setting. For installations created by earlier releases, LiveSync preserves whether setup had been completed and saves a missing legacy case setting as case-insensitive. -- P2P connections now restart reliably after settings are reapplied or the local database is reset. Setup on an additional device asks you to select the source device once. Disconnecting leaves the LiveSync room and closes its signalling relay connections so that reconnecting can establish a new room. -- Action buttons are stacked vertically, long setup dialogues keep their controls reachable on mobile screens, and persistent Notices no longer cover close controls. Hidden File Sync reload and restart requests are grouped into one message, including the case reported in issue #555. -- Warnings about estimated remote storage size now appear as long-lived clickable Notices instead of timed dialogues. Initial uploads and Rebuild operations no longer prompt to send every chunk in advance; ordinary replication completes the transfer. -- Removed the obsolete **Use the trash bin** control and the setting for fixed chunk revisions. Remote deletion still follows Obsidian's preference, and chunk revisions remain content-derived. The Change Log remains available but no longer opens automatically or tracks unread versions. - -### Fixed - -- The optional Custom HTTP Handler used by Object Storage now sends the correct byte range from binary request bodies and reports unsupported body types instead of silently sending an empty request. -- When selectors, ignore files, size limits, modification-time limits, or file-name case settings are broadened, LiveSync now rechecks previously received files without requiring another remote update. -- P2P setup on the first device no longer displays reset or upload steps for a central database, and Config Doctor now offers its chunk size recommendation for CouchDB only when a CouchDB remote profile is selected. - -### Security - -- Fly.io setup now generates CouchDB and Vault encryption secrets with cryptographically secure randomness. Dependency updates prevent excessive CPU use from specially crafted path patterns and `mailto:` links. The CLI rejects path traversal and symbolic-link components detected before Vault operations. - -### Miscellaneous - -- Self-hosted LiveSync now owns its translation catalogue. Commonlib provides English messages to other applications, and translation contributions can be made directly to the Self-hosted LiveSync repository. - -### Testing - -- Expanded automated testing in Obsidian for upgrades, synchronisation between two devices, CouchDB, Object Storage, P2P, Hidden File Sync, mobile dialogues, and clean-up after failures. +- The plug-in code in this release was installed through BRAT and validated on macOS, iOS, and Android, including upgrade from 0.25.83, bidirectional synchronisation, P2P setup, conflict handling, recovery controls, mobile layouts, and start-up with existing configurations. +- Native and non-root Docker CLI scenarios cover setup, write, read, list, information, deletion, conflict resolution, and revision retrieval with the packaged Commonlib dependency. diff --git a/updates_old.md b/updates_old.md index de9a2896..6d6e4ee6 100644 --- a/updates_old.md +++ b/updates_old.md @@ -3,6 +3,7 @@ The release history is now kept as one chronological sequence across smaller files: - [Current 1.x releases](updates.md) +- [1.0 beta and release-candidate history](docs/releases/1.0-previews.md) - [0.25 releases](docs/releases/0.25.md) - [Releases before 0.25](docs/releases/legacy.md) diff --git a/versions.json b/versions.json index ac318380..6ffd7703 100644 --- a/versions.json +++ b/versions.json @@ -1,8 +1,6 @@ { "0.25.61": "1.7.2", "0.25.60": "1.7.2", - "1.0.1": "0.9.12", - "1.0.0": "0.9.7", "0.25.81": "1.7.2", "0.25.82": "1.7.2", "0.25.83": "1.7.2", @@ -13,5 +11,6 @@ "1.0.0-beta.4": "1.7.2", "1.0.0-beta.5": "1.7.2", "1.0.0-rc.0": "1.7.2", - "1.0.0-rc.1": "1.7.2" + "1.0.0-rc.1": "1.7.2", + "1.0.0": "1.7.2" } From 2b95766d4f82dae9ae87b8224affd9c9be3f8025 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 27 Jul 2026 08:28:16 +0000 Subject: [PATCH 116/117] Correct stable release promotion order --- .github/workflows/finalise-release.yml | 3 ++- devs.md | 5 +++-- utils/release-pr-body.mjs | 7 ++++--- utils/release-process.unit.spec.ts | 23 ++++++++++++++++++++--- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/workflows/finalise-release.yml b/.github/workflows/finalise-release.yml index 9451fc7e..d472012d 100644 --- a/.github/workflows/finalise-release.yml +++ b/.github/workflows/finalise-release.yml @@ -137,7 +137,8 @@ jobs: echo "Keep the release pull request in draft and unmerged after BRAT validation; close it only through a separate maintainer action." elif [[ "${PRERELEASE}" == "true" ]]; then echo "Publish the draft initially as a pre-release without replacing the latest stable release." - echo "After BRAT validation, remove the pre-release designation and make this exact release the latest stable release before merging the release pull request." + echo "After BRAT validation, merge the release pull request into its reviewed base branch and integrate the exact release commit into the default branch." + echo "Only after the default branch contains the exact release metadata, remove the pre-release designation and make this exact release the latest stable release." echo "Create the stable CLI tag and publish its latest and major-minor image tags through a separate maintainer gate." else echo "Publish the draft as the latest stable release, keep the release pull request in draft, and merge only after BRAT validation succeeds." diff --git a/devs.md b/devs.md index d80a0b1c..a6acd9c6 100644 --- a/devs.md +++ b/devs.md @@ -246,6 +246,7 @@ export class ModuleExample extends AbstractObsidianModule { - A plug-in review release may omit the CLI image when the CLI artefact is not part of the required validation. When a pre-release CLI image is published, it receives immutable version and SHA-qualified tags only; it must not advance `latest` or a stable major-minor tag. - Keep a hyphenated pre-release's release pull request in draft and unmerged after BRAT validation. Reconcile the published version's metadata into its base branch through a reviewed metadata-only commit, then close the release pull request only through a separate maintainer action. - Stage a stable version for BRAT by publishing its exact `x.y.z` tag initially as a GitHub pre-release with `prerelease=true` and `publish_cli=false`. The stable manifest version would otherwise make the CLI workflow advance `latest` and the major-minor image tag before validation. +- After a staged stable version passes BRAT validation, merge its exact release commit into the reviewed base branch and integrate it through the reviewed branch chain into the repository's default branch. Promote the GitHub Release only after the default branch contains the exact stable metadata; publish stable CLI tags through a separate maintainer gate. - If validation fails, leave every published tag unchanged and prepare the next pre-release or patch version. ## Release Notes @@ -271,7 +272,7 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel - Publish the draft as a GitHub pre-release without replacing the latest stable release. Keep its release pull request in draft and leave its base branch unchanged throughout BRAT validation. Record that state in the pull request. - Validate the published release through BRAT. Confirm start-up, ordinary bidirectional synchronisation, and any regression scenario relevant to the release. - After a hyphenated pre-release passes, keep its release pull request unmerged. Add a reviewed metadata-only commit to the selected base branch which records the published version in `versions.json` and moves its exact tagged release notes out of `## Unreleased`, then close the release pull request only through a separate maintainer action. -- After a stable version passes, remove its GitHub pre-release designation and make that exact release the latest stable release. Create the stable CLI tag and publish its `latest` and major-minor image tags, if selected, through a separate maintainer gate. Only then mark the stable release pull request ready and merge it into the selected base branch with a merge commit. +- After a stable version passes, mark its release pull request ready and merge the exact release commit into the selected base branch with a merge commit. Integrate that exact commit through the reviewed branch chain into the repository's default branch. Only after the default branch contains the matching stable metadata, remove the GitHub pre-release designation and make that exact release the latest stable release. Create the stable CLI tag and publish its `latest` and major-minor image tags, if selected, through a separate maintainer gate. - If BRAT validation fails, keep the release PR in draft and do not move published tags. Before preparing the next version, add a reviewed metadata-only commit to the selected base branch which records the published version in `versions.json` and moves its exact tagged release notes out of `## Unreleased`. Keep only changes made after that tag under `## Unreleased`. Compare the historical section with `git show :updates.md`; do not merge the failed release PR or describe it as validated. The next release PR can then rotate only the correction notes while preserving the immutable release history. - Prepare and publish the next patch or pre-release version from that reconciled base. Leave the failed release PR draft until it is deliberately closed as superseded under a separate maintainer action. - A hyphenated version is rejected unless `prerelease=true`. A stable version staged with `prerelease=true` is rejected unless `publish_cli=false`. @@ -302,7 +303,7 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel 8. Update the PR state message to describe the published pre-release and state that merging remains on hold. 9. Validate the published release through BRAT, including start-up, ordinary bidirectional synchronisation, and any release-specific regression scenario. 10. After a hyphenated pre-release passes, keep its release PR unmerged, reconcile its `versions.json` entry and exact release-note section into the selected base branch as metadata only, then close the PR through a separate maintainer action. -11. After a stable version passes, remove its pre-release designation, make the exact release the latest stable release, publish the stable CLI tags through a separate maintainer gate if selected, then mark the release PR ready and merge it into the selected base branch. +11. After a stable version passes, mark the release PR ready and merge the exact release commit into the selected base branch. Integrate that commit through the reviewed branch chain into the repository's default branch. Once the default branch contains the matching stable metadata, remove the pre-release designation, make the exact release the latest stable release, and publish stable CLI tags through a separate maintainer gate if selected. 12. If validation fails, leave the PR in draft and do not move the published tags. Reconcile the published version's `updates.md` section and `versions.json` entry into the base branch as metadata only, then prepare the next patch or pre-release version from the remaining `## Unreleased` entries. ## Contribution Guidelines diff --git a/utils/release-pr-body.mjs b/utils/release-pr-body.mjs index f48f159f..18baa602 100644 --- a/utils/release-pr-body.mjs +++ b/utils/release-pr-body.mjs @@ -52,15 +52,16 @@ export function renderReleasePrBody(version, baseBranch) { : "Confirm the draft GitHub Release assets; keep stable CLI publication deferred until BRAT validation passes"; const holdInstruction = isPrerelease ? `Publishing and validating this pre-release does not unblock this pull request. Keep it in draft and unmerged, and leave ${baseBranchCode} unchanged.` - : `Publishing the GitHub pre-release does not unblock this pull request. Keep it in draft, and leave ${baseBranchCode} unchanged, until the exact published build has passed BRAT validation and has been promoted to the latest stable release.`; + : `Publishing the GitHub pre-release does not unblock this pull request. Keep it in draft, and leave ${baseBranchCode} unchanged, until the exact published build has passed BRAT validation. Promotion remains on hold until the exact release commit has been integrated into the repository's default branch.`; const completionInstructions = isPrerelease ? [ "- [ ] Keep this pre-release pull request unmerged; close it only through a separate maintainer action", ] : [ - "- [ ] Remove the pre-release designation and make this exact release the latest stable release", + `- [ ] After BRAT validation passes, mark this pull request ready and merge it into ${baseBranchCode} with a merge commit`, + "- [ ] Integrate the exact release commit through the reviewed branch chain into the repository's default branch", + "- [ ] Confirm the default branch contains the exact release metadata, then remove the pre-release designation and make this exact release the latest stable release", "- [ ] Create the stable CLI tag and publish its `latest` and major-minor image tags, if selected, through a separate maintainer gate", - `- [ ] Mark this pull request ready and merge it into ${baseBranchCode} with a merge commit`, ]; return [ diff --git a/utils/release-process.unit.spec.ts b/utils/release-process.unit.spec.ts index c9595e1b..8f6edf90 100644 --- a/utils/release-process.unit.spec.ts +++ b/utils/release-process.unit.spec.ts @@ -207,12 +207,26 @@ describe("release workflow", () => { "Publish the GitHub Release initially as a pre-release without replacing the latest stable release" ); expect(stable).toContain( - "Remove the pre-release designation and make this exact release the latest stable release" + "After BRAT validation passes, mark this pull request ready and merge it into `main` with a merge commit" + ); + expect(stable).toContain( + "Integrate the exact release commit through the reviewed branch chain into the repository's default branch" + ); + expect(stable).toContain( + "Confirm the default branch contains the exact release metadata, then remove the pre-release designation and make this exact release the latest stable release" ); expect(stable).toContain( "Create the stable CLI tag and publish its `latest` and major-minor image tags, if selected, through a separate maintainer gate" ); - expect(stable).toContain("Mark this pull request ready and merge it into `main` with a merge commit"); + expect(stable.indexOf("After BRAT validation passes")).toBeLessThan( + stable.indexOf("Integrate the exact release commit") + ); + expect(stable.indexOf("Integrate the exact release commit")).toBeLessThan( + stable.indexOf("Confirm the default branch contains the exact release metadata") + ); + expect(stable.indexOf("Confirm the default branch contains the exact release metadata")).toBeLessThan( + stable.indexOf("Create the stable CLI tag") + ); expect(stable).not.toContain("prerelease=false"); }); @@ -224,7 +238,10 @@ describe("release workflow", () => { "Keep the release pull request in draft and unmerged after BRAT validation; close it only through a separate maintainer action." ); expect(workflow).toContain( - "After BRAT validation, remove the pre-release designation and make this exact release the latest stable release before merging the release pull request." + "After BRAT validation, merge the release pull request into its reviewed base branch and integrate the exact release commit into the default branch." + ); + expect(workflow).toContain( + "Only after the default branch contains the exact release metadata, remove the pre-release designation and make this exact release the latest stable release." ); expect(workflow).toContain( 'if [[ "${VERSION}" != *-* && "${PRERELEASE}" == "true" && "${PUBLISH_CLI}" == "true" ]]; then' From f1e382c6ed45a8b9111f2fb9cab36ce9bf9bdcf5 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 27 Jul 2026 09:07:20 +0000 Subject: [PATCH 117/117] Check repository tools with community lint --- _tools/bakei18n.ts | 7 ++- _tools/checkI18nCoverage.ts | 21 ++++--- _tools/decompileRosetta.ts | 5 +- _tools/decompileRosettaToJson.ts | 19 ++---- _tools/inspect-troubleshooting-docs.ts | 25 ++++---- _tools/json2yaml.ts | 24 ++++---- _tools/messagelib.ts | 83 +++++++++++++++----------- _tools/messagelib.unit.spec.ts | 41 +++++++++++++ _tools/yaml2json.ts | 21 +++---- package.json | 3 +- 10 files changed, 151 insertions(+), 98 deletions(-) create mode 100644 _tools/messagelib.unit.spec.ts diff --git a/_tools/bakei18n.ts b/_tools/bakei18n.ts index 9ded7145..b86cc390 100644 --- a/_tools/bakei18n.ts +++ b/_tools/bakei18n.ts @@ -1,11 +1,12 @@ -import { writeFileSync } from "fs"; import { allMessages } from "../src/common/messages/combinedMessages.dev.ts"; -import path from "path"; + +const { writeFileSync } = process.getBuiltinModule("node:fs"); +const path = process.getBuiltinModule("node:path"); const __dirname = import.meta.dirname; const currentPath = __dirname; const outDir = path.resolve(currentPath, "../src/common/messages/combinedMessages.prod.ts"); -console.log(`Writing to ${outDir}`); +process.stdout.write(`Writing to ${outDir}\n`); writeFileSync( outDir, `export const allMessages: Readonly>>> = ${JSON.stringify(allMessages, null, 4)};` diff --git a/_tools/checkI18nCoverage.ts b/_tools/checkI18nCoverage.ts index 6b51f33b..f8c6d744 100644 --- a/_tools/checkI18nCoverage.ts +++ b/_tools/checkI18nCoverage.ts @@ -1,14 +1,14 @@ -import { readFile } from "fs/promises"; -import { join, resolve } from "path"; import { glob } from "tinyglobby"; import { parse } from "yaml"; import { objectToDotted } from "./messagelib.ts"; +const fsPromises = process.getBuiltinModule("node:fs/promises"); +const path = process.getBuiltinModule("node:path"); const __dirname = import.meta.dirname; -const targetDir = resolve(join(__dirname, "../src/common/messagesYAML/")); +const targetDir = path.resolve(path.join(__dirname, "../src/common/messagesYAML/")); const files = (await glob(`*.yaml`, { expandDirectories: false, absolute: true, cwd: targetDir })).sort(); -function flattenMessages(src: Record) { +function flattenMessages(src: unknown) { return Object.fromEntries( Object.entries(objectToDotted(src)) .map(([key, value]) => [key.endsWith("._value") ? key.slice(0, -7) : key, value] as const) @@ -20,9 +20,14 @@ function flattenMessages(src: Record) { const localeData = new Map>(); for (const file of files) { const segments = file.split(/[/\\]/); - const locale = segments[segments.length - 1]!.replace(/\.yaml$/, ""); - const content = await readFile(file, "utf-8"); - localeData.set(locale, flattenMessages(parse(content) ?? {})); + const localeFilename = segments[segments.length - 1]; + if (localeFilename === undefined) { + throw new Error(`Could not determine the locale name for ${file}`); + } + const locale = localeFilename.replace(/\.yaml$/, ""); + const content = await fsPromises.readFile(file, "utf-8"); + const parsed: unknown = parse(content); + localeData.set(locale, flattenMessages(parsed ?? {})); } const baseLocale = "en"; @@ -55,4 +60,4 @@ const report = Object.fromEntries( }) ); -console.log(JSON.stringify(report, null, 2)); +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); diff --git a/_tools/decompileRosetta.ts b/_tools/decompileRosetta.ts index 9c8b2600..c2716382 100644 --- a/_tools/decompileRosetta.ts +++ b/_tools/decompileRosetta.ts @@ -1,9 +1,8 @@ -import { writeFileSync } from "fs"; - import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../src/common/rosetta"; import { allMessages } from "../src/common/messages/combinedMessages.dev.ts"; -import path from "path"; +const { writeFileSync } = process.getBuiltinModule("node:fs"); +const path = process.getBuiltinModule("node:path"); const thisFileDir = __dirname; const outDir = path.join(thisFileDir, "i18n"); diff --git a/_tools/decompileRosettaToJson.ts b/_tools/decompileRosettaToJson.ts index 5eb8a65d..030c067e 100644 --- a/_tools/decompileRosettaToJson.ts +++ b/_tools/decompileRosettaToJson.ts @@ -1,26 +1,17 @@ -import { writeFileSync } from "fs"; - import { allMessages } from "../src/common/messages/combinedMessages.prod.ts"; const __dirname = import.meta.dirname; -import path from "path"; + +const { writeFileSync } = process.getBuiltinModule("node:fs"); +const path = process.getBuiltinModule("node:path"); const thisFileDir = __dirname; const outDir = path.resolve(thisFileDir, "../src/common/messagesJson"); const out = {} as Record; for (const [key, value] of Object.entries(allMessages)) { - //@ts-ignore - for (const [lang, langValue] of Object.entries(allMessages[key])) { + for (const [lang, langValue] of Object.entries(value)) { if (!out[lang]) out[lang] = {}; - if (lang in value) { - out[lang][key] = langValue as string; - } else { - if (lang === "def") { - out[lang][key] = key; - } else { - out[lang][key] = undefined; - } - } + out[lang][key] = langValue; } } diff --git a/_tools/inspect-troubleshooting-docs.ts b/_tools/inspect-troubleshooting-docs.ts index e7de1545..245bf3da 100644 --- a/_tools/inspect-troubleshooting-docs.ts +++ b/_tools/inspect-troubleshooting-docs.ts @@ -1,6 +1,6 @@ -import { access, readFile } from "node:fs/promises"; -import { dirname, relative, resolve } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; +const fsPromises = process.getBuiltinModule("node:fs/promises"); +const path = process.getBuiltinModule("node:path"); +const url = process.getBuiltinModule("node:url"); type InspectionError = { check: "current-label" | "local-reference" | "retired-label"; @@ -20,7 +20,7 @@ const messageCataloguePath = "src/common/messagesJson/en.json"; const markdownLinkPattern = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^)]*["'])?\)/gu; function repositoryRootFromThisFile(): string { - return resolve(dirname(fileURLToPath(import.meta.url)), ".."); + return path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), ".."); } function normaliseReferenceTarget(rawTarget: string): string { @@ -48,14 +48,14 @@ async function inspectLocalReferences( const [pathPart] = target.split("#", 1); if (!pathPart) continue; checked++; - const referencedPath = resolve(repositoryRoot, dirname(documentPath), pathPart); + const referencedPath = path.resolve(repositoryRoot, path.dirname(documentPath), pathPart); try { - await access(referencedPath); + await fsPromises.access(referencedPath); } catch { errors.push({ check: "local-reference", file: documentPath, - detail: `Missing local reference: ${relative(repositoryRoot, referencedPath)}`, + detail: `Missing local reference: ${path.relative(repositoryRoot, referencedPath)}`, }); } } @@ -68,14 +68,13 @@ export async function inspectTroubleshootingDocs( const errors: InspectionError[] = []; const documents = new Map(); for (const guidePath of guidePaths) { - documents.set(guidePath, await readFile(resolve(repositoryRoot, guidePath), "utf8")); + documents.set(guidePath, await fsPromises.readFile(path.resolve(repositoryRoot, guidePath), "utf8")); } const troubleshooting = documents.get("docs/troubleshooting.md")!; - const catalogue = JSON.parse(await readFile(resolve(repositoryRoot, messageCataloguePath), "utf8")) as Record< - string, - string - >; + const catalogue = JSON.parse( + await fsPromises.readFile(path.resolve(repositoryRoot, messageCataloguePath), "utf8") + ) as Record; const requiredMessageKeys = [ "TweakMismatchResolve.Action.UseConfigured", "TweakMismatchResolve.Action.UseMine", @@ -132,7 +131,7 @@ async function runCli(): Promise { if (!result.ok) process.exitCode = 1; } -const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : undefined; +const invokedPath = process.argv[1] ? url.pathToFileURL(path.resolve(process.argv[1])).href : undefined; if (invokedPath === import.meta.url) { await runCli(); } diff --git a/_tools/json2yaml.ts b/_tools/json2yaml.ts index de8cae87..299b5d6b 100644 --- a/_tools/json2yaml.ts +++ b/_tools/json2yaml.ts @@ -1,27 +1,31 @@ // Convert Application convenient Message Resources (JSON) to Human-Editable format (YAML) -import { readFile, writeFile } from "fs/promises"; -import { join, resolve } from "path"; import { stringify } from "yaml"; import { glob } from "tinyglobby"; import { dottedToObject } from "./messagelib"; + +const fsPromises = process.getBuiltinModule("node:fs/promises"); +const path = process.getBuiltinModule("node:path"); const __dirname = import.meta.dirname; -const targetDir = resolve(join(__dirname, "../src/common/messagesJson/")); -console.log(`Target directory: ${targetDir}`); +const targetDir = path.resolve(path.join(__dirname, "../src/common/messagesJson/")); +process.stdout.write(`Target directory: ${targetDir}\n`); const files = await glob(`*.json`, { expandDirectories: false, absolute: true, cwd: targetDir }); for (const file of files) { - const filePath = resolve(file); - console.log(`Processing file: ${filePath}`); - const content = await readFile(filePath, "utf-8"); - const jsonDataSrc = JSON.parse(content); + const filePath = path.resolve(file); + process.stdout.write(`Processing file: ${filePath}\n`); + const content = await fsPromises.readFile(filePath, "utf-8"); + const jsonDataSrc: unknown = JSON.parse(content); + if (typeof jsonDataSrc !== "object" || jsonDataSrc === null || Array.isArray(jsonDataSrc)) { + throw new TypeError(`Expected ${filePath} to contain a JSON object`); + } const jsonDataD2 = Object.fromEntries( Object.entries(jsonDataSrc).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) ); const jsonData = dottedToObject(jsonDataD2); const yamlData = stringify(jsonData, { indent: 2 }); const yamlFilePath = filePath.replace(/\.json$/, ".yaml").replace("Json", "YAML"); - await writeFile(yamlFilePath, yamlData, "utf-8"); - console.log(`Converted ${filePath} to ${yamlFilePath}`); + await fsPromises.writeFile(yamlFilePath, yamlData, "utf-8"); + process.stdout.write(`Converted ${filePath} to ${yamlFilePath}\n`); } // console.dir(files, { depth: 0 }); diff --git a/_tools/messagelib.ts b/_tools/messagelib.ts index 6243ffad..95a4c664 100644 --- a/_tools/messagelib.ts +++ b/_tools/messagelib.ts @@ -1,38 +1,49 @@ -export function objectToDotted(obj: any, prefix = ""): Record { - return Object.entries(obj).reduce( - (acc, [key, value]) => { - const newKey = prefix ? `${prefix}.${key}` : key; - if (typeof value === "object" && value !== null && !Array.isArray(value)) { - Object.assign(acc, objectToDotted(value, newKey)); - } else { - acc[newKey] = value; - } - return acc; - }, - {} as Record - ); +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); } -export function dottedToObject(obj: Record): Record { - return Object.entries(obj).reduce( - (acc, [key, value]) => { - if (key.includes(" ")) { - // Return as is. - return { ...acc, [key]: value }; // Skip keys with spaces - } - const keys = key.split("."); - keys.reduce((nestedAcc, currKey, index) => { - if (currKey in nestedAcc && typeof nestedAcc[currKey] !== "object") { - nestedAcc[currKey] = { _value: nestedAcc[currKey] }; // Convert to object if not already - } - if (index === keys.length - 1) { - nestedAcc[currKey] = value; - } else { - nestedAcc[currKey] = nestedAcc[currKey] || {}; - } - return nestedAcc[currKey]; - }, acc); - return acc; - }, - {} as Record - ); + +export function objectToDotted(obj: unknown, prefix = ""): Record { + if (!isRecord(obj)) { + throw new TypeError("Expected a message catalogue object"); + } + const flattened: Record = {}; + for (const [key, value] of Object.entries(obj)) { + const newKey = prefix ? `${prefix}.${key}` : key; + if (isRecord(value)) { + Object.assign(flattened, objectToDotted(value, newKey)); + } else { + flattened[newKey] = value; + } + } + return flattened; +} + +export function dottedToObject(obj: unknown): Record { + if (!isRecord(obj)) { + throw new TypeError("Expected a dotted message catalogue object"); + } + const nestedResult: Record = {}; + for (const [key, value] of Object.entries(obj)) { + if (key.includes(" ")) { + nestedResult[key] = value; + continue; + } + const keys = key.split("."); + let nested = nestedResult; + for (const [index, currentKey] of keys.entries()) { + if (index === keys.length - 1) { + nested[currentKey] = value; + continue; + } + const currentValue = nested[currentKey]; + if (isRecord(currentValue)) { + nested = currentValue; + continue; + } + const replacement = currentValue === undefined || currentValue === null ? {} : { _value: currentValue }; + nested[currentKey] = replacement; + nested = replacement; + } + } + return nestedResult; } diff --git a/_tools/messagelib.unit.spec.ts b/_tools/messagelib.unit.spec.ts new file mode 100644 index 00000000..ae8f612a --- /dev/null +++ b/_tools/messagelib.unit.spec.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { dottedToObject, objectToDotted } from "./messagelib"; + +describe("message catalogue conversion", () => { + it("flattens nested objects while preserving leaf values", () => { + expect( + objectToDotted({ + dialogue: { + title: "Title", + options: ["first", "second"], + }, + "literal key": "Literal", + }) + ).toEqual({ + "dialogue.title": "Title", + "dialogue.options": ["first", "second"], + "literal key": "Literal", + }); + }); + + it("preserves an existing leaf under _value when a dotted child follows it", () => { + expect( + dottedToObject({ + section: "Base value", + "section.child": "Child value", + "literal key": "Literal", + }) + ).toEqual({ + section: { + _value: "Base value", + child: "Child value", + }, + "literal key": "Literal", + }); + }); + + it("rejects non-object catalogue roots", () => { + expect(() => objectToDotted("not an object")).toThrow("Expected a message catalogue object"); + expect(() => dottedToObject(["not", "an", "object"])).toThrow("Expected a dotted message catalogue object"); + }); +}); diff --git a/_tools/yaml2json.ts b/_tools/yaml2json.ts index 484ef2a5..d35a39d3 100644 --- a/_tools/yaml2json.ts +++ b/_tools/yaml2json.ts @@ -1,29 +1,30 @@ // Convert Human-Editable format (YAML) to Application convenient Message Resources (JSON) -import { readFile, writeFile } from "fs/promises"; -import { join, resolve } from "path"; import { parse } from "yaml"; import { glob } from "tinyglobby"; import { objectToDotted } from "./messagelib"; + +const fsPromises = process.getBuiltinModule("node:fs/promises"); +const path = process.getBuiltinModule("node:path"); const __dirname = import.meta.dirname; -const targetDir = resolve(join(__dirname, "../src/common/messagesYAML/")); -console.log(`Target directory: ${targetDir}`); +const targetDir = path.resolve(path.join(__dirname, "../src/common/messagesYAML/")); +process.stdout.write(`Target directory: ${targetDir}\n`); const files = await glob(`*.yaml`, { expandDirectories: false, absolute: true, cwd: targetDir }); for (const file of files) { - const filePath = resolve(file); - const content = await readFile(filePath, "utf-8"); - const jsonDataSrc = parse(content); + const filePath = path.resolve(file); + const content = await fsPromises.readFile(filePath, "utf-8"); + const jsonDataSrc: unknown = parse(content); const jsonDataD2 = objectToDotted(jsonDataSrc); const jsonData = Object.fromEntries( Object.entries(jsonDataD2) - .map(([key, value]) => [key.endsWith("._value") ? key.slice(0, -7) : key, value]) + .map(([key, value]): [string, unknown] => [key.endsWith("._value") ? key.slice(0, -7) : key, value]) .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) ); const yamlData = JSON.stringify(jsonData, null, 4) + "\n"; const yamlFilePath = filePath.replace(/\.yaml$/, ".json").replace("YAML", "Json"); - await writeFile(yamlFilePath, yamlData, "utf-8"); - console.log(`Converted ${filePath} to ${yamlFilePath}`); + await fsPromises.writeFile(yamlFilePath, yamlData, "utf-8"); + process.stdout.write(`Converted ${filePath} to ${yamlFilePath}\n`); } // console.dir(files, { depth: 0 }); diff --git a/package.json b/package.json index d2ab993c..3d66a4b5 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "buildDev": "node esbuild.config.mjs dev", "lint": "eslint --cache --cache-strategy content --concurrency off src", "lint:community": "eslint --config eslint.community.config.mjs --concurrency off src", + "lint:community:tools": "eslint --config eslint.community.config.mjs --concurrency off --max-warnings 0 _tools", "svelte-check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings", "tsc-check": "tsc --noEmit", "tsc-check:apps": "tsc --noEmit -p src/apps/browser/tsconfig.json && tsc --noEmit -p src/apps/cli/tsconfig.json && tsc --noEmit -p src/apps/webapp/tsconfig.json && tsc --noEmit -p src/apps/webpeer/tsconfig.app.json && tsc --noEmit -p src/apps/webpeer/tsconfig.node.json", @@ -22,7 +23,7 @@ "prettyNoWrite": "prettier --config ./.prettierrc.mjs \"**/*.js\" \"**/*.ts\" \"**/*.json\" ", "precheck:compatibility": "npm run build", "check:compatibility": "node utils/check-compatibility.js --file main.js --ios 15", - "check": "npm run tsc-check && npm run tsc-check:apps && npm run lint && npm run lint:community -- --quiet && npm run svelte-check && npm run check:compatibility", + "check": "npm run tsc-check && npm run tsc-check:apps && npm run lint && npm run lint:community -- --quiet && npm run lint:community:tools && npm run svelte-check && npm run check:compatibility", "i18n:bake": "npm run i18n:yaml2json && npm run i18n:bakejson && npm run i18n:format", "i18n:bakejson": "tsx _tools/bakei18n.ts", "i18n:format": "prettier --config .prettierrc.mjs --write --log-level error 'src/common/messagesJson/*.json' 'src/common/messages/*.ts'",

    a