mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-25 12:57:07 +00:00
Compare commits
33
Commits
1.0.16-cli
..
1_0_19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45a44d3820 | ||
|
|
b40c6363ee | ||
|
|
1c3feb526c | ||
|
|
aebf4874b3 | ||
|
|
3ea8eac212 | ||
|
|
f6eefed97c | ||
|
|
131ffeb0ef | ||
|
|
070ce0e307 | ||
|
|
8adc88b8cb | ||
|
|
dd11a753d5 | ||
|
|
47759f6205 | ||
|
|
d8f7762d01 | ||
|
|
ebaf89f822 | ||
|
|
06b9f32bdf | ||
|
|
32e827692f | ||
|
|
cdf6935042 | ||
|
|
665a5b5b61 | ||
|
|
83faec9280 | ||
|
|
d0bdae2676 | ||
|
|
cfbf94a38a | ||
|
|
35e170463e | ||
|
|
694371898a | ||
|
|
3f1d4efd86 | ||
|
|
d27a5b3725 | ||
|
|
cbd42bdecb | ||
|
|
e20386b8e2 | ||
|
|
f09b4c779e | ||
|
|
552d286392 | ||
|
|
22ae1b4a6e | ||
|
|
d1ae42a134 | ||
|
|
c7443ee728 | ||
|
|
f8ee3c8662 | ||
|
|
fbe868092a |
@@ -3,6 +3,8 @@
|
||||
A fully self-hosted CouchDB stack for the [obsidian-livesync](https://github.com/vrtmrz/obsidian-livesync) plugin.
|
||||
**No fly.io. No IBM Cloudant. No cloud accounts required for basic use.**
|
||||
|
||||
The optional [Coturn Compose starter](coturn/README.md) is a separate Linux-only service for P2P connectivity. It is not part of the CouchDB stack below.
|
||||
|
||||
> ✅ **Tested on Docker Desktop for Windows (Docker 29.2, Compose v5, WSL2 backend)** — full init, CORS, auth, and idempotent restart verified.
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Public DNS name used as the TURN authentication realm.
|
||||
TURN_REALM=
|
||||
|
||||
# Public IPv4 address advertised by Coturn. If Coturn is behind NAT, forward
|
||||
# port 3478 (TCP and UDP) and the UDP relay range to this host.
|
||||
TURN_EXTERNAL_IP=
|
||||
|
||||
# Static long-term credential used by the LiveSync P2P profile. Use a simple
|
||||
# username without a colon and a high-entropy password, such as hexadecimal.
|
||||
TURN_USERNAME=
|
||||
TURN_PASSWORD=
|
||||
@@ -0,0 +1 @@
|
||||
/.env
|
||||
@@ -0,0 +1,81 @@
|
||||
# Coturn starter for LiveSync P2P
|
||||
|
||||
This optional Compose project runs a small, static-credential TURN service for LiveSync P2P. It uses the upstream `coturn/coturn` image directly; the repository does not maintain a separate Coturn Dockerfile.
|
||||
|
||||
The starter is deliberately limited to a Linux server with a public IPv4 address, TURN over UDP and TCP on port 3478, and UDP relay ports 49160–49200. It does not configure TLS, automatic certificate renewal, monitoring, quotas, or a managed credential endpoint.
|
||||
|
||||
## Before starting
|
||||
|
||||
Prepare:
|
||||
|
||||
- a Linux host with Docker Engine and the Compose plug-in;
|
||||
- a public IPv4 address, either on the host or forwarded to it;
|
||||
- a DNS name such as `turn.example.com`;
|
||||
- firewall and NAT rules for TCP and UDP port 3478, and UDP ports 49160–49200; and
|
||||
- enough bandwidth for every relayed P2P transfer.
|
||||
|
||||
Docker host networking is intentional. Coturn's upstream image recommends it because forwarding a large relay port range through Docker performs poorly. This starter therefore does not support Docker Desktop.
|
||||
|
||||
## Configure and start
|
||||
|
||||
From this directory:
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
chmod 600 .env
|
||||
```
|
||||
|
||||
Set every value in `.env`. Generate a high-entropy password, for example:
|
||||
|
||||
```sh
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
Use a simple username without a colon. The static username and password are passed to Coturn as process arguments. They are visible to a local Docker administrator, who already controls the host. The `.env` file is excluded from Git and should remain private. The resolved output of `docker compose config` also contains the credential, so do not publish it.
|
||||
|
||||
Validate the resolved configuration, then start it:
|
||||
|
||||
```sh
|
||||
docker compose config
|
||||
docker compose up -d
|
||||
docker compose logs -f coturn
|
||||
```
|
||||
|
||||
The pinned image version is deliberate. Review the upstream Coturn release notes and update the pin explicitly rather than following `latest` automatically.
|
||||
|
||||
## Configure LiveSync
|
||||
|
||||
Enter both client paths in the P2P profile's TURN server list:
|
||||
|
||||
```text
|
||||
turn:turn.example.com:3478?transport=udp,turn:turn.example.com:3478?transport=tcp
|
||||
```
|
||||
|
||||
Use `TURN_USERNAME` and `TURN_PASSWORD` as the TURN username and credential. Keep the normal `Automatic` ICE policy unless a future LiveSync release offers `TURN relay only` and the direct path needs to be excluded deliberately.
|
||||
|
||||
Both synchronising devices must be able to reach the server. Prove an explicit two-way `Replicate now` round trip on the intended networks before relying on the configuration.
|
||||
|
||||
The repository check can validate Compose expansion and local TURN allocations. It cannot prove the public firewall, NAT, carrier, or client path for a particular deployment. Validate both UDP and TCP from outside the server network.
|
||||
|
||||
## TLS and port 443 are advanced extensions
|
||||
|
||||
This starter does not recommend putting Coturn on port 443. Coturn cannot bind to the same IP address and TCP port as Caddy or another HTTPS entry point. In particular, it conflicts with the bundled CouchDB Caddy profile when both use the same host address.
|
||||
|
||||
If a restrictive network requires TURN over TLS on port 443, prefer a separate TURN host or a separate public IP address. An outbound tunnel used for CouchDB may also leave the host's public port 443 available for Coturn, provided that TURN uses a separate DNS record which resolves directly to that host. The tunnel itself does not carry TURN traffic.
|
||||
|
||||
A single public IP can technically be shared when one layer-4 TLS router owns port 443 and routes separate CouchDB and TURN hostnames by Server Name Indication (SNI). This adds another certificate and connection-routing boundary, depends on every intended TURN client supplying usable SNI, and is outside this starter. The standard Caddy image used by the bundled CouchDB profile does not provide that layer-4 routing.
|
||||
|
||||
TURN over TLS is not HTTP. An ordinary HTTP reverse proxy or Cloudflare Tunnel route is not a substitute for a TURN listener. Follow Coturn's upstream configuration guidance for `tls-listening-port`, `cert`, and `pkey`, arrange renewal and restart behaviour, and test the resulting `turns:` URL from outside the server network.
|
||||
|
||||
This starter disables Coturn's TLS listener and does not add a TURN-over-DTLS path, so it cannot appear to provide a secure TURN port without those operator-owned prerequisites. This does not disable the end-to-end DTLS encryption used by the WebRTC peer connection carried through TURN.
|
||||
|
||||
## Security and operations
|
||||
|
||||
- Rotate the static credential if the Setup URI, `.env` file, or credential is exposed.
|
||||
- Treat TURN as an internet-facing bandwidth service and monitor traffic and logs.
|
||||
- Add appropriate allocation and bandwidth quotas for a shared or public deployment.
|
||||
- Keep the private-address restrictions unless the TURN server is intentionally permitted to relay to those networks.
|
||||
- Keep independent Vault backups. TURN improves connection reachability; it does not store a backup of Vault data.
|
||||
- A TURN operator can observe endpoint addresses, timing, and traffic volume even though LiveSync content remains end-to-end encrypted.
|
||||
|
||||
The authoritative image and configuration references are the [Coturn Docker image guide](https://github.com/coturn/coturn/blob/master/docker/coturn/README.md) and [Coturn server documentation](https://github.com/coturn/coturn/blob/master/README.turnserver).
|
||||
@@ -0,0 +1,33 @@
|
||||
name: livesync-coturn
|
||||
|
||||
services:
|
||||
coturn:
|
||||
image: coturn/coturn:4.17.2
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
# Compose has already interpolated the environment values. Invoke Coturn
|
||||
# directly so that the image's shell entrypoint does not expand them again.
|
||||
entrypoint:
|
||||
- turnserver
|
||||
command:
|
||||
- "-n"
|
||||
- "--log-file=stdout"
|
||||
- "--pidfile=/tmp/turnserver.pid"
|
||||
- "--listening-ip=0.0.0.0"
|
||||
- "--listening-port=3478"
|
||||
- "--min-port=49160"
|
||||
- "--max-port=49200"
|
||||
- "--external-ip=${TURN_EXTERNAL_IP:?Set TURN_EXTERNAL_IP in docker/coturn/.env}"
|
||||
- "--realm=${TURN_REALM:?Set TURN_REALM in docker/coturn/.env}"
|
||||
- "--user=${TURN_USERNAME:?Set TURN_USERNAME in docker/coturn/.env}:${TURN_PASSWORD:?Set TURN_PASSWORD in docker/coturn/.env}"
|
||||
- "--fingerprint"
|
||||
- "--lt-cred-mech"
|
||||
- "--stale-nonce=600"
|
||||
- "--unauthorized-ratelimit"
|
||||
- "--no-multicast-peers"
|
||||
- "--denied-peer-ip=10.0.0.0-10.255.255.255"
|
||||
- "--denied-peer-ip=100.64.0.0-100.127.255.255"
|
||||
- "--denied-peer-ip=169.254.0.0-169.254.255.255"
|
||||
- "--denied-peer-ip=172.16.0.0-172.31.255.255"
|
||||
- "--denied-peer-ip=192.168.0.0-192.168.255.255"
|
||||
- "--no-tls"
|
||||
@@ -267,7 +267,7 @@ The Community directory scanner preview completes with no source-code errors. Th
|
||||
|
||||
The remaining source warnings belong to application code. Browser dialogue visibility now uses DOM state instead of inline static styling, so the earlier styling warning is absent. Direct diagnostic output was resolved at its existing ownership boundaries: Webapp components use an injected log function backed by `BrowserAPIService`, WebPeer retains output in its Svelte log store, Obsidian modules use the established Logger path, and duplicate console emission was removed from `ModuleLog`. The later Webapp and WebPeer recomposition around maintained Context and serviceFeature APIs should preserve these explicit output paths.
|
||||
|
||||
The final Community lint inventory for this boundary has no errors and 126 warnings: 67 sentence-case findings, 58 deprecated-API findings, and one declarative setting-definition suggestion. The sentence-case strings and deprecated interfaces are retained deliberately to avoid an unrelated localisation and host-lifecycle change. Declarative definitions would migrate the complete Obsidian setting tab into the 1.13 settings-search model; that is a separate visible UI project after LiveSync 1.0, not a hidden package-boundary release gate. Revisit each category through focused UI and compatibility work rather than suppressing the rules or treating the warning count as zero.
|
||||
The final Community lint inventory for this boundary has no errors and 126 warnings: 67 sentence-case findings, 58 deprecated-API findings, and one declarative setting-definition suggestion. The sentence-case strings and deprecated interfaces are retained deliberately to avoid an unrelated localisation and host-lifecycle change. Declarative definitions would migrate the complete Obsidian setting tab into the 1.13 settings-search model; that is a separate visible UI project after LiveSync 1.0, not a hidden package-boundary release gate. Revisit each category through focused UI and compatibility work rather than suppressing the rules or treating the warning count as zero. The declarative-settings suggestion was subsequently addressed by [Adapt Standard Settings to Obsidian's Declarative API](2026_08_declarative_settings_adapter.md); the figures above remain the historical package-boundary inventory.
|
||||
|
||||
WebPeer's production build still reports that Vite externalises the guarded Node `crypto` fallback reached through a compatibility path. Browser execution selects `globalThis.crypto`, and the focused root, `context`, and `browser` bundle checks do not include the Node fallback, so this is not a leak in the reviewed public browser entries. Removing the compatibility-build warning requires a focused crypto-capability contract or a platform-specific implementation split and remains part of compatibility-surface narrowing.
|
||||
|
||||
@@ -275,7 +275,7 @@ The dependency preview also reports `uuid`, but the installed and locked graph r
|
||||
|
||||
The 1.0 dependency review found newly disclosed parser and denial-of-service advisories with compatible fixes. All locked `brace-expansion` generations now use their patched releases, including the production generation reached by Commonlib path matching and the CLI's user-configured ignore patterns. The development-only ESLint and Istanbul `js-yaml` generations likewise use patched releases. The production Markdown parser uses the patched `linkify-it` release to avoid quadratic processing of maliciously structured `mailto:` links, while the development-only JSON Schema toolchain uses the patched `fast-uri` release for unambiguous hostname parsing. A clean install and both complete and production-only `npm audit` checks no longer report these packages.
|
||||
|
||||
The remaining audit report is the existing `werift` and `werift-ice` dependency on `ip`, for which npm offers no patched version. The advisory concerns `ip.isPublic()` misclassifying unusual loopback representations. The locked werift implementation uses `ip` for address encoding, decoding, format detection, and loopback filtering, but does not call `isPublic()` or `isPrivate()`. LiveSync reaches werift only through the Node CLI's injected `RTCPeerConnection`; the Obsidian plug-in and browser applications use their platform WebRTC implementation, and the plug-in artefact does not contain werift. The package-level finding is therefore accepted for the 1.0 integration preview as a non-reachable advisory in the reviewed call path, not as a general waiver. Revisit it when werift or `ip` publishes a replacement, or before any change which delegates address trust, routing, or URL access decisions to that dependency.
|
||||
The initial 1.0 dependency review also found `werift` and `werift-ice` pulling in `ip`, for which npm offers no patched version. The advisory concerns `ip.isPublic()` misclassifying unusual loopback representations. LiveSync reached werift only through the Node CLI's injected `RTCPeerConnection`, and the reviewed call path did not use `isPublic()` or `isPrivate()`. This temporary acceptance ended when werift removed the dependency: the CLI now uses `werift` 0.24.4 or later, the installed production graph contains no `ip` package, and `npm audit --omit=dev` reports no vulnerabilities.
|
||||
|
||||
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, setting Markdown export, and two-device CouchDB, Object Storage, and P2P Setup URI workflows. These checks establish observable results and host composition rather than relying on declaration compatibility alone.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Version 1.0 needs to distinguish supported opt-in features from previews and fro
|
||||
- `xxhash64` is the current hash contract. Other hash algorithms remain available for existing databases and edge-case recovery, not as experimental alternatives for new Vaults.
|
||||
- Eden chunks remain accepted at runtime and in transported settings, but are not offered in the settings interface.
|
||||
- `doNotUseFixedRevisionForChunks` remains an inert compatibility input. Chunk revisions are always content-derived.
|
||||
- The deprecated cleaned-database reconciliation callback remains internal while an old IndexedDB client may still encounter that remote state. It is not a user-selectable maintenance action and is omitted from the settings reference.
|
||||
- The cleaned-database compatibility path remains internal while an old IndexedDB client may still encounter that remote state. It is not a user-selectable maintenance action and is omitted from the settings reference.
|
||||
|
||||
### Already removed
|
||||
|
||||
|
||||
@@ -0,0 +1,688 @@
|
||||
---
|
||||
date: 2026-08-25
|
||||
commonlib-version: "0.1.19"
|
||||
self-hosted-livesync-version: "1.0.18"
|
||||
status: accepted
|
||||
---
|
||||
|
||||
# Architectural Decision Record: Adapt Standard Settings to Obsidian's Declarative API
|
||||
|
||||
## Status
|
||||
|
||||
Accepted and implemented through Stage C1 and two bounded Stage C2
|
||||
landing-page improvements. The shared specification remains deliberately
|
||||
limited to one-key, immediately persisted controls. Complex pages retain their
|
||||
existing renderers instead of being forced through a general abstraction.
|
||||
Settings pending application which require database initialisation now delegate
|
||||
their decision, scheduling, and restart boundary to `SetupManager` and
|
||||
`Rebuilder`.
|
||||
|
||||
## Context
|
||||
|
||||
Obsidian 1.13 introduced declarative plug-in settings through
|
||||
`PluginSettingTab.getSettingDefinitions()`. Declarative definitions are used for
|
||||
native rendering, validation, navigation, and global settings search. When the
|
||||
method returns a non-empty array, Obsidian does not call the existing
|
||||
`display()` implementation.
|
||||
|
||||
Self-hosted LiveSync still supports Obsidian versions before 1.13 through its
|
||||
`minAppVersion` of 1.7.2. It must therefore retain an imperative `display()`
|
||||
fallback unless the minimum supported Obsidian version is raised separately.
|
||||
Maintaining an unrelated declarative definition and imperative implementation
|
||||
for each setting would allow the two interfaces to drift.
|
||||
|
||||
The current `LiveSyncSetting` AutoWire implementation combines several
|
||||
responsibilities:
|
||||
|
||||
- Commonlib setting metadata supplies setting names, descriptions, maturity, and
|
||||
configuration level;
|
||||
- pane functions decide page and group membership, control type, options, and
|
||||
conditional visibility;
|
||||
- `LiveSyncSetting` creates and updates Obsidian DOM components;
|
||||
- `ObsidianLiveSyncSettingTab` owns an editing buffer, dirty state, local and
|
||||
persisted settings, and save operations; and
|
||||
- selected controls add staged Apply behaviour, derived values, or effects
|
||||
which run after a successful save.
|
||||
|
||||
These responsibilities are not all declarative setting data. In particular,
|
||||
Remote Configuration, Hatch, Maintenance, Help, and Selector contain dynamic
|
||||
lists, Svelte components, diagnostic results, multi-step actions, and
|
||||
destructive confirmations. Encoding those interactions in a general settings
|
||||
DSL would increase the abstraction before a second renderer had proved which
|
||||
parts are genuinely shared.
|
||||
|
||||
Commonlib's `SettingInformation` setting metadata contains more entries than
|
||||
the current settings interface exposes. Generating definitions from all of it
|
||||
would therefore make compatibility or internal settings searchable merely
|
||||
because they have labels. Page membership must remain an explicit LiveSync
|
||||
decision.
|
||||
|
||||
The existing legacy settings wizard also changes DOM classes and selects panes
|
||||
through `enableMinimalSetup()`. The maintained onboarding path now uses
|
||||
`SetupManager`. The current call graph has no caller for
|
||||
`askAgainForSetupURI()`: it is the only emitter of
|
||||
`EVENT_REQUEST_OPEN_SETTING_WIZARD`, and that event's only handler calls
|
||||
`enableMinimalSetup()`. The old route is therefore obsolete rather than a
|
||||
second onboarding interface which the declarative renderer must preserve.
|
||||
Historically, it was the second prompt after a user reported having no Setup
|
||||
URI. It offered the in-settings wizard, P2P setup, manual settings, or a reminder
|
||||
at the next launch, then stopped initialisation while the selected interface
|
||||
took over. `SetupManager` now owns that decision and continuation.
|
||||
|
||||
## Decision
|
||||
|
||||
### Retire the obsolete in-settings wizard first
|
||||
|
||||
The old in-settings wizard will be removed as a focused prerequisite. This is
|
||||
cleanup of an unreachable interface, not part of the declarative settings
|
||||
model. The cleanup will remove:
|
||||
|
||||
- `askAgainForSetupURI()`, `EVENT_REQUEST_OPEN_SETTING_WIZARD`, its handler, and
|
||||
`enableMinimalSetup()`;
|
||||
- the `inWizard` completion branch in Sync Settings;
|
||||
- the `isWizard`, `wizardHidden`, and `wizardOnly` styling contract;
|
||||
- the General-page `Next` control and the already commented Remote
|
||||
Configuration `Next` control;
|
||||
- the now-unnecessary `wizardHidden` argument on the old pane builder; and
|
||||
- message keys whose final production consumer is the removed route, followed
|
||||
by the normal catalogue regeneration.
|
||||
|
||||
This cleanup does not affect `SetupManager`, Setup URI onboarding, QR-code
|
||||
navigation, document-history navigation, or any other control which happens to
|
||||
use the word 'Next'. The existing onboarding and ordinary settings E2E paths
|
||||
must pass before the declarative work begins.
|
||||
|
||||
The English quick-setup documentation already describes the maintained
|
||||
onboarding. Older localised quick-setup pages which still describe the removed
|
||||
interface are documentation maintenance rather than a prerequisite for this
|
||||
runtime cleanup.
|
||||
|
||||
### Use one explicit page catalogue
|
||||
|
||||
LiveSync will define one ordered page catalogue. It will be the sole source for
|
||||
page identity, name, configuration level, visibility, and content ownership.
|
||||
Each entry keeps the existing pane renderer for the legacy path and selects one
|
||||
of two native content forms:
|
||||
|
||||
```typescript
|
||||
type SettingsPageEntry = {
|
||||
id: string;
|
||||
name: () => string;
|
||||
icon: string;
|
||||
order: number;
|
||||
level?: ConfigLevel;
|
||||
content: "native" | "custom";
|
||||
legacy: PaneRenderer;
|
||||
};
|
||||
```
|
||||
|
||||
In Stage C1, `native` identifies the Advanced proof page, whose definitions are
|
||||
supplied by the adapter, and `custom` selects the shared lazy custom-page
|
||||
factory. The catalogue will gain a per-page native factory only when a second
|
||||
native page requires one; Stage C1 does not introduce that abstraction in
|
||||
advance.
|
||||
|
||||
A native `items` page may mix groups of `SettingSpec` controls with Obsidian's
|
||||
direct action, render, list, and nested-page definitions. A native custom
|
||||
`SettingPage` is the final escape hatch when the page cannot yet be divided
|
||||
safely. The imperative and declarative interfaces consume the same catalogue:
|
||||
|
||||
| Catalogue content | Obsidian before 1.13 | Obsidian 1.13 and later |
|
||||
| ------------------------------------- | -------------------------------- | ------------------------------------------ |
|
||||
| Standard `SettingSpec` | Render through `LiveSyncSetting` | Convert to a control definition |
|
||||
| Native group, action, or rendered row | Use the existing pane renderer | Use `SettingDefinitionPage.items` |
|
||||
| Full custom page | Use the existing pane renderer | Open a lazily created custom `SettingPage` |
|
||||
|
||||
This makes page names and visibility consistent without requiring every page
|
||||
to migrate at once. Page names must be unique because Obsidian uses them for
|
||||
nested navigation.
|
||||
|
||||
`SettingDefinitionPage` does not expose a separate icon field. The declarative
|
||||
renderer therefore prefixes each native page name with the emoji already held
|
||||
by the catalogue, while the imperative renderer continues to pass the same
|
||||
emoji to its existing menu button. This preserves the established visual
|
||||
identity without adding host-DOM manipulation.
|
||||
|
||||
`SettingDefinitionGroup` likewise exposes only a string heading. Root groups
|
||||
therefore have semantic identifiers whose catalogue entries keep their emoji
|
||||
and late-translated names separate. The adapter combines those fields only
|
||||
when constructing the Obsidian definition, so callers select a group by its
|
||||
identifier instead of repeating presentation strings.
|
||||
|
||||
### Compose the native landing page around common tasks
|
||||
|
||||
The declarative root is a composition of native groups and catalogue pages,
|
||||
not a second flat copy of the legacy tab menu. General Settings contains the
|
||||
native Appearance, Logging, and Extra menus child pages. Their standard
|
||||
`SettingSpec` controls remain searchable without crowding the root. The small
|
||||
Quick Setup actions are native action rows on the root. The old Setup child
|
||||
page is not retained: its feature-level controls move to Extra menus, its full
|
||||
reset moves to Maintenance, and its online guidance becomes Help and
|
||||
troubleshooting. The pane-based interface exposes Quick Setup as a pane and
|
||||
renders the same controls within General Settings.
|
||||
|
||||
Remote Configuration and Sync Settings remain catalogue pages. Obsidian's
|
||||
native group contract permits navigable pages as group items, so both pages are
|
||||
placed inside an explicit Synchronisation group. This keeps them near the top
|
||||
for narrow mobile displays while preventing the unheaded page entries from
|
||||
appearing to continue the preceding Quick Setup group. The root order reflects
|
||||
the current task:
|
||||
|
||||
| Current state | First root sections |
|
||||
| --------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| Synchronisation is inactive | Quick Setup, Synchronisation (Remote Configuration and Sync Settings), then General |
|
||||
| Synchronisation is active | Synchronisation (Remote Configuration and Sync Settings), General, then Quick Setup |
|
||||
|
||||
Set up other devices follows the Quick Setup and General groups when the
|
||||
plug-in is configured. The remaining destinations are grouped explicitly:
|
||||
|
||||
| Group | Pages |
|
||||
| ------------------------ | ---------------------------------------- |
|
||||
| Maintenance and recovery | Maintenance and Hatch |
|
||||
| Extra features | Selector and Customisation sync |
|
||||
| Advanced settings | Advanced, Power users, and Patches |
|
||||
| Help and information | Help and troubleshooting, and Change Log |
|
||||
|
||||
This prevents Obsidian from presenting them as one undifferentiated 'Detailed
|
||||
settings' continuation. Changing a setting which can move or reveal a page
|
||||
requests a catalogue refresh after persistence. External setting reloads use
|
||||
the same boundary. Constructing the definitions still performs no persistence,
|
||||
service, file, database, or network operation.
|
||||
|
||||
The imperative renderer retains its existing default-page selection: Quick Setup for
|
||||
an inactive configuration and General for an active configuration. The landing
|
||||
composition is therefore a native 1.13 improvement rather than a behaviour
|
||||
change for earlier supported Obsidian versions.
|
||||
|
||||
The custom `SettingPage` adapter class will be constructed lazily from the
|
||||
1.13-or-later path. `SettingPage` may remain a normal runtime import because the
|
||||
bundle reads Obsidian exports through its namespace object, but the import must
|
||||
not be subclassed or instantiated while the module is loading. The factory
|
||||
will first use `requireApiVersion("1.13.0")`, then verify that `SettingPage` is
|
||||
available. Older supported Obsidian versions therefore continue to call the
|
||||
imperative `display()` fallback without requiring a dynamic import or a
|
||||
polyfill for host behaviour which does not exist in those versions.
|
||||
|
||||
The adapter sets `title` from the catalogue and renders pane content into the
|
||||
host-provided `containerEl`. Its `hide()` boundary will unload the page-owned
|
||||
`Component`, unmount Svelte and markdown content, and remove page-owned update
|
||||
handlers. The parent tab's `hide()` remains a final cleanup boundary because
|
||||
Obsidian does not guarantee a page-level `hide()` call when the host window is
|
||||
destroyed.
|
||||
|
||||
Custom pages receive only the current page's `containerEl` and the existing
|
||||
`addPanel` helper. They do not recreate the old top-level tab menu inside each
|
||||
native page.
|
||||
|
||||
### Prefer native groups and searchable rows to full custom pages
|
||||
|
||||
An existing `addPanel` section maps naturally to a
|
||||
`SettingDefinitionGroup`. Within that group:
|
||||
|
||||
- ordinary value controls use `SettingSpec`;
|
||||
- a simple button operation may use `SettingDefinitionAction` directly;
|
||||
- a Svelte control or a specialised Obsidian row uses
|
||||
`SettingDefinitionRender` and returns its cleanup callback; and
|
||||
- a truly dynamic collection may use `SettingDefinitionList` when its existing
|
||||
behaviour already matches the list contract.
|
||||
|
||||
These Obsidian-specific definitions are written directly in the page adapter.
|
||||
They are not added to the shared `SettingSpec` vocabulary. This keeps the shared
|
||||
model small while allowing page, panel, and row names, descriptions, and aliases
|
||||
to participate in settings search.
|
||||
|
||||
Search indexes the metadata on a definition; it does not infer searchable
|
||||
entries from arbitrary DOM created inside a `render` callback. A whole panel
|
||||
wrapped in one rendered row therefore provides panel-level search only.
|
||||
Individual controls or actions require individual standard, action, or render
|
||||
definitions when control-level search is worthwhile.
|
||||
|
||||
A full custom `SettingPage` remains acceptable for a workflow which cannot yet
|
||||
be split without nesting several existing setting rows inside one synthetic
|
||||
row. It is a compatibility escape hatch, not the default representation for
|
||||
every complex pane.
|
||||
|
||||
### Keep `SettingSpec` intentionally small
|
||||
|
||||
`SettingSpec` describes only controls which have all of the following
|
||||
properties:
|
||||
|
||||
- one explicit persisted setting key, excluding keys from `OnDialogSettings`;
|
||||
- one standard toggle, number, or dropdown control in the first proof page;
|
||||
- a value which is read from the current editing buffer;
|
||||
- a change which can be persisted immediately through the existing
|
||||
`saveSettings([key])` path; and
|
||||
- no additional operation which must run after saving.
|
||||
|
||||
A representative, key-safe shape is:
|
||||
|
||||
```typescript
|
||||
type PersistedBooleanSettingKey = Exclude<AllBooleanItemKey, keyof OnDialogSettings>;
|
||||
type PersistedStringSettingKey = Exclude<AllStringItemKey, keyof OnDialogSettings>;
|
||||
type PersistedNumericSettingKey = Exclude<AllNumericItemKey, keyof OnDialogSettings>;
|
||||
type PersistedSettingKey = PersistedBooleanSettingKey | PersistedStringSettingKey | PersistedNumericSettingKey;
|
||||
|
||||
type SettingSpecBase<K extends PersistedSettingKey, C> = {
|
||||
key: K;
|
||||
control: C;
|
||||
visible?: () => boolean;
|
||||
disabled?: () => boolean;
|
||||
aliases?: string[];
|
||||
};
|
||||
|
||||
type SettingSpec =
|
||||
| SettingSpecBase<PersistedBooleanSettingKey, { type: "toggle"; defaultValue?: boolean }>
|
||||
| SettingSpecBase<
|
||||
PersistedNumericSettingKey,
|
||||
{
|
||||
type: "number";
|
||||
min?: number;
|
||||
max?: number;
|
||||
allowZero?: boolean;
|
||||
}
|
||||
>
|
||||
| SettingSpecBase<
|
||||
PersistedStringSettingKey,
|
||||
{
|
||||
type: "dropdown";
|
||||
options: () => Record<string, string>;
|
||||
}
|
||||
>;
|
||||
```
|
||||
|
||||
The initial union contains only the three control types used by the Advanced
|
||||
proof page. Text and textarea controls will be added when a migrated page
|
||||
provides a concrete use for them. Number validation is derived from `min`,
|
||||
`max`, and `allowZero`, so the native and imperative renderers enforce the same
|
||||
constraints without introducing an arbitrary validation language.
|
||||
|
||||
Names, descriptions, maturity labels, and placeholders come from the translated
|
||||
Commonlib setting metadata by default. The native renderer appends the existing
|
||||
maturity marker to `name`, and maps the description and supported placeholder
|
||||
directly. Configuration level remains a page and renderer concern: the legacy
|
||||
renderer retains its existing DOM classes, while the native page catalogue owns
|
||||
page-level visibility. A mixed-level native group must provide an explicit
|
||||
visibility predicate at that boundary rather than inferring one in the pure
|
||||
control converter. The specification may override a label only where the
|
||||
current interface already uses a deliberate product-specific label. Options
|
||||
remain LiveSync owned because they can depend on the active remote, platform,
|
||||
or language. A control which needs the current obsolete-row styling remains
|
||||
custom because the native definition does not provide an equivalent per-row
|
||||
class contract.
|
||||
|
||||
The catalogue explicitly lists each exposed key. It does not enumerate
|
||||
`SettingInformation` automatically.
|
||||
|
||||
The following behaviours are outside the standard specification and remain a
|
||||
custom row or custom page:
|
||||
|
||||
- `holdValue` and Apply buttons;
|
||||
- `invert` bindings;
|
||||
- password inputs;
|
||||
- a control which maps one displayed value to several stored keys, such as
|
||||
`syncMode`;
|
||||
- a control whose save effect cannot remain an explicit tab-owned handler;
|
||||
- button clusters, dynamic lists, Svelte components, rich diagnostic output,
|
||||
or destructive actions; and
|
||||
- styling which exists only to support the old wizard or tab menu.
|
||||
|
||||
This is a migration boundary, not a permanent prohibition. A second concrete
|
||||
use may justify a focused extension, but the first implementation will not add
|
||||
a generic action language, transaction language, or lifecycle hook system.
|
||||
|
||||
### Retain the existing editing and persistence owner
|
||||
|
||||
`ObsidianLiveSyncSettingTab` remains the owner of editing values and saves. The
|
||||
first implementation will expose a small adapter over its existing methods
|
||||
rather than move settings persistence into a new service.
|
||||
|
||||
For declarative controls:
|
||||
|
||||
- `getControlValue(key)` reads `editingSettings[key]`;
|
||||
- `setControlValue(key, value)` resolves an explicitly registered standard
|
||||
specification, updates the editing value, and calls `saveSettings([key])`;
|
||||
- successful saves continue to pass through `saveLocalSetting()` or
|
||||
`services.setting.saveSettingData()` as appropriate; and
|
||||
- the tab calls `refreshDomState()` after a value changes when another
|
||||
definition's `visible` or `disabled` predicate can depend on it.
|
||||
|
||||
An unknown key is an implementation error. The adapter must not fall through
|
||||
to `plugin.settings`, because LiveSync does not use that conventional storage
|
||||
shape.
|
||||
|
||||
Specification construction and `getSettingDefinitions()` must remain cheap
|
||||
and side-effect free. Obsidian calls the method during search indexing and
|
||||
again on updates; it must perform no file, database, network, or settings
|
||||
write.
|
||||
|
||||
### Give imperative pages an explicit lifetime and refresh boundary
|
||||
|
||||
The present `display()` renders every pane together, so arrays of
|
||||
`settingComponents`, controlled DOM updates, and `onSavedHandlers` can be
|
||||
cleared and rebuilt as one unit. Native page navigation mounts one custom page
|
||||
at a time. Reusing those arrays without a page boundary would leak updates from
|
||||
a hidden page or remove effects which a staged edit still needs.
|
||||
|
||||
Each imperative render will therefore receive a small page scope containing:
|
||||
|
||||
- its `Component` lifetime;
|
||||
- its `LiveSyncSetting` instances;
|
||||
- its controlled DOM update functions; and
|
||||
- its explicit cleanup callbacks for Svelte, markdown, and other mounted
|
||||
content.
|
||||
|
||||
The legacy `display()` fallback uses one scope for the complete old tab. A
|
||||
custom declarative page creates one scope when opened and disposes it when
|
||||
hidden. This scope is renderer state and is not part of `SettingSpec`.
|
||||
Pane-construction callbacks which are queued by the existing helpers run only
|
||||
whilst the scope which requested them remains current. Closing or replacing a
|
||||
page therefore cannot attach delayed controls or cleanup callbacks to its
|
||||
successor.
|
||||
|
||||
Saved-setting effects remain owned by the tab session, not by a DOM page. The
|
||||
existing handlers are unique by setting key, so `addOnSaved()` will replace the
|
||||
handler for that key instead of appending duplicate closures when a page is
|
||||
reopened. A later migration may declare those effects in a separate catalogue,
|
||||
but they will not be added to the standard control specification merely to
|
||||
support page navigation.
|
||||
|
||||
Direct calls to `this.display()` from pane code and the tab's own reload path
|
||||
will be replaced by an explicit refresh request with one of two scopes:
|
||||
|
||||
- `page` re-renders the active custom page, or the legacy tab; and
|
||||
- `catalogue` calls the declarative tab's `update()` so translated page names,
|
||||
page visibility, and search definitions are rebuilt, or re-renders the
|
||||
legacy tab.
|
||||
|
||||
Changing the display language or the Advanced, Power User, or Edge Case mode
|
||||
uses a catalogue refresh. Dynamic Selector rows, Maintenance status, and
|
||||
Hidden File Sync status use a page refresh. This keeps the renderer choice out
|
||||
of pane actions and prevents a direct `display()` call from replacing native
|
||||
declarative navigation.
|
||||
|
||||
### Preserve imperative rendering as a renderer
|
||||
|
||||
The existing AutoWire calls are not the shared model. Instead,
|
||||
`LiveSyncSetting` becomes the legacy renderer for `SettingSpec` where a pane
|
||||
has migrated. It continues to own DOM classes, dirty-value decoration, and
|
||||
component updates for older Obsidian versions.
|
||||
|
||||
Unmigrated pane functions continue to call `LiveSyncSetting` directly inside a
|
||||
custom page. This allows incremental migration without first rewriting their
|
||||
behaviour.
|
||||
|
||||
The initial implementation must not modify Commonlib's setting metadata.
|
||||
Commonlib owns setting identity and shared labels; LiveSync owns page placement,
|
||||
Obsidian controls, persistence routing, and side effects.
|
||||
|
||||
### Use Advanced as the first proof page
|
||||
|
||||
The Advanced page is the first page whose groups contain only standard
|
||||
`SettingSpec` controls. It provides a useful proof without introducing
|
||||
unrelated workflows:
|
||||
|
||||
- number, dropdown, and toggle controls;
|
||||
- translated Commonlib labels;
|
||||
- minimum-value validation and a default value;
|
||||
- CouchDB-dependent visibility; and
|
||||
- configuration-level page visibility.
|
||||
|
||||
It has no current `onSaved` handler, Svelte component, staged Apply group, or
|
||||
destructive action. General was not the first proof because changing the
|
||||
display language re-renders the interface and other controls emit status events
|
||||
after saving. After the standard binding was proven, these effects remained
|
||||
explicit, tab-owned saved handlers while their one-key controls adopted
|
||||
`SettingSpec`.
|
||||
|
||||
The first native activation does not also divide other pages into searchable
|
||||
rows. It exposes their established pane renderers as custom pages, limited to
|
||||
page-level search. A later, optional migration can replace an individual custom
|
||||
page with standard, action, or rendered rows without changing the page
|
||||
catalogue.
|
||||
|
||||
## Implementation Stages and Checkpoint
|
||||
|
||||
### Stage A: remove the old wizard
|
||||
|
||||
Complete the focused prerequisite described above. This removes a DOM contract
|
||||
which would otherwise distort both renderers.
|
||||
|
||||
### Stage B: prove the shared standard-control model
|
||||
|
||||
The first declarative-settings change remains deliberately small. It will:
|
||||
|
||||
1. add the minimal `SettingSpec` type and pure conversion functions;
|
||||
2. describe only the Advanced controls as specifications;
|
||||
3. render those specifications through the existing `LiveSyncSetting` path;
|
||||
4. prove conversion to Obsidian setting definitions with focused tests; and
|
||||
5. retain the current `display()` behaviour, page menu, persistence owner, and
|
||||
`minAppVersion`, without returning non-empty setting definitions.
|
||||
|
||||
Stage B does not enable the native declarative renderer. It proves that the
|
||||
shared model can express a real page without first taking ownership of every
|
||||
page's lifetime. Returning an empty definition array merely to silence review
|
||||
output is not an outcome of this stage.
|
||||
|
||||
### Stage C1: activate the native catalogue
|
||||
|
||||
Activation is a separate checkpoint because it is the first cross-cutting
|
||||
change. It will add the page catalogue, custom `SettingPage` adapter, scoped
|
||||
imperative lifetime, renderer-neutral refresh operation, declarative control
|
||||
read and write overrides, and non-empty definitions on Obsidian 1.13 or later.
|
||||
|
||||
The non-empty definition array replaces `display()` completely. Partial
|
||||
activation is therefore not safe: all 12 existing pages must enter the native
|
||||
catalogue together. Advanced is the only page represented by native groups in
|
||||
this stage. The other 11 pages use their existing pane renderers inside lazy
|
||||
custom pages. Obsidian versions before 1.13 retain the complete imperative
|
||||
renderer and its menu.
|
||||
|
||||
The existing rebuild-required action remains available while navigating native
|
||||
pages. Custom pages render the established action at their page boundary, and
|
||||
the Advanced definition includes an equivalent action item whose visibility is
|
||||
derived from the same dirty-state predicate. Both forms call the existing
|
||||
`confirmRebuild()` owner rather than introducing another apply workflow.
|
||||
|
||||
This stage necessarily touches direct `display()` callers, saved-handler
|
||||
ownership, and cleanup for Svelte and markdown content. It does not expand
|
||||
`SettingSpec` to absorb those concerns merely to make activation appear
|
||||
smaller.
|
||||
|
||||
### Stage C2: improve search coverage selectively
|
||||
|
||||
After activation, an individual custom page may be replaced with native groups,
|
||||
actions, and rendered rows where the existing panel boundary maps cleanly to
|
||||
Obsidian's definitions. The bounded improvement converts the General and
|
||||
Logging controls and the simple Quick Setup actions, then organises Appearance,
|
||||
Logging, and Extra menus as child pages of General Settings. It also removes
|
||||
the now-misleading Setup child page and assigns its remaining responsibilities
|
||||
to their existing owners: Extra menus, Maintenance, and Help and
|
||||
troubleshooting. Further conversions remain optional follow-up work rather than
|
||||
a condition of Stage C1. Complex workflows may remain custom pages indefinitely.
|
||||
|
||||
Stage C2 must not introduce a general action or lifecycle language. Each page
|
||||
conversion should be justified by useful settings-search coverage and retain
|
||||
the catalogue, persistence owner, and refresh boundaries established by Stage
|
||||
C1.
|
||||
|
||||
### Centralise initialisation for settings pending application
|
||||
|
||||
Settings which cannot take effect safely through immediate persistence remain
|
||||
in the settings tab's editing buffer. Their Apply action delegates to a focused
|
||||
`SetupManager` dialogue which asks whether the next start should use existing
|
||||
synchronisation data or the files in the current Vault. `SetupManager` reports
|
||||
user cancellation and validation or reservation failure distinctly instead of
|
||||
collapsing them into a boolean setup outcome. A settings-persistence exception
|
||||
still propagates after `Rebuilder` removes the reserved flag.
|
||||
|
||||
`Rebuilder.scheduleFetch()` and `Rebuilder.scheduleRebuild()` are the only
|
||||
owners of the corresponding flag files. They reserve the next-start operation
|
||||
before the callback persists the edited settings, remove the flag if that
|
||||
callback fails, and request the restart only after preparation succeeds. The
|
||||
settings tab must not write those flag files or request the restart directly.
|
||||
|
||||
A user cancellation returns to a separate confirmation which offers either to
|
||||
keep editing or to apply the settings without initialisation. This preserves
|
||||
the former advanced fallback without presenting it as an equal data-source
|
||||
choice. A validation or flag-reservation failure is not a cancellation and
|
||||
must not offer that bypass. The pending action is present on the native root
|
||||
settings page as well as within custom and native child pages.
|
||||
|
||||
## Verification
|
||||
|
||||
Stage A runs the maintained onboarding E2E scenario and an ordinary settings
|
||||
navigation scenario. A source check confirms that no old wizard event, state,
|
||||
class, or message consumer remains.
|
||||
|
||||
Stage B focused unit tests verify:
|
||||
|
||||
- only explicitly listed Advanced controls become specifications;
|
||||
- synthetic `OnDialogSettings` keys cannot be standard specifications;
|
||||
- control type, options, defaults, validation, metadata, and visibility map
|
||||
consistently to the legacy and native representations; and
|
||||
- rendering the Advanced specifications through `LiveSyncSetting` preserves
|
||||
the current save behaviour.
|
||||
|
||||
Stage C1 and the landing-page focused unit tests verify:
|
||||
|
||||
- the page catalogue contains all 13 pane-based destinations with stable, unique
|
||||
identifiers and names;
|
||||
- Appearance, Logging, Extra menus, and Advanced are native-items child pages,
|
||||
and ten child pages retain custom factories;
|
||||
- inactive and active configurations use their specified landing-page order;
|
||||
- Remote Configuration and Sync Settings remain native navigable pages inside
|
||||
the separate Synchronisation group;
|
||||
- maintenance, extra features, advanced settings, and help have explicit page
|
||||
groups, and the old Setup child page is absent;
|
||||
- all eight General and Logging controls are registered once, with their
|
||||
existing conditional visibility;
|
||||
- the three Extra menus controls are registered once and refresh page
|
||||
visibility after persistence;
|
||||
- every standard setting key is registered once;
|
||||
- a setting pending application exposes its Apply action on the native root
|
||||
page;
|
||||
- cancelling the initialisation choice preserves the editing buffer, while the
|
||||
separately confirmed settings-only path persists it;
|
||||
- Fetch and Rebuild reserve their flag through `Rebuilder` before pending
|
||||
settings are persisted, and a reservation failure leaves them unapplied;
|
||||
- reads use the editing buffer;
|
||||
- writes use `saveSettings([key])` and never `plugin.settings`;
|
||||
- custom pages dispose their page-owned resources and do not duplicate saved
|
||||
handlers when reopened; and
|
||||
- importing and opening the imperative fallback does not evaluate or require
|
||||
`SettingPage` on Obsidian before 1.13.
|
||||
|
||||
Real-Obsidian verification on 1.13 or later confirms:
|
||||
|
||||
- the common landing controls and actions render before native page navigation;
|
||||
- the Quick Setup action opens the maintained onboarding dialogue;
|
||||
- Remote Configuration remains visible without initial scrolling in mobile
|
||||
test mode;
|
||||
- native page navigation opens every remaining child page;
|
||||
- Advanced controls appear in global settings search;
|
||||
- Advanced values persist and are restored after reopening settings;
|
||||
- CouchDB-dependent controls and Advanced-mode visibility update correctly;
|
||||
- a representative custom page, including its cleanup, still works;
|
||||
- page and catalogue refreshes preserve native navigation and the
|
||||
rebuild-required action; and
|
||||
- no duplicate save, update handler, or saved-setting effect occurs after
|
||||
leaving and reopening a page.
|
||||
|
||||
The maintained real-Obsidian settings scenario also changes a setting which
|
||||
remains pending until initialisation, captures the source-choice and
|
||||
settings-only fallback dialogues, and confirms that keeping the setting
|
||||
pending does not persist it. It mounts the P2P variant directly to confirm that
|
||||
it offers a source device and local Vault preparation without presenting a
|
||||
central-server overwrite operation.
|
||||
|
||||
Because the manifest continues to support earlier Obsidian versions, a
|
||||
pre-1.13 real-runtime smoke test must confirm that the imperative fallback
|
||||
still opens, navigates, saves one Advanced value, and opens one custom page. If
|
||||
the maintained E2E runner cannot install that runtime, the exact manual version
|
||||
and procedure must be recorded before the implementation is merged.
|
||||
|
||||
The current real-Obsidian runner defaults to Obsidian 1.12.7, so it owns the
|
||||
fallback smoke path. The declarative path uses a separately installed
|
||||
1.13-or-later AppImage selected through `OBSIDIAN_BINARY` and `OBSIDIAN_CLI`.
|
||||
`E2E_OBSIDIAN_SETTINGS_ONLY=true` limits that run to the settings contract so
|
||||
the same scenario can validate a second Obsidian runtime without repeating its
|
||||
unrelated compatibility-review and mobile-layout coverage.
|
||||
|
||||
Existing E2E scenarios must use one shared settings-page navigation helper.
|
||||
That helper uses the current `.sls-setting-menu-btn` contract on the legacy
|
||||
runtime and accessible native page names on 1.13 or later. Individual scenarios
|
||||
must not duplicate version checks or retain selectors for a menu which the
|
||||
declarative renderer does not create.
|
||||
|
||||
The initial Stage C1 implementation was exercised against the official Obsidian
|
||||
1.13.4 arm64 AppImage with SHA-256
|
||||
`20d0b13c6d40bb3d7e73d9b4be6d2e21dfcc145b2106a747d0c1b81e651dabfe`.
|
||||
That run opened all 12 pages from the native page catalogue, found the Advanced
|
||||
control through global settings search, persisted a numeric value on Enter,
|
||||
and restored it after the settings dialogue was closed and reopened. The
|
||||
complete default scenario also passed on Obsidian 1.12.7, including
|
||||
compatibility review, mobile layout, imperative page navigation, and immediate
|
||||
persistence of the same Advanced value. The shared E2E navigator owns both the
|
||||
separate settings renderer used by Obsidian 1.13 and the legacy
|
||||
`.sls-setting-menu-btn` interface.
|
||||
|
||||
The Stage C2 landing composition was then exercised on Obsidian 1.13.4. With
|
||||
synchronisation inactive, the real interface rendered Quick Setup, a separate
|
||||
Synchronisation group containing Remote Configuration and Sync Settings, and a
|
||||
General Settings group containing Appearance, Logging, and Extra menus in the
|
||||
specified order. It opened all 14 nested settings pages, found the Advanced
|
||||
control through global settings search, and restored its saved value after
|
||||
reopening settings. In mobile test mode, Remote Configuration remained inside
|
||||
the initial viewport below the two Quick Setup actions and the Synchronisation
|
||||
heading. The complete scenario also passed with the same bundle on Obsidian
|
||||
1.12.7, confirming that the imperative fallback retained its navigation and
|
||||
save behaviour.
|
||||
|
||||
## Expansion Checkpoints
|
||||
|
||||
Review the scope with the maintainer before any implementation adds one of the
|
||||
following:
|
||||
|
||||
- a generic representation of actions, confirmations, rebuilds, or service
|
||||
lifecycles;
|
||||
- staged multi-setting transactions in `SettingSpec`;
|
||||
- a replacement for the current onboarding workflow;
|
||||
- a Commonlib setting metadata contract change;
|
||||
- a minimum Obsidian version increase; or
|
||||
- further conversion of Remote Configuration, Hatch, Maintenance, Help, or the
|
||||
Svelte-based Selector controls.
|
||||
|
||||
These may become worthwhile after the first proof, but none is required to
|
||||
establish a shared standard-control model and native settings search.
|
||||
|
||||
## Alternatives Rejected
|
||||
|
||||
### Return an empty definition array
|
||||
|
||||
This can satisfy a syntactic lint check while retaining `display()`, but it
|
||||
does not add native settings search or prove a migration path.
|
||||
|
||||
### Generate every setting from Commonlib setting metadata
|
||||
|
||||
Metadata does not define current page membership, control type, options,
|
||||
visibility, save policy, or whether a compatibility key should be exposed.
|
||||
Automatic generation would expose settings which the current interface omits.
|
||||
|
||||
### Teach `LiveSyncSetting` to run against a simulated DOM
|
||||
|
||||
The existing class is a renderer with direct component and element access.
|
||||
Making it emulate declarative output would preserve its mixed responsibilities
|
||||
and make the new API depend on implementation details of the old one.
|
||||
|
||||
### Model every pane before adopting the API
|
||||
|
||||
This would require a general action and lifecycle language for approximately
|
||||
50 buttons, several dynamic lists, five Svelte-based regular-expression
|
||||
controls, and multiple recovery workflows. The resulting framework would be
|
||||
larger than the standard-control problem it is intended to solve.
|
||||
|
||||
## References
|
||||
|
||||
- [Migrate to declarative settings](https://docs.obsidian.md/plugins/guides/migrate-declarative-settings)
|
||||
- Obsidian `PluginSettingTab`, `SettingDefinitionItem`, and `SettingPage` type
|
||||
declarations from the dependency version locked by this repository
|
||||
@@ -0,0 +1,179 @@
|
||||
# Architectural Decision Record: P2P Transport Compatibility Controls
|
||||
|
||||
## Status
|
||||
|
||||
Accepted — the user-facing controls will be introduced in stages. This record defines their boundaries before Commonlib settings and LiveSync interfaces are changed.
|
||||
|
||||
## Context
|
||||
|
||||
WebRTC connectivity depends on both devices, their browsers or embedded WebViews, NAT behaviour, carrier networks, VPNs, firewalls, and the path between them. A configuration which works on desktop Wi-Fi may fail on a mobile carrier, and moving the same devices through a mesh VPN may change the result without changing LiveSync.
|
||||
|
||||
The current P2P transport has several relevant properties:
|
||||
|
||||
- Trystero supplies the Nostr signalling strategy and the browser-owned WebRTC connection.
|
||||
- Commonlib limits one RPC wire payload to 15,360 bytes so it remains below Trystero's own action-chunk boundary.
|
||||
- Trystero supplies ordinary STUN servers and accepts an optional TURN server list with one username and credential.
|
||||
- ICE chooses a direct, server-reflexive, or TURN-relayed path automatically.
|
||||
- Commonlib can collect raw WebRTC statistics, but LiveSync does not yet present the selected candidate route in a concise diagnostic result.
|
||||
|
||||
Issue reports suggest that reducing the application payload may improve some mobile and constrained-network paths. A VPN such as Tailscale may also turn an unreliable route into a reliable one. These observations are consistent with NAT, path-MTU, fragmentation, or intermediary behaviour, but they do not prove one universal cause. Browser WebRTC implementations retain responsibility for SCTP, DTLS, ICE, packetisation, congestion control, and retransmission.
|
||||
|
||||
One low-level number cannot represent all of these concerns. Users need a small set of meaningful compatibility choices, while transport-internal controls which cannot be selected safely should remain implementation details.
|
||||
|
||||
## Decision
|
||||
|
||||
### Message-size presets
|
||||
|
||||
LiveSync will expose a `P2P message size` choice with four presets:
|
||||
|
||||
| Label | Maximum RPC wire payload | Intended use |
|
||||
| ----------------------- | -----------------------: | ------------------------------------------------------------------------------- |
|
||||
| `Standard` | 15,360 bytes | Existing default and best throughput. |
|
||||
| `Reduced` | 2,048 bytes | First compatibility step for an unreliable path. |
|
||||
| `Conservative` | 1,024 bytes | Stronger compatibility at greater framing and processing cost. |
|
||||
| `Maximum compatibility` | 800 bytes | Most conservative offered value for paths suspected of dropping larger packets. |
|
||||
|
||||
This value limits Commonlib RPC wire payloads before Trystero applies its own framing. It is not a LiveSync file Chunk size, an IP MTU, an SCTP fragment size, or a guarantee that lower layers will avoid fragmentation. The smaller presets reduce the amount presented to the transport at once and trade throughput for compatibility.
|
||||
|
||||
The bound applies to outgoing messages. A device which only lowers its own value still receives messages produced under the sender's value. The selected preset therefore belongs to the P2P profile and is included in an encrypted Setup URI for additional devices. A device which was configured earlier must be changed separately; the interface and troubleshooting guidance must state that the same conservative preset should be selected on every participating device. An absent key preserves the current 15,360-byte default.
|
||||
|
||||
Automatic negotiation or fallback between presets is deferred. A failed ordered data channel may require connection replacement before a smaller retry can prove anything, and changing transport parameters during a replication session would broaden the lifecycle contract considerably. The first implementation remains explicit, stable for one room lifetime, and inspectable.
|
||||
|
||||
### Connection path
|
||||
|
||||
LiveSync will expose a separate `Connection path` choice:
|
||||
|
||||
- `Automatic` retains normal ICE selection and is the default.
|
||||
- `TURN relay only` supplies `iceTransportPolicy: 'relay'` and prevents direct or server-reflexive candidates from being selected.
|
||||
|
||||
`TURN relay only` is enabled only when at least one syntactically valid `turn:` or `turns:` URL is configured. If the last valid TURN URL is removed while relay-only mode is selected, the dialogue restores `Automatic` and displays a concise explanation.
|
||||
|
||||
The route policy is an ordinary P2P profile property. It is retained in P2P connection strings and encrypted Setup URIs so that an imported compatibility profile has reproducible transport behaviour.
|
||||
|
||||
Multiple P2P profiles may intentionally use the same Group ID, passphrase, and relay list while selecting different compatibility settings. For example, one profile may use `Standard` and `Automatic`, while another uses `Maximum compatibility` and `TURN relay only`. Only the selected P2P profile joins the group, so each device can select the profile appropriate to its current network without a separate device-local override system.
|
||||
|
||||
No `Direct only` choice will be added. `Automatic` already prefers viable non-relayed candidates, and preventing TURN fallback would mainly create another failure mode.
|
||||
|
||||
### TURN server presentation
|
||||
|
||||
The first settings revision retains the existing storage and dialogue contract of one comma-separated TURN URL list, one username, and one credential. The connection-path choice is presented separately under `Connection compatibility`, while the TURN values remain under `Advanced Settings`.
|
||||
|
||||
A future interface may present the existing comma-separated value as ordered `turn:` and `turns:` URL rows without changing its serialised representation. A structured list of multiple credential profiles is deferred until a provider or self-hosted use case requires different credentials in the same P2P profile.
|
||||
|
||||
Static long-term credentials are the supported first stage. Managed providers may return short-lived credentials, but LiveSync must not store a provider API token or a Coturn shared authentication secret. A future managed-credential design needs a separately trusted HTTPS endpoint, expiry handling, refresh behaviour, failure reporting, and a clear Setup URI policy. It is not represented as another static password field.
|
||||
|
||||
### TURN allocation check and route diagnostics
|
||||
|
||||
A future `Test TURN server` action should create a disposable WebRTC check with `iceTransportPolicy: 'relay'`, request candidate gathering, and require at least one relay candidate. It must not read a Vault, join a LiveSync P2P room, or claim that document synchronisation has succeeded.
|
||||
|
||||
Where the browser exposes the evidence, the result should report:
|
||||
|
||||
- whether a relay candidate was gathered;
|
||||
- the TURN URL used for that candidate;
|
||||
- UDP, TCP, or TLS transport; and
|
||||
- a bounded failure or inconclusive result.
|
||||
|
||||
Ordinary P2P diagnostics should later summarise the selected candidate pair as direct, server-reflexive, or relayed, with its transport. Raw `getStats()` output remains supporting evidence rather than the primary interface.
|
||||
|
||||
### Placement and defaults
|
||||
|
||||
These controls belong inside `P2P Configuration` under a `Connection compatibility` section. They do not require the repository-wide Advanced, Power User, or Edge Case modes. P2P itself remains a supported opt-in feature.
|
||||
|
||||
Existing profiles retain the following defaults:
|
||||
|
||||
- `P2P message size`: `Standard`;
|
||||
- `Connection path`: `Automatic`; and
|
||||
- TURN credentials and URLs: unchanged.
|
||||
|
||||
Settings which replace a room continue to use the established P2P room and transport lifecycle. No new reconnect interval, handshake timeout, keepalive interval, trickle-ICE, candidate-pool, data-channel reliability, or backpressure setting is exposed.
|
||||
|
||||
## Self-hosted TURN example
|
||||
|
||||
The repository supplies an optional Coturn Compose example under `docker/coturn/`. It uses a versioned upstream `coturn/coturn` image rather than maintaining another LiveSync Dockerfile.
|
||||
|
||||
The example deliberately covers one small static-credential deployment:
|
||||
|
||||
- Linux host networking, which avoids Docker's large port-range forwarding cost;
|
||||
- TURN over UDP and TCP on port 3478;
|
||||
- a bounded UDP relay port range;
|
||||
- explicit long-term credentials;
|
||||
- an explicit public IPv4 address;
|
||||
- no TLS or DTLS in the starter configuration; and
|
||||
- restrictions which prevent relaying to common private IPv4 ranges.
|
||||
|
||||
The starter does not recommend `turns:` on port 443. It conflicts with an HTTPS entry point which already owns the same IP address and TCP port, including the bundled CouchDB Caddy profile. When a restrictive network requires this path, the preferred deployment uses a separate TURN host or public IP address.
|
||||
|
||||
An outbound tunnel used for CouchDB may leave the host's public port 443 available when TURN uses a separate DNS record which resolves directly to that host, but the tunnel itself cannot carry TURN traffic. A layer-4 TLS router can also own the shared port and select separate CouchDB and TURN backends by SNI. That alternative adds certificate and routing responsibilities, depends on the intended TURN clients supplying usable SNI, and is outside the supplied Compose example. The standard Caddy image used by the CouchDB profile does not provide that layer-4 routing.
|
||||
|
||||
TURN over TLS is not HTTP and must reach Coturn directly or through a compatible layer-4 proxy. Supporting it also adds private-key, renewal, privileged-port, and real-network verification responsibilities.
|
||||
|
||||
The Compose example is not a hosted service supplied by the project, an availability guarantee, or a substitute for firewall and abuse controls. Operators remain responsible for DNS, certificates when enabled, port forwarding, bandwidth, quotas, monitoring, software updates, credential rotation, and legal or provider constraints.
|
||||
|
||||
## Security and privacy
|
||||
|
||||
TURN relays the already encrypted WebRTC connection. A TURN operator cannot read LiveSync's end-to-end encrypted Vault contents, but can observe endpoint addresses, timing, traffic volume, and service credentials.
|
||||
|
||||
Static credentials allow use of the operator's bandwidth until they are changed. They should be unique, high entropy, and limited to the intended deployment. Setup URIs are encrypted but still contain the P2P connection profile; they and their separate passphrases must be protected.
|
||||
|
||||
The Coturn Docker example uses environment interpolation for its static credential. A local Docker administrator can inspect the resulting container arguments and already has equivalent control of that host. The `.env` file remains untracked and should be readable only by the operator.
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
### Expose a free-form byte field
|
||||
|
||||
Most users cannot infer a safe application payload from a network MTU, and an arbitrary value makes reports difficult to compare. Four named presets provide a bounded troubleshooting ladder.
|
||||
|
||||
### Apply the smaller payload only on the affected mobile device
|
||||
|
||||
The bound controls outgoing messages. This would leave larger messages from another sender unchanged and could fail during the direction which matters most for an initial fetch.
|
||||
|
||||
### Force TURN whenever a TURN server is configured
|
||||
|
||||
TURN is normally a fallback. Forcing it by default adds latency and bandwidth cost, and exposes more connection metadata even when a direct path works.
|
||||
|
||||
### Store the connection path in a device-local overlay
|
||||
|
||||
A second layer of device-specific profile overrides would make imported profile behaviour less reproducible and add another identity, mapping, and lifecycle contract. Separate named P2P profiles already let each device select an explicit transport policy, including when those profiles share the same Group ID and credentials.
|
||||
|
||||
### Automatically decrease the payload after a transfer failure
|
||||
|
||||
A transfer failure does not identify message size as the cause. Reusing a possibly wedged ordered channel would also make the retry inconclusive, while rebuilding the connection expands the lifecycle and user-notification design.
|
||||
|
||||
### Add browser-specific defaults
|
||||
|
||||
Safari, mobile Safari, Chrome, and Chrome on Android use different platform WebRTC implementations and lifecycle policies, but the failing route also depends on both networks and the remote peer. There is not enough stable evidence for a browser-name heuristic. Explicit cross-platform presets are more predictable.
|
||||
|
||||
### Build and maintain a LiveSync Coturn image
|
||||
|
||||
The upstream project already publishes a multi-platform image and documents its configuration contract. A local Dockerfile would duplicate security updates and release work without adding a LiveSync-specific server component.
|
||||
|
||||
### Bundle a shared port-443 router
|
||||
|
||||
A layer-4 TLS router could share one public address between distinct CouchDB and TURN hostnames by inspecting SNI. Bundling that topology would replace the current Caddy ownership of port 443, add another certificate and routing lifecycle, and rely on the intended TURN clients presenting usable SNI. A separate TURN host or public IP address keeps those failure and ownership boundaries explicit.
|
||||
|
||||
## Verification
|
||||
|
||||
The first implementation stage must add focused tests before production changes:
|
||||
|
||||
- settings-schema defaults for absent keys;
|
||||
- P2P connection-string and Setup URI round trips which retain both transport compatibility settings;
|
||||
- compatibility parsing and serialisation of the existing TURN URL string;
|
||||
- mapping each message-size preset to the exact Commonlib wire bound;
|
||||
- mapping relay-only mode to `iceTransportPolicy: 'relay'`;
|
||||
- rejection or automatic reset of relay-only mode without a valid TURN URL;
|
||||
- room replacement after either effective transport setting changes; and
|
||||
- the real Obsidian dialogue, profile, and connection-string round trip.
|
||||
|
||||
The future TURN allocation action requires its own focused tests using injected WebRTC boundaries, followed by a real transport test only for the device- or network-owned behaviour which deterministic injection cannot prove.
|
||||
|
||||
The Coturn example is checked independently with `docker compose config`. Runtime verification uses a real Coturn allocation from outside the server network and confirms both UDP and TCP client paths before it is presented as a known-working deployment.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Users gain a small compatibility ladder without learning WebRTC internals.
|
||||
- A conservative message size affects throughput wherever it is selected or imported, and must be applied to every participating device to protect all transfer directions.
|
||||
- Profiles may intentionally share the same P2P group identity while offering different transport compatibility choices; the selected profile determines the active connection behaviour.
|
||||
- TURN can be forced for diagnosis or hostile networks without making relay use the global default.
|
||||
- Static and managed TURN credentials have separate, explicit responsibility boundaries.
|
||||
- Browser-specific heuristics, automatic payload fallback, and low-level transport knobs remain out of scope.
|
||||
- A reproducible self-hosted starter is available without making LiveSync responsible for a separate TURN image.
|
||||
+11
@@ -44,6 +44,17 @@ Both settings contain server addresses, but they are not interchangeable.
|
||||
|
||||
A TURN provider cannot read LiveSync's encrypted Vault contents, but it can observe connection metadata and traffic volume. Use a provider you trust. The project does not operate an official TURN service.
|
||||
|
||||
## Connection compatibility profiles
|
||||
|
||||
`P2P Configuration` includes a separate `Connection compatibility` section. Its defaults preserve the existing transport behaviour:
|
||||
|
||||
- **P2P message size** defaults to **Standard**. **Reduced**, **Conservative**, and **Maximum compatibility** progressively limit outgoing P2P messages when a network path appears to drop larger WebRTC messages. This is not a Vault Chunk size or an IP MTU. Smaller values add framing and processing overhead.
|
||||
- **Connection path** defaults to **Automatic**, which lets WebRTC select a viable direct or TURN-relayed path. **TURN relay only** forces the encrypted connection through TURN and is available only when the profile contains at least one valid `turn:` or `turns:` URL.
|
||||
|
||||
The sending device controls its outgoing message size. Select the same conservative preset on every device which may send across the constrained path. Existing devices do not receive the choice retrospectively merely because another device changed it.
|
||||
|
||||
Both compatibility choices belong to the saved P2P profile and are retained in P2P connection strings and encrypted Setup URIs. Separate profiles may use the same Group ID, passphrase, and relay list while selecting different compatibility choices. Only the selected P2P profile joins the group.
|
||||
|
||||
## P2P Status
|
||||
|
||||
The **P2P Status** pane is the current Obsidian interface for P2P connections.
|
||||
|
||||
@@ -2,6 +2,48 @@
|
||||
|
||||
This document contains earlier published releases from the 1.0 line of the [current Self-hosted LiveSync release history](../../updates.md). Beta and release-candidate builds published before 1.0.0 are recorded in the [1.0 preview history](1.0-previews.md). Earlier release lines continue in the [0.25 history](0.25.md) and the [legacy history](legacy.md).
|
||||
|
||||
## 1.0.14
|
||||
|
||||
14th August, 2026
|
||||
|
||||
Thank you for your patience. At last, it looks as though we can clear some of the Community Review warnings.
|
||||
|
||||
### Synchronisation and storage
|
||||
|
||||
#### Fixed
|
||||
|
||||
- CouchDB operations which run to completion now close their temporary remote database connections after use across both Commonlib and LiveSync, including Setup Wizard and settings probes, command-line milestone verification, database maintenance, Security Seed refreshes, status queries, and retry and error paths (Commonlib PR #112).
|
||||
- This covers every temporary connection currently identified as a possible contributor to the long-running resource growth tracked in #1034. Validation over extended sessions is continuing.
|
||||
- Thank you to @apple-ouyang for the contribution!
|
||||
|
||||
## 1.0.13
|
||||
|
||||
13th August, 2026
|
||||
|
||||
### Conflict handling and recovery
|
||||
|
||||
#### Improved
|
||||
|
||||
- **Inspect conflicts and file/database differences** now reports local Metadata whose stored document ID does not match the ID derived from its recorded path. Ordinary scans leave unresolved entries and their corresponding Vault paths unchanged, while allowing consistently addressed Metadata for the same logical path to proceed normally.
|
||||
- When the current winner has no conflict leaves and has an unambiguous target, its wrench menu can repair that one local Metadata document after separate confirmation. The target is written and verified before the mismatched source ID is removed; ambiguous or otherwise unsafe entries remain read-only.
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Fast Fetch now writes deletion tombstones to the local database without attempting to decrypt them. A tombstone has no encrypted payload, and decryption previously aborted the whole fetch at the first deleted document. New devices could not complete their initial sync on vaults that contain old deletions (Commonlib PR #108).
|
||||
- Thank you to @KennethLloyd for the contribution!
|
||||
|
||||
## 1.0.12
|
||||
|
||||
11th August, 2026
|
||||
|
||||
### Synchronisation and storage
|
||||
|
||||
#### Fixed
|
||||
|
||||
- One-shot CouchDB replication now closes its temporary remote database after each run and before retrying, preventing inactive PouchDB instances from accumulating during long-running periodic synchronisation (Commonlib PR #75). Thank you to @apple-ouyang for the contribution!
|
||||
- Start-up and recovery scans now keep failed database-to-Vault writes retryable instead of recording them as successful and later mistaking the still-missing file for a local deletion (Commonlib PR #106).
|
||||
- Files over the size limit or in conflict remain deliberately skipped, while actual write failures are reported to Fast Setup, CLI mirror, and daemon callers.
|
||||
|
||||
## 1.0.11
|
||||
|
||||
9th August, 2026
|
||||
|
||||
+66
-16
@@ -4,6 +4,19 @@ NOTE: This document not completed. I'll improve this doc in a while. but your co
|
||||
|
||||
There are many settings in Self-hosted LiveSync. This document describes each setting in detail (not how-to). Configuration and settings are divided into several categories and indicated by icons. The icon is as follows:
|
||||
|
||||
On Obsidian 1.13 or later, the root settings page is organised by task. When synchronisation is inactive, **Quick Setup** appears first. Once any synchronisation mode is active, **Synchronisation** and **General Settings** move ahead of **Quick Setup**. **Set up other devices** appears after this plug-in has been configured. Earlier supported Obsidian versions retain a pane-based interface with the same controls.
|
||||
|
||||
| Icon | Root group | Contents or availability |
|
||||
| :--: | ------------------------ | ------------------------------------------------------------- |
|
||||
| 🧙♂️ | Quick Setup | Setup URI, onboarding, and enable actions |
|
||||
| 🔄 | Synchronisation | Remote Configuration and Sync Settings |
|
||||
| ⚙️ | General Settings | Appearance, Logging, and Extra menus |
|
||||
| 📲 | Set up other devices | Copy a Setup URI or show its QR code after configuration |
|
||||
| 🛠️ | Maintenance and recovery | Maintenance and Hatch |
|
||||
| 🧩 | Extra features | Selector and Customisation sync when advanced features appear |
|
||||
| 🔧 | Advanced settings | Advanced, Power users, and Patches when their modes appear |
|
||||
| ℹ️ | Help and information | Help and troubleshooting, and Change Log |
|
||||
|
||||
## Feature maturity for 1.0
|
||||
|
||||
The following status applies to optional and compatibility features in the 1.0 line:
|
||||
@@ -15,10 +28,32 @@ The following status applies to optional and compatibility features in the 1.0 l
|
||||
| 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. |
|
||||
|
||||
### Apply changes which require initialisation
|
||||
|
||||
Some compatibility settings are not saved immediately. They remain pending
|
||||
until **Apply** is selected. The Apply action remains visible on the root
|
||||
settings page and on the relevant child page. The following dialogue asks which
|
||||
existing data should be used after restarting:
|
||||
|
||||
- **Reset Synchronisation on This Device** reconstructs this device's local
|
||||
database from the configured remote. For P2P, an online source device is
|
||||
selected after restart.
|
||||
- **Overwrite Server Data with This Device's Files** reconstructs the local and
|
||||
remote databases from this Vault. The P2P equivalent prepares only this
|
||||
device from its current Vault files.
|
||||
- **Review another way to apply these settings** returns to a separate choice
|
||||
between keeping the changes pending and applying them without initialisation.
|
||||
Applying them alone is an advanced compatibility fallback and can make the
|
||||
device incompatible with its existing synchronisation data.
|
||||
|
||||
LiveSync reserves the selected next-start operation before saving the pending
|
||||
settings. If validation or that reservation fails, the settings remain
|
||||
unapplied and the settings-only fallback is not offered.
|
||||
|
||||
| Icon | Description |
|
||||
| :--: | ------------------------------------------------------------------ |
|
||||
| 💬 | [0. Change Log](#0-change-log) |
|
||||
| 🧙♂️ | [1. Setup](#1-setup) |
|
||||
| 🧙♂️ | [1. Quick Setup and Extra menus](#1-quick-setup-and-extra-menus) |
|
||||
| ⚙️ | [2. General Settings](#2-general-settings) |
|
||||
| 🛰️ | [3. Remote Configuration](#3-remote-configuration) |
|
||||
| 🔄 | [4. Sync Settings](#4-sync-settings) |
|
||||
@@ -34,17 +69,19 @@ The following status applies to optional and compatibility features in the 1.0 l
|
||||
|
||||
This pane always shows the current release history. It does not track whether a particular plug-in version has been read and does not open automatically after an ordinary update.
|
||||
|
||||
Internal database or settings compatibility reviews use a separate safety dialogue, not this pane. The dialogue explains why remote synchronisation has been paused and preserves the automatic synchronisation choices which were configured before the update. A configured Vault which was copied, restored, or opened in a new Obsidian profile can require this review because its device-local acknowledgement is not part of the Vault data. An empty local database is not accepted as evidence that it is safe to continue. An existing unconfigured Vault remains in onboarding without this synchronisation warning; its missing acknowledgement is not filled in automatically, so it is evaluated if the Vault is configured later. Closing the dialogue keeps synchronisation paused. When the detected state can be handled by the running version, the explicit resume action records the current internal database version and restores the configured behaviour. A persistent Notice and the `Review why synchronisation is paused` command reopen the review. An older installation cannot dismiss a pause caused by a newer database or settings version.
|
||||
Internal database or settings compatibility reviews use a separate safety dialogue, not this pane. After the Obsidian layout is ready, a pending review opens as **Synchronisation paused for compatibility review**. The dialogue explains why remote synchronisation has been paused and preserves the automatic synchronisation choices which were configured before the update. Closing it or selecting **Keep synchronisation paused** leaves synchronisation paused. Use the persistent Notice's **Review why** link, or run the `Review why synchronisation is paused` command, to reopen it. Opening **Change Log** does not acknowledge the review.
|
||||
|
||||
## 1. Setup
|
||||
A configured Vault which was copied, restored, or opened in a new Obsidian profile can require this review because its device-local acknowledgement is not part of the Vault data. An empty local database is not accepted as evidence that it is safe to continue. An existing unconfigured Vault remains in onboarding without this synchronisation warning; its missing acknowledgement is not filled in automatically, so it is evaluated if the Vault is configured later. When the detected state can be handled by the running version, **Resume synchronisation** records the current internal database version and restores the configured behaviour. An older installation cannot dismiss a pause caused by a newer database or settings version.
|
||||
|
||||
This pane is used for setting up Self-hosted LiveSync. There are several options to set up Self-hosted LiveSync.
|
||||
## 1. Quick Setup and Extra menus
|
||||
|
||||
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**.
|
||||
Quick Setup contains the actions used to configure Self-hosted LiveSync. On Obsidian 1.13 or later these actions appear on the root settings page. In the pane-based interface, they remain available together on the **Quick Setup** pane.
|
||||
|
||||
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, use **Rerun Onboarding Wizard** in the root **Quick Setup** group on Obsidian 1.13 or later. On earlier supported Obsidian versions, open **Self-hosted LiveSync settings** → **Quick 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.
|
||||
|
||||
### 1. Quick Setup
|
||||
### 1. Setup actions
|
||||
|
||||
Most preferred method to setup Self-hosted LiveSync. You can setup Self-hosted LiveSync with a few clicks.
|
||||
|
||||
@@ -64,22 +101,15 @@ Completing manual CouchDB, Object Storage, or P2P setup creates the correspondin
|
||||
|
||||
This button only appears when the setup was not completed. If you have completed the setup manually, you can enable LiveSync on this device by this button.
|
||||
|
||||
### 2. To setup other devices
|
||||
### 2. Set up other devices
|
||||
|
||||
#### Copy the current settings to a Setup URI
|
||||
|
||||
You can copy the current settings as a new setup URI. And this URI can be used to setup the other devices as [Use the copied setup URI](#use-the-copied-setup-uri).
|
||||
|
||||
### 3. Reset
|
||||
### 3. Extra menus
|
||||
|
||||
#### Discard existing settings and databases
|
||||
|
||||
Reset the Self-hosted LiveSync settings and databases.
|
||||
**Hazardous operation. Please be careful when using this.**
|
||||
|
||||
### 4. Enable extra and advanced features
|
||||
|
||||
To keep the set-up dialogue simple, some panes are hidden in default. You can enable them here.
|
||||
To keep the settings dialogue concise, some menus and features are hidden by default. On Obsidian 1.13 or later, enable them through **General Settings** → **Extra menus**. In the pane-based interface, the same controls appear in General Settings.
|
||||
|
||||
#### Enable advanced features
|
||||
|
||||
@@ -465,6 +495,22 @@ Setting key: P2P_turnCredential
|
||||
|
||||
The password or credential for authentication with the TURN server.
|
||||
|
||||
#### P2P message size
|
||||
|
||||
Setting key: P2P_maxWirePayloadBytes
|
||||
|
||||
This profile setting limits each outgoing Commonlib RPC message before Trystero applies its own framing. It is not a Vault Chunk size, an IP MTU, or an SCTP fragment size. The available presets are **Standard** (15,360 bytes), **Reduced** (2,048 bytes), **Conservative** (1,024 bytes), and **Maximum compatibility** (800 bytes). Smaller values trade throughput for compatibility on paths which appear to drop larger WebRTC messages.
|
||||
|
||||
The sender controls the size of its outgoing messages. Select the same conservative preset on every device which may send across the constrained path. Existing profiles without this key use **Standard**. P2P connection strings and encrypted Setup URIs retain the selected preset.
|
||||
|
||||
#### Connection path
|
||||
|
||||
Setting key: P2P_connectionPath
|
||||
|
||||
**Automatic** lets WebRTC select a viable direct or TURN-relayed path and is the default. **TURN relay only** forces `iceTransportPolicy: 'relay'` and is available only when the profile contains at least one valid `turn:` or `turns:` URL. Removing the last valid TURN URL while relay-only mode is selected restores **Automatic** and displays a Notice.
|
||||
|
||||
This choice belongs to the P2P profile and is retained in P2P connection strings and encrypted Setup URIs. Separate profiles may use the same Group ID and credentials with different compatibility choices; only the selected P2P profile is active.
|
||||
|
||||
## 4. Sync Settings
|
||||
|
||||
### 1. Synchronisation Preset
|
||||
@@ -1069,4 +1115,8 @@ Use it only when the Vault, local database, and remote are healthy, and every re
|
||||
|
||||
### 7. Reset
|
||||
|
||||
#### Discard existing settings and databases
|
||||
|
||||
Reset the Self-hosted LiveSync settings and local database. This is a hazardous operation; make a backup before using it.
|
||||
|
||||
#### Delete local database to reset or uninstall Self-hosted LiveSync
|
||||
|
||||
@@ -36,9 +36,20 @@ Try these in order:
|
||||
1. Put both devices on the same ordinary network and retry.
|
||||
2. Remove a VPN temporarily if it blocks peer traffic, or use a trusted VPN such as Tailscale when it provides a reachable path between the devices.
|
||||
3. In `P2P Configuration` -> `Advanced Settings`, configure a trusted TURN service.
|
||||
4. Under `Connection compatibility`, select `TURN relay only` to test the configured TURN path without direct ICE candidates.
|
||||
|
||||
TURN is a fallback for encrypted WebRTC traffic. It is different from the required signalling relay. The project does not operate an official TURN service. A TURN provider cannot read encrypted Vault contents, but it can observe connection metadata and traffic volume.
|
||||
|
||||
For a small self-hosted deployment, the repository includes an optional [Coturn Compose starter](../../docker/coturn/README.md). It uses static credentials and does not include TLS or a managed credential service; review its network and security boundaries before exposing it.
|
||||
|
||||
## A connection opens but a transfer stalls
|
||||
|
||||
If peers can connect but a transfer repeatedly stalls on one network path, try the `P2P message size` presets under `Connection compatibility`. Start with `Reduced`, then try `Conservative` and `Maximum compatibility` only if needed.
|
||||
|
||||
The preset limits outgoing messages, so select the same value on every device which may send across the affected path. Smaller values add overhead and do not prove that packet fragmentation was the cause. Return to `Standard` when the path works reliably without the compatibility setting.
|
||||
|
||||
Compatibility choices are saved with the P2P profile. You may keep separate standard and compatibility profiles with the same Group ID and credentials, then select the profile appropriate to the current network.
|
||||
|
||||
## A connected peer does not receive later edits
|
||||
|
||||
An open signalling connection does not automatically move every change.
|
||||
|
||||
@@ -47,6 +47,14 @@ Do not switch to P2P or reset the database as the first response. Check:
|
||||
|
||||
If the remote is healthy but one device's local database is not, use [Reset Synchronisation on This Device](recovery.md#reset-synchronisation-on-this-device) only after backing up unsynchronised local files.
|
||||
|
||||
## Synchronisation is paused for compatibility review
|
||||
|
||||
A compatibility review is separate from the Change Log. It can appear after an internal database or settings-format change, or when a configured Vault is copied, restored, or opened in a new Obsidian profile without its device-local acknowledgement.
|
||||
|
||||
The **Synchronisation paused for compatibility review** dialogue opens after the Obsidian layout is ready. If it has been closed, use the persistent Notice's **Review why** link, or run `Review why synchronisation is paused` from the command palette. Opening **Change Log** does not clear the pause.
|
||||
|
||||
Review the stated reason before continuing. When **Resume synchronisation** is available, first update every synchronising device, then use that action to record the current internal database version and restore the configured synchronisation behaviour. If the action is unavailable, the running installation is older than the recorded database or settings format. Update that installation instead of resetting the database merely to remove the warning.
|
||||
|
||||
## Files are missing or excluded
|
||||
|
||||
Check Obsidian's `Detect all file extensions`, LiveSync selectors, ignore files, file-size limits, modification-time limits, and Hidden File Sync rules. A filtered file is different from a file which reached the database but could not be reconstructed from its chunks.
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.19",
|
||||
"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",
|
||||
|
||||
Generated
+170
-280
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.19",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.19",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"src/apps/cli",
|
||||
@@ -23,8 +23,8 @@
|
||||
"@smithy/types": "^4.14.3",
|
||||
"@smithy/util-retry": "^4.4.5",
|
||||
"@vrtmrz/browser-ui-kit": "0.1.0",
|
||||
"@vrtmrz/livesync-commonlib": "0.1.17",
|
||||
"@vrtmrz/obsidian-plugin-kit": "0.1.3",
|
||||
"@vrtmrz/livesync-commonlib": "0.1.19",
|
||||
"@vrtmrz/obsidian-plugin-kit": "0.1.4",
|
||||
"@vrtmrz/ui-interactions": "0.1.2",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"fflate": "^0.8.2",
|
||||
@@ -1192,15 +1192,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
||||
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||
@@ -2321,12 +2312,6 @@
|
||||
"ret": "~0.1.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@minhducsun2002/leb128": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@minhducsun2002/leb128/-/leb128-1.0.0.tgz",
|
||||
"integrity": "sha512-eFrYUPDVHeuwWHluTG1kwNQUEUcFjVKYwPkU8z9DR1JH3AW7JtJsG9cRVGmwz809kKtGfwGJj58juCZxEvnI/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
||||
@@ -2443,128 +2428,167 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-cms": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz",
|
||||
"integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==",
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.9.4.tgz",
|
||||
"integrity": "sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.6.0",
|
||||
"@peculiar/asn1-x509": "^2.6.1",
|
||||
"@peculiar/asn1-x509-attr": "^2.6.1",
|
||||
"asn1js": "^3.0.6",
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"@peculiar/asn1-x509-attr": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-csr": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz",
|
||||
"integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==",
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.9.4.tgz",
|
||||
"integrity": "sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.6.0",
|
||||
"@peculiar/asn1-x509": "^2.6.1",
|
||||
"asn1js": "^3.0.6",
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-ecc": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz",
|
||||
"integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==",
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.9.4.tgz",
|
||||
"integrity": "sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.6.0",
|
||||
"@peculiar/asn1-x509": "^2.6.1",
|
||||
"asn1js": "^3.0.6",
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-pfx": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz",
|
||||
"integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==",
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.9.4.tgz",
|
||||
"integrity": "sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-cms": "^2.6.1",
|
||||
"@peculiar/asn1-pkcs8": "^2.6.1",
|
||||
"@peculiar/asn1-rsa": "^2.6.1",
|
||||
"@peculiar/asn1-schema": "^2.6.0",
|
||||
"asn1js": "^3.0.6",
|
||||
"@peculiar/asn1-cms": "^2.9.4",
|
||||
"@peculiar/asn1-pkcs8": "^2.9.4",
|
||||
"@peculiar/asn1-rsa": "^2.9.4",
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-pkcs8": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz",
|
||||
"integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==",
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.9.4.tgz",
|
||||
"integrity": "sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.6.0",
|
||||
"@peculiar/asn1-x509": "^2.6.1",
|
||||
"asn1js": "^3.0.6",
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-pkcs9": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz",
|
||||
"integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==",
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.9.4.tgz",
|
||||
"integrity": "sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-cms": "^2.6.1",
|
||||
"@peculiar/asn1-pfx": "^2.6.1",
|
||||
"@peculiar/asn1-pkcs8": "^2.6.1",
|
||||
"@peculiar/asn1-schema": "^2.6.0",
|
||||
"@peculiar/asn1-x509": "^2.6.1",
|
||||
"@peculiar/asn1-x509-attr": "^2.6.1",
|
||||
"asn1js": "^3.0.6",
|
||||
"@peculiar/asn1-cms": "^2.9.4",
|
||||
"@peculiar/asn1-pfx": "^2.9.4",
|
||||
"@peculiar/asn1-pkcs8": "^2.9.4",
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"@peculiar/asn1-x509-attr": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-rsa": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz",
|
||||
"integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==",
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.9.4.tgz",
|
||||
"integrity": "sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.6.0",
|
||||
"@peculiar/asn1-x509": "^2.6.1",
|
||||
"asn1js": "^3.0.6",
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-schema": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz",
|
||||
"integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==",
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.4.tgz",
|
||||
"integrity": "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asn1js": "^3.0.6",
|
||||
"pvtsutils": "^1.3.6",
|
||||
"@peculiar/utils": "^2.0.2",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-x509": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz",
|
||||
"integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==",
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.9.4.tgz",
|
||||
"integrity": "sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.6.0",
|
||||
"asn1js": "^3.0.6",
|
||||
"pvtsutils": "^1.3.6",
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/utils": "^2.0.2",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-x509-attr": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz",
|
||||
"integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==",
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.9.4.tgz",
|
||||
"integrity": "sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/utils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz",
|
||||
"integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.6.0",
|
||||
"@peculiar/asn1-x509": "^2.6.1",
|
||||
"asn1js": "^3.0.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
@@ -3001,11 +3025,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@shinyoshiaki/jspack": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@shinyoshiaki/jspack/-/jspack-0.0.6.tgz",
|
||||
"integrity": "sha512-SdsNhLjQh4onBlyPrn4ia1Pdx5bXT88G/LIEpOYAjx2u4xeY/m/HB5yHqlkJB1uQR3Zw4R3hBWLj46STRAN0rg=="
|
||||
},
|
||||
"node_modules/@smithy/abort-controller": {
|
||||
"version": "4.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz",
|
||||
@@ -3922,6 +3941,21 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/dom-mediacapture-transform": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.12.tgz",
|
||||
"integrity": "sha512-d7/QsLRwF864A5mgIM/YrfiglHoYn7zgCcAoJgW404r+2DwnNr7EBbLnCWpmOMgH8y0te73L1AV6H1bmauaWFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/dom-webcodecs": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dom-webcodecs": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz",
|
||||
"integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||
@@ -4771,9 +4805,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vrtmrz/livesync-commonlib": {
|
||||
"version": "0.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.17.tgz",
|
||||
"integrity": "sha512-5EGZOKOfoe8jUKwr7Yecyxi6SuHYtMPpWVygKTtkuC8Nf9ZayvYqd6R3VpPqtRoD/nD1KqxrbwJzUbS+qAJgHw==",
|
||||
"version": "0.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.19.tgz",
|
||||
"integrity": "sha512-sVQcVJaelm575xoopay1yUWsIlfh2Um6Oi70Jp4WLdO3vlPCoYUbCKH+zGDwU/r05BbB2xGwU+kgy6OjxyxGeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.808.0",
|
||||
@@ -4834,9 +4868,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vrtmrz/obsidian-plugin-kit": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-plugin-kit/-/obsidian-plugin-kit-0.1.3.tgz",
|
||||
"integrity": "sha512-6fsKdhFZtBv6FXlZHtSmpqwROohFzDmres6q08nr2xYGVeh2ooBGU3zJS94WN/tOjDT+wa/Vr3yE42wmI0pIZA==",
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-plugin-kit/-/obsidian-plugin-kit-0.1.4.tgz",
|
||||
"integrity": "sha512-MxZgd7UOr8DXk0e+JsAXJmYj/8bfIoEliI4ruvp1Cu4U+mmVI/p63nZ5we3dDW+EaCgRAgF+ztuVDkcMUk1DwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vrtmrz/ui-interactions": "0.1.2"
|
||||
@@ -5151,12 +5185,6 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aes-js": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz",
|
||||
"integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
@@ -5601,13 +5629,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/asn1js": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz",
|
||||
"integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==",
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz",
|
||||
"integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"pvtsutils": "^1.3.6",
|
||||
"pvutils": "^1.1.3",
|
||||
"pvutils": "^1.1.5",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6067,6 +6095,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
|
||||
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
@@ -6634,26 +6663,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -8434,6 +8448,7 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
@@ -9397,12 +9412,6 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/int64-buffer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-1.1.0.tgz",
|
||||
"integrity": "sha512-94smTCQOvigN4d/2R/YDjz8YVG0Sufvv2aAh8P5m42gwhCsDAJqnbNOrxJsrADuAFAA69Q/ptGzxvNcNuIJcvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||
@@ -9418,12 +9427,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
|
||||
"integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
@@ -10919,6 +10922,7 @@
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
@@ -11087,6 +11091,24 @@
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mediabunny": {
|
||||
"version": "1.55.2",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.55.2.tgz",
|
||||
"integrity": "sha512-EEx4O6qYddAdCyWPMZNDwI7uc5hewNHrPAf9jLcVhIbXoPsiqNQ+D9i1pfadmGkjN2V318jSrZljkpoziYm6Lg==",
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
".",
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@types/dom-mediacapture-transform": "^0.1.11",
|
||||
"@types/dom-webcodecs": "0.1.13"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/Vanilagy"
|
||||
}
|
||||
},
|
||||
"node_modules/memdown": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memdown/-/memdown-1.4.1.tgz",
|
||||
@@ -11204,12 +11226,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mp4box": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/mp4box/-/mp4box-0.5.4.tgz",
|
||||
"integrity": "sha512-GcCH0fySxBurJtvr0dfhz0IxHZjc1RP+F+I8xw+LIwkU1a+7HJx8NCDiww1I5u4Hz6g4eR1JlGADEGJ9r4lSfA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
@@ -11577,15 +11593,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/p-cancelable": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
@@ -12571,9 +12578,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pvutils": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz",
|
||||
"integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz",
|
||||
"integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@@ -12947,11 +12954,6 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rx.mini": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/rx.mini/-/rx.mini-1.4.0.tgz",
|
||||
"integrity": "sha512-8w5cSc1mwNja7fl465DXOkVvIOkpvh2GW4jo31nAIvX4WTXCsRnKJGUfiDBzWtYRInEcHAUYIZfzusjIrea8gA=="
|
||||
},
|
||||
"node_modules/sade": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||
@@ -13877,9 +13879,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check/node_modules/picomatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
||||
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -15249,137 +15251,25 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/werift": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/werift/-/werift-0.23.0.tgz",
|
||||
"integrity": "sha512-/WcIN5DHFG9Ri4anGOmIkp8gxBGFMWSIB/m4sfZ5CWlLfD3iMhiaAUuTBuc+KV3SY9NDmvmLtiN2uaM7k3lVzw==",
|
||||
"version": "0.24.4",
|
||||
"resolved": "https://registry.npmjs.org/werift/-/werift-0.24.4.tgz",
|
||||
"integrity": "sha512-NoROZ11L/ZqAB5ombCB4VBuVNdIZfwI493zlxlIX7BlwfWRy55eDJdtWMC2VX35yBXFaWwA8NtYbmJ8qWtVk9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fidm/x509": "^1.2.1",
|
||||
"@minhducsun2002/leb128": "^1.0.0",
|
||||
"@noble/curves": "^1.8.1",
|
||||
"@peculiar/x509": "^1.12.3",
|
||||
"@shinyoshiaki/binary-data": "^0.6.1",
|
||||
"@shinyoshiaki/jspack": "^0.0.6",
|
||||
"aes-js": "^3.1.2",
|
||||
"buffer": "^6.0.3",
|
||||
"debug": "4.4.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"int64-buffer": "1.1.0",
|
||||
"ip": "^2.0.1",
|
||||
"mp4box": "^0.5.3",
|
||||
"mediabunny": "^1.45.2",
|
||||
"multicast-dns": "^7.2.5",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"werift-common": "*",
|
||||
"werift-dtls": "*",
|
||||
"werift-ice": "*",
|
||||
"werift-rtp": "*",
|
||||
"werift-sctp": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/werift-common": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/werift-common/-/werift-common-0.0.3.tgz",
|
||||
"integrity": "sha512-ma3E4BqKTyZVLhrdfTVs2T1tg9seeUtKMRn5e64LwgrogWa62+3LAUoLBUSl1yPWhgSkXId7GmcHuWDen9IJeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shinyoshiaki/jspack": "^0.0.6",
|
||||
"debug": "^4.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/werift-dtls": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/werift-dtls/-/werift-dtls-0.5.7.tgz",
|
||||
"integrity": "sha512-z2fjbP7fFUFmu/Ky4bCKXzdgPTtmSY1DYi0TUf3GG2zJT4jMQ3TQmGY8y7BSSNGetvL4h3pRZ5un0EcSOWpPog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fidm/x509": "^1.2.1",
|
||||
"@noble/curves": "^1.3.0",
|
||||
"@peculiar/x509": "^1.9.2",
|
||||
"@shinyoshiaki/binary-data": "^0.6.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"lodash": "^4.17.21",
|
||||
"rx.mini": "^1.2.2",
|
||||
"tweetnacl": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/werift-ice": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/werift-ice/-/werift-ice-0.2.2.tgz",
|
||||
"integrity": "sha512-td52pHp+JmFnUn5jfDr/SSNO0dMCbknhuPdN1tFp9cfRj5jaktN63qnAdUuZC20QCC3ETWdsOthcm+RalHpFCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shinyoshiaki/jspack": "^0.0.6",
|
||||
"buffer-crc32": "^1.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"int64-buffer": "^1.0.1",
|
||||
"ip": "^2.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"multicast-dns": "^7.2.5",
|
||||
"p-cancelable": "^2.1.1",
|
||||
"rx.mini": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/werift-rtp": {
|
||||
"version": "0.8.8",
|
||||
"resolved": "https://registry.npmjs.org/werift-rtp/-/werift-rtp-0.8.8.tgz",
|
||||
"integrity": "sha512-GiYMSdvCyScQaw5bnEsraSoHUVZpjfokJAiLV4R1FsiB06t6XiebPYPpkqB9nYNNKiA8Z/cYWsym7wISq1sYSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@minhducsun2002/leb128": "^1.0.0",
|
||||
"@shinyoshiaki/jspack": "^0.0.6",
|
||||
"aes-js": "^3.1.2",
|
||||
"buffer": "^6.0.3",
|
||||
"mp4box": "^0.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/werift-rtp/node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/werift-sctp": {
|
||||
"version": "0.0.11",
|
||||
"resolved": "https://registry.npmjs.org/werift-sctp/-/werift-sctp-0.0.11.tgz",
|
||||
"integrity": "sha512-7109yuI5U7NTEHjqjn0A8VeynytkgVaxM6lRr1Ziv0D8bPcaB8A7U/P88M7WaCpWDoELHoXiRUjQycMWStIgjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shinyoshiaki/jspack": "^0.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/werift/node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
@@ -15923,7 +15813,7 @@
|
||||
},
|
||||
"src/apps/cli": {
|
||||
"name": "self-hosted-livesync-cli",
|
||||
"version": "1.0.16-cli",
|
||||
"version": "1.0.19-cli",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"minimatch": "^10.2.5",
|
||||
@@ -15938,7 +15828,7 @@
|
||||
"pouchdb-replication": "^9.0.0",
|
||||
"pouchdb-utils": "^9.0.0",
|
||||
"transform-pouch": "^2.0.0",
|
||||
"werift": "^0.23.0"
|
||||
"werift": "^0.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "5.9.3",
|
||||
@@ -15948,7 +15838,7 @@
|
||||
},
|
||||
"src/apps/webapp": {
|
||||
"name": "livesync-webapp",
|
||||
"version": "1.0.16-webapp",
|
||||
"version": "1.0.19-webapp",
|
||||
"dependencies": {
|
||||
"octagonal-wheels": "^0.1.53"
|
||||
},
|
||||
@@ -15960,7 +15850,7 @@
|
||||
}
|
||||
},
|
||||
"src/apps/webpeer": {
|
||||
"version": "1.0.16-webpeer",
|
||||
"version": "1.0.19-webpeer",
|
||||
"dependencies": {
|
||||
"octagonal-wheels": "^0.1.53"
|
||||
},
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.19",
|
||||
"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",
|
||||
@@ -178,8 +178,8 @@
|
||||
"@smithy/types": "^4.14.3",
|
||||
"@smithy/util-retry": "^4.4.5",
|
||||
"@vrtmrz/browser-ui-kit": "0.1.0",
|
||||
"@vrtmrz/livesync-commonlib": "0.1.17",
|
||||
"@vrtmrz/obsidian-plugin-kit": "0.1.3",
|
||||
"@vrtmrz/livesync-commonlib": "0.1.19",
|
||||
"@vrtmrz/obsidian-plugin-kit": "0.1.4",
|
||||
"@vrtmrz/ui-interactions": "0.1.2",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"fflate": "^0.8.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "self-hosted-livesync-cli",
|
||||
"private": true,
|
||||
"version": "1.0.16-cli",
|
||||
"version": "1.0.19-cli",
|
||||
"main": "dist/index.cjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -48,7 +48,7 @@
|
||||
"pouchdb-replication": "^9.0.0",
|
||||
"pouchdb-utils": "^9.0.0",
|
||||
"transform-pouch": "^2.0.0",
|
||||
"werift": "^0.23.0"
|
||||
"werift": "^0.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "5.9.3",
|
||||
|
||||
@@ -111,6 +111,7 @@ export function initialiseServiceModulesCLI(
|
||||
UI: services.UI,
|
||||
vault: services.vault,
|
||||
fileHandler: fileHandler,
|
||||
fileProcessing: services.fileProcessing,
|
||||
storageAccess: storageAccess,
|
||||
control: services.control,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "livesync-webapp",
|
||||
"private": true,
|
||||
"version": "1.0.16-webapp",
|
||||
"version": "1.0.19-webapp",
|
||||
"type": "module",
|
||||
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
||||
"scripts": {
|
||||
|
||||
@@ -102,6 +102,7 @@ export function initialiseServiceModulesFSAPI(
|
||||
UI: services.UI,
|
||||
vault: services.vault,
|
||||
fileHandler: fileHandler,
|
||||
fileProcessing: services.fileProcessing,
|
||||
storageAccess: storageAccess,
|
||||
control: services.control,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "webpeer",
|
||||
"private": true,
|
||||
"version": "1.0.16-webpeer",
|
||||
"version": "1.0.19-webpeer",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
type LegacyLocalDatabaseSelection = {
|
||||
useIndexedDBAdapter: boolean;
|
||||
};
|
||||
|
||||
type LegacyBulkChunkPreSendSettings = {
|
||||
sendChunksBulk: boolean;
|
||||
sendChunksBulkMaxSize: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether persisted settings select the legacy PouchDB IndexedDB adapter.
|
||||
*
|
||||
* New local databases use IDB. Existing devices must retain this operative value until their local database has
|
||||
* been explicitly migrated, so compatibility code must not treat the setting as inert.
|
||||
*/
|
||||
export function usesLegacyIndexedDBAdapter(settings: LegacyLocalDatabaseSelection): boolean {
|
||||
return settings.useIndexedDBAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the removed automatic bulk chunk pre-send option in persisted settings.
|
||||
*
|
||||
* The field remains readable only so older settings and Setup URIs can be migrated to the supported behaviour.
|
||||
*
|
||||
* @returns `true` when the legacy setting was enabled and has been changed.
|
||||
*/
|
||||
export function disableLegacyBulkChunkPreSend(settings: LegacyBulkChunkPreSendSettings): boolean {
|
||||
if (!settings.sendChunksBulk) return false;
|
||||
settings.sendChunksBulk = false;
|
||||
settings.sendChunksBulkMaxSize = 1;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { disableLegacyBulkChunkPreSend, usesLegacyIndexedDBAdapter } from "./compatibilitySettings.ts";
|
||||
|
||||
describe("compatibility settings", () => {
|
||||
it.each([true, false])("preserves the operative legacy adapter selection (%s)", (useIndexedDBAdapter) => {
|
||||
expect(usesLegacyIndexedDBAdapter({ useIndexedDBAdapter })).toBe(useIndexedDBAdapter);
|
||||
});
|
||||
|
||||
it("disables automatic bulk chunk pre-send and restores its inert size value", () => {
|
||||
const settings = { sendChunksBulk: true, sendChunksBulkMaxSize: 16 };
|
||||
|
||||
expect(disableLegacyBulkChunkPreSend(settings)).toBe(true);
|
||||
expect(settings).toEqual({ sendChunksBulk: false, sendChunksBulkMaxSize: 1 });
|
||||
});
|
||||
|
||||
it("leaves an already migrated bulk chunk setting unchanged", () => {
|
||||
const settings = { sendChunksBulk: false, sendChunksBulkMaxSize: 4 };
|
||||
|
||||
expect(disableLegacyBulkChunkPreSend(settings)).toBe(false);
|
||||
expect(settings).toEqual({ sendChunksBulk: false, sendChunksBulkMaxSize: 4 });
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,6 @@ export const EVENT_FILE_SAVED = "file-saved";
|
||||
export const EVENT_LEAF_ACTIVE_CHANGED = "leaf-active-changed";
|
||||
|
||||
export const EVENT_REQUEST_OPEN_SETTINGS = "request-open-settings";
|
||||
export const EVENT_REQUEST_OPEN_SETTING_WIZARD = "request-open-setting-wizard";
|
||||
export const EVENT_REQUEST_OPEN_SETUP_URI = "request-open-setup-uri";
|
||||
export const EVENT_REQUEST_COPY_SETUP_URI = "request-copy-setup-uri";
|
||||
export const EVENT_REQUEST_SHOW_SETUP_QR = "request-show-setup-qr";
|
||||
@@ -30,7 +29,6 @@ declare global {
|
||||
[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;
|
||||
|
||||
@@ -32,6 +32,20 @@ export const liveSyncProvisionalEnglishMessages = {
|
||||
"Learn more about signalling and TURN": "Learn more about signalling and TURN",
|
||||
"TURN relays the encrypted WebRTC connection only when a direct path cannot be established. A TURN provider cannot read encrypted Vault contents, but it can observe connection metadata and traffic volume. Use a provider you trust.":
|
||||
"TURN relays the encrypted WebRTC connection only when a direct path cannot be established. A TURN provider cannot read encrypted Vault contents, but it can observe connection metadata and traffic volume. Use a provider you trust.",
|
||||
"Connection compatibility": "Connection compatibility",
|
||||
"P2P message size": "P2P message size",
|
||||
Standard: "Standard",
|
||||
Reduced: "Reduced",
|
||||
Conservative: "Conservative",
|
||||
"Maximum compatibility": "Maximum compatibility",
|
||||
"Smaller messages can improve compatibility on paths which fragment or drop larger WebRTC messages. This setting limits outgoing P2P messages, so use a compatible profile on each sending device when required.":
|
||||
"Smaller messages can improve compatibility on paths which fragment or drop larger WebRTC messages. This setting limits outgoing P2P messages, so use a compatible profile on each sending device when required.",
|
||||
"Connection path": "Connection path",
|
||||
"TURN relay only": "TURN relay only",
|
||||
"TURN relay only is available when at least one valid TURN server URL is configured under Advanced Settings.":
|
||||
"TURN relay only is available when at least one valid TURN server URL is configured under Advanced Settings.",
|
||||
"TURN relay only requires at least one valid TURN server URL. Connection path has been restored to Automatic.":
|
||||
"TURN relay only requires at least one valid TURN server URL. Connection path has been restored to Automatic.",
|
||||
"Announce changes": "Announce changes",
|
||||
"Announce changes automatically after connecting": "Announce changes automatically after connecting",
|
||||
"When enabled, this device notifies connected peers after a local change. The notification contains no Vault data; a peer which follows this device then fetches the change through the encrypted P2P connection.":
|
||||
|
||||
@@ -4206,16 +4206,6 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "显示日志",
|
||||
"zh-tw": "顯示日誌",
|
||||
},
|
||||
"moduleMigration.docUri": {
|
||||
def: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use",
|
||||
es: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README_ES.md#how-to-use",
|
||||
fr: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use",
|
||||
he: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use",
|
||||
ja: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use",
|
||||
ko: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use",
|
||||
ru: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use",
|
||||
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",
|
||||
},
|
||||
"moduleMigration.fix0256.buttons.checkItLater": {
|
||||
def: "Check it later",
|
||||
es: "Comprobarlo más tarde",
|
||||
@@ -4451,30 +4441,6 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
"zh-tw":
|
||||
"你可能已經知道,Self-hosted LiveSync 變更了預設行為與資料庫結構。\n\n值得慶幸的是,在你的努力下,遠端資料庫似乎已經完成遷移。恭喜!\n\n不過,還需要再多做一點。此裝置的設定與遠端資料庫不相容,我們需要再次從遠端資料庫抓取。現在要再次從遠端抓取嗎?\n\n___注意:在變更設定並再次抓取資料庫之前,我們無法進行同步。___\n___注意 2:chunks 完全不可變,我們只需要抓取中繼資料與差異部分。___",
|
||||
},
|
||||
"moduleMigration.msgInitialSetup": {
|
||||
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}).",
|
||||
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}).",
|
||||
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}).",
|
||||
he: "המכשיר שלך **טרם הוגדר**. אנחנו כאן לעזור לך בתהליך ההגדרה.\n\nשים לב שניתן להעתיק את תוכן כל דיאלוג ללוח. אם צריך לחזור אליו מאוחר יותר, ניתן להדביק אותו כפתק ב-Obsidian. ניתן גם לתרגם לשפתך בעזרת כלי תרגום.\n\nראשית, האם יש לך **Setup URI**?\n\nהערה: אם אינך יודע מהו, אנא עיין ב[תיעוד](${URI_DOC}).",
|
||||
ja: "このデバイスは**まだセットアップされていません**。セットアッププロセスをご案内します。\n\nすべてのダイアログの内容はクリップボードにコピーできます。後で参照する必要があれば、Obsidianのノートに貼り付けてください。翻訳ツールを使ってお使いの言語に翻訳することもできます。\n\nまず、**セットアップURI**をお持ちですか?\n\n注意: それが何か分からない場合は、[documentation](${URI_DOC})を参照してください。",
|
||||
ko: "이 기기는 **아직 초기 설정이 완료되지 않았습니다**. 지금부터 설정 과정을 안내해 드리겠습니다.\n\n모든 대화 내용은 클립보드에 복사할 수 있습니다. 나중에 참고하려면 Obsidian 노트에 붙여넣거나 번역 도구를 활용해 번역하셔도 됩니다.\n\n먼저, **Setup URI**를 가지고 계신가요?\n\n참고: Setup URI가 무엇인지 잘 모르시겠다면 [문서](${URI_DOC})를 참고해 주세요.",
|
||||
ru: "Ваше устройство ещё не настроено. У вас есть Setup URI?",
|
||||
zh: "您的设备**尚未设置**。让我引导您完成设置过程。\n\n请记住,每个对话框内容都可以复制到剪贴板。如果以后需要参考,可以将其粘贴到 Obsidian 的笔记中。您也可以使用翻译工具将其翻译成您的语言。\n\n首先,您有**设置 URI** 吗?\n\n注意:如果您不知道这是什么,请参阅[文档](${URI_DOC})",
|
||||
"zh-tw":
|
||||
"你的裝置**尚未完成設定**。讓我引導你完成設定流程。\n\n請記得,每個對話框的內容都可以複製到剪貼簿。如果日後需要參考,可以貼到 Obsidian 的筆記中,也可以用翻譯工具翻成你的語言。\n\n首先,你有 **Setup URI** 嗎?\n\n注意:如果你不知道那是什麼,請參閱[說明文件](${URI_DOC})。",
|
||||
},
|
||||
"moduleMigration.msgRecommendSetupUri": {
|
||||
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?",
|
||||
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?",
|
||||
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 ?",
|
||||
he: "אנו ממליצים בחום לייצר Setup URI ולהשתמש בו.\nאם אין לך ידע בנושא, אנא עיין ב[תיעוד](${URI_DOC}) (מתנצלים שוב, אך זה חשוב).\n\nכיצד ברצונך להגדיר ידנית?",
|
||||
ja: "セットアップURIを生成して使用することを強くお勧めします。\nこれについて知識がない場合は、[documentation](${URI_DOC})を参照してください(重要です)。\n\n手動でセットアップしますか?",
|
||||
ko: "Setup URI를 생성해 사용하는 것을 강력히 권장합니다.\nSetup URI가 무엇인지 잘 모르시겠다면 [문서](${URI_DOC})를 참고해 주세요. 중요한 내용이니 꼭 확인하시기 바랍니다.\n\n직접 수동 설정을 진행하시겠습니까?",
|
||||
ru: "Мы рекомендуем сгенерировать Setup URI.",
|
||||
zh: "我们强烈建议您生成一个设置 URI 并使用它。\n如果您对此不了解,请参阅[文档](${URI_DOC})(再次抱歉,但这很重要)。\n\n您想如何手动设置?",
|
||||
"zh-tw":
|
||||
"我們強烈建議你產生 Setup URI 並使用它。\n如果你對此不熟悉,請參閱[說明文件](${URI_DOC})(再次抱歉,但這很重要)。\n\n你想要如何手動設定?",
|
||||
},
|
||||
"moduleMigration.msgSinceV02321": {
|
||||
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.",
|
||||
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.",
|
||||
@@ -4542,17 +4508,6 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "仅启用 #2",
|
||||
"zh-tw": "僅啟用",
|
||||
},
|
||||
"moduleMigration.optionHaveSetupUri": {
|
||||
def: "Yes, I have",
|
||||
es: "Sí, tengo",
|
||||
fr: "Oui, j'en ai une",
|
||||
he: "כן, יש לי",
|
||||
ja: "はい、持っています",
|
||||
ko: "예, 있습니다",
|
||||
ru: "Да, есть",
|
||||
zh: "是的,我有",
|
||||
"zh-tw": "有,我有",
|
||||
},
|
||||
"moduleMigration.optionKeepPreviousBehaviour": {
|
||||
def: "Keep previous behaviour",
|
||||
es: "Mantener comportamiento anterior",
|
||||
@@ -4564,17 +4519,6 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "保持以前的行为",
|
||||
"zh-tw": "保留先前的行為",
|
||||
},
|
||||
"moduleMigration.optionManualSetup": {
|
||||
def: "Set it up all manually",
|
||||
es: "Configurarlo todo manualmente",
|
||||
fr: "Tout configurer manuellement",
|
||||
he: "הגדר הכל ידנית",
|
||||
ja: "すべて手動でセットアップ",
|
||||
ko: "모든 것을 수동으로 설정",
|
||||
ru: "Настроить всё вручную",
|
||||
zh: "全部手动设置",
|
||||
"zh-tw": "全部手動設定",
|
||||
},
|
||||
"moduleMigration.optionNoAskAgain": {
|
||||
def: "No, please ask again",
|
||||
es: "No, por favor pregúntame de nuevo",
|
||||
@@ -4586,50 +4530,6 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "不,请稍后再次询问",
|
||||
"zh-tw": "不,請稍後再問我",
|
||||
},
|
||||
"moduleMigration.optionNoSetupUri": {
|
||||
def: "No, I do not have",
|
||||
es: "No, no tengo",
|
||||
fr: "Non, je n'en ai pas",
|
||||
he: "לא, אין לי",
|
||||
ja: "いいえ、持っていません",
|
||||
ko: "아니요, 없습니다",
|
||||
ru: "Нет, нет",
|
||||
zh: "不,我没有",
|
||||
"zh-tw": "沒有,我沒有",
|
||||
},
|
||||
"moduleMigration.optionRemindNextLaunch": {
|
||||
def: "Remind me at the next launch",
|
||||
es: "Recordármelo en el próximo inicio",
|
||||
fr: "Me rappeler au prochain lancement",
|
||||
he: "הזכר לי בהפעלה הבאה",
|
||||
ja: "次回起動時にリマインド",
|
||||
ko: "다음 시작 시 알림",
|
||||
ru: "Напомнить при следующем запуске",
|
||||
zh: "下次启动时提醒我",
|
||||
"zh-tw": "下次啟動時提醒我",
|
||||
},
|
||||
"moduleMigration.optionSetupViaP2P": {
|
||||
def: "Use P2P Sync to set up",
|
||||
es: "Usar Sincronización P2P para configurarlo",
|
||||
fr: "Utiliser Sync P2P pour configurer",
|
||||
he: "השתמש ב-%{short_p2p_sync} להגדרה",
|
||||
ja: "P2P Sync (試験機能)を使ってセットアップ",
|
||||
ko: "P2P 동기화를 사용하여 설정",
|
||||
ru: "Использовать short_p2p_sync для настройки",
|
||||
zh: "Use P2P同步(实验性) to set up",
|
||||
"zh-tw": "使用「P2P 同步」進行設定",
|
||||
},
|
||||
"moduleMigration.optionSetupWizard": {
|
||||
def: "Take me into the setup wizard",
|
||||
es: "Llévame al asistente de configuración",
|
||||
fr: "Ouvrir l'assistant de configuration",
|
||||
he: "קח אותי לאשף ההגדרה",
|
||||
ja: "セットアップウィザードへ",
|
||||
ko: "설정 마법사로 안내",
|
||||
ru: "Перейти в мастер настройки",
|
||||
zh: "带我进入设置向导",
|
||||
"zh-tw": "帶我進入設定精靈",
|
||||
},
|
||||
"moduleMigration.optionYesFetchAgain": {
|
||||
def: "Yes, fetch again",
|
||||
es: "Sí, obtener de nuevo",
|
||||
@@ -4652,28 +4552,6 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "大小写敏感性",
|
||||
"zh-tw": "大小寫敏感性",
|
||||
},
|
||||
"moduleMigration.titleRecommendSetupUri": {
|
||||
def: "Recommendation to use Setup URI",
|
||||
es: "Recomendación de usar un Setup URI",
|
||||
fr: "Recommandation d'utilisation de l'URI de configuration",
|
||||
he: "המלצה לשימוש ב-Setup URI",
|
||||
ja: "セットアップURIの使用を推奨",
|
||||
ko: "Setup URI 사용 권장",
|
||||
ru: "Рекомендация использовать Setup URI",
|
||||
zh: "推荐使用设置 URI",
|
||||
"zh-tw": "建議使用 Setup URI",
|
||||
},
|
||||
"moduleMigration.titleWelcome": {
|
||||
def: "Welcome to Self-hosted LiveSync",
|
||||
es: "Bienvenido a Self-hosted LiveSync",
|
||||
fr: "Bienvenue dans Self-hosted LiveSync",
|
||||
he: "ברוך הבא ל-Self-hosted LiveSync",
|
||||
ja: "Self-hosted LiveSyncへようこそ",
|
||||
ko: "Self-hosted LiveSync에 오신 것을 환영합니다",
|
||||
ru: "Добро пожаловать в Self-hosted LiveSync",
|
||||
zh: "欢迎使用 Self-hosted LiveSync",
|
||||
"zh-tw": "歡迎使用 Self-hosted LiveSync",
|
||||
},
|
||||
"moduleObsidianMenu.replicate": {
|
||||
def: "Replicate",
|
||||
es: "Replicar",
|
||||
@@ -5061,17 +4939,6 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "我明白了并且已更新",
|
||||
"zh-tw": "我知道了,且已更新。",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.btnNext": {
|
||||
def: "Next",
|
||||
es: "Siguiente",
|
||||
fr: "Suivant",
|
||||
he: "הבא",
|
||||
ja: "次へ",
|
||||
ko: "다음",
|
||||
ru: "Далее",
|
||||
zh: "下一步",
|
||||
"zh-tw": "下一步",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.btnStart": {
|
||||
def: "Start",
|
||||
es: "Iniciar",
|
||||
@@ -5116,17 +4983,6 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "获取",
|
||||
"zh-tw": "抓取",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.buttonNext": {
|
||||
def: "Next",
|
||||
es: "Siguiente",
|
||||
fr: "Suivant",
|
||||
he: "הבא",
|
||||
ja: "次へ",
|
||||
ko: "다음",
|
||||
ru: "Далее",
|
||||
zh: "下一步",
|
||||
"zh-tw": "下一步",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.defaultLanguage": {
|
||||
def: "Default",
|
||||
es: "Predeterminado",
|
||||
@@ -5812,17 +5668,6 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "要从远端服务器获取配置吗?",
|
||||
"zh-tw": "要從遠端伺服器抓取設定嗎?",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.msgGenerateSetupURI": {
|
||||
def: "All done! Do you want to generate a setup URI to set up other devices?",
|
||||
es: "¡Todo listo! ¿Quieres generar un URI de configuración para configurar otros dispositivos?",
|
||||
fr: "Tout est prêt ! Voulez-vous générer une URI de configuration pour configurer d'autres appareils ?",
|
||||
he: "הכל מוכן! האם ברצונך לייצר Setup URI להגדרת מכשירים אחרים?",
|
||||
ja: "完了!他のデバイスをセットアップするためのセットアップURIを生成しますか?",
|
||||
ko: "모든 작업이 완료되었습니다! 다른 기기를 설정하기 위해 Setup URI를 생성하시겠습니까?",
|
||||
ru: "Всё готово! Вы хотите сгенерировать Setup URI для настройки других устройств?",
|
||||
zh: "全部完成!要生成设置 URI 以便配置其他设备吗?",
|
||||
"zh-tw": "全部完成!要產生 Setup URI 以便設定其他裝置嗎?",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": {
|
||||
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.",
|
||||
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.",
|
||||
@@ -5925,17 +5770,6 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
"zh-tw":
|
||||
"需要重建資料庫才能套用變更。請選擇套用變更的方式。\n\n<details>\n<summary>圖例</summary>\n\n| 符號 | 含意 |\n|: ------ :| ------- |\n| ⇔ | 已是最新 |\n| ⇄ | 同步以取得一致 |\n| ⇐,⇒ | 傳輸並覆寫 |\n| ⇠,⇢ | 從另一端傳輸並覆寫 |\n\n</details>\n\n## ${OPTION_REBUILD_BOTH}\n概覽: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\n以此裝置上現有的檔案重建本機與遠端資料庫。\n這會鎖定其他裝置,它們必須執行抓取。\n## ${OPTION_FETCH}\n概覽: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\n初始化本機資料庫,並以從遠端資料庫抓取的資料重建。\n這也涵蓋你已重建遠端資料庫的情況。\n## ${OPTION_ONLY_SETTING}\n僅儲存設定。**注意:這可能導致資料損毀**;通常仍需要重建資料庫。",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": {
|
||||
def: "Please select and apply any preset item to complete the wizard.",
|
||||
es: "Por favor, selecciona y aplica cualquier elemento preestablecido para completar el asistente.",
|
||||
fr: "Veuillez sélectionner et appliquer un préréglage pour terminer l'assistant.",
|
||||
he: "אנא בחר והחל פריט קבוע מראש כלשהו להשלמת האשף.",
|
||||
ja: "ウィザードを完了するには、プリセット項目を選択して適用してください。",
|
||||
ko: "마법사를 완료하려면 프리셋 항목을 선택하고 적용해 주세요.",
|
||||
ru: "Выберите и примените любой пресет для завершения мастера.",
|
||||
zh: "请选择并应用任一预设项以完成向导。",
|
||||
"zh-tw": "請選擇並套用任一預設項目以完成精靈。",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsCredentials": {
|
||||
def: "Set cors.credentials",
|
||||
es: "Configurar cors.credentials",
|
||||
@@ -6530,6 +6364,9 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "活动远程服务器",
|
||||
"zh-tw": "使用中的遠端伺服器",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleAdvancedSettings": {
|
||||
def: "Advanced settings",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleAppearance": {
|
||||
def: "Appearance",
|
||||
es: "Apariencia",
|
||||
@@ -6552,17 +6389,6 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "冲突处理",
|
||||
"zh-tw": "衝突處理",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleCongratulations": {
|
||||
def: "Congratulations!",
|
||||
es: "¡Felicidades!",
|
||||
fr: "Félicitations !",
|
||||
he: "מזל טוב!",
|
||||
ja: "おめでとうございます!",
|
||||
ko: "축하합니다!",
|
||||
ru: "Поздравляем!",
|
||||
zh: "恭喜!",
|
||||
"zh-tw": "恭喜!",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleCouchDB": {
|
||||
def: "CouchDB",
|
||||
es: "Servidor CouchDB",
|
||||
@@ -6618,6 +6444,12 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "启用额外和高级功能",
|
||||
"zh-tw": "啟用額外與進階功能",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleExtraFeaturesGroup": {
|
||||
def: "Extra features",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleExtraMenus": {
|
||||
def: "Extra menus",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleFetchConfig": {
|
||||
def: "Fetch Config",
|
||||
es: "Obtener configuración",
|
||||
@@ -6651,6 +6483,12 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "获取设置",
|
||||
"zh-tw": "抓取設定",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleHelpAndInformation": {
|
||||
def: "Help and information",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleHelpAndTroubleshooting": {
|
||||
def: "Help and troubleshooting",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleHiddenFiles": {
|
||||
def: "Hidden Files",
|
||||
es: "Archivos ocultos",
|
||||
@@ -6673,6 +6511,9 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "日志",
|
||||
"zh-tw": "記錄",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleMaintenanceAndRecovery": {
|
||||
def: "Maintenance and recovery",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleMinioS3R2": {
|
||||
def: "Minio,S3,R2",
|
||||
es: "MinIO, S3, R2",
|
||||
@@ -6762,7 +6603,7 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
"zh-tw": "重設",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleSetupOtherDevices": {
|
||||
def: "To setup other devices",
|
||||
def: "Set up other devices",
|
||||
es: "Para configurar otros dispositivos",
|
||||
fr: "Pour configurer d'autres appareils",
|
||||
he: "להגדרת מכשירים אחרים",
|
||||
@@ -6772,6 +6613,9 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "设置其他设备",
|
||||
"zh-tw": "設定其他裝置",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleSynchronisation": {
|
||||
def: "Synchronisation",
|
||||
},
|
||||
"obsidianLiveSyncSettingTab.titleSynchronizationMethod": {
|
||||
def: "Synchronization Method",
|
||||
es: "Método de sincronización",
|
||||
@@ -8187,7 +8031,7 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
"zh-tw": "複寫因某個模組失敗而取消",
|
||||
},
|
||||
"Replicator.Message.VersionUpFlash": {
|
||||
def: "An update has been detected. Please open the Settings dialogue and check the Change Log. Replication has been cancelled.",
|
||||
def: "Remote synchronisation is paused for compatibility review. Run the 'Review why synchronisation is paused' command for details and available actions.",
|
||||
es: "Se ha detectado una actualización. Abre el diálogo de ajustes y consulta el registro de cambios. La replicación se ha cancelado.",
|
||||
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.",
|
||||
he: "זוהה עדכון. אנא פתח את דיאלוג ההגדרות ובדוק את יומן השינויים. השכפול בוטל.",
|
||||
@@ -11871,6 +11715,72 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "覆盖",
|
||||
"zh-tw": "覆寫",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.ApplyWithoutInitialisation": {
|
||||
def: "Apply without Initialisation",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.Back": {
|
||||
def: "Review another way to apply these settings",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.BypassGuidance": {
|
||||
def: "Applying these settings alone can make this device incompatible with its existing synchronisation data. Use this only when you have confirmed that reconstruction is unnecessary.",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.BypassTitle": {
|
||||
def: "Apply Settings without Initialisation?",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.ContinueFetch": {
|
||||
def: "Continue with Fetch",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.FetchOption": {
|
||||
def: "Reset Synchronisation on This Device",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.FetchOptionDesc": {
|
||||
def: "After restarting, rebuild this device's local database from the current remote synchronisation data. Files in the Vault will then be reconciled with that data.",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.FetchOptionP2PDesc": {
|
||||
def: "After restarting, select an online source device. This device's local LiveSync database will be rebuilt from that source.",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.Guidance": {
|
||||
def: "These setting changes alter how synchronisation data is interpreted. Apply them together with an initialisation operation after restart.",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.KeepEditing": {
|
||||
def: "Keep Editing",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.ProceedFetch": {
|
||||
def: "Restart and Fetch Synchronisation Data",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.ProceedFetchP2P": {
|
||||
def: "Restart and Select a Source Device",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.ProceedRebuild": {
|
||||
def: "Restart and Overwrite Server Data",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.ProceedRebuildP2P": {
|
||||
def: "Restart and Prepare This Device",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.Question": {
|
||||
def: "Which existing data should be used after restart?",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.RebuildOption": {
|
||||
def: "Overwrite Server Data with This Device's Files",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.RebuildOptionDesc": {
|
||||
def: "Rebuild the local and remote databases from the files currently in this Vault. Other synchronising devices must reset their local synchronisation afterwards.",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.RebuildOptionP2P": {
|
||||
def: "Prepare This Device from This Vault",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.RebuildOptionP2PDesc": {
|
||||
def: "Rebuild this device's local LiveSync database from the files currently in this Vault. This does not overwrite another device.",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.RemoteVerificationGuidance": {
|
||||
def: "The configured remote could not be verified with the current credentials and encryption settings. Continuing may make the Fetch fail after restart.",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.RemoteVerificationTitle": {
|
||||
def: "Remote Synchronisation Data Could Not Be Verified",
|
||||
},
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.Title": {
|
||||
def: "Apply Settings and Reinitialise Synchronisation",
|
||||
},
|
||||
"Ui.SetupWizard.Common.Back": {
|
||||
def: "No, please take me back",
|
||||
es: "No, volver atrás",
|
||||
|
||||
@@ -112,8 +112,6 @@
|
||||
"Obsidian version": "Obsidian-Version",
|
||||
"obsidianLiveSyncSettingTab.btnApply": "Anwenden",
|
||||
"obsidianLiveSyncSettingTab.btnDisable": "Deaktivieren",
|
||||
"obsidianLiveSyncSettingTab.btnNext": "Weiter",
|
||||
"obsidianLiveSyncSettingTab.buttonNext": "Weiter",
|
||||
"obsidianLiveSyncSettingTab.defaultLanguage": "Standardsprache",
|
||||
"obsidianLiveSyncSettingTab.labelDisabled": "⏹️ : Deaktiviert",
|
||||
"obsidianLiveSyncSettingTab.labelEnabled": "🔁 : Aktiviert",
|
||||
@@ -124,9 +122,7 @@
|
||||
"obsidianLiveSyncSettingTab.msgConfigCheckFailed": "Die Konfigurationsprüfung ist fehlgeschlagen. Trotzdem fortfahren?",
|
||||
"obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": "Wir empfehlen, Ende-zu-Ende-Verschlüsselung und Pfadverschleierung zu aktivieren. Möchten Sie wirklich ohne Verschlüsselung fortfahren?",
|
||||
"obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": "Möchten Sie die Konfiguration vom Remote-Server abrufen?",
|
||||
"obsidianLiveSyncSettingTab.msgGenerateSetupURI": "Alles fertig! Möchten Sie eine Setup-URI erzeugen, um andere Geräte einzurichten?",
|
||||
"obsidianLiveSyncSettingTab.msgInvalidPassphrase": "Ihre Verschlüsselungs-Passphrase könnte ungültig sein. Möchten Sie wirklich fortfahren?",
|
||||
"obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": "Bitte wählen und übernehmen Sie eine beliebige Voreinstellung, um den Assistenten abzuschließen.",
|
||||
"obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": "Synchronisation versteckter Dateien deaktivieren",
|
||||
"obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": "Synchronisation versteckter Dateien aktivieren",
|
||||
"obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": "Synchronisation versteckter Dateien",
|
||||
@@ -137,7 +133,6 @@
|
||||
"obsidianLiveSyncSettingTab.optionPeriodicWithBatch": "Periodisch mit Stapelverarbeitung",
|
||||
"obsidianLiveSyncSettingTab.titleAppearance": "Darstellung",
|
||||
"obsidianLiveSyncSettingTab.titleConflictResolution": "Konfliktbehandlung",
|
||||
"obsidianLiveSyncSettingTab.titleCongratulations": "Glückwunsch!",
|
||||
"obsidianLiveSyncSettingTab.titleCouchDB": "CouchDB-Server",
|
||||
"obsidianLiveSyncSettingTab.titleDeletionPropagation": "Weitergabe von Löschungen",
|
||||
"obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": "Verschlüsselung ist nicht aktiviert",
|
||||
|
||||
@@ -478,7 +478,6 @@
|
||||
"moduleLiveSyncMain.titleScramEnabled": "Scram Enabled",
|
||||
"moduleLocalDatabase.logWaitingForReady": "Waiting for ready...",
|
||||
"moduleLog.showLog": "Show Log",
|
||||
"moduleMigration.docUri": "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use",
|
||||
"moduleMigration.fix0256.buttons.checkItLater": "Check it later",
|
||||
"moduleMigration.fix0256.buttons.DismissForever": "I have fixed it, and do not ask again",
|
||||
"moduleMigration.fix0256.buttons.fix": "Fix",
|
||||
@@ -500,26 +499,16 @@
|
||||
"moduleMigration.logRemoteTweakUnavailable": "Could not get remote tweak values",
|
||||
"moduleMigration.logSetupCancelled": "The setup has been cancelled, Self-hosted LiveSync waiting for your setup!",
|
||||
"moduleMigration.msgFetchRemoteAgain": "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.___",
|
||||
"moduleMigration.msgInitialSetup": "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}).",
|
||||
"moduleMigration.msgRecommendSetupUri": "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?",
|
||||
"moduleMigration.msgSinceV02321": "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.",
|
||||
"moduleMigration.optionAdjustRemote": "Adjust to remote",
|
||||
"moduleMigration.optionDecideLater": "Decide it later",
|
||||
"moduleMigration.optionEnableBoth": "Enable both",
|
||||
"moduleMigration.optionEnableFilenameCaseInsensitive": "Enable only #1",
|
||||
"moduleMigration.optionEnableFixedRevisionForChunks": "Enable only #2",
|
||||
"moduleMigration.optionHaveSetupUri": "Yes, I have",
|
||||
"moduleMigration.optionKeepPreviousBehaviour": "Keep previous behaviour",
|
||||
"moduleMigration.optionManualSetup": "Set it up all manually",
|
||||
"moduleMigration.optionNoAskAgain": "No, please ask again",
|
||||
"moduleMigration.optionNoSetupUri": "No, I do not have",
|
||||
"moduleMigration.optionRemindNextLaunch": "Remind me at the next launch",
|
||||
"moduleMigration.optionSetupViaP2P": "Use %{short_p2p_sync} to set up",
|
||||
"moduleMigration.optionSetupWizard": "Take me into the setup wizard",
|
||||
"moduleMigration.optionYesFetchAgain": "Yes, fetch again",
|
||||
"moduleMigration.titleCaseSensitivity": "Case Sensitivity",
|
||||
"moduleMigration.titleRecommendSetupUri": "Recommendation to use Setup URI",
|
||||
"moduleMigration.titleWelcome": "Welcome to Self-hosted LiveSync",
|
||||
"moduleObsidianMenu.replicate": "Replicate",
|
||||
"More actions": "More actions",
|
||||
"Mostly Complete: Decision Required": "Mostly Complete: Decision Required",
|
||||
@@ -564,12 +553,10 @@
|
||||
"obsidianLiveSyncSettingTab.btnEnable": "Enable",
|
||||
"obsidianLiveSyncSettingTab.btnFix": "Fix",
|
||||
"obsidianLiveSyncSettingTab.btnGotItAndUpdated": "I got it and updated.",
|
||||
"obsidianLiveSyncSettingTab.btnNext": "Next",
|
||||
"obsidianLiveSyncSettingTab.btnStart": "Start",
|
||||
"obsidianLiveSyncSettingTab.btnTest": "Test",
|
||||
"obsidianLiveSyncSettingTab.btnUse": "Use",
|
||||
"obsidianLiveSyncSettingTab.buttonFetch": "Fetch",
|
||||
"obsidianLiveSyncSettingTab.buttonNext": "Next",
|
||||
"obsidianLiveSyncSettingTab.defaultLanguage": "Default",
|
||||
"obsidianLiveSyncSettingTab.descConnectSetupURI": "This is the recommended method to set up Self-hosted LiveSync with a Setup URI.",
|
||||
"obsidianLiveSyncSettingTab.descCopySetupURI": "Perfect for setting up a new device!",
|
||||
@@ -633,7 +620,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgEnableCorsChttpd": "Set chttpd.enable_cors",
|
||||
"obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": "We recommend enabling End-To-End Encryption, and Path Obfuscation. Are you sure you want to continue without encryption?",
|
||||
"obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": "Do you want to fetch the config from the remote server?",
|
||||
"obsidianLiveSyncSettingTab.msgGenerateSetupURI": "All done! Do you want to generate a setup URI to set up other devices?",
|
||||
"obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": "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.",
|
||||
"obsidianLiveSyncSettingTab.msgInvalidPassphrase": "Your encryption passphrase might be invalid. Are you sure you want to continue?",
|
||||
"obsidianLiveSyncSettingTab.msgNewVersionNote": "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.",
|
||||
@@ -643,7 +629,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgObjectStorageWarning": "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.",
|
||||
"obsidianLiveSyncSettingTab.msgOriginCheck": "Origin check: ${org}",
|
||||
"obsidianLiveSyncSettingTab.msgRebuildRequired": "Rebuilding Databases are required to apply the changes.. Please select the method to apply the changes.\n\n<details>\n<summary>Legends</summary>\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</details>\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.",
|
||||
"obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": "Please select and apply any preset item to complete the wizard.",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsCredentials": "Set cors.credentials",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsOrigins": "Set cors.origins",
|
||||
"obsidianLiveSyncSettingTab.msgSetMaxDocSize": "Set couchdb.max_document_size",
|
||||
@@ -698,19 +683,24 @@
|
||||
"obsidianLiveSyncSettingTab.panelSetup": "Setup",
|
||||
"obsidianLiveSyncSettingTab.serverVersion": "Server info: ${info}",
|
||||
"obsidianLiveSyncSettingTab.titleActiveRemoteServer": "Active Remote Server",
|
||||
"obsidianLiveSyncSettingTab.titleAdvancedSettings": "Advanced settings",
|
||||
"obsidianLiveSyncSettingTab.titleAppearance": "Appearance",
|
||||
"obsidianLiveSyncSettingTab.titleConflictResolution": "Conflict resolution",
|
||||
"obsidianLiveSyncSettingTab.titleCongratulations": "Congratulations!",
|
||||
"obsidianLiveSyncSettingTab.titleCouchDB": "CouchDB",
|
||||
"obsidianLiveSyncSettingTab.titleDeletionPropagation": "Deletion Propagation",
|
||||
"obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": "Encryption is not enabled",
|
||||
"obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": "Encryption Passphrase Invalid",
|
||||
"obsidianLiveSyncSettingTab.titleExtraFeatures": "Enable extra and advanced features",
|
||||
"obsidianLiveSyncSettingTab.titleExtraFeaturesGroup": "Extra features",
|
||||
"obsidianLiveSyncSettingTab.titleExtraMenus": "Extra menus",
|
||||
"obsidianLiveSyncSettingTab.titleFetchConfig": "Fetch Config",
|
||||
"obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": "Fetch config from remote server",
|
||||
"obsidianLiveSyncSettingTab.titleFetchSettings": "Fetch Settings",
|
||||
"obsidianLiveSyncSettingTab.titleHelpAndInformation": "Help and information",
|
||||
"obsidianLiveSyncSettingTab.titleHelpAndTroubleshooting": "Help and troubleshooting",
|
||||
"obsidianLiveSyncSettingTab.titleHiddenFiles": "Hidden Files",
|
||||
"obsidianLiveSyncSettingTab.titleLogging": "Logging",
|
||||
"obsidianLiveSyncSettingTab.titleMaintenanceAndRecovery": "Maintenance and recovery",
|
||||
"obsidianLiveSyncSettingTab.titleMinioS3R2": "Minio,S3,R2",
|
||||
"obsidianLiveSyncSettingTab.titleNotification": "Notification",
|
||||
"obsidianLiveSyncSettingTab.titleOnlineTips": "Online Tips",
|
||||
@@ -719,7 +709,8 @@
|
||||
"obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": "Remote Configuration Check Failed",
|
||||
"obsidianLiveSyncSettingTab.titleRemoteServer": "Remote Server",
|
||||
"obsidianLiveSyncSettingTab.titleReset": "Reset",
|
||||
"obsidianLiveSyncSettingTab.titleSetupOtherDevices": "To setup other devices",
|
||||
"obsidianLiveSyncSettingTab.titleSetupOtherDevices": "Set up other devices",
|
||||
"obsidianLiveSyncSettingTab.titleSynchronisation": "Synchronisation",
|
||||
"obsidianLiveSyncSettingTab.titleSynchronizationMethod": "Synchronization Method",
|
||||
"obsidianLiveSyncSettingTab.titleSynchronizationPreset": "Synchronization Preset",
|
||||
"obsidianLiveSyncSettingTab.titleSyncSettings": "Sync Settings",
|
||||
@@ -878,7 +869,7 @@
|
||||
"Replicator.Message.InitialiseFatalError": "No replicator is available, this is the fatal error.",
|
||||
"Replicator.Message.Pending": "Some file events are pending. Replication has been cancelled.",
|
||||
"Replicator.Message.SomeModuleFailed": "Replication has been cancelled by some module failure",
|
||||
"Replicator.Message.VersionUpFlash": "An update has been detected. Please open the Settings dialogue and check the Change Log. Replication has been cancelled.",
|
||||
"Replicator.Message.VersionUpFlash": "Remote synchronisation is paused for compatibility review. Run the 'Review why synchronisation is paused' command for details and available actions.",
|
||||
"Requires restart of Obsidian": "Requires restart of Obsidian",
|
||||
"Requires restart of Obsidian.": "Requires restart of Obsidian.",
|
||||
"Rerun Onboarding Wizard": "Rerun Onboarding Wizard",
|
||||
@@ -1330,6 +1321,28 @@
|
||||
"Ui.Settings.SyncSettings.Fetch": "Fetch",
|
||||
"Ui.Settings.SyncSettings.Merge": "Merge",
|
||||
"Ui.Settings.SyncSettings.Overwrite": "Overwrite",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.ApplyWithoutInitialisation": "Apply without Initialisation",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.Back": "Review another way to apply these settings",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.BypassGuidance": "Applying these settings alone can make this device incompatible with its existing synchronisation data. Use this only when you have confirmed that reconstruction is unnecessary.",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.BypassTitle": "Apply Settings without Initialisation?",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.ContinueFetch": "Continue with Fetch",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.FetchOption": "Reset Synchronisation on This Device",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.FetchOptionDesc": "After restarting, rebuild this device's local database from the current remote synchronisation data. Files in the Vault will then be reconciled with that data.",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.FetchOptionP2PDesc": "After restarting, select an online source device. This device's local LiveSync database will be rebuilt from that source.",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.Guidance": "These setting changes alter how synchronisation data is interpreted. Apply them together with an initialisation operation after restart.",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.KeepEditing": "Keep Editing",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.ProceedFetch": "Restart and Fetch Synchronisation Data",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.ProceedFetchP2P": "Restart and Select a Source Device",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.ProceedRebuild": "Restart and Overwrite Server Data",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.ProceedRebuildP2P": "Restart and Prepare This Device",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.Question": "Which existing data should be used after restart?",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.RebuildOption": "Overwrite Server Data with This Device's Files",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.RebuildOptionDesc": "Rebuild the local and remote databases from the files currently in this Vault. Other synchronising devices must reset their local synchronisation afterwards.",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.RebuildOptionP2P": "Prepare This Device from This Vault",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.RebuildOptionP2PDesc": "Rebuild this device's local LiveSync database from the files currently in this Vault. This does not overwrite another device.",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.RemoteVerificationGuidance": "The configured remote could not be verified with the current credentials and encryption settings. Continuing may make the Fetch fail after restart.",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.RemoteVerificationTitle": "Remote Synchronisation Data Could Not Be Verified",
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.Title": "Apply Settings and Reinitialise Synchronisation",
|
||||
"Ui.SetupWizard.Common.Back": "No, please take me back",
|
||||
"Ui.SetupWizard.Common.Cancel": "Cancel",
|
||||
"Ui.SetupWizard.Common.ProceedSelectOption": "Please select an option to proceed",
|
||||
|
||||
@@ -479,7 +479,6 @@
|
||||
"moduleLiveSyncMain.titleScramEnabled": "Scram habilitado",
|
||||
"moduleLocalDatabase.logWaitingForReady": "Esperando a que la base de datos esté lista...",
|
||||
"moduleLog.showLog": "Mostrar registro",
|
||||
"moduleMigration.docUri": "https://github.com/vrtmrz/obsidian-livesync/blob/main/README_ES.md#how-to-use",
|
||||
"moduleMigration.fix0256.buttons.checkItLater": "Comprobarlo más tarde",
|
||||
"moduleMigration.fix0256.buttons.DismissForever": "Ya lo he corregido, no volver a preguntar",
|
||||
"moduleMigration.fix0256.buttons.fix": "Corregir",
|
||||
@@ -501,26 +500,16 @@
|
||||
"moduleMigration.logRemoteTweakUnavailable": "No se pudieron obtener los valores de ajuste remoto",
|
||||
"moduleMigration.logSetupCancelled": "La configuración ha sido cancelada, ¡Self-hosted LiveSync está esperando tu configuración!",
|
||||
"moduleMigration.msgFetchRemoteAgain": "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.___",
|
||||
"moduleMigration.msgInitialSetup": "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}).",
|
||||
"moduleMigration.msgRecommendSetupUri": "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?",
|
||||
"moduleMigration.msgSinceV02321": "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.",
|
||||
"moduleMigration.optionAdjustRemote": "Ajustar al remoto",
|
||||
"moduleMigration.optionDecideLater": "Decidirlo más tarde",
|
||||
"moduleMigration.optionEnableBoth": "Habilitar ambos",
|
||||
"moduleMigration.optionEnableFilenameCaseInsensitive": "Habilitar solo #1",
|
||||
"moduleMigration.optionEnableFixedRevisionForChunks": "Habilitar solo #2",
|
||||
"moduleMigration.optionHaveSetupUri": "Sí, tengo",
|
||||
"moduleMigration.optionKeepPreviousBehaviour": "Mantener comportamiento anterior",
|
||||
"moduleMigration.optionManualSetup": "Configurarlo todo manualmente",
|
||||
"moduleMigration.optionNoAskAgain": "No, por favor pregúntame de nuevo",
|
||||
"moduleMigration.optionNoSetupUri": "No, no tengo",
|
||||
"moduleMigration.optionRemindNextLaunch": "Recordármelo en el próximo inicio",
|
||||
"moduleMigration.optionSetupViaP2P": "Usar %{short_p2p_sync} para configurarlo",
|
||||
"moduleMigration.optionSetupWizard": "Llévame al asistente de configuración",
|
||||
"moduleMigration.optionYesFetchAgain": "Sí, obtener de nuevo",
|
||||
"moduleMigration.titleCaseSensitivity": "Distinción de mayúsculas y minúsculas",
|
||||
"moduleMigration.titleRecommendSetupUri": "Recomendación de usar un Setup URI",
|
||||
"moduleMigration.titleWelcome": "Bienvenido a Self-hosted LiveSync",
|
||||
"moduleObsidianMenu.replicate": "Replicar",
|
||||
"More actions": "Más acciones",
|
||||
"Mostly Complete: Decision Required": "Casi terminado: se requiere una decisión",
|
||||
@@ -565,12 +554,10 @@
|
||||
"obsidianLiveSyncSettingTab.btnEnable": "Activar",
|
||||
"obsidianLiveSyncSettingTab.btnFix": "Corregir",
|
||||
"obsidianLiveSyncSettingTab.btnGotItAndUpdated": "Lo entendí y actualicé.",
|
||||
"obsidianLiveSyncSettingTab.btnNext": "Siguiente",
|
||||
"obsidianLiveSyncSettingTab.btnStart": "Iniciar",
|
||||
"obsidianLiveSyncSettingTab.btnTest": "Probar",
|
||||
"obsidianLiveSyncSettingTab.btnUse": "Usar",
|
||||
"obsidianLiveSyncSettingTab.buttonFetch": "Obtener",
|
||||
"obsidianLiveSyncSettingTab.buttonNext": "Siguiente",
|
||||
"obsidianLiveSyncSettingTab.defaultLanguage": "Predeterminado",
|
||||
"obsidianLiveSyncSettingTab.descConnectSetupURI": "Este es el método recomendado para configurar Self-hosted LiveSync con una URI de configuración.",
|
||||
"obsidianLiveSyncSettingTab.descCopySetupURI": "¡Perfecto para configurar un nuevo dispositivo!",
|
||||
@@ -633,7 +620,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgEnableCorsChttpd": "Establecer chttpd.enable_cors",
|
||||
"obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": "Recomendamos habilitar el cifrado de extremo a extremo y la obfuscación de ruta. ¿Estás seguro de querer continuar sin cifrado?",
|
||||
"obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": "¿Quieres obtener la configuración del servidor remoto?",
|
||||
"obsidianLiveSyncSettingTab.msgGenerateSetupURI": "¡Todo listo! ¿Quieres generar un URI de configuración para configurar otros dispositivos?",
|
||||
"obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": "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.",
|
||||
"obsidianLiveSyncSettingTab.msgInvalidPassphrase": "Tu frase de contraseña de cifrado podría ser inválida. ¿Estás seguro de querer continuar?",
|
||||
"obsidianLiveSyncSettingTab.msgNewVersionNote": "¿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.",
|
||||
@@ -643,7 +629,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgObjectStorageWarning": "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.",
|
||||
"obsidianLiveSyncSettingTab.msgOriginCheck": "Verificación de origen: {org}",
|
||||
"obsidianLiveSyncSettingTab.msgRebuildRequired": "Es necesario reconstruir las bases de datos para aplicar los cambios. Por favor selecciona el método para aplicar los cambios.\n\n<details>\n<summary>Legendas</summary>\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</details>\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.",
|
||||
"obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": "Por favor, selecciona y aplica cualquier elemento preestablecido para completar el asistente.",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsCredentials": "Configurar cors.credentials",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsOrigins": "Configurar cors.origins",
|
||||
"obsidianLiveSyncSettingTab.msgSetMaxDocSize": "Configurar couchdb.max_document_size",
|
||||
@@ -700,7 +685,6 @@
|
||||
"obsidianLiveSyncSettingTab.titleActiveRemoteServer": "Servidor remoto activo",
|
||||
"obsidianLiveSyncSettingTab.titleAppearance": "Apariencia",
|
||||
"obsidianLiveSyncSettingTab.titleConflictResolution": "Resolución de conflictos",
|
||||
"obsidianLiveSyncSettingTab.titleCongratulations": "¡Felicidades!",
|
||||
"obsidianLiveSyncSettingTab.titleCouchDB": "Servidor CouchDB",
|
||||
"obsidianLiveSyncSettingTab.titleDeletionPropagation": "Propagación de eliminación",
|
||||
"obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": "El cifrado no está habilitado",
|
||||
|
||||
@@ -190,7 +190,6 @@
|
||||
"moduleLiveSyncMain.titleScramEnabled": "Mode Scram activé",
|
||||
"moduleLocalDatabase.logWaitingForReady": "En attente de disponibilité...",
|
||||
"moduleLog.showLog": "Afficher le journal",
|
||||
"moduleMigration.docUri": "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use",
|
||||
"moduleMigration.fix0256.buttons.checkItLater": "Vérifier plus tard",
|
||||
"moduleMigration.fix0256.buttons.DismissForever": "J'ai corrigé, et ne plus demander",
|
||||
"moduleMigration.fix0256.buttons.fix": "Corriger",
|
||||
@@ -212,26 +211,16 @@
|
||||
"moduleMigration.logRemoteTweakUnavailable": "Impossible d'obtenir les valeurs d'ajustement distantes",
|
||||
"moduleMigration.logSetupCancelled": "La configuration a été annulée, Self-hosted LiveSync attend votre configuration !",
|
||||
"moduleMigration.msgFetchRemoteAgain": "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.___",
|
||||
"moduleMigration.msgInitialSetup": "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}).",
|
||||
"moduleMigration.msgRecommendSetupUri": "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 ?",
|
||||
"moduleMigration.msgSinceV02321": "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.",
|
||||
"moduleMigration.optionAdjustRemote": "Ajuster au distant",
|
||||
"moduleMigration.optionDecideLater": "Décider plus tard",
|
||||
"moduleMigration.optionEnableBoth": "Activer les deux",
|
||||
"moduleMigration.optionEnableFilenameCaseInsensitive": "Activer seulement #1",
|
||||
"moduleMigration.optionEnableFixedRevisionForChunks": "Activer seulement #2",
|
||||
"moduleMigration.optionHaveSetupUri": "Oui, j'en ai une",
|
||||
"moduleMigration.optionKeepPreviousBehaviour": "Conserver le comportement précédent",
|
||||
"moduleMigration.optionManualSetup": "Tout configurer manuellement",
|
||||
"moduleMigration.optionNoAskAgain": "Non, demandez à nouveau",
|
||||
"moduleMigration.optionNoSetupUri": "Non, je n'en ai pas",
|
||||
"moduleMigration.optionRemindNextLaunch": "Me rappeler au prochain lancement",
|
||||
"moduleMigration.optionSetupViaP2P": "Utiliser %{short_p2p_sync} pour configurer",
|
||||
"moduleMigration.optionSetupWizard": "Ouvrir l'assistant de configuration",
|
||||
"moduleMigration.optionYesFetchAgain": "Oui, récupérer à nouveau",
|
||||
"moduleMigration.titleCaseSensitivity": "Sensibilité à la casse",
|
||||
"moduleMigration.titleRecommendSetupUri": "Recommandation d'utilisation de l'URI de configuration",
|
||||
"moduleMigration.titleWelcome": "Bienvenue dans Self-hosted LiveSync",
|
||||
"moduleObsidianMenu.replicate": "Répliquer",
|
||||
"Move remotely deleted files to the trash, instead of deleting.": "Déplacer les fichiers supprimés à distance vers la corbeille, au lieu de les supprimer.",
|
||||
"Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": "Tous les messages n'ont pas été traduits. Et veuillez revenir à « Par défaut » lorsque vous signalez des erreurs.",
|
||||
@@ -249,12 +238,10 @@
|
||||
"obsidianLiveSyncSettingTab.btnEnable": "Activer",
|
||||
"obsidianLiveSyncSettingTab.btnFix": "Corriger",
|
||||
"obsidianLiveSyncSettingTab.btnGotItAndUpdated": "J'ai compris et mis à jour.",
|
||||
"obsidianLiveSyncSettingTab.btnNext": "Suivant",
|
||||
"obsidianLiveSyncSettingTab.btnStart": "Démarrer",
|
||||
"obsidianLiveSyncSettingTab.btnTest": "Tester",
|
||||
"obsidianLiveSyncSettingTab.btnUse": "Utiliser",
|
||||
"obsidianLiveSyncSettingTab.buttonFetch": "Récupérer",
|
||||
"obsidianLiveSyncSettingTab.buttonNext": "Suivant",
|
||||
"obsidianLiveSyncSettingTab.defaultLanguage": "Par défaut",
|
||||
"obsidianLiveSyncSettingTab.descConnectSetupURI": "Méthode recommandée pour configurer Self-hosted LiveSync avec une URI de configuration.",
|
||||
"obsidianLiveSyncSettingTab.descCopySetupURI": "Parfait pour configurer un nouvel appareil !",
|
||||
@@ -317,7 +304,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgEnableCorsChttpd": "Définir chttpd.enable_cors",
|
||||
"obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": "Nous recommandons d'activer le chiffrement de bout en bout et l'obfuscation des chemins. Êtes-vous sûr de vouloir continuer sans chiffrement ?",
|
||||
"obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": "Voulez-vous récupérer la configuration depuis le serveur distant ?",
|
||||
"obsidianLiveSyncSettingTab.msgGenerateSetupURI": "Tout est prêt ! Voulez-vous générer une URI de configuration pour configurer d'autres appareils ?",
|
||||
"obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": "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.",
|
||||
"obsidianLiveSyncSettingTab.msgInvalidPassphrase": "Votre phrase secrète de chiffrement peut être invalide. Êtes-vous sûr de vouloir continuer ?",
|
||||
"obsidianLiveSyncSettingTab.msgNewVersionNote": "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.",
|
||||
@@ -327,7 +313,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgObjectStorageWarning": "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.",
|
||||
"obsidianLiveSyncSettingTab.msgOriginCheck": "Vérification d'origine : ${org}",
|
||||
"obsidianLiveSyncSettingTab.msgRebuildRequired": "La reconstruction des bases de données est nécessaire pour appliquer les changements. Veuillez sélectionner la méthode d'application.\n\n<details>\n<summary>Légende</summary>\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</details>\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.",
|
||||
"obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": "Veuillez sélectionner et appliquer un préréglage pour terminer l'assistant.",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsCredentials": "Définir cors.credentials",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsOrigins": "Définir cors.origins",
|
||||
"obsidianLiveSyncSettingTab.msgSetMaxDocSize": "Définir couchdb.max_document_size",
|
||||
@@ -384,7 +369,6 @@
|
||||
"obsidianLiveSyncSettingTab.titleActiveRemoteServer": "Serveur distant actif",
|
||||
"obsidianLiveSyncSettingTab.titleAppearance": "Apparence",
|
||||
"obsidianLiveSyncSettingTab.titleConflictResolution": "Résolution des conflits",
|
||||
"obsidianLiveSyncSettingTab.titleCongratulations": "Félicitations !",
|
||||
"obsidianLiveSyncSettingTab.titleCouchDB": "CouchDB",
|
||||
"obsidianLiveSyncSettingTab.titleDeletionPropagation": "Propagation des suppressions",
|
||||
"obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": "Le chiffrement n'est pas activé",
|
||||
|
||||
@@ -191,7 +191,6 @@
|
||||
"moduleLiveSyncMain.titleScramEnabled": "מצב בלימה פעיל",
|
||||
"moduleLocalDatabase.logWaitingForReady": "ממתין לכשירות...",
|
||||
"moduleLog.showLog": "הצג יומן",
|
||||
"moduleMigration.docUri": "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use",
|
||||
"moduleMigration.fix0256.buttons.checkItLater": "בדוק מאוחר יותר",
|
||||
"moduleMigration.fix0256.buttons.DismissForever": "תיקנתי, ואל תשאל שוב",
|
||||
"moduleMigration.fix0256.buttons.fix": "תקן",
|
||||
@@ -213,26 +212,16 @@
|
||||
"moduleMigration.logRemoteTweakUnavailable": "לא ניתן לקבל ערכי כיוונון מרוחקים",
|
||||
"moduleMigration.logSetupCancelled": "ההגדרה בוטלה, Self-hosted LiveSync ממתין להגדרתך!",
|
||||
"moduleMigration.msgFetchRemoteAgain": "כפי שייתכן שכבר ידוע לך, Self-hosted LiveSync שינה את התנהגות ברירת המחדל ומבנה מסד הנתונים.\n\nובזכות זמנך ומאמציך, מסד הנתונים המרוחד נראה כבר הוגר. ברכות!\n\nעם זאת, נדרש עוד קצת. תצורת מכשיר זה אינה תואמת למסד הנתונים המרוחד. נצטרך למשוך את מסד הנתונים המרוחד שוב. האם למשוך מהשרת המרוחד עכשיו?\n\n___הערה: לא ניתן לסנכרן עד שהתצורה תשתנה ומסד הנתונים יימשך שוב.___\n___הערה 2: הנתחים הם בלתי-ניתנים לשינוי לחלוטין, ניתן למשוך רק את המטה-נתונים וההפרש.___",
|
||||
"moduleMigration.msgInitialSetup": "המכשיר שלך **טרם הוגדר**. אנחנו כאן לעזור לך בתהליך ההגדרה.\n\nשים לב שניתן להעתיק את תוכן כל דיאלוג ללוח. אם צריך לחזור אליו מאוחר יותר, ניתן להדביק אותו כפתק ב-Obsidian. ניתן גם לתרגם לשפתך בעזרת כלי תרגום.\n\nראשית, האם יש לך **Setup URI**?\n\nהערה: אם אינך יודע מהו, אנא עיין ב[תיעוד](${URI_DOC}).",
|
||||
"moduleMigration.msgRecommendSetupUri": "אנו ממליצים בחום לייצר Setup URI ולהשתמש בו.\nאם אין לך ידע בנושא, אנא עיין ב[תיעוד](${URI_DOC}) (מתנצלים שוב, אך זה חשוב).\n\nכיצד ברצונך להגדיר ידנית?",
|
||||
"moduleMigration.msgSinceV02321": "מאז גרסה 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}` ונסה לסנכרן שוב. מאחר שזוהה הפרש, תקבל תזכורת שוב.",
|
||||
"moduleMigration.optionAdjustRemote": "התאם לשרת המרוחד",
|
||||
"moduleMigration.optionDecideLater": "החלט מאוחר יותר",
|
||||
"moduleMigration.optionEnableBoth": "הפעל את שניהם",
|
||||
"moduleMigration.optionEnableFilenameCaseInsensitive": "הפעל רק #1",
|
||||
"moduleMigration.optionEnableFixedRevisionForChunks": "הפעל רק #2",
|
||||
"moduleMigration.optionHaveSetupUri": "כן, יש לי",
|
||||
"moduleMigration.optionKeepPreviousBehaviour": "שמור על התנהגות קודמת",
|
||||
"moduleMigration.optionManualSetup": "הגדר הכל ידנית",
|
||||
"moduleMigration.optionNoAskAgain": "לא, אנא שאל שוב",
|
||||
"moduleMigration.optionNoSetupUri": "לא, אין לי",
|
||||
"moduleMigration.optionRemindNextLaunch": "הזכר לי בהפעלה הבאה",
|
||||
"moduleMigration.optionSetupViaP2P": "השתמש ב-%{short_p2p_sync} להגדרה",
|
||||
"moduleMigration.optionSetupWizard": "קח אותי לאשף ההגדרה",
|
||||
"moduleMigration.optionYesFetchAgain": "כן, משוך שוב",
|
||||
"moduleMigration.titleCaseSensitivity": "תלות רישיות",
|
||||
"moduleMigration.titleRecommendSetupUri": "המלצה לשימוש ב-Setup URI",
|
||||
"moduleMigration.titleWelcome": "ברוך הבא ל-Self-hosted LiveSync",
|
||||
"moduleObsidianMenu.replicate": "שכפל",
|
||||
"Move remotely deleted files to the trash, instead of deleting.": "העבר קבצים שנמחקו מרחוק לאשפה, במקום למחוק.",
|
||||
"Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": "לא כל ההודעות תורגמו. בנוסף, אנא חזור ל\"ברירת מחדל\" בעת דיווח על שגיאות.",
|
||||
@@ -250,12 +239,10 @@
|
||||
"obsidianLiveSyncSettingTab.btnEnable": "הפעל",
|
||||
"obsidianLiveSyncSettingTab.btnFix": "תקן",
|
||||
"obsidianLiveSyncSettingTab.btnGotItAndUpdated": "הבנתי ועדכנתי.",
|
||||
"obsidianLiveSyncSettingTab.btnNext": "הבא",
|
||||
"obsidianLiveSyncSettingTab.btnStart": "התחל",
|
||||
"obsidianLiveSyncSettingTab.btnTest": "בדוק",
|
||||
"obsidianLiveSyncSettingTab.btnUse": "השתמש",
|
||||
"obsidianLiveSyncSettingTab.buttonFetch": "משוך",
|
||||
"obsidianLiveSyncSettingTab.buttonNext": "הבא",
|
||||
"obsidianLiveSyncSettingTab.defaultLanguage": "ברירת מחדל",
|
||||
"obsidianLiveSyncSettingTab.descConnectSetupURI": "זוהי השיטה המומלצת להגדרת Self-hosted LiveSync עם Setup URI.",
|
||||
"obsidianLiveSyncSettingTab.descCopySetupURI": "מושלם להגדרת מכשיר חדש!",
|
||||
@@ -318,7 +305,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgEnableCorsChttpd": "הגדר chttpd.enable_cors",
|
||||
"obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": "אנו ממליצים להפעיל הצפנה מקצה לקצה ואת ערפול הנתיב. האם אתה בטוח שברצונך להמשיך ללא הצפנה?",
|
||||
"obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": "האם ברצונך למשוך את התצורה מהשרת המרוחד?",
|
||||
"obsidianLiveSyncSettingTab.msgGenerateSetupURI": "הכל מוכן! האם ברצונך לייצר Setup URI להגדרת מכשירים אחרים?",
|
||||
"obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": "אם תצורת השרת אינה קבועה (למשל, פועלת ב-docker), הערכים כאן עשויים להשתנות. לאחר שתצליח להתחבר, אנא עדכן את ההגדרות ב-local.ini של השרת.",
|
||||
"obsidianLiveSyncSettingTab.msgInvalidPassphrase": "ביטוי הסיסמה להצפנה שלך עשוי להיות לא תקין. האם אתה בטוח שברצונך להמשיך?",
|
||||
"obsidianLiveSyncSettingTab.msgNewVersionNote": "הגעת כאן בשל הודעת שדרוג? אנא עיין בהיסטוריית הגרסאות. אם אתה מרוצה, לחץ על הכפתור. עדכון חדש יציג זאת שוב.",
|
||||
@@ -328,7 +314,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgObjectStorageWarning": "אזהרה: תכונה זו בשלב פיתוח, לכן שים לב לנקודות הבאות:\n- ארכיטקטורת הוספה בלבד. נדרשת בנייה מחדש לצמצום האחסון.\n- קצת רגיש.\n- בסנכרון הראשון, כל ההיסטוריה תועבר מהשרת המרוחד. שים לב למגבלות נתונים ומהירות.\n- רק הפרשים מסונכרנים בזמן אמת.\n\nאם נתקלת בבעיות, או שיש לך רעיונות לגבי תכונה זו, אנא פתח Issue ב-GitHub.\nאנחנו מעריכים את ההקדשה הגדולה שלך.",
|
||||
"obsidianLiveSyncSettingTab.msgOriginCheck": "בדיקת מקור: ${org}",
|
||||
"obsidianLiveSyncSettingTab.msgRebuildRequired": "נדרשת בנייה מחדש של מסדי הנתונים כדי להחיל את השינויים. אנא בחר את השיטה.\n\n<details>\n<summary>מקרא</summary>\n\n| סמל | משמעות |\n|: ------ :| ------- |\n| ⇔ | מעודכן |\n| ⇄ | סנכרן לאיזון |\n| ⇐,⇒ | העבר לדריסה |\n| ⇠,⇢ | העבר לדריסה מהצד השני |\n\n</details>\n\n## ${OPTION_REBUILD_BOTH}\nבמבט: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nבנה מחדש גם את מסד הנתונים המקומי וגם המרוחד תוך שימוש בקבצים קיימים ממכשיר זה.\nפעולה זו תנעל מכשירים אחרים שיצטרכו לבצע משיכה.\n## ${OPTION_FETCH}\nבמבט: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nאתחל את מסד הנתונים המקומי ובנה אותו מחדש תוך שימוש בנתונים שנמשכו ממסד הנתונים המרוחד.\nכולל את המקרה שבו בנית מחדש את מסד הנתונים המרוחד.\n## ${OPTION_ONLY_SETTING}\nשמור רק את ההגדרות. **זהירות: עלול לגרום לפגיעה בנתונים**; בנייה מחדש של מסד הנתונים נדרשת בדרך כלל.",
|
||||
"obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": "אנא בחר והחל פריט קבוע מראש כלשהו להשלמת האשף.",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsCredentials": "הגדר cors.credentials",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsOrigins": "הגדר cors.origins",
|
||||
"obsidianLiveSyncSettingTab.msgSetMaxDocSize": "הגדר couchdb.max_document_size",
|
||||
@@ -385,7 +370,6 @@
|
||||
"obsidianLiveSyncSettingTab.titleActiveRemoteServer": "שרת מרוחד פעיל",
|
||||
"obsidianLiveSyncSettingTab.titleAppearance": "מראה",
|
||||
"obsidianLiveSyncSettingTab.titleConflictResolution": "פתרון קונפליקטים",
|
||||
"obsidianLiveSyncSettingTab.titleCongratulations": "מזל טוב!",
|
||||
"obsidianLiveSyncSettingTab.titleCouchDB": "CouchDB",
|
||||
"obsidianLiveSyncSettingTab.titleDeletionPropagation": "הפצת מחיקות",
|
||||
"obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": "ההצפנה אינה מופעלת",
|
||||
|
||||
@@ -291,7 +291,6 @@
|
||||
"moduleLiveSyncMain.titleScramEnabled": "緊急停止(Scram)が有効",
|
||||
"moduleLocalDatabase.logWaitingForReady": "しばらくお待ちください...",
|
||||
"moduleLog.showLog": "ログを表示",
|
||||
"moduleMigration.docUri": "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use",
|
||||
"moduleMigration.fix0256.buttons.checkItLater": "後で確認する",
|
||||
"moduleMigration.fix0256.buttons.DismissForever": "修正済み、今後確認しない",
|
||||
"moduleMigration.fix0256.buttons.fix": "修正",
|
||||
@@ -313,26 +312,16 @@
|
||||
"moduleMigration.logRemoteTweakUnavailable": "リモートの調整値を取得できませんでした",
|
||||
"moduleMigration.logSetupCancelled": "セットアップがキャンセルされました。Self-hosted LiveSyncはセットアップを待っています!",
|
||||
"moduleMigration.msgFetchRemoteAgain": "ご存知のとおり、self-hosted LiveSyncはデフォルトの動作とデータベース構造を変更しました。\n\nご協力のおかげで、リモートデータベースはすでに移行されているようです。おめでとうございます!\n\nしかし、もう少し必要です。このデバイスの設定はリモートデータベースと互換性がありません。リモートデータベースを再度フェッチする必要があります。今すぐリモートから再フェッチしますか?\n\n___注意: 設定が変更され、データベースが再フェッチされるまで同期できません。___\n___注意2: チャンクは完全に不変なので、メタデータと差分のみフェッチできます。___",
|
||||
"moduleMigration.msgInitialSetup": "このデバイスは**まだセットアップされていません**。セットアッププロセスをご案内します。\n\nすべてのダイアログの内容はクリップボードにコピーできます。後で参照する必要があれば、Obsidianのノートに貼り付けてください。翻訳ツールを使ってお使いの言語に翻訳することもできます。\n\nまず、**セットアップURI**をお持ちですか?\n\n注意: それが何か分からない場合は、[documentation](${URI_DOC})を参照してください。",
|
||||
"moduleMigration.msgRecommendSetupUri": "セットアップURIを生成して使用することを強くお勧めします。\nこれについて知識がない場合は、[documentation](${URI_DOC})を参照してください(重要です)。\n\n手動でセットアップしますか?",
|
||||
"moduleMigration.msgSinceV02321": "v0.23.21以降、self-hosted LiveSyncはデフォルトの動作とデータベース構造を変更しました。以下の変更が行われました:\n\n1. **ファイル名の大文字小文字の区別**\n ファイル名の処理が大文字小文字を区別しなくなりました。これは、ファイル名の大文字小文字を効果的に管理しないLinuxとiOS以外のほとんどのプラットフォームにとって有益な変更です。\n (これらの環境では、同じ名前で大文字小文字が異なるファイルに対して警告が表示されます)。\n\n2. **チャンクのリビジョン処理**\n チャンクは不変であり、リビジョンを固定できます。この変更により、ファイル保存のパフォーマンスが向上します。\n\n___しかし、これらの変更を有効にするには、リモートとローカルの両方のデータベースを再構築する必要があります。このプロセスは数分かかります。時間に余裕があるときに行うことをお勧めします。___\n\n- 以前の動作を維持したい場合は、`${KEEP}`を使用してこのプロセスをスキップできます。\n- 時間がない場合は、`${DISMISS}`を選択してください。後で再度確認されます。\n- 別のデバイスでデータベースを再構築した場合は、`${DISMISS}`を選択して再度同期してみてください。差異が検出されたため、再度確認されます。",
|
||||
"moduleMigration.optionAdjustRemote": "リモートに合わせる",
|
||||
"moduleMigration.optionDecideLater": "後で決める",
|
||||
"moduleMigration.optionEnableBoth": "両方を有効にする",
|
||||
"moduleMigration.optionEnableFilenameCaseInsensitive": "#1のみ有効にする",
|
||||
"moduleMigration.optionEnableFixedRevisionForChunks": "#2のみ有効にする",
|
||||
"moduleMigration.optionHaveSetupUri": "はい、持っています",
|
||||
"moduleMigration.optionKeepPreviousBehaviour": "以前の動作を維持",
|
||||
"moduleMigration.optionManualSetup": "すべて手動でセットアップ",
|
||||
"moduleMigration.optionNoAskAgain": "いいえ、後で確認する",
|
||||
"moduleMigration.optionNoSetupUri": "いいえ、持っていません",
|
||||
"moduleMigration.optionRemindNextLaunch": "次回起動時にリマインド",
|
||||
"moduleMigration.optionSetupViaP2P": "%{short_p2p_sync}を使ってセットアップ",
|
||||
"moduleMigration.optionSetupWizard": "セットアップウィザードへ",
|
||||
"moduleMigration.optionYesFetchAgain": "はい、再フェッチする",
|
||||
"moduleMigration.titleCaseSensitivity": "大文字小文字の区別",
|
||||
"moduleMigration.titleRecommendSetupUri": "セットアップURIの使用を推奨",
|
||||
"moduleMigration.titleWelcome": "Self-hosted LiveSyncへようこそ",
|
||||
"moduleObsidianMenu.replicate": "レプリケート",
|
||||
"More actions": "その他の操作",
|
||||
"Move remotely deleted files to the trash, instead of deleting.": "リモートで削除されたファイルを削除せずにゴミ箱に移動する。",
|
||||
@@ -361,12 +350,10 @@
|
||||
"obsidianLiveSyncSettingTab.btnEnable": "有効化",
|
||||
"obsidianLiveSyncSettingTab.btnFix": "修正",
|
||||
"obsidianLiveSyncSettingTab.btnGotItAndUpdated": "理解しました、更新しました。",
|
||||
"obsidianLiveSyncSettingTab.btnNext": "次へ",
|
||||
"obsidianLiveSyncSettingTab.btnStart": "開始",
|
||||
"obsidianLiveSyncSettingTab.btnTest": "テスト",
|
||||
"obsidianLiveSyncSettingTab.btnUse": "使用",
|
||||
"obsidianLiveSyncSettingTab.buttonFetch": "フェッチ",
|
||||
"obsidianLiveSyncSettingTab.buttonNext": "次へ",
|
||||
"obsidianLiveSyncSettingTab.defaultLanguage": "デフォルト",
|
||||
"obsidianLiveSyncSettingTab.descConnectSetupURI": "セットアップURIを使用してSelf-hosted LiveSyncをセットアップする推奨方法です。",
|
||||
"obsidianLiveSyncSettingTab.descCopySetupURI": "新しいデバイスのセットアップにおすすめ!",
|
||||
@@ -429,7 +416,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgEnableCorsChttpd": "chttpd.enable_corsを設定",
|
||||
"obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": "エンドツーエンド暗号化とパス難読化を有効にすることをお勧めします。暗号化なしで続行してもよろしいですか?",
|
||||
"obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": "リモートサーバーから設定を取得しますか?",
|
||||
"obsidianLiveSyncSettingTab.msgGenerateSetupURI": "完了!他のデバイスをセットアップするためのセットアップURIを生成しますか?",
|
||||
"obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": "サーバー設定が永続的でない場合(例: Dockerで実行中)、ここの値は変更される可能性があります。接続できるようになったら、サーバーのlocal.iniの設定を更新してください。",
|
||||
"obsidianLiveSyncSettingTab.msgInvalidPassphrase": "暗号化パスフレーズが無効かもしれません。続行してもよろしいですか?",
|
||||
"obsidianLiveSyncSettingTab.msgNewVersionNote": "アップグレード通知でここに来ましたか?バージョン履歴を確認してください。納得したらボタンをクリックしてください。新しい更新があると再度確認されます。",
|
||||
@@ -439,7 +425,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgObjectStorageWarning": "警告: この機能は開発中です。以下の点にご注意ください:\n- 追記専用アーキテクチャ。ストレージを縮小するには再構築が必要です。\n- やや不安定です。\n- 初回同期時、すべての履歴がリモートから転送されます。データ制限と速度に注意してください。\n- ライブ同期は差分のみです。\n\n問題があれば、またはこの機能についてアイデアがあれば、GitHubにIssueを作成してください。\nご協力に感謝します。",
|
||||
"obsidianLiveSyncSettingTab.msgOriginCheck": "オリジン確認: ${org}",
|
||||
"obsidianLiveSyncSettingTab.msgRebuildRequired": "変更を適用するにはデータベースの再構築が必要です。変更を適用する方法を選択してください。\n\n<details>\n<summary>凡例</summary>\n\n| 記号 | 意味 |\n|: ------ :| ------- |\n| ⇔ | 最新 |\n| ⇄ | 同期してバランスを取る |\n| ⇐,⇒ | 上書きするため転送 |\n| ⇠,⇢ | 反対側から上書きするため転送 |\n\n</details>\n\n## ${OPTION_REBUILD_BOTH}\n概要: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nこのデバイスの既存ファイルを使用してローカルとリモートの両方のデータベースを再構築します。\n他のデバイスはロックアウトされ、フェッチが必要です。\n## ${OPTION_FETCH}\n概要: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nローカルデータベースを初期化し、リモートデータベースから取得したデータを使用して再構築します。\nリモートデータベースを再構築した場合も含まれます。\n## ${OPTION_ONLY_SETTING}\n設定のみを保存します。**注意: データ破損につながる可能性があります**。通常、データベースの再構築が必要です。",
|
||||
"obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": "ウィザードを完了するには、プリセット項目を選択して適用してください。",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsCredentials": "cors.credentialsを設定",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsOrigins": "cors.originsを設定",
|
||||
"obsidianLiveSyncSettingTab.msgSetMaxDocSize": "couchdb.max_document_sizeを設定",
|
||||
@@ -496,7 +481,6 @@
|
||||
"obsidianLiveSyncSettingTab.titleActiveRemoteServer": "アクティブなリモートサーバー",
|
||||
"obsidianLiveSyncSettingTab.titleAppearance": "外観",
|
||||
"obsidianLiveSyncSettingTab.titleConflictResolution": "競合解決",
|
||||
"obsidianLiveSyncSettingTab.titleCongratulations": "おめでとうございます!",
|
||||
"obsidianLiveSyncSettingTab.titleCouchDB": "CouchDB サーバー",
|
||||
"obsidianLiveSyncSettingTab.titleDeletionPropagation": "削除の伝播",
|
||||
"obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": "暗号化が有効になっていません",
|
||||
|
||||
@@ -478,7 +478,6 @@
|
||||
"moduleLiveSyncMain.titleScramEnabled": "긴급 정지 활성화됨",
|
||||
"moduleLocalDatabase.logWaitingForReady": "준비 대기 중...",
|
||||
"moduleLog.showLog": "로그 표시",
|
||||
"moduleMigration.docUri": "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use",
|
||||
"moduleMigration.fix0256.buttons.checkItLater": "나중에 확인",
|
||||
"moduleMigration.fix0256.buttons.DismissForever": "이미 해결했으니 다시 묻지 않기",
|
||||
"moduleMigration.fix0256.buttons.fix": "수정",
|
||||
@@ -500,26 +499,16 @@
|
||||
"moduleMigration.logRemoteTweakUnavailable": "원격 조정 값을 가져올 수 없습니다",
|
||||
"moduleMigration.logSetupCancelled": "설정이 취소되었습니다. Self-hosted LiveSync가 설정을 기다리고 있습니다!",
|
||||
"moduleMigration.msgFetchRemoteAgain": "이미 알고 계시겠지만, Self-hosted LiveSync의 기본 동작 방식과 데이터베이스 구조가 변경되었습니다.\n\n다행히도 여러분의 노력 덕분에 원격 데이터베이스는 이미 성공적으로 데이터 구조 전환이 완료된 것으로 보입니다. 축하드립니다!\n\n하지만 아직 일부 추가 작업이 필요합니다. 이 기기의 설정이 원격 데이터베이스와 호환되지 않으므로, 원격 데이터를 다시 가져와야 합니다. 지금 원격 데이터베이스를 다시 가져오시겠습니까?\n\n___참고: 설정이 변경되고 데이터베이스를 다시 불러오기 전까지는 동기화가 불가능합니다.___\n___참고2: 청크는 변경이 불가능한 구조이므로, 메타데이터와 차이점만 가져올 수 있습니다.___",
|
||||
"moduleMigration.msgInitialSetup": "이 기기는 **아직 초기 설정이 완료되지 않았습니다**. 지금부터 설정 과정을 안내해 드리겠습니다.\n\n모든 대화 내용은 클립보드에 복사할 수 있습니다. 나중에 참고하려면 Obsidian 노트에 붙여넣거나 번역 도구를 활용해 번역하셔도 됩니다.\n\n먼저, **Setup URI**를 가지고 계신가요?\n\n참고: Setup URI가 무엇인지 잘 모르시겠다면 [문서](${URI_DOC})를 참고해 주세요.",
|
||||
"moduleMigration.msgRecommendSetupUri": "Setup URI를 생성해 사용하는 것을 강력히 권장합니다.\nSetup URI가 무엇인지 잘 모르시겠다면 [문서](${URI_DOC})를 참고해 주세요. 중요한 내용이니 꼭 확인하시기 바랍니다.\n\n직접 수동 설정을 진행하시겠습니까?",
|
||||
"moduleMigration.msgSinceV02321": "v0.23.21부터 Self-hosted LiveSync의 기본 동작 방식과 데이터베이스 구조가 변경되었습니다. 변경 내용은 다음과 같습니다:\n\n1. **파일명의 대소문자 구분**\n 이제 파일명을 대소문자 구분 없이 처리합니다. 파일명의 대소문자를 제대로 관리하지 못하는 Linux와 iOS를 제외한 대부분의 플랫폼에서 유리한 변경입니다.\n (해당 플랫폼에서는 이름이 같고 대소문자만 다른 파일에 대해 경고가 표시됩니다)\n\n2. **청크의 리비전 처리**\n 청크는 변경 불가능하므로 리비전을 고정할 수 있습니다. 이 변경으로 파일 저장 성능이 향상됩니다.\n\n___다만 이 변경 중 어느 하나라도 적용하려면 원격과 로컬 데이터베이스를 모두 재구축해야 합니다. 이 과정은 몇 분이 걸리므로 시간이 충분할 때 진행하시기를 권장합니다.___\n\n- 기존 동작을 유지하려면 `${KEEP}`을 선택해 이 과정을 건너뛸 수 있습니다.\n- 시간이 충분하지 않다면 `${DISMISS}`를 선택해 주세요. 나중에 다시 여쭤보겠습니다.\n- 다른 기기에서 이미 데이터베이스를 재구축했다면 `${DISMISS}`를 선택한 뒤 다시 동기화해 보세요. 차이가 감지되면 다시 안내해 드립니다.",
|
||||
"moduleMigration.optionAdjustRemote": "원격에 맞추기",
|
||||
"moduleMigration.optionDecideLater": "나중에 결정하기",
|
||||
"moduleMigration.optionEnableBoth": "둘 다 활성화",
|
||||
"moduleMigration.optionEnableFilenameCaseInsensitive": "#1만 활성화",
|
||||
"moduleMigration.optionEnableFixedRevisionForChunks": "#2만 활성화",
|
||||
"moduleMigration.optionHaveSetupUri": "예, 있습니다",
|
||||
"moduleMigration.optionKeepPreviousBehaviour": "이전 동작 유지",
|
||||
"moduleMigration.optionManualSetup": "모든 것을 수동으로 설정",
|
||||
"moduleMigration.optionNoAskAgain": "아니요 (나중에 다시 물어보기)",
|
||||
"moduleMigration.optionNoSetupUri": "아니요, 없습니다",
|
||||
"moduleMigration.optionRemindNextLaunch": "다음 시작 시 알림",
|
||||
"moduleMigration.optionSetupViaP2P": "%{short_p2p_sync}를 사용하여 설정",
|
||||
"moduleMigration.optionSetupWizard": "설정 마법사로 안내",
|
||||
"moduleMigration.optionYesFetchAgain": "예 (다시 가져오기)",
|
||||
"moduleMigration.titleCaseSensitivity": "대소문자 구분",
|
||||
"moduleMigration.titleRecommendSetupUri": "Setup URI 사용 권장",
|
||||
"moduleMigration.titleWelcome": "Self-hosted LiveSync에 오신 것을 환영합니다",
|
||||
"moduleObsidianMenu.replicate": "복제",
|
||||
"More actions": "추가 작업",
|
||||
"Mostly Complete: Decision Required": "거의 완료: 결정이 필요합니다",
|
||||
@@ -564,12 +553,10 @@
|
||||
"obsidianLiveSyncSettingTab.btnEnable": "활성화",
|
||||
"obsidianLiveSyncSettingTab.btnFix": "수정",
|
||||
"obsidianLiveSyncSettingTab.btnGotItAndUpdated": "알겠습니다. 업데이트했습니다.",
|
||||
"obsidianLiveSyncSettingTab.btnNext": "다음",
|
||||
"obsidianLiveSyncSettingTab.btnStart": "시작",
|
||||
"obsidianLiveSyncSettingTab.btnTest": "테스트",
|
||||
"obsidianLiveSyncSettingTab.btnUse": "사용",
|
||||
"obsidianLiveSyncSettingTab.buttonFetch": "가져오기",
|
||||
"obsidianLiveSyncSettingTab.buttonNext": "다음",
|
||||
"obsidianLiveSyncSettingTab.defaultLanguage": "기본값",
|
||||
"obsidianLiveSyncSettingTab.descConnectSetupURI": "이것은 Setup URI로 Self-hosted LiveSync를 설정하는 권장 방법입니다.",
|
||||
"obsidianLiveSyncSettingTab.descCopySetupURI": "새 기기 설정에 완벽합니다!",
|
||||
@@ -633,7 +620,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgEnableCorsChttpd": "chttpd.enable_cors 설정",
|
||||
"obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": "종단 간 암호화와 경로 난독화를 활성화하는 것을 권장합니다. 정말로 암호화 없이 계속하시겠습니까?",
|
||||
"obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": "원격 서버에서 구성을 가져오시겠습니까?",
|
||||
"obsidianLiveSyncSettingTab.msgGenerateSetupURI": "모든 작업이 완료되었습니다! 다른 기기를 설정하기 위해 Setup URI를 생성하시겠습니까?",
|
||||
"obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": "서버 설정이 영구적으로 저장되지 않는 환경(예: Docker에서 실행 중)에서는 이곳의 값들이 변경될 수 있습니다. 연결이 가능해지면 서버의 local.ini 파일에서 설정을 수동으로 업데이트해 주세요.",
|
||||
"obsidianLiveSyncSettingTab.msgInvalidPassphrase": "암호화 패스프레이즈가 유효하지 않을 수 있습니다. 정말로 계속하시겠습니까?",
|
||||
"obsidianLiveSyncSettingTab.msgNewVersionNote": "업그레이드 알림으로 여기에 오셨나요? 버전 기록을 검토해 주세요. 만족하신다면 버튼을 클릭하세요. 새로운 업데이트 시 다시 안내됩니다.",
|
||||
@@ -643,7 +629,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgObjectStorageWarning": "경고: 이 기능은 아직 개발 중이므로 다음 사항을 유의해 주세요:\n- 추가 전용 구조로 동작합니다. 저장 용량을 줄이려면 재구축이 필요합니다.\n- 다소 불안정합니다.\n- 최초 동기화 시 모든 기록이 원격에서 전송됩니다. 데이터 사용량 제한과 느린 속도에 유의해 주세요.\n- 실시간 동기화는 변경분만 처리합니다.\n\n문제가 발생했거나 이 기능에 대한 아이디어가 있다면 GitHub에 이슈를 등록해 주세요.\n큰 관심에 깊이 감사드립니다.",
|
||||
"obsidianLiveSyncSettingTab.msgOriginCheck": "출처 확인: ${org}",
|
||||
"obsidianLiveSyncSettingTab.msgRebuildRequired": "변경 사항을 적용하려면 데이터베이스를 재구축해야 합니다. 변경 사항을 적용할 방법을 선택해 주세요.\n\n<details>\n<summary>범례</summary>\n\n| 기호 | 의미 |\n|: ------ :| ------- |\n| ⇔ | 최신 상태 |\n| ⇄ | 양쪽을 맞추는 동기화 |\n| ⇐,⇒ | 덮어쓰기 전송 |\n| ⇠,⇢ | 반대편에서 덮어쓰기 전송 |\n\n</details>\n\n## ${OPTION_REBUILD_BOTH}\n한눈에 보기: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\n이 기기의 기존 파일을 사용해 로컬과 원격 데이터베이스를 모두 재구축합니다.\n이 경우 다른 기기는 잠기며, 가져오기를 수행해야 합니다.\n## ${OPTION_FETCH}\n한눈에 보기: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\n로컬 데이터베이스를 초기화한 뒤, 원격 데이터베이스에서 가져온 데이터로 재구축합니다.\n원격 데이터베이스를 이미 재구축한 경우도 여기에 해당합니다.\n## ${OPTION_ONLY_SETTING}\n설정만 저장합니다. **주의: 데이터가 손상될 수 있습니다.** 일반적으로는 데이터베이스 재구축이 필요합니다.",
|
||||
"obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": "마법사를 완료하려면 프리셋 항목을 선택하고 적용해 주세요.",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsCredentials": "cors.credentials 설정",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsOrigins": "cors.origins 설정",
|
||||
"obsidianLiveSyncSettingTab.msgSetMaxDocSize": "couchdb.max_document_size 설정",
|
||||
@@ -700,7 +685,6 @@
|
||||
"obsidianLiveSyncSettingTab.titleActiveRemoteServer": "활성 원격 서버",
|
||||
"obsidianLiveSyncSettingTab.titleAppearance": "모양",
|
||||
"obsidianLiveSyncSettingTab.titleConflictResolution": "충돌 해결",
|
||||
"obsidianLiveSyncSettingTab.titleCongratulations": "축하합니다!",
|
||||
"obsidianLiveSyncSettingTab.titleCouchDB": "CouchDB",
|
||||
"obsidianLiveSyncSettingTab.titleDeletionPropagation": "삭제 전파",
|
||||
"obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": "암호화가 활성화되지 않음",
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"moduleLiveSyncMain.titleScramEnabled": "Экстренная остановка включена",
|
||||
"moduleLocalDatabase.logWaitingForReady": "Ожидание готовности...",
|
||||
"moduleLog.showLog": "Показать лог",
|
||||
"moduleMigration.docUri": "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use",
|
||||
"moduleMigration.fix0256.buttons.checkItLater": "Проверить позже",
|
||||
"moduleMigration.fix0256.buttons.DismissForever": "Исправлено, больше не спрашивать",
|
||||
"moduleMigration.fix0256.buttons.fix": "Исправить",
|
||||
@@ -328,26 +327,16 @@
|
||||
"moduleMigration.logRemoteTweakUnavailable": "Не удалось получить удалённые настройки",
|
||||
"moduleMigration.logSetupCancelled": "Настройка отменена, Self-hosted LiveSync ожидает вашей настройки!",
|
||||
"moduleMigration.msgFetchRemoteAgain": "Удалённая база данных, похоже, уже была мигрирована. Конфигурация этого устройства несовместима.",
|
||||
"moduleMigration.msgInitialSetup": "Ваше устройство ещё не настроено. У вас есть Setup URI?",
|
||||
"moduleMigration.msgRecommendSetupUri": "Мы рекомендуем сгенерировать Setup URI.",
|
||||
"moduleMigration.msgSinceV02321": "Начиная с v0.23.21, self-hosted LiveSync изменил поведение и структуру базы данных.",
|
||||
"moduleMigration.optionAdjustRemote": "Настроить под удалённую",
|
||||
"moduleMigration.optionDecideLater": "Решить позже",
|
||||
"moduleMigration.optionEnableBoth": "Включить оба",
|
||||
"moduleMigration.optionEnableFilenameCaseInsensitive": "Включить только #1",
|
||||
"moduleMigration.optionEnableFixedRevisionForChunks": "Включить только #2",
|
||||
"moduleMigration.optionHaveSetupUri": "Да, есть",
|
||||
"moduleMigration.optionKeepPreviousBehaviour": "Сохранить предыдущее поведение",
|
||||
"moduleMigration.optionManualSetup": "Настроить всё вручную",
|
||||
"moduleMigration.optionNoAskAgain": "Нет, спросить снова",
|
||||
"moduleMigration.optionNoSetupUri": "Нет, нет",
|
||||
"moduleMigration.optionRemindNextLaunch": "Напомнить при следующем запуске",
|
||||
"moduleMigration.optionSetupViaP2P": "Использовать short_p2p_sync для настройки",
|
||||
"moduleMigration.optionSetupWizard": "Перейти в мастер настройки",
|
||||
"moduleMigration.optionYesFetchAgain": "Да, загрузить снова",
|
||||
"moduleMigration.titleCaseSensitivity": "Чувствительность к регистру",
|
||||
"moduleMigration.titleRecommendSetupUri": "Рекомендация использовать Setup URI",
|
||||
"moduleMigration.titleWelcome": "Добро пожаловать в Self-hosted LiveSync",
|
||||
"moduleObsidianMenu.replicate": "Реплицировать",
|
||||
"More actions": "Другие действия",
|
||||
"Move remotely deleted files to the trash, instead of deleting.": "Перемещать удалённые на удалённом сервере файлы в корзину вместо удаления.",
|
||||
@@ -377,12 +366,10 @@
|
||||
"obsidianLiveSyncSettingTab.btnEnable": "Включить",
|
||||
"obsidianLiveSyncSettingTab.btnFix": "Исправить",
|
||||
"obsidianLiveSyncSettingTab.btnGotItAndUpdated": "Понял и обновил.",
|
||||
"obsidianLiveSyncSettingTab.btnNext": "Далее",
|
||||
"obsidianLiveSyncSettingTab.btnStart": "Старт",
|
||||
"obsidianLiveSyncSettingTab.btnTest": "Тест",
|
||||
"obsidianLiveSyncSettingTab.btnUse": "Использовать",
|
||||
"obsidianLiveSyncSettingTab.buttonFetch": "Загрузить",
|
||||
"obsidianLiveSyncSettingTab.buttonNext": "Далее",
|
||||
"obsidianLiveSyncSettingTab.defaultLanguage": "По умолчанию",
|
||||
"obsidianLiveSyncSettingTab.descConnectSetupURI": "Это рекомендуемый способ настройки Self-hosted LiveSync с помощью Setup URI.",
|
||||
"obsidianLiveSyncSettingTab.descCopySetupURI": "Идеально подходит для настройки нового устройства!",
|
||||
@@ -445,7 +432,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgEnableCorsChttpd": "Установить chttpd.enable_cors",
|
||||
"obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": "Мы рекомендуем включить сквозное шифрование. Вы уверены, что хотите продолжить без шифрования?",
|
||||
"obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": "Вы хотите загрузить конфигурацию с удалённого сервера?",
|
||||
"obsidianLiveSyncSettingTab.msgGenerateSetupURI": "Всё готово! Вы хотите сгенерировать Setup URI для настройки других устройств?",
|
||||
"obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": "Если конфигурация сервера непостоянна, значения здесь могут измениться.",
|
||||
"obsidianLiveSyncSettingTab.msgInvalidPassphrase": "Ваша парольная фраза шифрования может быть недействительна.",
|
||||
"obsidianLiveSyncSettingTab.msgNewVersionNote": "Вы пришли из-за уведомления об обновлении? Просмотрите историю версий.",
|
||||
@@ -455,7 +441,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgObjectStorageWarning": "ПРЕДУПРЕЖДЕНИЕ: Эта функция в разработке.",
|
||||
"obsidianLiveSyncSettingTab.msgOriginCheck": "Проверка origin: org",
|
||||
"obsidianLiveSyncSettingTab.msgRebuildRequired": "Требуется перестроение баз данных для применения изменений.",
|
||||
"obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": "Выберите и примените любой пресет для завершения мастера.",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsCredentials": "Установить cors.credentials",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsOrigins": "Установить cors.origins",
|
||||
"obsidianLiveSyncSettingTab.msgSetMaxDocSize": "Установить couchdb.max_document_size",
|
||||
@@ -512,7 +497,6 @@
|
||||
"obsidianLiveSyncSettingTab.titleActiveRemoteServer": "Активный удалённый сервер",
|
||||
"obsidianLiveSyncSettingTab.titleAppearance": "Внешний вид",
|
||||
"obsidianLiveSyncSettingTab.titleConflictResolution": "Разрешение конфликтов",
|
||||
"obsidianLiveSyncSettingTab.titleCongratulations": "Поздравляем!",
|
||||
"obsidianLiveSyncSettingTab.titleCouchDB": "Сервер CouchDB",
|
||||
"obsidianLiveSyncSettingTab.titleDeletionPropagation": "Распространение удалений",
|
||||
"obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": "Шифрование не включено",
|
||||
|
||||
@@ -498,26 +498,16 @@
|
||||
"moduleMigration.logRemoteTweakUnavailable": "無法取得遠端調校值",
|
||||
"moduleMigration.logSetupCancelled": "設定已取消,Self-hosted LiveSync 正在等候你完成設定!",
|
||||
"moduleMigration.msgFetchRemoteAgain": "你可能已經知道,Self-hosted LiveSync 變更了預設行為與資料庫結構。\n\n值得慶幸的是,在你的努力下,遠端資料庫似乎已經完成遷移。恭喜!\n\n不過,還需要再多做一點。此裝置的設定與遠端資料庫不相容,我們需要再次從遠端資料庫抓取。現在要再次從遠端抓取嗎?\n\n___注意:在變更設定並再次抓取資料庫之前,我們無法進行同步。___\n___注意 2:chunks 完全不可變,我們只需要抓取中繼資料與差異部分。___",
|
||||
"moduleMigration.msgInitialSetup": "你的裝置**尚未完成設定**。讓我引導你完成設定流程。\n\n請記得,每個對話框的內容都可以複製到剪貼簿。如果日後需要參考,可以貼到 Obsidian 的筆記中,也可以用翻譯工具翻成你的語言。\n\n首先,你有 **Setup URI** 嗎?\n\n注意:如果你不知道那是什麼,請參閱[說明文件](${URI_DOC})。",
|
||||
"moduleMigration.msgRecommendSetupUri": "我們強烈建議你產生 Setup URI 並使用它。\n如果你對此不熟悉,請參閱[說明文件](${URI_DOC})(再次抱歉,但這很重要)。\n\n你想要如何手動設定?",
|
||||
"moduleMigration.msgSinceV02321": "自 v0.23.21 起,Self-hosted LiveSync 變更了預設行為與資料庫結構,進行了以下變更:\n\n1. **檔名的大小寫敏感性**\n 現在處理檔名時不區分大小寫。這對多數平台是有益的變更,除了 Linux 與 iOS,它們無法有效處理檔名的大小寫敏感性。\n (在這些平台上,檔名相同但大小寫不同的檔案會顯示警告。)\n\n2. **chunks 的版本處理**\n chunks 是不可變的,因此可以固定其版本。這項變更會提升檔案儲存的效能。\n\n___不過,若要啟用這兩項變更中的任何一項,都需要重建遠端與本機資料庫。這個過程需要幾分鐘,建議在時間充裕時進行。___\n\n- 如果你想維持先前的行為,可以使用 `${KEEP}` 略過此程序。\n- 如果你目前沒有足夠的時間,請選擇 `${DISMISS}`,之後會再次提示你。\n- 如果你已在其他裝置上重建過資料庫,請選擇 `${DISMISS}` 並再次嘗試同步;由於偵測到差異,系統會再次提示你。",
|
||||
"moduleMigration.optionAdjustRemote": "調整為遠端設定",
|
||||
"moduleMigration.optionDecideLater": "稍後再決定",
|
||||
"moduleMigration.optionEnableBoth": "兩者都啟用",
|
||||
"moduleMigration.optionEnableFilenameCaseInsensitive": "僅啟用",
|
||||
"moduleMigration.optionEnableFixedRevisionForChunks": "僅啟用",
|
||||
"moduleMigration.optionHaveSetupUri": "有,我有",
|
||||
"moduleMigration.optionKeepPreviousBehaviour": "保留先前的行為",
|
||||
"moduleMigration.optionManualSetup": "全部手動設定",
|
||||
"moduleMigration.optionNoAskAgain": "不,請稍後再問我",
|
||||
"moduleMigration.optionNoSetupUri": "沒有,我沒有",
|
||||
"moduleMigration.optionRemindNextLaunch": "下次啟動時提醒我",
|
||||
"moduleMigration.optionSetupViaP2P": "使用「%{short_p2p_sync}」進行設定",
|
||||
"moduleMigration.optionSetupWizard": "帶我進入設定精靈",
|
||||
"moduleMigration.optionYesFetchAgain": "是,再次抓取",
|
||||
"moduleMigration.titleCaseSensitivity": "大小寫敏感性",
|
||||
"moduleMigration.titleRecommendSetupUri": "建議使用 Setup URI",
|
||||
"moduleMigration.titleWelcome": "歡迎使用 Self-hosted LiveSync",
|
||||
"moduleObsidianMenu.replicate": "複寫",
|
||||
"More actions": "更多操作",
|
||||
"Mostly Complete: Decision Required": "大致完成:需要你做出決定",
|
||||
@@ -562,12 +552,10 @@
|
||||
"obsidianLiveSyncSettingTab.btnEnable": "啟用",
|
||||
"obsidianLiveSyncSettingTab.btnFix": "修正",
|
||||
"obsidianLiveSyncSettingTab.btnGotItAndUpdated": "我知道了,且已更新。",
|
||||
"obsidianLiveSyncSettingTab.btnNext": "下一步",
|
||||
"obsidianLiveSyncSettingTab.btnStart": "開始",
|
||||
"obsidianLiveSyncSettingTab.btnTest": "測試",
|
||||
"obsidianLiveSyncSettingTab.btnUse": "使用",
|
||||
"obsidianLiveSyncSettingTab.buttonFetch": "抓取",
|
||||
"obsidianLiveSyncSettingTab.buttonNext": "下一步",
|
||||
"obsidianLiveSyncSettingTab.defaultLanguage": "預設語言",
|
||||
"obsidianLiveSyncSettingTab.descConnectSetupURI": "這是使用 Setup URI 設定 Self-hosted LiveSync 的建議方式。",
|
||||
"obsidianLiveSyncSettingTab.descCopySetupURI": "設定新裝置時非常方便!",
|
||||
@@ -629,7 +617,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgEnableCorsChttpd": "設定 chttpd.enable_cors",
|
||||
"obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": "我們建議啟用端對端加密與路徑混淆。你確定要在未加密的情況下繼續嗎?",
|
||||
"obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": "要從遠端伺服器抓取設定嗎?",
|
||||
"obsidianLiveSyncSettingTab.msgGenerateSetupURI": "全部完成!要產生 Setup URI 以便設定其他裝置嗎?",
|
||||
"obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": "如果伺服器設定並非持久保存(例如在 Docker 上執行),這裡的值可能會變動。一旦能夠連線,請更新伺服器 local.ini 中的設定。",
|
||||
"obsidianLiveSyncSettingTab.msgInvalidPassphrase": "你的加密密語可能無效。你確定要繼續嗎?",
|
||||
"obsidianLiveSyncSettingTab.msgNewVersionNote": "因為升級通知才來到這裡嗎?請查看版本歷程。若你已確認無誤,請點選按鈕。下次更新時會再次提示。",
|
||||
@@ -639,7 +626,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgObjectStorageWarning": "警告:此功能仍在開發中,請注意以下事項:\n- 僅附加架構。需要重建才能縮小儲存空間。\n- 較不穩定。\n- 首次同步時,所有歷史記錄都會從遠端傳輸,請留意流量上限與速度較慢的情況。\n- 之後只會即時同步差異部分。\n\n如果你遇到任何問題,或對此功能有任何想法,請到 GitHub 建立 issue。\n感謝你的大力支持。",
|
||||
"obsidianLiveSyncSettingTab.msgOriginCheck": "來源檢查:${org}",
|
||||
"obsidianLiveSyncSettingTab.msgRebuildRequired": "需要重建資料庫才能套用變更。請選擇套用變更的方式。\n\n<details>\n<summary>圖例</summary>\n\n| 符號 | 含意 |\n|: ------ :| ------- |\n| ⇔ | 已是最新 |\n| ⇄ | 同步以取得一致 |\n| ⇐,⇒ | 傳輸並覆寫 |\n| ⇠,⇢ | 從另一端傳輸並覆寫 |\n\n</details>\n\n## ${OPTION_REBUILD_BOTH}\n概覽: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\n以此裝置上現有的檔案重建本機與遠端資料庫。\n這會鎖定其他裝置,它們必須執行抓取。\n## ${OPTION_FETCH}\n概覽: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\n初始化本機資料庫,並以從遠端資料庫抓取的資料重建。\n這也涵蓋你已重建遠端資料庫的情況。\n## ${OPTION_ONLY_SETTING}\n僅儲存設定。**注意:這可能導致資料損毀**;通常仍需要重建資料庫。",
|
||||
"obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": "請選擇並套用任一預設項目以完成精靈。",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsCredentials": "設定 cors.credentials",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsOrigins": "設定 cors.origins",
|
||||
"obsidianLiveSyncSettingTab.msgSetMaxDocSize": "設定 couchdb.max_document_size",
|
||||
@@ -696,7 +682,6 @@
|
||||
"obsidianLiveSyncSettingTab.titleActiveRemoteServer": "使用中的遠端伺服器",
|
||||
"obsidianLiveSyncSettingTab.titleAppearance": "外觀",
|
||||
"obsidianLiveSyncSettingTab.titleConflictResolution": "衝突處理",
|
||||
"obsidianLiveSyncSettingTab.titleCongratulations": "恭喜!",
|
||||
"obsidianLiveSyncSettingTab.titleCouchDB": "CouchDB 伺服器",
|
||||
"obsidianLiveSyncSettingTab.titleDeletionPropagation": "刪除傳播",
|
||||
"obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": "尚未啟用加密",
|
||||
|
||||
@@ -293,7 +293,6 @@
|
||||
"moduleLiveSyncMain.titleScramEnabled": "紧急停止已启用",
|
||||
"moduleLocalDatabase.logWaitingForReady": "等待就绪...",
|
||||
"moduleLog.showLog": "显示日志",
|
||||
"moduleMigration.docUri": "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",
|
||||
"moduleMigration.fix0256.buttons.checkItLater": "稍后检查",
|
||||
"moduleMigration.fix0256.buttons.DismissForever": "我已经修复了,不再询问",
|
||||
"moduleMigration.fix0256.buttons.fix": "修复",
|
||||
@@ -315,26 +314,16 @@
|
||||
"moduleMigration.logRemoteTweakUnavailable": "无法获取远程调整值",
|
||||
"moduleMigration.logSetupCancelled": "设置已取消,Self-hosted LiveSync 正在等待您的设置!",
|
||||
"moduleMigration.msgFetchRemoteAgain": "您可能已经知道,Self-hosted LiveSync 更改了其默认行为和数据库结构。\n\n值得庆幸的是,在您的时间和努力下,远程数据库似乎已经迁移完成。恭喜!\n\n但是,我们还需要一点点操作。此设备的配置与远程数据库不兼容。我们需要再次从远程数据库获取。我们现在应该再次从远程获取吗?\n\n___注意:在更改配置并再次获取数据库之前,我们无法进行同步。___\n___注意2:chunks 是完全不可变的,我们只能获取元数据和差异",
|
||||
"moduleMigration.msgInitialSetup": "您的设备**尚未设置**。让我引导您完成设置过程。\n\n请记住,每个对话框内容都可以复制到剪贴板。如果以后需要参考,可以将其粘贴到 Obsidian 的笔记中。您也可以使用翻译工具将其翻译成您的语言。\n\n首先,您有**设置 URI** 吗?\n\n注意:如果您不知道这是什么,请参阅[文档](${URI_DOC})",
|
||||
"moduleMigration.msgRecommendSetupUri": "我们强烈建议您生成一个设置 URI 并使用它。\n如果您对此不了解,请参阅[文档](${URI_DOC})(再次抱歉,但这很重要)。\n\n您想如何手动设置?",
|
||||
"moduleMigration.msgSinceV02321": "自 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}` 并尝试再次同步。由于检测到差异,系统会再次提示您",
|
||||
"moduleMigration.optionAdjustRemote": "调整到远程设置",
|
||||
"moduleMigration.optionDecideLater": "稍后决定",
|
||||
"moduleMigration.optionEnableBoth": "启用两者",
|
||||
"moduleMigration.optionEnableFilenameCaseInsensitive": "仅启用 #1",
|
||||
"moduleMigration.optionEnableFixedRevisionForChunks": "仅启用 #2",
|
||||
"moduleMigration.optionHaveSetupUri": "是的,我有",
|
||||
"moduleMigration.optionKeepPreviousBehaviour": "保持以前的行为",
|
||||
"moduleMigration.optionManualSetup": "全部手动设置",
|
||||
"moduleMigration.optionNoAskAgain": "不,请稍后再次询问",
|
||||
"moduleMigration.optionNoSetupUri": "不,我没有",
|
||||
"moduleMigration.optionRemindNextLaunch": "下次启动时提醒我",
|
||||
"moduleMigration.optionSetupViaP2P": "Use %{short_p2p_sync} to set up",
|
||||
"moduleMigration.optionSetupWizard": "带我进入设置向导",
|
||||
"moduleMigration.optionYesFetchAgain": "是的,再次获取",
|
||||
"moduleMigration.titleCaseSensitivity": "大小写敏感性",
|
||||
"moduleMigration.titleRecommendSetupUri": "推荐使用设置 URI",
|
||||
"moduleMigration.titleWelcome": "欢迎使用 Self-hosted LiveSync",
|
||||
"moduleObsidianMenu.replicate": "复制",
|
||||
"More actions": "更多操作",
|
||||
"Move remotely deleted files to the trash, instead of deleting.": "将远程删除的文件移至回收站,而不是直接删除",
|
||||
@@ -363,12 +352,10 @@
|
||||
"obsidianLiveSyncSettingTab.btnEnable": "启用",
|
||||
"obsidianLiveSyncSettingTab.btnFix": "修复",
|
||||
"obsidianLiveSyncSettingTab.btnGotItAndUpdated": "我明白了并且已更新",
|
||||
"obsidianLiveSyncSettingTab.btnNext": "下一步",
|
||||
"obsidianLiveSyncSettingTab.btnStart": "开始",
|
||||
"obsidianLiveSyncSettingTab.btnTest": "测试",
|
||||
"obsidianLiveSyncSettingTab.btnUse": "使用",
|
||||
"obsidianLiveSyncSettingTab.buttonFetch": "获取",
|
||||
"obsidianLiveSyncSettingTab.buttonNext": "下一步",
|
||||
"obsidianLiveSyncSettingTab.defaultLanguage": "默认语言",
|
||||
"obsidianLiveSyncSettingTab.descConnectSetupURI": "这是使用设置 URI 设置 Self-hosted LiveSync 的推荐方法",
|
||||
"obsidianLiveSyncSettingTab.descCopySetupURI": "非常适合设置新设备!",
|
||||
@@ -431,7 +418,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgEnableCorsChttpd": "设置 chttpd.enable_cors",
|
||||
"obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": "建议启用端到端加密和路径混淆。你确定要在未加密的情况下继续吗?",
|
||||
"obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": "要从远端服务器获取配置吗?",
|
||||
"obsidianLiveSyncSettingTab.msgGenerateSetupURI": "全部完成!要生成设置 URI 以便配置其他设备吗?",
|
||||
"obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": "如果服务器配置不是持久的(例如,在 docker 上运行),此处的值可能会更改。一旦能够连接,请更新服务器 local.ini 中的设置",
|
||||
"obsidianLiveSyncSettingTab.msgInvalidPassphrase": "你的加密密码短语可能无效。你确定要继续吗?",
|
||||
"obsidianLiveSyncSettingTab.msgNewVersionNote": "因为升级通知来到这里?请查看版本历史。如果您满意,请点击按钮。新的更新将再次提示此信息",
|
||||
@@ -441,7 +427,6 @@
|
||||
"obsidianLiveSyncSettingTab.msgObjectStorageWarning": "警告:此功能仍在开发中,请注意以下几点:\n- 仅追加架构。需要重建才能缩小存储空间。\n- 有点脆弱。\n- 首次同步时,所有历史记录将从远程传输。注意数据上限和慢速。\n- 只有差异会实时同步。\n\n如果您遇到任何问题,或对此功能有任何想法,请在 GitHub 上创建 issue。\n感谢您的巨大贡献",
|
||||
"obsidianLiveSyncSettingTab.msgOriginCheck": "源检查: {org}",
|
||||
"obsidianLiveSyncSettingTab.msgRebuildRequired": "需要重建数据库以应用更改。请选择应用更改的方法。\n\n<details>\n<summary>图例</summary>\n\n| 符号 | 含义 |\n|: ------ :| ------- |\n| ⇔ | 最新 |\n| ⇄ | 同步以平衡 |\n| ⇐,⇒ | 传输以覆盖 |\n| ⇠,⇢ | 从另一侧传输以覆盖 |\n\n</details>\n\n## ${OPTION_REBUILD_BOTH}\n概览:📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\n使用此设备的现有文件重建本地和远程数据库。\n这将导致其他设备被锁定,并且它们需要执行获取操作。\n## ${OPTION_FETCH}\n概览:📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\n初始化本地数据库并使用从远程数据库获取的数据重建它。\n这种情况包括您已经重建了远程数据库的情况。\n## ${OPTION_ONLY_SETTING}\n仅存储设置。**注意:这可能导致数据损坏**;通常需要重建数据库",
|
||||
"obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": "请选择并应用任一预设项以完成向导。",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsCredentials": "设置 cors.credentials",
|
||||
"obsidianLiveSyncSettingTab.msgSetCorsOrigins": "设置 cors.origins",
|
||||
"obsidianLiveSyncSettingTab.msgSetMaxDocSize": "设置 couchdb.max_document_size",
|
||||
@@ -498,7 +483,6 @@
|
||||
"obsidianLiveSyncSettingTab.titleActiveRemoteServer": "活动远程服务器",
|
||||
"obsidianLiveSyncSettingTab.titleAppearance": "外观",
|
||||
"obsidianLiveSyncSettingTab.titleConflictResolution": "冲突处理",
|
||||
"obsidianLiveSyncSettingTab.titleCongratulations": "恭喜!",
|
||||
"obsidianLiveSyncSettingTab.titleCouchDB": "CouchDB 服务器",
|
||||
"obsidianLiveSyncSettingTab.titleDeletionPropagation": "删除传播",
|
||||
"obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": "尚未启用加密",
|
||||
|
||||
@@ -92,8 +92,6 @@ Normal Files: Normale Dateien
|
||||
obsidianLiveSyncSettingTab:
|
||||
btnApply: Anwenden
|
||||
btnDisable: Deaktivieren
|
||||
btnNext: Weiter
|
||||
buttonNext: Weiter
|
||||
defaultLanguage: Standardsprache
|
||||
labelDisabled: "⏹️ : Deaktiviert"
|
||||
labelEnabled: "🔁 : Aktiviert"
|
||||
@@ -106,12 +104,8 @@ obsidianLiveSyncSettingTab:
|
||||
und Pfadverschleierung zu aktivieren. Möchten Sie wirklich ohne
|
||||
Verschlüsselung fortfahren?
|
||||
msgFetchConfigFromRemote: Möchten Sie die Konfiguration vom Remote-Server abrufen?
|
||||
msgGenerateSetupURI: Alles fertig! Möchten Sie eine Setup-URI erzeugen, um
|
||||
andere Geräte einzurichten?
|
||||
msgInvalidPassphrase: Ihre Verschlüsselungs-Passphrase könnte ungültig sein.
|
||||
Möchten Sie wirklich fortfahren?
|
||||
msgSelectAndApplyPreset: Bitte wählen und übernehmen Sie eine beliebige
|
||||
Voreinstellung, um den Assistenten abzuschließen.
|
||||
nameDisableHiddenFileSync: Synchronisation versteckter Dateien deaktivieren
|
||||
nameEnableHiddenFileSync: Synchronisation versteckter Dateien aktivieren
|
||||
nameHiddenFileSynchronization: Synchronisation versteckter Dateien
|
||||
@@ -122,7 +116,6 @@ obsidianLiveSyncSettingTab:
|
||||
optionPeriodicWithBatch: Periodisch mit Stapelverarbeitung
|
||||
titleAppearance: Darstellung
|
||||
titleConflictResolution: Konfliktbehandlung
|
||||
titleCongratulations: Glückwunsch!
|
||||
titleCouchDB: CouchDB-Server
|
||||
titleDeletionPropagation: Weitergabe von Löschungen
|
||||
titleEncryptionNotEnabled: Verschlüsselung ist nicht aktiviert
|
||||
|
||||
@@ -725,7 +725,6 @@ moduleLocalDatabase:
|
||||
moduleLog:
|
||||
showLog: Show Log
|
||||
moduleMigration:
|
||||
docUri: https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use
|
||||
fix0256:
|
||||
buttons:
|
||||
checkItLater: Check it later
|
||||
@@ -819,30 +818,6 @@ moduleMigration:
|
||||
|
||||
___Note2: The chunks are completely immutable, we can fetch only the
|
||||
metadata and difference.___
|
||||
msgInitialSetup: >-
|
||||
Your device has **not been set up yet**. Let me guide you through the setup
|
||||
process.
|
||||
|
||||
|
||||
Please 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.
|
||||
|
||||
|
||||
First, do you have **Setup URI**?
|
||||
|
||||
|
||||
Note: If you do not know what it is, please refer to the
|
||||
[documentation](${URI_DOC}).
|
||||
msgRecommendSetupUri: >-
|
||||
We strongly recommend that you generate a set-up URI and use it.
|
||||
|
||||
If you do not have knowledge about it, please refer to the
|
||||
[documentation](${URI_DOC}) (Sorry again, but it is important).
|
||||
|
||||
|
||||
How do you want to set it up manually?
|
||||
msgSinceV02321: >-
|
||||
Since v0.23.21, the self-hosted LiveSync has changed the default behaviour
|
||||
and database structure. The following changes have been made:
|
||||
@@ -874,18 +849,10 @@ moduleMigration:
|
||||
optionEnableBoth: Enable both
|
||||
optionEnableFilenameCaseInsensitive: "Enable only #1"
|
||||
optionEnableFixedRevisionForChunks: "Enable only #2"
|
||||
optionHaveSetupUri: Yes, I have
|
||||
optionKeepPreviousBehaviour: Keep previous behaviour
|
||||
optionManualSetup: Set it up all manually
|
||||
optionNoAskAgain: No, please ask again
|
||||
optionNoSetupUri: No, I do not have
|
||||
optionRemindNextLaunch: Remind me at the next launch
|
||||
optionSetupViaP2P: Use %{short_p2p_sync} to set up
|
||||
optionSetupWizard: Take me into the setup wizard
|
||||
optionYesFetchAgain: Yes, fetch again
|
||||
titleCaseSensitivity: Case Sensitivity
|
||||
titleRecommendSetupUri: Recommendation to use Setup URI
|
||||
titleWelcome: Welcome to Self-hosted LiveSync
|
||||
moduleObsidianMenu:
|
||||
replicate: Replicate
|
||||
More actions: More actions
|
||||
@@ -940,12 +907,10 @@ obsidianLiveSyncSettingTab:
|
||||
btnEnable: Enable
|
||||
btnFix: Fix
|
||||
btnGotItAndUpdated: I got it and updated.
|
||||
btnNext: Next
|
||||
btnStart: Start
|
||||
btnTest: Test
|
||||
btnUse: Use
|
||||
buttonFetch: Fetch
|
||||
buttonNext: Next
|
||||
defaultLanguage: Default
|
||||
descConnectSetupURI: This is the recommended method to set up Self-hosted
|
||||
LiveSync with a Setup URI.
|
||||
@@ -1018,7 +983,6 @@ obsidianLiveSyncSettingTab:
|
||||
msgEnableEncryptionRecommendation: We recommend enabling End-To-End Encryption,
|
||||
and Path Obfuscation. Are you sure you want to continue without encryption?
|
||||
msgFetchConfigFromRemote: Do you want to fetch the config from the remote server?
|
||||
msgGenerateSetupURI: All done! Do you want to generate a setup URI to set up other devices?
|
||||
msgIfConfigNotPersistent: 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.
|
||||
@@ -1098,7 +1062,6 @@ obsidianLiveSyncSettingTab:
|
||||
|
||||
Store only the settings. **Caution: This may lead to data corruption**;
|
||||
database reconstruction is generally necessary.
|
||||
msgSelectAndApplyPreset: Please select and apply any preset item to complete the wizard.
|
||||
msgSetCorsCredentials: Set cors.credentials
|
||||
msgSetCorsOrigins: Set cors.origins
|
||||
msgSetMaxDocSize: Set couchdb.max_document_size
|
||||
@@ -1158,12 +1121,17 @@ obsidianLiveSyncSettingTab:
|
||||
titleActiveRemoteServer: Active Remote Server
|
||||
titleAppearance: Appearance
|
||||
titleConflictResolution: Conflict resolution
|
||||
titleCongratulations: Congratulations!
|
||||
titleCouchDB: CouchDB
|
||||
titleDeletionPropagation: Deletion Propagation
|
||||
titleEncryptionNotEnabled: Encryption is not enabled
|
||||
titleEncryptionPassphraseInvalid: Encryption Passphrase Invalid
|
||||
titleExtraFeatures: Enable extra and advanced features
|
||||
titleExtraFeaturesGroup: Extra features
|
||||
titleExtraMenus: Extra menus
|
||||
titleHelpAndInformation: Help and information
|
||||
titleHelpAndTroubleshooting: Help and troubleshooting
|
||||
titleMaintenanceAndRecovery: Maintenance and recovery
|
||||
titleAdvancedSettings: Advanced settings
|
||||
titleFetchConfig: Fetch Config
|
||||
titleFetchConfigFromRemote: Fetch config from remote server
|
||||
titleFetchSettings: Fetch Settings
|
||||
@@ -1177,7 +1145,8 @@ obsidianLiveSyncSettingTab:
|
||||
titleRemoteConfigCheckFailed: Remote Configuration Check Failed
|
||||
titleRemoteServer: Remote Server
|
||||
titleReset: Reset
|
||||
titleSetupOtherDevices: To setup other devices
|
||||
titleSetupOtherDevices: Set up other devices
|
||||
titleSynchronisation: Synchronisation
|
||||
titleSynchronizationMethod: Synchronization Method
|
||||
titleSynchronizationPreset: Synchronization Preset
|
||||
titleSyncSettings: Sync Settings
|
||||
@@ -1469,8 +1438,8 @@ Replicator:
|
||||
InitialiseFatalError: No replicator is available, this is the fatal error.
|
||||
Pending: Some file events are pending. Replication has been cancelled.
|
||||
SomeModuleFailed: Replication has been cancelled by some module failure
|
||||
VersionUpFlash: An update has been detected. Please open the Settings dialogue
|
||||
and check the Change Log. Replication has been cancelled.
|
||||
VersionUpFlash: Remote synchronisation is paused for compatibility review. Run the
|
||||
'Review why synchronisation is paused' command for details and available actions.
|
||||
Requires restart of Obsidian: Requires restart of Obsidian
|
||||
Requires restart of Obsidian.: Requires restart of Obsidian.
|
||||
Rerun Onboarding Wizard: Rerun Onboarding Wizard
|
||||
@@ -2340,6 +2309,29 @@ Ui:
|
||||
Fetch: Fetch
|
||||
Overwrite: Overwrite
|
||||
SetupWizard:
|
||||
ApplySettingsInitialisation:
|
||||
ApplyWithoutInitialisation: Apply without Initialisation
|
||||
Back: Review another way to apply these settings
|
||||
BypassGuidance: Applying these settings alone can make this device incompatible with its existing synchronisation data. Use this only when you have confirmed that reconstruction is unnecessary.
|
||||
BypassTitle: Apply Settings without Initialisation?
|
||||
ContinueFetch: Continue with Fetch
|
||||
FetchOption: Reset Synchronisation on This Device
|
||||
FetchOptionDesc: After restarting, rebuild this device's local database from the current remote synchronisation data. Files in the Vault will then be reconciled with that data.
|
||||
FetchOptionP2PDesc: After restarting, select an online source device. This device's local LiveSync database will be rebuilt from that source.
|
||||
Guidance: These setting changes alter how synchronisation data is interpreted. Apply them together with an initialisation operation after restart.
|
||||
KeepEditing: Keep Editing
|
||||
ProceedFetch: Restart and Fetch Synchronisation Data
|
||||
ProceedFetchP2P: Restart and Select a Source Device
|
||||
ProceedRebuild: Restart and Overwrite Server Data
|
||||
ProceedRebuildP2P: Restart and Prepare This Device
|
||||
Question: Which existing data should be used after restart?
|
||||
RebuildOption: Overwrite Server Data with This Device's Files
|
||||
RebuildOptionDesc: Rebuild the local and remote databases from the files currently in this Vault. Other synchronising devices must reset their local synchronisation afterwards.
|
||||
RebuildOptionP2P: Prepare This Device from This Vault
|
||||
RebuildOptionP2PDesc: Rebuild this device's local LiveSync database from the files currently in this Vault. This does not overwrite another device.
|
||||
RemoteVerificationGuidance: The configured remote could not be verified with the current credentials and encryption settings. Continuing may make the Fetch fail after restart.
|
||||
RemoteVerificationTitle: Remote Synchronisation Data Could Not Be Verified
|
||||
Title: Apply Settings and Reinitialise Synchronisation
|
||||
Common:
|
||||
Back: No, please take me back
|
||||
Cancel: Cancel
|
||||
|
||||
@@ -751,7 +751,6 @@ moduleLocalDatabase:
|
||||
moduleLog:
|
||||
showLog: Mostrar registro
|
||||
moduleMigration:
|
||||
docUri: https://github.com/vrtmrz/obsidian-livesync/blob/main/README_ES.md#how-to-use
|
||||
logBulkSendCorrupted: El envío de fragmentos en bloque se ha habilitado, sin
|
||||
embargo, esta función se ha corrompido. Disculpe las molestias.
|
||||
Deshabilitado automáticamente.
|
||||
@@ -782,30 +781,6 @@ moduleMigration:
|
||||
|
||||
___Nota2: Los fragmentos son completamente inmutables, solo podemos obtener
|
||||
los metadatos y diferencias.___
|
||||
msgInitialSetup: >-
|
||||
Tu dispositivo **aún no ha sido configurado**. Permíteme guiarte a través
|
||||
del proceso de configuración.
|
||||
|
||||
|
||||
Ten 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.
|
||||
|
||||
|
||||
Primero, ¿tienes **URI de configuración**?
|
||||
|
||||
|
||||
Nota: Si no sabes qué es, consulta la [documentación](${URI_DOC}).
|
||||
msgRecommendSetupUri: >-
|
||||
Te recomendamos encarecidamente que generes una URI de configuración y la
|
||||
utilices.
|
||||
|
||||
Si no tienes conocimientos al respecto, consulta la
|
||||
[documentación](${URI_DOC}) (Lo siento de nuevo, pero es importante).
|
||||
|
||||
|
||||
¿Cómo quieres configurarlo manualmente?
|
||||
msgSinceV02321: >-
|
||||
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
|
||||
@@ -838,9 +813,7 @@ moduleMigration:
|
||||
optionEnableBoth: Habilitar ambos
|
||||
optionEnableFilenameCaseInsensitive: "Habilitar solo #1"
|
||||
optionEnableFixedRevisionForChunks: "Habilitar solo #2"
|
||||
optionHaveSetupUri: Sí, tengo
|
||||
optionKeepPreviousBehaviour: Mantener comportamiento anterior
|
||||
optionManualSetup: Configurarlo todo manualmente
|
||||
optionNoAskAgain: No, por favor pregúntame de nuevo
|
||||
fix0256:
|
||||
buttons:
|
||||
@@ -909,14 +882,8 @@ moduleMigration:
|
||||
Nota 2: reconstruir todo y obtener los datos consume algo de tiempo y de
|
||||
tráfico; hazlo en horas de poco uso y con una conexión de red estable.
|
||||
title: ¡Se han encontrado chunks no seguros!
|
||||
optionNoSetupUri: No, no tengo
|
||||
optionRemindNextLaunch: Recordármelo en el próximo inicio
|
||||
optionSetupViaP2P: Usar %{short_p2p_sync} para configurarlo
|
||||
optionSetupWizard: Llévame al asistente de configuración
|
||||
optionYesFetchAgain: Sí, obtener de nuevo
|
||||
titleCaseSensitivity: Distinción de mayúsculas y minúsculas
|
||||
titleRecommendSetupUri: Recomendación de usar un Setup URI
|
||||
titleWelcome: Bienvenido a Self-hosted LiveSync
|
||||
"Mostly Complete: Decision Required": "Casi terminado: se requiere una decisión"
|
||||
My remote server is already set up. I want to join this device.: Mi servidor remoto ya está configurado. Quiero añadir este dispositivo.
|
||||
Name: Nombre
|
||||
@@ -1506,12 +1473,10 @@ obsidianLiveSyncSettingTab:
|
||||
btnEnable: Activar
|
||||
btnFix: Corregir
|
||||
btnGotItAndUpdated: Lo entendí y actualicé.
|
||||
btnNext: Siguiente
|
||||
btnStart: Iniciar
|
||||
btnTest: Probar
|
||||
btnUse: Usar
|
||||
buttonFetch: Obtener
|
||||
buttonNext: Siguiente
|
||||
defaultLanguage: Predeterminado
|
||||
descConnectSetupURI: Este es el método recomendado para configurar Self-hosted
|
||||
LiveSync con una URI de configuración.
|
||||
@@ -1585,8 +1550,6 @@ obsidianLiveSyncSettingTab:
|
||||
a extremo y la obfuscación de ruta. ¿Estás seguro de querer continuar sin
|
||||
cifrado?
|
||||
msgFetchConfigFromRemote: ¿Quieres obtener la configuración del servidor remoto?
|
||||
msgGenerateSetupURI: ¡Todo listo! ¿Quieres generar un URI de configuración para
|
||||
configurar otros dispositivos?
|
||||
msgIfConfigNotPersistent: 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
|
||||
@@ -1670,8 +1633,6 @@ obsidianLiveSyncSettingTab:
|
||||
|
||||
Almacena solo la configuración. **Precaución: esto puede provocar corrupción
|
||||
de datos**; generalmente es necesario reconstruir la base de datos.
|
||||
msgSelectAndApplyPreset: Por favor, selecciona y aplica cualquier elemento
|
||||
preestablecido para completar el asistente.
|
||||
msgSetCorsCredentials: Configurar cors.credentials
|
||||
msgSetCorsOrigins: Configurar cors.origins
|
||||
msgSetMaxDocSize: Configurar couchdb.max_document_size
|
||||
@@ -1729,7 +1690,6 @@ obsidianLiveSyncSettingTab:
|
||||
panelSetup: Configuración
|
||||
titleAppearance: Apariencia
|
||||
titleConflictResolution: Resolución de conflictos
|
||||
titleCongratulations: ¡Felicidades!
|
||||
titleCouchDB: Servidor CouchDB
|
||||
titleDeletionPropagation: Propagación de eliminación
|
||||
titleEncryptionNotEnabled: El cifrado no está habilitado
|
||||
|
||||
@@ -384,7 +384,6 @@ moduleLocalDatabase:
|
||||
moduleLog:
|
||||
showLog: Afficher le journal
|
||||
moduleMigration:
|
||||
docUri: https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use
|
||||
fix0256:
|
||||
buttons:
|
||||
checkItLater: Vérifier plus tard
|
||||
@@ -482,31 +481,6 @@ moduleMigration:
|
||||
|
||||
___Note 2 : Les fragments sont complètement immuables, nous ne pouvons
|
||||
récupérer que les métadonnées et les différences.___
|
||||
msgInitialSetup: >-
|
||||
Votre appareil n'a **pas encore été configuré**. Laissez-moi vous guider
|
||||
dans le processus de configuration.
|
||||
|
||||
|
||||
Veuillez 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.
|
||||
|
||||
|
||||
Tout d'abord, disposez-vous d'une **URI de configuration** ?
|
||||
|
||||
|
||||
Note : Si vous ne savez pas ce que c'est, consultez la
|
||||
[documentation](${URI_DOC}).
|
||||
msgRecommendSetupUri: >-
|
||||
Nous recommandons vivement de générer une URI de configuration et de
|
||||
l'utiliser.
|
||||
|
||||
Si vous ne connaissez pas, veuillez consulter la
|
||||
[documentation](${URI_DOC}) (Désolé encore, mais c'est important).
|
||||
|
||||
|
||||
Comment souhaitez-vous effectuer la configuration manuellement ?
|
||||
msgSinceV02321: >-
|
||||
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é
|
||||
@@ -539,18 +513,10 @@ moduleMigration:
|
||||
optionEnableBoth: Activer les deux
|
||||
optionEnableFilenameCaseInsensitive: "Activer seulement #1"
|
||||
optionEnableFixedRevisionForChunks: "Activer seulement #2"
|
||||
optionHaveSetupUri: Oui, j'en ai une
|
||||
optionKeepPreviousBehaviour: Conserver le comportement précédent
|
||||
optionManualSetup: Tout configurer manuellement
|
||||
optionNoAskAgain: Non, demandez à nouveau
|
||||
optionNoSetupUri: Non, je n'en ai pas
|
||||
optionRemindNextLaunch: Me rappeler au prochain lancement
|
||||
optionSetupViaP2P: Utiliser %{short_p2p_sync} pour configurer
|
||||
optionSetupWizard: Ouvrir l'assistant de configuration
|
||||
optionYesFetchAgain: Oui, récupérer à nouveau
|
||||
titleCaseSensitivity: Sensibilité à la casse
|
||||
titleRecommendSetupUri: Recommandation d'utilisation de l'URI de configuration
|
||||
titleWelcome: Bienvenue dans Self-hosted LiveSync
|
||||
moduleObsidianMenu:
|
||||
replicate: Répliquer
|
||||
Move remotely deleted files to the trash, instead of deleting.: Déplacer les fichiers supprimés à distance vers la corbeille, au lieu de les supprimer.
|
||||
@@ -574,12 +540,10 @@ obsidianLiveSyncSettingTab:
|
||||
btnEnable: Activer
|
||||
btnFix: Corriger
|
||||
btnGotItAndUpdated: J'ai compris et mis à jour.
|
||||
btnNext: Suivant
|
||||
btnStart: Démarrer
|
||||
btnTest: Tester
|
||||
btnUse: Utiliser
|
||||
buttonFetch: Récupérer
|
||||
buttonNext: Suivant
|
||||
defaultLanguage: Par défaut
|
||||
descConnectSetupURI: Méthode recommandée pour configurer Self-hosted LiveSync
|
||||
avec une URI de configuration.
|
||||
@@ -653,7 +617,6 @@ obsidianLiveSyncSettingTab:
|
||||
de bout en bout et l'obfuscation des chemins. Êtes-vous sûr de vouloir
|
||||
continuer sans chiffrement ?
|
||||
msgFetchConfigFromRemote: Voulez-vous récupérer la configuration depuis le serveur distant ?
|
||||
msgGenerateSetupURI: Tout est prêt ! Voulez-vous générer une URI de configuration pour configurer d'autres appareils ?
|
||||
msgIfConfigNotPersistent: 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
|
||||
@@ -736,7 +699,6 @@ obsidianLiveSyncSettingTab:
|
||||
Ne 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.
|
||||
msgSelectAndApplyPreset: Veuillez sélectionner et appliquer un préréglage pour terminer l'assistant.
|
||||
msgSetCorsCredentials: Définir cors.credentials
|
||||
msgSetCorsOrigins: Définir cors.origins
|
||||
msgSetMaxDocSize: Définir couchdb.max_document_size
|
||||
@@ -797,7 +759,6 @@ obsidianLiveSyncSettingTab:
|
||||
titleActiveRemoteServer: Serveur distant actif
|
||||
titleAppearance: Apparence
|
||||
titleConflictResolution: Résolution des conflits
|
||||
titleCongratulations: Félicitations !
|
||||
titleCouchDB: CouchDB
|
||||
titleDeletionPropagation: Propagation des suppressions
|
||||
titleEncryptionNotEnabled: Le chiffrement n'est pas activé
|
||||
|
||||
@@ -344,7 +344,6 @@ moduleLocalDatabase:
|
||||
moduleLog:
|
||||
showLog: הצג יומן
|
||||
moduleMigration:
|
||||
docUri: https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use
|
||||
fix0256:
|
||||
buttons:
|
||||
checkItLater: בדוק מאוחר יותר
|
||||
@@ -430,25 +429,6 @@ moduleMigration:
|
||||
|
||||
___הערה 2: הנתחים הם בלתי-ניתנים לשינוי לחלוטין, ניתן למשוך רק את המטה-נתונים
|
||||
וההפרש.___
|
||||
msgInitialSetup: >-
|
||||
המכשיר שלך **טרם הוגדר**. אנחנו כאן לעזור לך בתהליך ההגדרה.
|
||||
|
||||
|
||||
שים לב שניתן להעתיק את תוכן כל דיאלוג ללוח. אם צריך לחזור אליו מאוחר יותר,
|
||||
ניתן להדביק אותו כפתק ב-Obsidian. ניתן גם לתרגם לשפתך בעזרת כלי תרגום.
|
||||
|
||||
|
||||
ראשית, האם יש לך **Setup URI**?
|
||||
|
||||
|
||||
הערה: אם אינך יודע מהו, אנא עיין ב[תיעוד](${URI_DOC}).
|
||||
msgRecommendSetupUri: >-
|
||||
אנו ממליצים בחום לייצר Setup URI ולהשתמש בו.
|
||||
|
||||
אם אין לך ידע בנושא, אנא עיין ב[תיעוד](${URI_DOC}) (מתנצלים שוב, אך זה חשוב).
|
||||
|
||||
|
||||
כיצד ברצונך להגדיר ידנית?
|
||||
msgSinceV02321: >-
|
||||
מאז גרסה 0.23.21, Self-hosted LiveSync שינה את התנהגות ברירת המחדל ומבנה מסד
|
||||
הנתונים. השינויים הבאים בוצעו:
|
||||
@@ -478,18 +458,10 @@ moduleMigration:
|
||||
optionEnableBoth: הפעל את שניהם
|
||||
optionEnableFilenameCaseInsensitive: "הפעל רק #1"
|
||||
optionEnableFixedRevisionForChunks: "הפעל רק #2"
|
||||
optionHaveSetupUri: כן, יש לי
|
||||
optionKeepPreviousBehaviour: שמור על התנהגות קודמת
|
||||
optionManualSetup: הגדר הכל ידנית
|
||||
optionNoAskAgain: לא, אנא שאל שוב
|
||||
optionNoSetupUri: לא, אין לי
|
||||
optionRemindNextLaunch: הזכר לי בהפעלה הבאה
|
||||
optionSetupViaP2P: השתמש ב-%{short_p2p_sync} להגדרה
|
||||
optionSetupWizard: קח אותי לאשף ההגדרה
|
||||
optionYesFetchAgain: כן, משוך שוב
|
||||
titleCaseSensitivity: תלות רישיות
|
||||
titleRecommendSetupUri: המלצה לשימוש ב-Setup URI
|
||||
titleWelcome: ברוך הבא ל-Self-hosted LiveSync
|
||||
moduleObsidianMenu:
|
||||
replicate: שכפל
|
||||
Move remotely deleted files to the trash, instead of deleting.: העבר קבצים שנמחקו מרחוק לאשפה, במקום למחוק.
|
||||
@@ -512,12 +484,10 @@ obsidianLiveSyncSettingTab:
|
||||
btnEnable: הפעל
|
||||
btnFix: תקן
|
||||
btnGotItAndUpdated: הבנתי ועדכנתי.
|
||||
btnNext: הבא
|
||||
btnStart: התחל
|
||||
btnTest: בדוק
|
||||
btnUse: השתמש
|
||||
buttonFetch: משוך
|
||||
buttonNext: הבא
|
||||
defaultLanguage: ברירת מחדל
|
||||
descConnectSetupURI: זוהי השיטה המומלצת להגדרת Self-hosted LiveSync עם Setup URI.
|
||||
descCopySetupURI: מושלם להגדרת מכשיר חדש!
|
||||
@@ -586,7 +556,6 @@ obsidianLiveSyncSettingTab:
|
||||
msgEnableEncryptionRecommendation: אנו ממליצים להפעיל הצפנה מקצה לקצה ואת ערפול
|
||||
הנתיב. האם אתה בטוח שברצונך להמשיך ללא הצפנה?
|
||||
msgFetchConfigFromRemote: האם ברצונך למשוך את התצורה מהשרת המרוחד?
|
||||
msgGenerateSetupURI: הכל מוכן! האם ברצונך לייצר Setup URI להגדרת מכשירים אחרים?
|
||||
msgIfConfigNotPersistent: אם תצורת השרת אינה קבועה (למשל, פועלת ב-docker), הערכים
|
||||
כאן עשויים להשתנות. לאחר שתצליח להתחבר, אנא עדכן את ההגדרות ב-local.ini של השרת.
|
||||
msgInvalidPassphrase: ביטוי הסיסמה להצפנה שלך עשוי להיות לא תקין. האם אתה בטוח
|
||||
@@ -659,7 +628,6 @@ obsidianLiveSyncSettingTab:
|
||||
|
||||
שמור רק את ההגדרות. **זהירות: עלול לגרום לפגיעה בנתונים**; בנייה מחדש של מסד
|
||||
הנתונים נדרשת בדרך כלל.
|
||||
msgSelectAndApplyPreset: אנא בחר והחל פריט קבוע מראש כלשהו להשלמת האשף.
|
||||
msgSetCorsCredentials: הגדר cors.credentials
|
||||
msgSetCorsOrigins: הגדר cors.origins
|
||||
msgSetMaxDocSize: הגדר couchdb.max_document_size
|
||||
@@ -718,7 +686,6 @@ obsidianLiveSyncSettingTab:
|
||||
titleActiveRemoteServer: שרת מרוחד פעיל
|
||||
titleAppearance: מראה
|
||||
titleConflictResolution: פתרון קונפליקטים
|
||||
titleCongratulations: מזל טוב!
|
||||
titleCouchDB: CouchDB
|
||||
titleDeletionPropagation: הפצת מחיקות
|
||||
titleEncryptionNotEnabled: ההצפנה אינה מופעלת
|
||||
|
||||
@@ -356,7 +356,6 @@ moduleLocalDatabase:
|
||||
moduleLog:
|
||||
showLog: ログを表示
|
||||
moduleMigration:
|
||||
docUri: https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use
|
||||
fix0256:
|
||||
buttons:
|
||||
checkItLater: 後で確認する
|
||||
@@ -416,19 +415,6 @@ moduleMigration:
|
||||
|
||||
___注意: 設定が変更され、データベースが再フェッチされるまで同期できません。___
|
||||
___注意2: チャンクは完全に不変なので、メタデータと差分のみフェッチできます。___
|
||||
msgInitialSetup: |-
|
||||
このデバイスは**まだセットアップされていません**。セットアッププロセスをご案内します。
|
||||
|
||||
すべてのダイアログの内容はクリップボードにコピーできます。後で参照する必要があれば、Obsidianのノートに貼り付けてください。翻訳ツールを使ってお使いの言語に翻訳することもできます。
|
||||
|
||||
まず、**セットアップURI**をお持ちですか?
|
||||
|
||||
注意: それが何か分からない場合は、[documentation](${URI_DOC})を参照してください。
|
||||
msgRecommendSetupUri: |-
|
||||
セットアップURIを生成して使用することを強くお勧めします。
|
||||
これについて知識がない場合は、[documentation](${URI_DOC})を参照してください(重要です)。
|
||||
|
||||
手動でセットアップしますか?
|
||||
msgSinceV02321: |-
|
||||
v0.23.21以降、self-hosted LiveSyncはデフォルトの動作とデータベース構造を変更しました。以下の変更が行われました:
|
||||
|
||||
@@ -449,18 +435,10 @@ moduleMigration:
|
||||
optionEnableBoth: 両方を有効にする
|
||||
optionEnableFilenameCaseInsensitive: "#1のみ有効にする"
|
||||
optionEnableFixedRevisionForChunks: "#2のみ有効にする"
|
||||
optionHaveSetupUri: はい、持っています
|
||||
optionKeepPreviousBehaviour: 以前の動作を維持
|
||||
optionManualSetup: すべて手動でセットアップ
|
||||
optionNoAskAgain: いいえ、後で確認する
|
||||
optionNoSetupUri: いいえ、持っていません
|
||||
optionRemindNextLaunch: 次回起動時にリマインド
|
||||
optionSetupViaP2P: "%{short_p2p_sync}を使ってセットアップ"
|
||||
optionSetupWizard: セットアップウィザードへ
|
||||
optionYesFetchAgain: はい、再フェッチする
|
||||
titleCaseSensitivity: 大文字小文字の区別
|
||||
titleRecommendSetupUri: セットアップURIの使用を推奨
|
||||
titleWelcome: Self-hosted LiveSyncへようこそ
|
||||
moduleObsidianMenu:
|
||||
replicate: レプリケート
|
||||
More actions: その他の操作
|
||||
@@ -486,12 +464,10 @@ obsidianLiveSyncSettingTab:
|
||||
btnEnable: 有効化
|
||||
btnFix: 修正
|
||||
btnGotItAndUpdated: 理解しました、更新しました。
|
||||
btnNext: 次へ
|
||||
btnStart: 開始
|
||||
btnTest: テスト
|
||||
btnUse: 使用
|
||||
buttonFetch: フェッチ
|
||||
buttonNext: 次へ
|
||||
defaultLanguage: デフォルト
|
||||
descConnectSetupURI: セットアップURIを使用してSelf-hosted LiveSyncをセットアップする推奨方法です。
|
||||
descCopySetupURI: 新しいデバイスのセットアップにおすすめ!
|
||||
@@ -556,7 +532,6 @@ obsidianLiveSyncSettingTab:
|
||||
msgEnableCorsChttpd: chttpd.enable_corsを設定
|
||||
msgEnableEncryptionRecommendation: エンドツーエンド暗号化とパス難読化を有効にすることをお勧めします。暗号化なしで続行してもよろしいですか?
|
||||
msgFetchConfigFromRemote: リモートサーバーから設定を取得しますか?
|
||||
msgGenerateSetupURI: 完了!他のデバイスをセットアップするためのセットアップURIを生成しますか?
|
||||
msgIfConfigNotPersistent: "サーバー設定が永続的でない場合(例:
|
||||
Dockerで実行中)、ここの値は変更される可能性があります。接続できるようになったら、サーバーのlocal.iniの設定を更新してください。"
|
||||
msgInvalidPassphrase: 暗号化パスフレーズが無効かもしれません。続行してもよろしいですか?
|
||||
@@ -599,7 +574,6 @@ obsidianLiveSyncSettingTab:
|
||||
リモートデータベースを再構築した場合も含まれます。
|
||||
## ${OPTION_ONLY_SETTING}
|
||||
設定のみを保存します。**注意: データ破損につながる可能性があります**。通常、データベースの再構築が必要です。
|
||||
msgSelectAndApplyPreset: ウィザードを完了するには、プリセット項目を選択して適用してください。
|
||||
msgSetCorsCredentials: cors.credentialsを設定
|
||||
msgSetCorsOrigins: cors.originsを設定
|
||||
msgSetMaxDocSize: couchdb.max_document_sizeを設定
|
||||
@@ -656,7 +630,6 @@ obsidianLiveSyncSettingTab:
|
||||
titleActiveRemoteServer: アクティブなリモートサーバー
|
||||
titleAppearance: 外観
|
||||
titleConflictResolution: 競合解決
|
||||
titleCongratulations: おめでとうございます!
|
||||
titleCouchDB: CouchDB サーバー
|
||||
titleDeletionPropagation: 削除の伝播
|
||||
titleEncryptionNotEnabled: 暗号化が有効になっていません
|
||||
|
||||
@@ -571,7 +571,6 @@ moduleLocalDatabase:
|
||||
moduleLog:
|
||||
showLog: 로그 표시
|
||||
moduleMigration:
|
||||
docUri: https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use
|
||||
fix0256:
|
||||
buttons:
|
||||
checkItLater: 나중에 확인
|
||||
@@ -631,19 +630,6 @@ moduleMigration:
|
||||
|
||||
___참고: 설정이 변경되고 데이터베이스를 다시 불러오기 전까지는 동기화가 불가능합니다.___
|
||||
___참고2: 청크는 변경이 불가능한 구조이므로, 메타데이터와 차이점만 가져올 수 있습니다.___
|
||||
msgInitialSetup: |-
|
||||
이 기기는 **아직 초기 설정이 완료되지 않았습니다**. 지금부터 설정 과정을 안내해 드리겠습니다.
|
||||
|
||||
모든 대화 내용은 클립보드에 복사할 수 있습니다. 나중에 참고하려면 Obsidian 노트에 붙여넣거나 번역 도구를 활용해 번역하셔도 됩니다.
|
||||
|
||||
먼저, **Setup URI**를 가지고 계신가요?
|
||||
|
||||
참고: Setup URI가 무엇인지 잘 모르시겠다면 [문서](${URI_DOC})를 참고해 주세요.
|
||||
msgRecommendSetupUri: |-
|
||||
Setup URI를 생성해 사용하는 것을 강력히 권장합니다.
|
||||
Setup URI가 무엇인지 잘 모르시겠다면 [문서](${URI_DOC})를 참고해 주세요. 중요한 내용이니 꼭 확인하시기 바랍니다.
|
||||
|
||||
직접 수동 설정을 진행하시겠습니까?
|
||||
msgSinceV02321: |-
|
||||
v0.23.21부터 Self-hosted LiveSync의 기본 동작 방식과 데이터베이스 구조가 변경되었습니다. 변경 내용은 다음과 같습니다:
|
||||
|
||||
@@ -664,18 +650,10 @@ moduleMigration:
|
||||
optionEnableBoth: 둘 다 활성화
|
||||
optionEnableFilenameCaseInsensitive: "#1만 활성화"
|
||||
optionEnableFixedRevisionForChunks: "#2만 활성화"
|
||||
optionHaveSetupUri: 예, 있습니다
|
||||
optionKeepPreviousBehaviour: 이전 동작 유지
|
||||
optionManualSetup: 모든 것을 수동으로 설정
|
||||
optionNoAskAgain: 아니요 (나중에 다시 물어보기)
|
||||
optionNoSetupUri: 아니요, 없습니다
|
||||
optionRemindNextLaunch: 다음 시작 시 알림
|
||||
optionSetupViaP2P: "%{short_p2p_sync}를 사용하여 설정"
|
||||
optionSetupWizard: 설정 마법사로 안내
|
||||
optionYesFetchAgain: 예 (다시 가져오기)
|
||||
titleCaseSensitivity: 대소문자 구분
|
||||
titleRecommendSetupUri: Setup URI 사용 권장
|
||||
titleWelcome: Self-hosted LiveSync에 오신 것을 환영합니다
|
||||
moduleObsidianMenu:
|
||||
replicate: 복제
|
||||
More actions: 추가 작업
|
||||
@@ -722,12 +700,10 @@ obsidianLiveSyncSettingTab:
|
||||
btnEnable: 활성화
|
||||
btnFix: 수정
|
||||
btnGotItAndUpdated: 알겠습니다. 업데이트했습니다.
|
||||
btnNext: 다음
|
||||
btnStart: 시작
|
||||
btnTest: 테스트
|
||||
btnUse: 사용
|
||||
buttonFetch: 가져오기
|
||||
buttonNext: 다음
|
||||
defaultLanguage: 기본값
|
||||
descConnectSetupURI: 이것은 Setup URI로 Self-hosted LiveSync를 설정하는 권장 방법입니다.
|
||||
descCopySetupURI: 새 기기 설정에 완벽합니다!
|
||||
@@ -793,7 +769,6 @@ obsidianLiveSyncSettingTab:
|
||||
msgEnableCorsChttpd: chttpd.enable_cors 설정
|
||||
msgEnableEncryptionRecommendation: 종단 간 암호화와 경로 난독화를 활성화하는 것을 권장합니다. 정말로 암호화 없이 계속하시겠습니까?
|
||||
msgFetchConfigFromRemote: 원격 서버에서 구성을 가져오시겠습니까?
|
||||
msgGenerateSetupURI: 모든 작업이 완료되었습니다! 다른 기기를 설정하기 위해 Setup URI를 생성하시겠습니까?
|
||||
msgIfConfigNotPersistent: "서버 설정이 영구적으로 저장되지 않는 환경(예: Docker에서 실행 중)에서는 이곳의 값들이 변경될 수 있습니다. 연결이 가능해지면 서버의 local.ini 파일에서 설정을 수동으로 업데이트해 주세요."
|
||||
msgInvalidPassphrase: 암호화 패스프레이즈가 유효하지 않을 수 있습니다. 정말로 계속하시겠습니까?
|
||||
msgNewVersionNote: 업그레이드 알림으로 여기에 오셨나요? 버전 기록을 검토해 주세요. 만족하신다면 버튼을 클릭하세요. 새로운 업데이트 시 다시 안내됩니다.
|
||||
@@ -835,7 +810,6 @@ obsidianLiveSyncSettingTab:
|
||||
원격 데이터베이스를 이미 재구축한 경우도 여기에 해당합니다.
|
||||
## ${OPTION_ONLY_SETTING}
|
||||
설정만 저장합니다. **주의: 데이터가 손상될 수 있습니다.** 일반적으로는 데이터베이스 재구축이 필요합니다.
|
||||
msgSelectAndApplyPreset: 마법사를 완료하려면 프리셋 항목을 선택하고 적용해 주세요.
|
||||
msgSetCorsCredentials: cors.credentials 설정
|
||||
msgSetCorsOrigins: cors.origins 설정
|
||||
msgSetMaxDocSize: couchdb.max_document_size 설정
|
||||
@@ -892,7 +866,6 @@ obsidianLiveSyncSettingTab:
|
||||
titleActiveRemoteServer: 활성 원격 서버
|
||||
titleAppearance: 모양
|
||||
titleConflictResolution: 충돌 해결
|
||||
titleCongratulations: 축하합니다!
|
||||
titleCouchDB: CouchDB
|
||||
titleDeletionPropagation: 삭제 전파
|
||||
titleEncryptionNotEnabled: 암호화가 활성화되지 않음
|
||||
|
||||
@@ -413,7 +413,6 @@ moduleLocalDatabase:
|
||||
moduleLog:
|
||||
showLog: Показать лог
|
||||
moduleMigration:
|
||||
docUri: https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use
|
||||
fix0256:
|
||||
buttons:
|
||||
checkItLater: Проверить позже
|
||||
@@ -441,8 +440,6 @@ moduleMigration:
|
||||
logSetupCancelled: Настройка отменена, Self-hosted LiveSync ожидает вашей настройки!
|
||||
msgFetchRemoteAgain: Удалённая база данных, похоже, уже была мигрирована.
|
||||
Конфигурация этого устройства несовместима.
|
||||
msgInitialSetup: Ваше устройство ещё не настроено. У вас есть Setup URI?
|
||||
msgRecommendSetupUri: Мы рекомендуем сгенерировать Setup URI.
|
||||
msgSinceV02321: Начиная с v0.23.21, self-hosted LiveSync изменил поведение и
|
||||
структуру базы данных.
|
||||
optionAdjustRemote: Настроить под удалённую
|
||||
@@ -450,18 +447,10 @@ moduleMigration:
|
||||
optionEnableBoth: Включить оба
|
||||
optionEnableFilenameCaseInsensitive: "Включить только #1"
|
||||
optionEnableFixedRevisionForChunks: "Включить только #2"
|
||||
optionHaveSetupUri: Да, есть
|
||||
optionKeepPreviousBehaviour: Сохранить предыдущее поведение
|
||||
optionManualSetup: Настроить всё вручную
|
||||
optionNoAskAgain: Нет, спросить снова
|
||||
optionNoSetupUri: Нет, нет
|
||||
optionRemindNextLaunch: Напомнить при следующем запуске
|
||||
optionSetupViaP2P: Использовать short_p2p_sync для настройки
|
||||
optionSetupWizard: Перейти в мастер настройки
|
||||
optionYesFetchAgain: Да, загрузить снова
|
||||
titleCaseSensitivity: Чувствительность к регистру
|
||||
titleRecommendSetupUri: Рекомендация использовать Setup URI
|
||||
titleWelcome: Добро пожаловать в Self-hosted LiveSync
|
||||
moduleObsidianMenu:
|
||||
replicate: Реплицировать
|
||||
More actions: Другие действия
|
||||
@@ -492,12 +481,10 @@ obsidianLiveSyncSettingTab:
|
||||
btnEnable: Включить
|
||||
btnFix: Исправить
|
||||
btnGotItAndUpdated: Понял и обновил.
|
||||
btnNext: Далее
|
||||
btnStart: Старт
|
||||
btnTest: Тест
|
||||
btnUse: Использовать
|
||||
buttonFetch: Загрузить
|
||||
buttonNext: Далее
|
||||
defaultLanguage: По умолчанию
|
||||
descConnectSetupURI: Это рекомендуемый способ настройки Self-hosted LiveSync с помощью Setup URI.
|
||||
descCopySetupURI: Идеально подходит для настройки нового устройства!
|
||||
@@ -562,7 +549,6 @@ obsidianLiveSyncSettingTab:
|
||||
msgEnableEncryptionRecommendation: Мы рекомендуем включить сквозное шифрование.
|
||||
Вы уверены, что хотите продолжить без шифрования?
|
||||
msgFetchConfigFromRemote: Вы хотите загрузить конфигурацию с удалённого сервера?
|
||||
msgGenerateSetupURI: Всё готово! Вы хотите сгенерировать Setup URI для настройки других устройств?
|
||||
msgIfConfigNotPersistent: Если конфигурация сервера непостоянна, значения здесь могут измениться.
|
||||
msgInvalidPassphrase: Ваша парольная фраза шифрования может быть недействительна.
|
||||
msgNewVersionNote: Вы пришли из-за уведомления об обновлении? Просмотрите историю версий.
|
||||
@@ -572,7 +558,6 @@ obsidianLiveSyncSettingTab:
|
||||
msgObjectStorageWarning: "ПРЕДУПРЕЖДЕНИЕ: Эта функция в разработке."
|
||||
msgOriginCheck: "Проверка origin: org"
|
||||
msgRebuildRequired: Требуется перестроение баз данных для применения изменений.
|
||||
msgSelectAndApplyPreset: Выберите и примените любой пресет для завершения мастера.
|
||||
msgSetCorsCredentials: Установить cors.credentials
|
||||
msgSetCorsOrigins: Установить cors.origins
|
||||
msgSetMaxDocSize: Установить couchdb.max_document_size
|
||||
@@ -629,7 +614,6 @@ obsidianLiveSyncSettingTab:
|
||||
titleActiveRemoteServer: Активный удалённый сервер
|
||||
titleAppearance: Внешний вид
|
||||
titleConflictResolution: Разрешение конфликтов
|
||||
titleCongratulations: Поздравляем!
|
||||
titleCouchDB: Сервер CouchDB
|
||||
titleDeletionPropagation: Распространение удалений
|
||||
titleEncryptionNotEnabled: Шифрование не включено
|
||||
|
||||
@@ -745,27 +745,6 @@ moduleMigration:
|
||||
optionNoAskAgain: 不,請稍後再問我
|
||||
optionYesFetchAgain: 是,再次抓取
|
||||
titleCaseSensitivity: 大小寫敏感性
|
||||
msgInitialSetup: |-
|
||||
你的裝置**尚未完成設定**。讓我引導你完成設定流程。
|
||||
|
||||
請記得,每個對話框的內容都可以複製到剪貼簿。如果日後需要參考,可以貼到 Obsidian 的筆記中,也可以用翻譯工具翻成你的語言。
|
||||
|
||||
首先,你有 **Setup URI** 嗎?
|
||||
|
||||
注意:如果你不知道那是什麼,請參閱[說明文件](${URI_DOC})。
|
||||
msgRecommendSetupUri: |-
|
||||
我們強烈建議你產生 Setup URI 並使用它。
|
||||
如果你對此不熟悉,請參閱[說明文件](${URI_DOC})(再次抱歉,但這很重要)。
|
||||
|
||||
你想要如何手動設定?
|
||||
optionHaveSetupUri: 有,我有
|
||||
optionManualSetup: 全部手動設定
|
||||
optionNoSetupUri: 沒有,我沒有
|
||||
optionRemindNextLaunch: 下次啟動時提醒我
|
||||
optionSetupViaP2P: 使用「%{short_p2p_sync}」進行設定
|
||||
optionSetupWizard: 帶我進入設定精靈
|
||||
titleRecommendSetupUri: 建議使用 Setup URI
|
||||
titleWelcome: 歡迎使用 Self-hosted LiveSync
|
||||
moduleObsidianMenu:
|
||||
replicate: 複寫
|
||||
More actions: 更多操作
|
||||
@@ -786,9 +765,7 @@ obsidianLiveSyncSettingTab:
|
||||
btnTest: 測試
|
||||
buttonFetch: 抓取
|
||||
btnFix: 修正
|
||||
btnNext: 下一步
|
||||
btnUse: 使用
|
||||
buttonNext: 下一步
|
||||
defaultLanguage: 預設語言
|
||||
descConnectSetupURI: 這是使用 Setup URI 設定 Self-hosted LiveSync 的建議方式。
|
||||
descCopySetupURI: 設定新裝置時非常方便!
|
||||
@@ -868,7 +845,6 @@ obsidianLiveSyncSettingTab:
|
||||
msgEnableCorsChttpd: 設定 chttpd.enable_cors
|
||||
msgEnableEncryptionRecommendation: 我們建議啟用端對端加密與路徑混淆。你確定要在未加密的情況下繼續嗎?
|
||||
msgFetchConfigFromRemote: 要從遠端伺服器抓取設定嗎?
|
||||
msgGenerateSetupURI: 全部完成!要產生 Setup URI 以便設定其他裝置嗎?
|
||||
msgIfConfigNotPersistent: 如果伺服器設定並非持久保存(例如在 Docker 上執行),這裡的值可能會變動。一旦能夠連線,請更新伺服器 local.ini 中的設定。
|
||||
msgInvalidPassphrase: 你的加密密語可能無效。你確定要繼續嗎?
|
||||
msgNotice: "---注意---"
|
||||
@@ -898,7 +874,6 @@ obsidianLiveSyncSettingTab:
|
||||
這也涵蓋你已重建遠端資料庫的情況。
|
||||
## ${OPTION_ONLY_SETTING}
|
||||
僅儲存設定。**注意:這可能導致資料損毀**;通常仍需要重建資料庫。
|
||||
msgSelectAndApplyPreset: 請選擇並套用任一預設項目以完成精靈。
|
||||
msgSetCorsCredentials: 設定 cors.credentials
|
||||
msgSetCorsOrigins: 設定 cors.origins
|
||||
msgSetMaxDocSize: 設定 couchdb.max_document_size
|
||||
@@ -951,7 +926,6 @@ obsidianLiveSyncSettingTab:
|
||||
titleFetchConfigFromRemote: 從遠端伺服器抓取設定
|
||||
titleFetchSettings: 抓取設定
|
||||
titleConflictResolution: 衝突處理
|
||||
titleCongratulations: 恭喜!
|
||||
titleCouchDB: CouchDB 伺服器
|
||||
titleDeletionPropagation: 刪除傳播
|
||||
titleEncryptionNotEnabled: 尚未啟用加密
|
||||
|
||||
@@ -358,7 +358,6 @@ moduleLocalDatabase:
|
||||
moduleLog:
|
||||
showLog: 显示日志
|
||||
moduleMigration:
|
||||
docUri: 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
|
||||
fix0256:
|
||||
buttons:
|
||||
checkItLater: 稍后检查
|
||||
@@ -420,19 +419,6 @@ moduleMigration:
|
||||
|
||||
___注意:在更改配置并再次获取数据库之前,我们无法进行同步。___
|
||||
___注意2:chunks 是完全不可变的,我们只能获取元数据和差异
|
||||
msgInitialSetup: |-
|
||||
您的设备**尚未设置**。让我引导您完成设置过程。
|
||||
|
||||
请记住,每个对话框内容都可以复制到剪贴板。如果以后需要参考,可以将其粘贴到 Obsidian 的笔记中。您也可以使用翻译工具将其翻译成您的语言。
|
||||
|
||||
首先,您有**设置 URI** 吗?
|
||||
|
||||
注意:如果您不知道这是什么,请参阅[文档](${URI_DOC})
|
||||
msgRecommendSetupUri: |-
|
||||
我们强烈建议您生成一个设置 URI 并使用它。
|
||||
如果您对此不了解,请参阅[文档](${URI_DOC})(再次抱歉,但这很重要)。
|
||||
|
||||
您想如何手动设置?
|
||||
msgSinceV02321: |-
|
||||
自 v0.23.21 起,Self-hosted LiveSync 更改了默认行为和数据库结构。进行了以下更改:
|
||||
|
||||
@@ -453,18 +439,10 @@ moduleMigration:
|
||||
optionEnableBoth: 启用两者
|
||||
optionEnableFilenameCaseInsensitive: "仅启用 #1"
|
||||
optionEnableFixedRevisionForChunks: "仅启用 #2"
|
||||
optionHaveSetupUri: 是的,我有
|
||||
optionKeepPreviousBehaviour: 保持以前的行为
|
||||
optionManualSetup: 全部手动设置
|
||||
optionNoAskAgain: 不,请稍后再次询问
|
||||
optionNoSetupUri: 不,我没有
|
||||
optionRemindNextLaunch: 下次启动时提醒我
|
||||
optionSetupViaP2P: Use %{short_p2p_sync} to set up
|
||||
optionSetupWizard: 带我进入设置向导
|
||||
optionYesFetchAgain: 是的,再次获取
|
||||
titleCaseSensitivity: 大小写敏感性
|
||||
titleRecommendSetupUri: 推荐使用设置 URI
|
||||
titleWelcome: 欢迎使用 Self-hosted LiveSync
|
||||
moduleObsidianMenu:
|
||||
replicate: 复制
|
||||
More actions: 更多操作
|
||||
@@ -490,12 +468,10 @@ obsidianLiveSyncSettingTab:
|
||||
btnEnable: 启用
|
||||
btnFix: 修复
|
||||
btnGotItAndUpdated: 我明白了并且已更新
|
||||
btnNext: 下一步
|
||||
btnStart: 开始
|
||||
btnTest: 测试
|
||||
btnUse: 使用
|
||||
buttonFetch: 获取
|
||||
buttonNext: 下一步
|
||||
defaultLanguage: 默认语言
|
||||
descConnectSetupURI: 这是使用设置 URI 设置 Self-hosted LiveSync 的推荐方法
|
||||
descCopySetupURI: 非常适合设置新设备!
|
||||
@@ -560,7 +536,6 @@ obsidianLiveSyncSettingTab:
|
||||
msgEnableCorsChttpd: 设置 chttpd.enable_cors
|
||||
msgEnableEncryptionRecommendation: 建议启用端到端加密和路径混淆。你确定要在未加密的情况下继续吗?
|
||||
msgFetchConfigFromRemote: 要从远端服务器获取配置吗?
|
||||
msgGenerateSetupURI: 全部完成!要生成设置 URI 以便配置其他设备吗?
|
||||
msgIfConfigNotPersistent: 如果服务器配置不是持久的(例如,在 docker 上运行),此处的值可能会更改。一旦能够连接,请更新服务器 local.ini 中的设置
|
||||
msgInvalidPassphrase: 你的加密密码短语可能无效。你确定要继续吗?
|
||||
msgNewVersionNote: 因为升级通知来到这里?请查看版本历史。如果您满意,请点击按钮。新的更新将再次提示此信息
|
||||
@@ -602,7 +577,6 @@ obsidianLiveSyncSettingTab:
|
||||
这种情况包括您已经重建了远程数据库的情况。
|
||||
## ${OPTION_ONLY_SETTING}
|
||||
仅存储设置。**注意:这可能导致数据损坏**;通常需要重建数据库
|
||||
msgSelectAndApplyPreset: 请选择并应用任一预设项以完成向导。
|
||||
msgSetCorsCredentials: 设置 cors.credentials
|
||||
msgSetCorsOrigins: 设置 cors.origins
|
||||
msgSetMaxDocSize: 设置 couchdb.max_document_size
|
||||
@@ -659,7 +633,6 @@ obsidianLiveSyncSettingTab:
|
||||
titleActiveRemoteServer: 活动远程服务器
|
||||
titleAppearance: 外观
|
||||
titleConflictResolution: 冲突处理
|
||||
titleCongratulations: 恭喜!
|
||||
titleCouchDB: CouchDB 服务器
|
||||
titleDeletionPropagation: 删除传播
|
||||
titleEncryptionNotEnabled: 尚未启用加密
|
||||
|
||||
@@ -27,6 +27,12 @@ describe("LiveSync-owned translation catalogue", () => {
|
||||
expect(translateLiveSyncMessage("Active Remote Type")).toBe(englishMessageTranslator("Active Remote Type"));
|
||||
});
|
||||
|
||||
it("directs a compatibility pause to the dedicated review workflow", () => {
|
||||
expect(translateLiveSyncMessage("Replicator.Message.VersionUpFlash")).toBe(
|
||||
"Remote synchronisation is paused for compatibility review. Run the 'Review why synchronisation is paused' command for details and available actions."
|
||||
);
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
@@ -16,6 +16,7 @@ export {
|
||||
requestUrl,
|
||||
sanitizeHTMLToDom,
|
||||
Setting,
|
||||
SettingPage,
|
||||
stringifyYaml,
|
||||
TAbstractFile,
|
||||
TextAreaComponent,
|
||||
|
||||
@@ -4,7 +4,6 @@ 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: {},
|
||||
}));
|
||||
@@ -30,13 +29,10 @@ vi.mock("@/common/types.ts", () => ({
|
||||
PERIODIC_PLUGIN_SWEEP: 60,
|
||||
}));
|
||||
vi.mock("@/common/utils.ts", () => ({
|
||||
cancelTask: vi.fn(),
|
||||
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", () => ({
|
||||
@@ -55,6 +51,7 @@ vi.mock("@/common/obsidianCommunityPlugins.ts", () => ({
|
||||
getObsidianCommunityPluginManager: vi.fn(),
|
||||
}));
|
||||
|
||||
import { cancelTask } from "@/common/utils.ts";
|
||||
import { ConfigSync } from "./CmdConfigSync";
|
||||
|
||||
describe("ConfigSync commands", () => {
|
||||
@@ -93,4 +90,24 @@ describe("ConfigSync commands", () => {
|
||||
expect(command?.checkCallback?.(false)).toBe(true);
|
||||
expect(showPluginSyncModal).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("cancels the pending configuration Notice before releasing its owned UI", () => {
|
||||
const notices = { hide: vi.fn() };
|
||||
const periodicPluginSweepProcessor = { disable: vi.fn() };
|
||||
const configSync = Object.create(ConfigSync.prototype) as ConfigSync;
|
||||
Object.assign(configSync, {
|
||||
core: {
|
||||
services: {
|
||||
context: { notices },
|
||||
},
|
||||
},
|
||||
periodicPluginSweepProcessor,
|
||||
});
|
||||
|
||||
configSync.onunload();
|
||||
|
||||
expect(cancelTask).toHaveBeenCalledWith("config-sync:updated-configuration");
|
||||
expect(notices.hide).toHaveBeenCalledWith("config-sync:updated-configuration");
|
||||
expect(periodicPluginSweepProcessor.disable).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { writable } from "svelte/store";
|
||||
import type PouchDB from "pouchdb-core";
|
||||
import {
|
||||
Notice,
|
||||
type PluginManifest,
|
||||
parseYaml,
|
||||
normalizePath,
|
||||
@@ -53,21 +52,11 @@ import {
|
||||
import { serialized, shareRunningResult } from "octagonal-wheels/concurrency/lock";
|
||||
import { LiveSyncCommands } from "@/features/LiveSyncCommands.ts";
|
||||
import { stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path";
|
||||
import {
|
||||
EVEN,
|
||||
disposeMemoObject,
|
||||
isCustomisationSyncMetadata,
|
||||
isPluginMetadata,
|
||||
memoIfNotExist,
|
||||
memoObject,
|
||||
retrieveMemoObject,
|
||||
scheduleTask,
|
||||
} from "@/common/utils.ts";
|
||||
import { cancelTask, EVEN, isCustomisationSyncMetadata, isPluginMetadata, scheduleTask } from "@/common/utils.ts";
|
||||
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
|
||||
import { JsonResolveModal } from "@/features/HiddenFileCommon/JsonResolveModal.ts";
|
||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||
import { pluginScanningCount } from "@vrtmrz/livesync-commonlib/compat/mock_and_interop/stores";
|
||||
import type ObsidianLiveSyncPlugin from "@/main.ts";
|
||||
import { base64ToArrayBuffer, base64ToString } from "octagonal-wheels/binary/base64";
|
||||
import { ConflictResolveModal } from "@/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts";
|
||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||
@@ -82,6 +71,7 @@ import { getObsidianCommunityPluginManager } from "@/common/obsidianCommunityPlu
|
||||
|
||||
const d = "\u200b";
|
||||
const d2 = "\n";
|
||||
const UPDATED_CONFIGURATION_NOTICE_KEY = "config-sync:updated-configuration";
|
||||
|
||||
function serialize(data: PluginDataEx): string {
|
||||
// For higher performance, create custom plug-in data strings.
|
||||
@@ -393,8 +383,8 @@ export type PluginDataEx = {
|
||||
};
|
||||
|
||||
export class ConfigSync extends LiveSyncCommands {
|
||||
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) {
|
||||
super(plugin, core);
|
||||
constructor(core: LiveSyncCore) {
|
||||
super(core);
|
||||
pluginScanningCount.onChanged((e) => {
|
||||
const total = e.value;
|
||||
pluginIsEnumerating.set(total != 0);
|
||||
@@ -428,7 +418,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
if (this.pluginDialog) {
|
||||
this.pluginDialog.open();
|
||||
} else {
|
||||
this.pluginDialog = new PluginDialogModal(this.app, this.plugin);
|
||||
this.pluginDialog = new PluginDialogModal(this.app, this.services.context.liveSyncPlugin);
|
||||
this.pluginDialog.open();
|
||||
}
|
||||
}
|
||||
@@ -440,8 +430,10 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
}
|
||||
onunload() {
|
||||
cancelTask(UPDATED_CONFIGURATION_NOTICE_KEY);
|
||||
this.hidePluginSyncModal();
|
||||
this.periodicPluginSweepProcessor?.disable();
|
||||
this.services.context.notices.hide(UPDATED_CONFIGURATION_NOTICE_KEY);
|
||||
}
|
||||
addRibbonIcon = this.services.API.addRibbonIcon.bind(this.services.API);
|
||||
onload() {
|
||||
@@ -1196,22 +1188,9 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
});
|
||||
});
|
||||
|
||||
const updatedPluginKey = "popupUpdated-plugins";
|
||||
scheduleTask(updatedPluginKey, 1000, async () => {
|
||||
const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0));
|
||||
//@ts-ignore -- retained for compatibility with Obsidian versions before Notice.messageEl.
|
||||
const isShown = popup?.noticeEl?.isShown();
|
||||
if (!isShown) {
|
||||
memoObject(updatedPluginKey, new Notice(fragment, 0));
|
||||
}
|
||||
scheduleTask(updatedPluginKey + "-close", 20000, () => {
|
||||
const popup = retrieveMemoObject<Notice>(updatedPluginKey);
|
||||
if (!popup) return;
|
||||
//@ts-ignore -- retained for compatibility with Obsidian versions before Notice.messageEl.
|
||||
if (popup?.noticeEl?.isShown()) {
|
||||
popup.hide();
|
||||
}
|
||||
disposeMemoObject(updatedPluginKey);
|
||||
scheduleTask(UPDATED_CONFIGURATION_NOTICE_KEY, 1000, () => {
|
||||
this.services.context.notices.show(UPDATED_CONFIGURATION_NOTICE_KEY, fragment, {
|
||||
durationMs: 20_000,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1716,8 +1695,6 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
async configureHiddenFileSync(mode: OptionalSyncFeatureMode) {
|
||||
if (mode == "DISABLE") {
|
||||
// this.plugin.settings.usePluginSync = false;
|
||||
// await this.plugin.saveSettings();
|
||||
await this.core.services.setting.applyPartial(
|
||||
{
|
||||
usePluginSync: false,
|
||||
@@ -1758,9 +1735,6 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
this.services.setting.setDeviceAndVaultName(name);
|
||||
}
|
||||
// this.core.settings.usePluginSync = true;
|
||||
// this.core.settings.useAdvancedMode = true;
|
||||
// await this.core.saveSettings();
|
||||
await this.core.services.setting.applyPartial(
|
||||
{
|
||||
usePluginSync: true,
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type FilePathWithPrefix,
|
||||
type LOG_LEVEL,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type ObsidianLiveSyncPlugin from "@/main.ts";
|
||||
import { MARK_DONE } from "@/modules/features/ModuleLog.ts";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
// import { __$checkInstanceBinding } from "@vrtmrz/livesync-commonlib/compat/dev/checks";
|
||||
@@ -16,13 +15,9 @@ import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/ser
|
||||
|
||||
let noticeIndex = 0;
|
||||
export abstract class LiveSyncCommands {
|
||||
/**
|
||||
* @deprecated This class is deprecated. Please use core
|
||||
*/
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
core: LiveSyncCore;
|
||||
get app() {
|
||||
return this.plugin.app;
|
||||
return this.services.context.app;
|
||||
}
|
||||
get settings() {
|
||||
return this.core.settings;
|
||||
@@ -34,9 +29,6 @@ export abstract class LiveSyncCommands {
|
||||
return this.core.services;
|
||||
}
|
||||
|
||||
// id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
|
||||
// return this.plugin.$$id2path(id, entry, stripPrefix);
|
||||
// }
|
||||
async path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID> {
|
||||
return await this.services.path.path2id(filename, prefix);
|
||||
}
|
||||
@@ -45,8 +37,7 @@ export abstract class LiveSyncCommands {
|
||||
return this.services.path.getPath(entry);
|
||||
}
|
||||
|
||||
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) {
|
||||
this.plugin = plugin;
|
||||
constructor(core: LiveSyncCore) {
|
||||
this.core = core;
|
||||
this.onBindFunction(this.core, this.core.services);
|
||||
this._log = createInstanceLogFunction(this.constructor.name, this.services.API);
|
||||
|
||||
@@ -35,7 +35,7 @@ export class LocalDatabaseMaintenance extends LiveSyncCommands {
|
||||
}
|
||||
onload(): void | Promise<void> {
|
||||
// NO OP.
|
||||
this.plugin.addCommand({
|
||||
this.services.API.addCommand({
|
||||
id: "analyse-database",
|
||||
name: "Analyse Database Usage (advanced)",
|
||||
icon: "database-search",
|
||||
@@ -47,7 +47,7 @@ export class LocalDatabaseMaintenance extends LiveSyncCommands {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
this.plugin.addCommand({
|
||||
this.services.API.addCommand({
|
||||
id: "gc-v3",
|
||||
name: "Garbage Collection V3 (advanced, beta)",
|
||||
icon: "trash-2",
|
||||
|
||||
@@ -18,10 +18,13 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/common/utils", async (importOriginal)
|
||||
});
|
||||
vi.mock("@/features/LiveSyncCommands", () => ({
|
||||
LiveSyncCommands: class LiveSyncCommands {
|
||||
core!: { settings: unknown };
|
||||
core!: { settings: unknown; services: unknown };
|
||||
get settings() {
|
||||
return this.core.settings;
|
||||
}
|
||||
get services() {
|
||||
return this.core.services;
|
||||
}
|
||||
},
|
||||
}));
|
||||
vi.mock("@/common/events", () => ({
|
||||
@@ -76,11 +79,13 @@ describe("LocalDatabaseMaintenance prerequisites", () => {
|
||||
};
|
||||
const maintenance = Object.create(LocalDatabaseMaintenance.prototype) as LocalDatabaseMaintenance;
|
||||
Object.assign(maintenance, {
|
||||
plugin: {
|
||||
addCommand: vi.fn((command) => commands.push(command)),
|
||||
},
|
||||
core: {
|
||||
settings,
|
||||
services: {
|
||||
API: {
|
||||
addCommand: vi.fn((command) => commands.push(command)),
|
||||
},
|
||||
},
|
||||
},
|
||||
_isDatabaseReady: vi.fn(() => true),
|
||||
});
|
||||
|
||||
+2
-5
@@ -120,6 +120,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
UI: services.UI,
|
||||
vault: services.vault,
|
||||
fileHandler: fileHandler,
|
||||
fileProcessing: services.fileProcessing,
|
||||
storageAccess: storageAccess,
|
||||
control: services.control,
|
||||
});
|
||||
@@ -169,11 +170,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
return extraModules;
|
||||
},
|
||||
(core) => {
|
||||
const addOns = [
|
||||
new ConfigSync(this, core),
|
||||
new HiddenFileSync(this, core),
|
||||
new LocalDatabaseMaintenance(this, core),
|
||||
];
|
||||
const addOns = [new ConfigSync(core), new HiddenFileSync(core), new LocalDatabaseMaintenance(core)];
|
||||
return addOns;
|
||||
},
|
||||
(core) => {
|
||||
|
||||
@@ -2,7 +2,11 @@ import type { LiveSyncCore } from "@/main";
|
||||
import type ObsidianLiveSyncPlugin from "@/main";
|
||||
import { AbstractModule } from "./AbstractModule.ts";
|
||||
|
||||
export abstract class AbstractObsidianModule extends AbstractModule {
|
||||
export abstract class AbstractObsidianModule extends AbstractModule<LiveSyncCore> {
|
||||
override get services() {
|
||||
return this.core.services;
|
||||
}
|
||||
|
||||
get app() {
|
||||
return this.plugin.app;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { UnresolvedErrorManager } from "@vrtmrz/livesync-commonlib/compat/servic
|
||||
import { clearHandlers } from "@vrtmrz/livesync-commonlib/compat/replication/SyncParamsHandler";
|
||||
import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
|
||||
import { MARK_LOG_NETWORK_ERROR } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
|
||||
import { usesLegacyIndexedDBAdapter } from "@/common/compatibilitySettings.ts";
|
||||
|
||||
function isOnlineAndCanReplicate(
|
||||
errorManager: UnresolvedErrorManager,
|
||||
@@ -145,10 +146,12 @@ export class ModuleReplicator extends AbstractModule {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Reconciles an IndexedDB-backed local database after replication reports that the remote was cleaned.
|
||||
*
|
||||
* The remote milestone remains a supported compatibility signal. The user can either fetch the remote
|
||||
* database again, or purge unreferenced local chunks before accepting this device again.
|
||||
*
|
||||
* @param showMessage Whether to show the recovery choices as user-facing notices.
|
||||
*/
|
||||
async cleaned(showMessage: boolean) {
|
||||
Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
|
||||
@@ -230,7 +233,7 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
||||
await this.services.tweakValue.askResolvingMismatched(activeReplicator.preferredTweakValue);
|
||||
} else {
|
||||
if (activeReplicator.remoteLockedAndDeviceNotAccepted) {
|
||||
if (activeReplicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
|
||||
if (activeReplicator.remoteCleaned && usesLegacyIndexedDBAdapter(this.settings)) {
|
||||
await this.cleaned(showMessage);
|
||||
} else {
|
||||
const message = $msg("Replicator.Dialogue.Locked.Message");
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
TweakValuesShouldMatchedTemplate,
|
||||
TweakValuesTemplate,
|
||||
IncompatibleChanges,
|
||||
confName,
|
||||
configurationNames,
|
||||
statusDisplay,
|
||||
type TweakValues,
|
||||
type ObsidianLiveSyncSettings,
|
||||
type RemoteDBSettings,
|
||||
@@ -15,11 +16,21 @@ import {
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { escapeMarkdownValue } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import { AbstractModule } from "@/modules/AbstractModule.ts";
|
||||
import { $msg } from "@/common/translation";
|
||||
import { $msg, translateIfAvailable } from "@/common/translation";
|
||||
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import { REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const";
|
||||
|
||||
/**
|
||||
* Localised counterpart of Commonlib's `confName()`, which takes no translator.
|
||||
* Same shape: label plus status suffix, and an empty string for an unknown key.
|
||||
*/
|
||||
function localisedConfName(key: keyof ObsidianLiveSyncSettings): string {
|
||||
const info = configurationNames[key];
|
||||
if (!info) return "";
|
||||
return `${translateIfAvailable(info.name)}${statusDisplay(info.status)}`;
|
||||
}
|
||||
|
||||
function valueToString(value: string | number | boolean | object | undefined): string {
|
||||
if (typeof value === "boolean") {
|
||||
return value ? "true" : "false";
|
||||
@@ -158,7 +169,7 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
|
||||
// table += `| ${confName(key)} | ${valueMine} | ${valuePreferred} | \n`;
|
||||
tableRows.push(
|
||||
$msg("TweakMismatchResolve.Table.Row", {
|
||||
name: confName(key),
|
||||
name: localisedConfName(key),
|
||||
self: valueToString(valueMine),
|
||||
remote: valueToString(valuePreferred),
|
||||
})
|
||||
@@ -342,7 +353,7 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
|
||||
}
|
||||
tableRows.push(
|
||||
$msg("TweakMismatchResolve.Table.Row", {
|
||||
name: confName(key),
|
||||
name: localisedConfName(key),
|
||||
self: currentValueForDisplay,
|
||||
remote: remoteValueForDisplay,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
REMOTE_COUCHDB,
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type TweakValues,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { ModuleResolvingMismatchedTweaks } from "./ModuleResolveMismatchedTweaks";
|
||||
import { setLang } from "@/common/translation";
|
||||
|
||||
function createModule(settingsOverride: Partial<typeof DEFAULT_SETTINGS> = {}) {
|
||||
const askSelectStringDialogue = vi.fn(async (..._args: unknown[]): Promise<string | undefined> => undefined);
|
||||
@@ -255,3 +256,43 @@ describe("ModuleResolvingMismatchedTweaks", () => {
|
||||
expect(calls).toEqual(["save", "reinitialise", "set-preferred"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ModuleResolvingMismatchedTweaks setting labels", () => {
|
||||
afterEach(() => setLang("def"));
|
||||
|
||||
async function renderMismatchTable() {
|
||||
const { module, askSelectStringDialogue } = createModule({
|
||||
autoAcceptCompatibleTweak: true,
|
||||
hashAlg: "xxhash64",
|
||||
encrypt: false,
|
||||
tweakModified: 100,
|
||||
});
|
||||
const preferred = {
|
||||
...(DEFAULT_SETTINGS as unknown as TweakValues),
|
||||
hashAlg: "xxhash32",
|
||||
encrypt: true,
|
||||
tweakModified: 200,
|
||||
} as Partial<TweakValues>;
|
||||
|
||||
await module._checkAndAskResolvingMismatchedTweaks(preferred);
|
||||
|
||||
return String(askSelectStringDialogue.mock.calls[0]?.[0] ?? "");
|
||||
}
|
||||
|
||||
it("localises the setting names and keeps the status suffix", async () => {
|
||||
setLang("zh-tw");
|
||||
|
||||
const message = await renderMismatchTable();
|
||||
|
||||
expect(message).toContain("chunk ID 的雜湊演算法 (Experimental)");
|
||||
expect(message).toContain("端對端加密");
|
||||
expect(message).not.toContain("The Hash algorithm for chunk IDs");
|
||||
});
|
||||
|
||||
it("leaves English unchanged", async () => {
|
||||
const message = await renderMismatchTable();
|
||||
|
||||
expect(message).toContain("The Hash algorithm for chunk IDs (Experimental)");
|
||||
expect(message).toContain("End-to-End Encryption");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,14 +4,7 @@ import {
|
||||
LOG_LEVEL_VERBOSE,
|
||||
Logger,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/logger";
|
||||
import {
|
||||
EVENT_REQUEST_OPEN_P2P,
|
||||
EVENT_REQUEST_OPEN_SETTING_WIZARD,
|
||||
EVENT_REQUEST_OPEN_SETTINGS,
|
||||
EVENT_REQUEST_RUN_DOCTOR,
|
||||
EVENT_REQUEST_RUN_FIX_INCOMPLETE,
|
||||
eventHub,
|
||||
} from "@/common/events.ts";
|
||||
import { EVENT_REQUEST_RUN_DOCTOR, EVENT_REQUEST_RUN_FIX_INCOMPLETE, eventHub } from "@/common/events.ts";
|
||||
import { AbstractModule } from "@/modules/AbstractModule.ts";
|
||||
import { $msg } from "@/common/translation";
|
||||
import { performDoctorConsultation, RebuildOptions } from "@vrtmrz/livesync-commonlib/compat/common/configForDoc";
|
||||
@@ -31,6 +24,7 @@ import {
|
||||
runConfiguredStartupLifecycle,
|
||||
runStartupEntryLifecycle,
|
||||
} from "@/serviceFeatures/configuredStartupLifecycle.ts";
|
||||
import { disableLegacyBulkChunkPreSend } from "@/common/compatibilitySettings.ts";
|
||||
|
||||
type ErrorInfo = {
|
||||
path: string;
|
||||
@@ -84,10 +78,8 @@ export class ModuleMigration extends AbstractModule<LiveSyncCore> {
|
||||
}
|
||||
|
||||
async migrateDisableBulkSend() {
|
||||
if (this.settings.sendChunksBulk) {
|
||||
if (disableLegacyBulkChunkPreSend(this.settings)) {
|
||||
this._log($msg("moduleMigration.logBulkSendCorrupted"), LOG_LEVEL_NOTICE);
|
||||
this.settings.sendChunksBulk = false;
|
||||
this.settings.sendChunksBulkMaxSize = 1;
|
||||
await this.saveSettings();
|
||||
}
|
||||
}
|
||||
@@ -95,53 +87,6 @@ export class ModuleMigration extends AbstractModule<LiveSyncCore> {
|
||||
initialMessage() {
|
||||
const manager = this.core.getModule(SetupManager);
|
||||
showOnboardingInvitation(this.core, manager);
|
||||
/*
|
||||
const message = $msg("moduleMigration.msgInitialSetup", {
|
||||
URI_DOC: $msg("moduleMigration.docUri"),
|
||||
});
|
||||
const USE_SETUP = $msg("moduleMigration.optionHaveSetupUri");
|
||||
const NEXT = $msg("moduleMigration.optionNoSetupUri");
|
||||
|
||||
const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_SETUP, NEXT], {
|
||||
title: $msg("moduleMigration.titleWelcome"),
|
||||
defaultAction: USE_SETUP,
|
||||
});
|
||||
if (ret === USE_SETUP) {
|
||||
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETUP_URI);
|
||||
return false;
|
||||
} else if (ret == NEXT) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
*/
|
||||
}
|
||||
|
||||
async askAgainForSetupURI() {
|
||||
const message = $msg("moduleMigration.msgRecommendSetupUri", { URI_DOC: $msg("moduleMigration.docUri") });
|
||||
const USE_MINIMAL = $msg("moduleMigration.optionSetupWizard");
|
||||
const USE_P2P = $msg("moduleMigration.optionSetupViaP2P");
|
||||
const USE_SETUP = $msg("moduleMigration.optionManualSetup");
|
||||
const NEXT = $msg("moduleMigration.optionRemindNextLaunch");
|
||||
|
||||
const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_MINIMAL, USE_SETUP, USE_P2P, NEXT], {
|
||||
title: $msg("moduleMigration.titleRecommendSetupUri"),
|
||||
defaultAction: USE_MINIMAL,
|
||||
});
|
||||
if (ret === USE_MINIMAL) {
|
||||
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTING_WIZARD);
|
||||
return false;
|
||||
}
|
||||
if (ret === USE_P2P) {
|
||||
eventHub.emitEvent(EVENT_REQUEST_OPEN_P2P);
|
||||
return false;
|
||||
}
|
||||
if (ret === USE_SETUP) {
|
||||
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTINGS);
|
||||
return false;
|
||||
} else if (ret == NEXT) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async hasIncompleteDocs(force: boolean = false): Promise<boolean> {
|
||||
|
||||
@@ -18,7 +18,10 @@ async function* failedDocumentScan() {
|
||||
throw new Error("scan failed");
|
||||
}
|
||||
|
||||
function createMigration(findAllNormalDocs: typeof noDocuments | typeof failedDocumentScan = noDocuments) {
|
||||
function createMigration(
|
||||
findAllNormalDocs: typeof noDocuments | typeof failedDocumentScan = noDocuments,
|
||||
settings = { sendChunksBulk: false, sendChunksBulkMaxSize: 1 }
|
||||
) {
|
||||
const noticeGroups = {
|
||||
setItem: vi.fn(),
|
||||
finish: vi.fn(() => true),
|
||||
@@ -32,6 +35,7 @@ function createMigration(findAllNormalDocs: typeof noDocuments | typeof failedDo
|
||||
registerProtocolHandler: vi.fn(),
|
||||
},
|
||||
context: { noticeGroups },
|
||||
setting: { saveSettingData: vi.fn(async () => undefined) },
|
||||
vault: { isTargetFile: vi.fn(async () => true) },
|
||||
path: { getPath: vi.fn() },
|
||||
};
|
||||
@@ -44,13 +48,37 @@ function createMigration(findAllNormalDocs: typeof noDocuments | typeof failedDo
|
||||
},
|
||||
localDatabase: { findAllNormalDocs },
|
||||
storageAccess: {},
|
||||
settings,
|
||||
};
|
||||
return {
|
||||
migration: new ModuleMigration(core as never),
|
||||
noticeGroups,
|
||||
saveSettingData: services.setting.saveSettingData,
|
||||
};
|
||||
}
|
||||
|
||||
describe("ModuleMigration obsolete-setting migration", () => {
|
||||
it("persists the removal of an enabled automatic bulk chunk pre-send setting", async () => {
|
||||
const settings = { sendChunksBulk: true, sendChunksBulkMaxSize: 16 };
|
||||
const { migration, saveSettingData } = createMigration(noDocuments, settings);
|
||||
|
||||
await migration.migrateDisableBulkSend();
|
||||
|
||||
expect(settings).toEqual({ sendChunksBulk: false, sendChunksBulkMaxSize: 1 });
|
||||
expect(saveSettingData).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not persist an already disabled automatic bulk chunk pre-send setting", async () => {
|
||||
const settings = { sendChunksBulk: false, sendChunksBulkMaxSize: 16 };
|
||||
const { migration, saveSettingData } = createMigration(noDocuments, settings);
|
||||
|
||||
await migration.migrateDisableBulkSend();
|
||||
|
||||
expect(settings).toEqual({ sendChunksBulk: false, sendChunksBulkMaxSize: 16 });
|
||||
expect(saveSettingData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ModuleMigration incomplete-document notice", () => {
|
||||
it("keeps the check and its result in one persistent named group", async () => {
|
||||
const { migration, noticeGroups } = createMigration();
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
// Original Implementation is here: https://github.com/remotely-save/remotely-save/blob/28b99557a864ef59c19d2ad96101196e401718f0/src/remoteForS3.ts
|
||||
|
||||
import { FetchHttpHandler, type FetchHttpHandlerOptions } from "@smithy/fetch-http-handler";
|
||||
import { HttpRequest, HttpResponse, type HttpHandlerOptions } from "@smithy/protocol-http";
|
||||
import { HttpRequest, HttpResponse } from "@smithy/protocol-http";
|
||||
import type { HttpHandlerOptions } from "@smithy/types";
|
||||
import { buildQueryString } from "@smithy/querystring-builder";
|
||||
import { requestUrl, type RequestUrlParam } from "@/deps.ts";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
|
||||
@@ -2,12 +2,9 @@ import { delay } from "octagonal-wheels/promises";
|
||||
import { __onMissingTranslation } from "@/common/translation";
|
||||
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
|
||||
import { LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||
// import { enableTestFunction } from "./devUtil/testUtils.ts";
|
||||
import { TestPaneView, VIEW_TYPE_TEST } from "./devUtil/TestPaneView.ts";
|
||||
import { writable } from "svelte/store";
|
||||
import type { FilePathWithPrefix } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import type { WorkspaceLeaf } from "@/deps.ts";
|
||||
export class ModuleDev extends AbstractObsidianModule {
|
||||
_everyOnloadStart(): Promise<boolean> {
|
||||
__onMissingTranslation(() => {});
|
||||
@@ -35,25 +32,8 @@ export class ModuleDev extends AbstractObsidianModule {
|
||||
}
|
||||
}
|
||||
|
||||
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
|
||||
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
||||
this.registerView(VIEW_TYPE_TEST, (leaf: WorkspaceLeaf) => new TestPaneView(leaf, this.plugin, this));
|
||||
this.addCommand({
|
||||
id: "view-test",
|
||||
name: "Open Test dialogue",
|
||||
callback: () => {
|
||||
void this.services.API.showWindow(VIEW_TYPE_TEST);
|
||||
},
|
||||
});
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
async _everyOnLayoutReady(): Promise<boolean> {
|
||||
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
||||
// if (await this.core.storageAccess.isExistsIncludeHidden("_SHOWDIALOGAUTO.md")) {
|
||||
// void this.core.$$showView(VIEW_TYPE_TEST);
|
||||
// }
|
||||
|
||||
this.addCommand({
|
||||
id: "test-create-conflict",
|
||||
name: "Create conflict",
|
||||
@@ -110,7 +90,6 @@ export class ModuleDev extends AbstractObsidianModule {
|
||||
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
|
||||
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
|
||||
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
|
||||
services.test.test.addHandler(this._everyModuleTest.bind(this));
|
||||
services.test.addTestResult.setHandler(this._addTestResult.bind(this));
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import type ObsidianLiveSyncPlugin from "@/main.ts";
|
||||
import { perf_trench } from "./tests.ts";
|
||||
import { MarkdownRenderer, Notice } from "@/deps.ts";
|
||||
import type { ModuleDev } from "@/modules/extras/ModuleDev.ts";
|
||||
import { fireAndForget } from "octagonal-wheels/promises";
|
||||
import { EVENT_LAYOUT_READY, eventHub } from "@/common/events.ts";
|
||||
export let plugin: ObsidianLiveSyncPlugin;
|
||||
export let moduleDev: ModuleDev;
|
||||
$: core = plugin.core;
|
||||
let performanceTestResult = "";
|
||||
let testRunning = false;
|
||||
let prefTestResultEl: HTMLDivElement;
|
||||
let isReady = false;
|
||||
$: {
|
||||
if (performanceTestResult != "" && isReady) {
|
||||
MarkdownRenderer.render(plugin.app, performanceTestResult, prefTestResultEl, "/", plugin);
|
||||
}
|
||||
}
|
||||
|
||||
async function performTest() {
|
||||
try {
|
||||
testRunning = true;
|
||||
performanceTestResult = await perf_trench(plugin);
|
||||
} finally {
|
||||
testRunning = false;
|
||||
}
|
||||
}
|
||||
function clearResult() {
|
||||
moduleDev.testResults.update((v) => {
|
||||
v = [];
|
||||
return v;
|
||||
});
|
||||
}
|
||||
function clearPerfTestResult() {
|
||||
prefTestResultEl.empty();
|
||||
}
|
||||
onMount(async () => {
|
||||
isReady = true;
|
||||
// performTest();
|
||||
|
||||
eventHub.onceEvent(EVENT_LAYOUT_READY, async () => {
|
||||
if (await core.storageAccess.isExistsIncludeHidden("_AUTO_TEST.md")) {
|
||||
new Notice("Auto test file found, running tests...");
|
||||
fireAndForget(async () => {
|
||||
await allTest();
|
||||
});
|
||||
} else {
|
||||
// new Notice("No auto test file found, skipping tests...");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let moduleTesting = false;
|
||||
function moduleMultiDeviceTest() {
|
||||
if (moduleTesting) return;
|
||||
moduleTesting = true;
|
||||
core.services.test.testMultiDevice().finally(() => {
|
||||
moduleTesting = false;
|
||||
});
|
||||
}
|
||||
function moduleSingleDeviceTest() {
|
||||
if (moduleTesting) return;
|
||||
moduleTesting = true;
|
||||
core.services.test.test().finally(() => {
|
||||
moduleTesting = false;
|
||||
});
|
||||
}
|
||||
async function allTest() {
|
||||
if (moduleTesting) return;
|
||||
moduleTesting = true;
|
||||
try {
|
||||
await core.services.test.test();
|
||||
await core.services.test.testMultiDevice();
|
||||
} finally {
|
||||
moduleTesting = false;
|
||||
}
|
||||
}
|
||||
|
||||
const results = moduleDev.testResults;
|
||||
$: resultLines = $results;
|
||||
|
||||
let syncStatus = [] as string[];
|
||||
eventHub.onEvent("debug-sync-status", (status) => {
|
||||
syncStatus = [...status];
|
||||
});
|
||||
</script>
|
||||
|
||||
<h2>TESTING BENCH: Self-hosted LiveSync</h2>
|
||||
|
||||
<h3>Module Checks</h3>
|
||||
<button on:click={() => moduleMultiDeviceTest()} disabled={moduleTesting}>MultiDevice Test</button>
|
||||
<button on:click={() => moduleSingleDeviceTest()} disabled={moduleTesting}>SingleDevice Test</button>
|
||||
<button on:click={() => allTest()} disabled={moduleTesting}>All Test</button>
|
||||
<button on:click={() => clearResult()}>Clear</button>
|
||||
|
||||
{#each resultLines as [result, line, message]}
|
||||
<details open={!result}>
|
||||
<summary>[{result ? "PASS" : "FAILED"}] {line}</summary>
|
||||
<pre>{message}</pre>
|
||||
</details>
|
||||
{/each}
|
||||
|
||||
<h3>Synchronisation Result Status</h3>
|
||||
<pre>{syncStatus.join("\n")}</pre>
|
||||
|
||||
<h3>Performance test</h3>
|
||||
<button on:click={() => performTest()} disabled={testRunning}>Test!</button>
|
||||
<button on:click={() => clearPerfTestResult()}>Clear</button>
|
||||
|
||||
<div bind:this={prefTestResultEl}></div>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -1,53 +0,0 @@
|
||||
import { ItemView, WorkspaceLeaf } from "@/deps.ts";
|
||||
import TestPaneComponent from "./TestPane.svelte";
|
||||
import type ObsidianLiveSyncPlugin from "@/main.ts";
|
||||
import type { ModuleDev } from "@/modules/extras/ModuleDev.ts";
|
||||
export const VIEW_TYPE_TEST = "ols-pane-test";
|
||||
declare global {
|
||||
interface LSEvents {
|
||||
"debug-sync-status": string[];
|
||||
}
|
||||
}
|
||||
//Log view
|
||||
export class TestPaneView extends ItemView {
|
||||
component?: TestPaneComponent;
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
moduleDev: ModuleDev;
|
||||
override icon = "view-log";
|
||||
title: string = "Self-hosted LiveSync Test and Results";
|
||||
override navigation = true;
|
||||
|
||||
override getIcon(): string {
|
||||
return "view-log";
|
||||
}
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin, moduleDev: ModuleDev) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
this.moduleDev = moduleDev;
|
||||
}
|
||||
|
||||
getViewType() {
|
||||
return VIEW_TYPE_TEST;
|
||||
}
|
||||
|
||||
getDisplayText() {
|
||||
return "Self-hosted LiveSync Test and Results";
|
||||
}
|
||||
|
||||
override async onOpen() {
|
||||
this.component = new TestPaneComponent({
|
||||
target: this.contentEl,
|
||||
props: {
|
||||
plugin: this.plugin,
|
||||
moduleDev: this.moduleDev,
|
||||
},
|
||||
});
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
override async onClose() {
|
||||
this.component?.$destroy();
|
||||
await Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { Trench } from "octagonal-wheels/memory/memutil";
|
||||
import type ObsidianLiveSyncPlugin from "@/main.ts";
|
||||
type MeasureResult = [times: number, spent: number];
|
||||
type NamedMeasureResult = [name: string, result: MeasureResult];
|
||||
const measures = new Map<string, MeasureResult>();
|
||||
|
||||
function clearResult(name: string) {
|
||||
measures.set(name, [0, 0]);
|
||||
}
|
||||
async function measureEach(name: string, proc: () => void | Promise<void>) {
|
||||
const [times, spent] = measures.get(name) ?? [0, 0];
|
||||
|
||||
const start = performance.now();
|
||||
const result = proc();
|
||||
if (result instanceof Promise) await result;
|
||||
const end = performance.now();
|
||||
measures.set(name, [times + 1, spent + (end - start)]);
|
||||
}
|
||||
function formatNumber(num: number) {
|
||||
return num.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
||||
}
|
||||
async function measure(
|
||||
name: string,
|
||||
proc: () => void | Promise<void>,
|
||||
times: number = 10000,
|
||||
duration: number = 1000
|
||||
): Promise<NamedMeasureResult> {
|
||||
const from = Date.now();
|
||||
let last = times;
|
||||
clearResult(name);
|
||||
do {
|
||||
await measureEach(name, proc);
|
||||
} while (last-- > 0 && Date.now() - from < duration);
|
||||
return [name, measures.get(name) as MeasureResult];
|
||||
}
|
||||
|
||||
function formatPerfResults(items: NamedMeasureResult[]) {
|
||||
return (
|
||||
`| Name | Runs | Each | Total |\n| --- | --- | --- | --- | \n` +
|
||||
items
|
||||
.map(
|
||||
(e) =>
|
||||
`| ${e[0]} | ${e[1][0]} | ${e[1][0] != 0 ? formatNumber(e[1][1] / e[1][0]) : "-"} | ${formatNumber(e[1][0])} |`
|
||||
)
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
export async function perf_trench(plugin: ObsidianLiveSyncPlugin) {
|
||||
clearResult("trench");
|
||||
const trench = new Trench(plugin.core.simpleStore);
|
||||
const result = [] as NamedMeasureResult[];
|
||||
result.push(
|
||||
await measure("trench-short-string", async () => {
|
||||
const p = trench.evacuate("string");
|
||||
await p();
|
||||
})
|
||||
);
|
||||
{
|
||||
const testBinary = await plugin.core.storageAccess.readHiddenFileBinary("testdata/10kb.png");
|
||||
const uint8Array = new Uint8Array(testBinary);
|
||||
result.push(
|
||||
await measure("trench-binary-10kb", async () => {
|
||||
const p = trench.evacuate(uint8Array);
|
||||
await p();
|
||||
})
|
||||
);
|
||||
}
|
||||
{
|
||||
const testBinary = await plugin.core.storageAccess.readHiddenFileBinary("testdata/100kb.jpeg");
|
||||
const uint8Array = new Uint8Array(testBinary);
|
||||
result.push(
|
||||
await measure("trench-binary-100kb", async () => {
|
||||
const p = trench.evacuate(uint8Array);
|
||||
await p();
|
||||
})
|
||||
);
|
||||
}
|
||||
{
|
||||
const testBinary = await plugin.core.storageAccess.readHiddenFileBinary("testdata/1mb.png");
|
||||
const uint8Array = new Uint8Array(testBinary);
|
||||
result.push(
|
||||
await measure("trench-binary-1mb", async () => {
|
||||
const p = trench.evacuate(uint8Array);
|
||||
await p();
|
||||
})
|
||||
);
|
||||
}
|
||||
return formatPerfResults(result);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type DatabaseConnectingStatus,
|
||||
type LOG_LEVEL,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { cancelTask, scheduleTask } from "octagonal-wheels/concurrency/task";
|
||||
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
||||
import { fireAndForget, isDirty, throttle } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import {
|
||||
collectingChunks,
|
||||
@@ -119,7 +119,7 @@ export class ModuleLog extends AbstractObsidianModule {
|
||||
statusBarLabels!: ReactiveValue<{ message: string; status: string }>;
|
||||
statusLog = reactiveSource("");
|
||||
activeFileStatus = reactiveSource("");
|
||||
notifies: { [key: string]: { notice: Notice; count: number } } = {};
|
||||
notifies: { [key: string]: { count: number } } = {};
|
||||
p2pLogCollector = new P2PLogCollector(this.services.context.events);
|
||||
|
||||
observeForLogs() {
|
||||
@@ -407,6 +407,10 @@ export class ModuleLog extends AbstractObsidianModule {
|
||||
}
|
||||
|
||||
private _allStartOnUnload(): Promise<boolean> {
|
||||
for (const key of Object.keys(this.notifies)) {
|
||||
this.services.context.notices.hide(`log:${key}`);
|
||||
}
|
||||
this.notifies = {};
|
||||
if (this.statusDiv) {
|
||||
this.statusDiv.remove();
|
||||
}
|
||||
@@ -559,35 +563,26 @@ ${stringifyYaml(info)}
|
||||
if (level >= LOG_LEVEL_NOTICE) {
|
||||
if (!key) key = messageContent;
|
||||
if (key in this.notifies) {
|
||||
// @ts-ignore
|
||||
const isShown = this.notifies[key].notice.noticeEl?.isShown();
|
||||
if (!isShown) {
|
||||
this.notifies[key].notice = new Notice(messageContent, 0);
|
||||
}
|
||||
cancelTask(`notify-${key}`);
|
||||
if (key == messageContent) {
|
||||
this.notifies[key].count++;
|
||||
this.notifies[key].notice.setMessage(`(${this.notifies[key].count}):${messageContent}`);
|
||||
} else {
|
||||
this.notifies[key].notice.setMessage(`${messageContent}`);
|
||||
}
|
||||
} else {
|
||||
const notify = new Notice(messageContent, 0);
|
||||
this.notifies[key] = {
|
||||
count: 0,
|
||||
notice: notify,
|
||||
};
|
||||
}
|
||||
const timeout = 5000;
|
||||
if (!key.startsWith("keepalive-") || messageContent.indexOf(MARK_DONE) !== -1) {
|
||||
const shouldExpire = !key.startsWith("keepalive-") || messageContent.indexOf(MARK_DONE) !== -1;
|
||||
const noticeMessage =
|
||||
key == messageContent && this.notifies[key].count > 0
|
||||
? `(${this.notifies[key].count}):${messageContent}`
|
||||
: messageContent;
|
||||
this.services.context.notices.show(`log:${key}`, noticeMessage, {
|
||||
durationMs: shouldExpire ? timeout : false,
|
||||
});
|
||||
if (shouldExpire) {
|
||||
scheduleTask(`notify-${key}`, timeout, () => {
|
||||
const notify = this.notifies[key].notice;
|
||||
delete this.notifies[key];
|
||||
try {
|
||||
notify.hide();
|
||||
} catch {
|
||||
// NO OP
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ObsidianLiveSyncSettingTab } from "./SettingDialogue/ObsidianLiveSyncSettingTab.ts";
|
||||
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
|
||||
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
|
||||
import { EVENT_REQUEST_OPEN_SETTING_WIZARD, EVENT_REQUEST_OPEN_SETTINGS, eventHub } from "@/common/events.ts";
|
||||
import { EVENT_REQUEST_OPEN_SETTINGS, eventHub } from "@/common/events.ts";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import { openObsidianSettings } from "@/common/obsidianSettings.ts";
|
||||
|
||||
@@ -12,10 +12,6 @@ export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
|
||||
this.settingTab = new ObsidianLiveSyncSettingTab(this.app, this.plugin);
|
||||
this.plugin.addSettingTab(this.settingTab);
|
||||
eventHub.onEvent(EVENT_REQUEST_OPEN_SETTINGS, () => this.openSetting());
|
||||
eventHub.onEvent(EVENT_REQUEST_OPEN_SETTING_WIZARD, () => {
|
||||
this.openSetting();
|
||||
void this.settingTab.enableMinimalSetup();
|
||||
});
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ChunkAlgorithmNames } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { SettingSpecGroup } from "./SettingSpec.ts";
|
||||
|
||||
export type AdvancedSettingSpecContext = {
|
||||
isCouchDB: () => boolean;
|
||||
};
|
||||
|
||||
/** Build the explicitly exposed standard controls for the existing Advanced page. */
|
||||
export function createAdvancedSettingSpecGroups({
|
||||
isCouchDB,
|
||||
}: AdvancedSettingSpecContext): readonly SettingSpecGroup[] {
|
||||
return [
|
||||
{
|
||||
heading: "Memory cache",
|
||||
items: [{ key: "hashCacheMaxCount", control: { type: "number", min: 10 } }],
|
||||
},
|
||||
{
|
||||
heading: "Local Database Tweak",
|
||||
items: [
|
||||
{
|
||||
key: "chunkSplitterVersion",
|
||||
control: { type: "dropdown", options: () => ChunkAlgorithmNames },
|
||||
},
|
||||
{ key: "customChunkSize", control: { type: "number", min: 0, allowZero: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Transfer Tweak",
|
||||
items: [
|
||||
{ key: "readChunksOnline", control: { type: "toggle" }, visible: isCouchDB },
|
||||
{ key: "useOnlyLocalChunk", control: { type: "toggle" }, visible: isCouchDB },
|
||||
{
|
||||
key: "concurrencyOfReadChunksOnline",
|
||||
control: { type: "number", min: 10 },
|
||||
visible: isCouchDB,
|
||||
},
|
||||
{
|
||||
key: "minimumIntervalOfReadChunksOnline",
|
||||
control: { type: "number", min: 10 },
|
||||
visible: isCouchDB,
|
||||
},
|
||||
{ key: "autoAcceptCompatibleTweak", control: { type: "toggle", defaultValue: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Remote Database Tweak",
|
||||
items: [{ key: "enableCompression", control: { type: "toggle" } }],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { $msg, $t } from "@/common/translation";
|
||||
import { SUPPORTED_I18N_LANGS } from "@/common/rosetta";
|
||||
import { NetworkWarningStyles } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const";
|
||||
import type { SettingSpecGroup } from "./SettingSpec.ts";
|
||||
|
||||
export type GeneralSettingSpecContext = {
|
||||
showEditorStatusDetails: () => boolean;
|
||||
showVerboseLog: () => boolean;
|
||||
};
|
||||
|
||||
/** Build the shared Appearance and Logging controls. */
|
||||
export function createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails,
|
||||
showVerboseLog,
|
||||
}: GeneralSettingSpecContext): readonly SettingSpecGroup[] {
|
||||
return [
|
||||
{
|
||||
heading: $msg("obsidianLiveSyncSettingTab.titleAppearance"),
|
||||
items: [
|
||||
{
|
||||
key: "displayLanguage",
|
||||
control: {
|
||||
type: "dropdown",
|
||||
options: () =>
|
||||
Object.fromEntries(
|
||||
SUPPORTED_I18N_LANGS.map((language) => [language, $t(`lang-${language}`)])
|
||||
),
|
||||
},
|
||||
},
|
||||
{ key: "showStatusOnEditor", control: { type: "toggle" } },
|
||||
{
|
||||
key: "showOnlyIconsOnEditor",
|
||||
control: { type: "toggle" },
|
||||
visible: showEditorStatusDetails,
|
||||
},
|
||||
{ key: "showStatusOnStatusbar", control: { type: "toggle" } },
|
||||
{ key: "hideFileWarningNotice", control: { type: "toggle" } },
|
||||
{
|
||||
key: "networkWarningStyle",
|
||||
control: {
|
||||
type: "dropdown",
|
||||
options: () => ({
|
||||
[NetworkWarningStyles.BANNER]: "Show full banner",
|
||||
[NetworkWarningStyles.ICON]: "Show icon only",
|
||||
[NetworkWarningStyles.HIDDEN]: "Hide completely",
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: $msg("obsidianLiveSyncSettingTab.titleLogging"),
|
||||
items: [
|
||||
{ key: "lessInformationInLog", control: { type: "toggle" } },
|
||||
{
|
||||
key: "showVerboseLog",
|
||||
control: { type: "toggle" },
|
||||
visible: showVerboseLog,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** Build the feature-level controls shown in General Settings under Extra menus. */
|
||||
export function createExtraMenuSettingSpecGroup(): SettingSpecGroup {
|
||||
return {
|
||||
heading: $msg("obsidianLiveSyncSettingTab.titleExtraMenus"),
|
||||
items: [
|
||||
{ key: "useAdvancedMode", control: { type: "toggle" } },
|
||||
{ key: "usePowerUserMode", control: { type: "toggle" } },
|
||||
{ key: "useEdgeCaseMode", control: { type: "toggle" } },
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -8,7 +8,12 @@ import {
|
||||
type ValueComponent,
|
||||
} from "@/deps.ts";
|
||||
import { unique } from "octagonal-wheels/collection";
|
||||
import { LEVEL_ADVANCED, LEVEL_POWER_USER, statusDisplay, type ConfigurationItem } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import {
|
||||
LEVEL_ADVANCED,
|
||||
LEVEL_POWER_USER,
|
||||
statusDisplay,
|
||||
type ConfigurationItem,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { type ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import {
|
||||
type AllSettingItemKey,
|
||||
@@ -19,7 +24,7 @@ import {
|
||||
type AllBooleanItemKey,
|
||||
} from "./settingConstants.ts";
|
||||
import { $msg } from "@/common/translation";
|
||||
import { wrapMemo, type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts";
|
||||
import { setButtonDestructiveState, wrapMemo, type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts";
|
||||
|
||||
export class LiveSyncSetting extends Setting {
|
||||
autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent;
|
||||
@@ -307,12 +312,7 @@ export class LiveSyncSetting extends Setting {
|
||||
{
|
||||
const component = this.autoWiredComponent;
|
||||
if (component instanceof ButtonComponent) {
|
||||
if (newConf[k]) {
|
||||
component.setWarning();
|
||||
} else {
|
||||
//TODO:IMPLEMENT
|
||||
// component.removeCta();
|
||||
}
|
||||
setButtonDestructiveState(component, newConf[k] ?? false);
|
||||
}
|
||||
this.prevStatus[k] = newConf[k];
|
||||
}
|
||||
|
||||
+524
@@ -0,0 +1,524 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { SettingDefinitionGroup, SettingDefinitionItem, SettingDefinitionPage } from "obsidian";
|
||||
import type { PageFunctions } from "./SettingPane.ts";
|
||||
|
||||
const runtime = vi.hoisted(() => ({
|
||||
components: [] as Array<{
|
||||
load: ReturnType<typeof vi.fn>;
|
||||
unload: ReturnType<typeof vi.fn>;
|
||||
callbacks: Array<() => unknown>;
|
||||
}>,
|
||||
paneChangeLog: vi.fn(),
|
||||
pageCleanup: vi.fn(),
|
||||
savedEffect: vi.fn(),
|
||||
superHide: vi.fn(),
|
||||
}));
|
||||
|
||||
function createElement(): HTMLElement {
|
||||
const element = {
|
||||
empty: vi.fn(),
|
||||
addClass: vi.fn(),
|
||||
removeClass: vi.fn(),
|
||||
toggleClass: vi.fn(),
|
||||
createEl: vi.fn(() => createElement()),
|
||||
createDiv: vi.fn(() => createElement()),
|
||||
querySelectorAll: vi.fn(() => []),
|
||||
};
|
||||
return element as unknown as HTMLElement;
|
||||
}
|
||||
|
||||
vi.mock("@/deps.ts", () => ({
|
||||
App: class {},
|
||||
Component: class {
|
||||
callbacks: Array<() => unknown> = [];
|
||||
load = vi.fn();
|
||||
unload = vi.fn(() => {
|
||||
for (const callback of this.callbacks.splice(0)) callback();
|
||||
});
|
||||
register = vi.fn((callback: () => unknown) => this.callbacks.push(callback));
|
||||
constructor() {
|
||||
runtime.components.push(this);
|
||||
}
|
||||
},
|
||||
PluginSettingTab: class {
|
||||
app: unknown;
|
||||
plugin: unknown;
|
||||
refreshDomState = vi.fn();
|
||||
update = vi.fn();
|
||||
constructor(app: unknown, plugin: unknown) {
|
||||
this.app = app;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
hide() {}
|
||||
},
|
||||
SettingPage: class {
|
||||
containerEl = createElement();
|
||||
title = "";
|
||||
display() {}
|
||||
hide() {
|
||||
runtime.superHide();
|
||||
}
|
||||
},
|
||||
requireApiVersion: vi.fn(() => true),
|
||||
}));
|
||||
vi.mock("@/main.ts", () => ({ default: class {} }));
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions", () => ({
|
||||
getLanguage: vi.fn(() => "en"),
|
||||
compatGlobal: {
|
||||
localStorage: {
|
||||
getItem: vi.fn(() => null),
|
||||
setItem: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("@/common/events.ts", () => ({
|
||||
EVENT_ON_UNRESOLVED_ERROR: "on-unresolved-error",
|
||||
EVENT_REQUEST_COPY_SETUP_URI: "request-copy-setup-uri",
|
||||
EVENT_REQUEST_OPEN_SETUP_URI: "request-open-setup-uri",
|
||||
EVENT_REQUEST_RELOAD_SETTING_TAB: "request-reload-setting-tab",
|
||||
EVENT_REQUEST_SHOW_SETUP_QR: "request-show-setup-qr",
|
||||
eventHub: { emitEvent: vi.fn(), onEvent: vi.fn() },
|
||||
}));
|
||||
vi.mock("@/modules/features/SetupManager.ts", () => ({ SetupManager: class {} }));
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/pouchdb/negotiation", () => ({ checkSyncInfo: vi.fn() }));
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator", () => ({
|
||||
LiveSyncCouchDBReplicator: class {},
|
||||
}));
|
||||
vi.mock("./LiveSyncSetting.ts", () => ({
|
||||
LiveSyncSetting: class {
|
||||
static env: unknown;
|
||||
},
|
||||
}));
|
||||
vi.mock("./SettingPane.ts", () => ({
|
||||
enableOnly: vi.fn((condition: () => boolean) => () => ({ disabled: !condition() })),
|
||||
setLevelClass: vi.fn(),
|
||||
setStyle: vi.fn(),
|
||||
visibleOnly: vi.fn((condition: () => boolean) => () => ({ visibility: condition() })),
|
||||
}));
|
||||
vi.mock("./PaneChangeLog.ts", () => ({ paneChangeLog: runtime.paneChangeLog }));
|
||||
vi.mock("./PaneQuickSetup.ts", () => ({ paneQuickSetup: vi.fn() }));
|
||||
vi.mock("./PaneHelp.ts", () => ({ paneHelp: vi.fn() }));
|
||||
vi.mock("./PaneGeneral.ts", () => ({ paneGeneral: vi.fn() }));
|
||||
vi.mock("./PaneRemoteConfig.ts", () => ({ paneRemoteConfig: vi.fn() }));
|
||||
vi.mock("./PaneSelector.ts", () => ({ paneSelector: vi.fn() }));
|
||||
vi.mock("./PaneSyncSettings.ts", () => ({ paneSyncSettings: vi.fn() }));
|
||||
vi.mock("./PaneCustomisationSync.ts", () => ({ paneCustomisationSync: vi.fn() }));
|
||||
vi.mock("./PaneHatch.ts", () => ({ paneHatch: vi.fn() }));
|
||||
vi.mock("./PaneAdvanced.ts", () => ({ paneAdvanced: vi.fn() }));
|
||||
vi.mock("./PanePowerUsers.ts", () => ({ panePowerUsers: vi.fn() }));
|
||||
vi.mock("./PanePatches.ts", () => ({ panePatches: vi.fn() }));
|
||||
vi.mock("./PaneMaintenance.ts", () => ({ paneMaintenance: vi.fn() }));
|
||||
|
||||
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import { createSettingsPageCatalogue } from "./SettingsPageCatalogue.ts";
|
||||
|
||||
function isPage(item: SettingDefinitionItem): item is SettingDefinitionPage {
|
||||
return "type" in item && item.type === "page";
|
||||
}
|
||||
|
||||
function isGroup(item: SettingDefinitionItem): item is SettingDefinitionGroup {
|
||||
return "type" in item && item.type === "group";
|
||||
}
|
||||
|
||||
function isAction(item: SettingDefinitionItem): item is Extract<SettingDefinitionItem, { action: unknown }> {
|
||||
return "action" in item && typeof item.action === "function";
|
||||
}
|
||||
|
||||
function itemLabel(item: SettingDefinitionItem): string {
|
||||
if (isPage(item)) return item.name;
|
||||
if (isGroup(item)) return item.heading ?? "";
|
||||
return item.name;
|
||||
}
|
||||
|
||||
function collectPages(items: readonly SettingDefinitionItem[]): SettingDefinitionPage[] {
|
||||
return items.flatMap((item) => {
|
||||
if (isPage(item)) return [item, ...collectPages(item.items ?? [])];
|
||||
if (isGroup(item)) return collectPages(item.items ?? []);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
function findPage(tab: ObsidianLiveSyncSettingTab, name: string): SettingDefinitionPage {
|
||||
const page = collectPages(tab.getSettingDefinitions()).find((candidate) => candidate.name.endsWith(` ${name}`));
|
||||
if (!page) throw new Error(`${name} custom page is unavailable`);
|
||||
return page;
|
||||
}
|
||||
|
||||
function createSettingsTab(): ObsidianLiveSyncSettingTab {
|
||||
const plugin = {
|
||||
app: {},
|
||||
core: {
|
||||
settings: { ...DEFAULT_SETTINGS, useAdvancedMode: true },
|
||||
confirm: {
|
||||
askInPopup: vi.fn(),
|
||||
},
|
||||
services: {
|
||||
setting: {
|
||||
getDeviceAndVaultName: vi.fn(() => ""),
|
||||
saveSettingData: vi.fn(async () => undefined),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const tab = new ObsidianLiveSyncSettingTab({} as never, plugin as never);
|
||||
Object.assign(tab, {
|
||||
_editingSettings: { ...DEFAULT_SETTINGS, useAdvancedMode: true },
|
||||
initialSettings: { ...DEFAULT_SETTINGS, useAdvancedMode: true },
|
||||
});
|
||||
return tab;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
runtime.components.length = 0;
|
||||
runtime.paneChangeLog.mockClear();
|
||||
runtime.paneChangeLog.mockImplementation(function (this: ObsidianLiveSyncSettingTab) {
|
||||
this.lifetimeComponent.register(runtime.pageCleanup);
|
||||
});
|
||||
runtime.pageCleanup.mockClear();
|
||||
runtime.savedEffect.mockClear();
|
||||
runtime.superHide.mockClear();
|
||||
});
|
||||
|
||||
describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
it("keeps Quick Setup first while synchronisation is inactive and separates synchronisation pages from it", () => {
|
||||
const tab = createSettingsTab();
|
||||
const definitions = tab.getSettingDefinitions().filter(isGroup);
|
||||
|
||||
expect(definitions.slice(0, 3).map(itemLabel)).toEqual([
|
||||
"🧙♂️ Quick Setup",
|
||||
"🔄 Synchronisation",
|
||||
"⚙️ General Settings",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the synchronisation group first and orders General Settings before Quick Setup while synchronisation is active", () => {
|
||||
const tab = createSettingsTab();
|
||||
tab.editingSettings.liveSync = true;
|
||||
const definitions = tab.getSettingDefinitions().filter(isGroup);
|
||||
|
||||
expect(definitions.slice(0, 3).map(itemLabel)).toEqual([
|
||||
"🔄 Synchronisation",
|
||||
"⚙️ General Settings",
|
||||
"🧙♂️ Quick Setup",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the pending initialisation action visible on the root settings page", () => {
|
||||
const tab = createSettingsTab();
|
||||
tab.editingSettings.handleFilenameCaseSensitive = !tab.initialSettings!.handleFilenameCaseSensitive;
|
||||
|
||||
const action = tab.getSettingDefinitions().find(isAction);
|
||||
|
||||
expect(action?.name).toBe("Apply");
|
||||
expect(typeof action?.visible === "function" ? action.visible() : action?.visible).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps Remote Configuration and Sync Settings as native pages inside the Synchronisation group", () => {
|
||||
const tab = createSettingsTab();
|
||||
const definitions = tab.getSettingDefinitions();
|
||||
const synchronisation = definitions.find(
|
||||
(item): item is SettingDefinitionGroup => isGroup(item) && item.heading === "🔄 Synchronisation"
|
||||
);
|
||||
|
||||
expect(synchronisation?.items?.filter(isPage).map(({ name }) => name)).toEqual([
|
||||
"🛰️ Remote Configuration",
|
||||
"🔄 Sync Settings",
|
||||
]);
|
||||
expect(
|
||||
definitions
|
||||
.filter(isPage)
|
||||
.map(({ name }) => name)
|
||||
.filter((name) => name.endsWith(" Remote Configuration") || name.endsWith(" Sync Settings"))
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("groups secondary pages by purpose instead of exposing a flat Detailed settings list", () => {
|
||||
const tab = createSettingsTab();
|
||||
const definitions = tab.getSettingDefinitions();
|
||||
const groups = definitions.filter(isGroup);
|
||||
|
||||
expect(groups.map(({ heading }) => heading)).toEqual([
|
||||
"🧙♂️ Quick Setup",
|
||||
"🔄 Synchronisation",
|
||||
"⚙️ General Settings",
|
||||
"📲 Set up other devices",
|
||||
"🛠️ Maintenance and recovery",
|
||||
"🧩 Extra features",
|
||||
"🔧 Advanced settings",
|
||||
"ℹ️ Help and information",
|
||||
]);
|
||||
expect(
|
||||
groups
|
||||
.find(({ heading }) => heading === "🛠️ Maintenance and recovery")
|
||||
?.items?.filter(isPage)
|
||||
.map(({ name }) => name)
|
||||
).toEqual(["🎛️ Maintenance", "🧰 Hatch"]);
|
||||
expect(
|
||||
groups
|
||||
.find(({ heading }) => heading === "🧩 Extra features")
|
||||
?.items?.filter(isPage)
|
||||
.map(({ name }) => name)
|
||||
).toEqual(["🚦 Selector", "🔌 Customisation sync"]);
|
||||
expect(
|
||||
groups
|
||||
.find(({ heading }) => heading === "🔧 Advanced settings")
|
||||
?.items?.filter(isPage)
|
||||
.map(({ name }) => name)
|
||||
).toEqual(["🔧 Advanced", "💪 Power users", "🩹 Patches"]);
|
||||
expect(
|
||||
groups
|
||||
.find(({ heading }) => heading === "ℹ️ Help and information")
|
||||
?.items?.filter(isPage)
|
||||
.map(({ name }) => name)
|
||||
).toEqual(["❓ Help and troubleshooting", "💬 Change Log"]);
|
||||
expect(
|
||||
groups.find(({ heading }) => heading === "📲 Set up other devices")?.items?.map(({ name }) => name)
|
||||
).toEqual(["Copy the current settings to a Setup URI", "Show QR code"]);
|
||||
});
|
||||
|
||||
it("keeps Appearance, Logging, and Extra menus inside General Settings", () => {
|
||||
const tab = createSettingsTab();
|
||||
const definitions = tab.getSettingDefinitions();
|
||||
const general = definitions.find(
|
||||
(item): item is SettingDefinitionGroup => isGroup(item) && item.heading === "⚙️ General Settings"
|
||||
);
|
||||
const generalPages = general?.items?.filter(isPage);
|
||||
const appearance = generalPages?.find(({ name }) => name === "🎨 Appearance");
|
||||
const logging = generalPages?.find(({ name }) => name === "📝 Logging");
|
||||
const extraMenus = general?.items?.find(
|
||||
(item): item is SettingDefinitionPage => isPage(item) && item.name === "🎚️ Extra menus"
|
||||
);
|
||||
|
||||
expect(generalPages?.map(({ name }) => name)).toEqual(["🎨 Appearance", "📝 Logging", "🎚️ Extra menus"]);
|
||||
expect(
|
||||
appearance?.items?.flatMap((item) => ("control" in item && item.control ? [item.control.key] : []))
|
||||
).toEqual([
|
||||
"displayLanguage",
|
||||
"showStatusOnEditor",
|
||||
"showOnlyIconsOnEditor",
|
||||
"showStatusOnStatusbar",
|
||||
"hideFileWarningNotice",
|
||||
"networkWarningStyle",
|
||||
]);
|
||||
expect(
|
||||
logging?.items?.flatMap((item) => ("control" in item && item.control ? [item.control.key] : []))
|
||||
).toEqual(["lessInformationInLog", "showVerboseLog"]);
|
||||
expect(
|
||||
extraMenus?.items?.flatMap((item) => ("control" in item && item.control ? [item.control.key] : []))
|
||||
).toEqual(["useAdvancedMode", "usePowerUserMode", "useEdgeCaseMode"]);
|
||||
});
|
||||
|
||||
it("omits the old Setup child page and keeps standard General and Advanced pages native", () => {
|
||||
const tab = createSettingsTab();
|
||||
const pages = collectPages(tab.getSettingDefinitions());
|
||||
|
||||
expect(pages).toHaveLength(14);
|
||||
expect(pages.map(({ name }) => name)).toEqual(
|
||||
expect.arrayContaining(
|
||||
createSettingsPageCatalogue()
|
||||
.filter(({ id }) => id !== "general" && id !== "quick-setup")
|
||||
.map((entry) => `${entry.icon} ${entry.name()}`)
|
||||
)
|
||||
);
|
||||
expect(pages.some(({ name }) => name.endsWith(" General Settings"))).toBe(false);
|
||||
expect(pages.some(({ name }) => name.endsWith(" Setup"))).toBe(false);
|
||||
const advanced = pages.find(({ name }) => name.endsWith(" Advanced"));
|
||||
expect(advanced?.items?.filter((item) => "type" in item && item.type === "group")).toHaveLength(4);
|
||||
expect(advanced?.items?.filter((item) => "action" in item && typeof item.action === "function")).toHaveLength(
|
||||
1
|
||||
);
|
||||
expect(advanced?.page).toBeUndefined();
|
||||
expect(pages.filter(({ page }) => page !== undefined)).toHaveLength(10);
|
||||
});
|
||||
|
||||
it("keeps simple setup actions on the landing page without a second Setup destination", () => {
|
||||
const tab = createSettingsTab();
|
||||
const definitions = tab.getSettingDefinitions();
|
||||
const quickSetup = definitions.find(
|
||||
(item): item is SettingDefinitionGroup => isGroup(item) && item.heading === "🧙♂️ Quick Setup"
|
||||
);
|
||||
|
||||
expect(quickSetup?.items?.map(({ name }) => name)).toEqual([
|
||||
"Connect with Setup URI",
|
||||
"Rerun Onboarding Wizard",
|
||||
"Enable LiveSync",
|
||||
]);
|
||||
expect(collectPages(definitions).some(({ name }) => name.endsWith(" Setup"))).toBe(false);
|
||||
});
|
||||
|
||||
it("constructs custom page state only when opened and disposes each rendered scope", () => {
|
||||
const tab = createSettingsTab();
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
|
||||
expect(runtime.components).toHaveLength(0);
|
||||
const page = changeLog.page();
|
||||
expect(runtime.components).toHaveLength(0);
|
||||
|
||||
page.display();
|
||||
expect(runtime.paneChangeLog).toHaveBeenCalledOnce();
|
||||
expect(runtime.components).toHaveLength(1);
|
||||
expect(runtime.components[0].load).toHaveBeenCalledOnce();
|
||||
|
||||
page.display();
|
||||
expect(runtime.components[0].unload).toHaveBeenCalledOnce();
|
||||
expect(runtime.pageCleanup).toHaveBeenCalledOnce();
|
||||
expect(runtime.components).toHaveLength(2);
|
||||
|
||||
page.hide();
|
||||
expect(runtime.components[1].unload).toHaveBeenCalledOnce();
|
||||
expect(runtime.pageCleanup).toHaveBeenCalledTimes(2);
|
||||
expect(runtime.superHide).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not run delayed pane work after its page scope has been disposed", async () => {
|
||||
runtime.paneChangeLog.mockImplementation(function (
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
_paneEl: HTMLElement,
|
||||
{ addPanel }: Pick<PageFunctions, "addPanel">
|
||||
) {
|
||||
void addPanel(createElement(), "Delayed panel").then(() => {
|
||||
this.lifetimeComponent.register(runtime.pageCleanup);
|
||||
});
|
||||
});
|
||||
const tab = createSettingsTab();
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
|
||||
const page = changeLog.page();
|
||||
page.display();
|
||||
page.hide();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(runtime.pageCleanup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("runs a delayed pane callback inside its active scope before a queued hide", async () => {
|
||||
runtime.paneChangeLog.mockImplementation(function (
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
_paneEl: HTMLElement,
|
||||
{ addPanel }: Pick<PageFunctions, "addPanel">
|
||||
) {
|
||||
void addPanel(createElement(), "Delayed panel").then(() => {
|
||||
this.lifetimeComponent.register(runtime.pageCleanup);
|
||||
});
|
||||
});
|
||||
const tab = createSettingsTab();
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
|
||||
const page = changeLog.page();
|
||||
page.display();
|
||||
queueMicrotask(() => page.hide());
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(runtime.pageCleanup).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("rebuilds the catalogue when an externally loaded setting changes page visibility", () => {
|
||||
const tab = createSettingsTab();
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
changeLog.page().display();
|
||||
tab.core.settings.usePowerUserMode = !tab.editingSettings.usePowerUserMode;
|
||||
|
||||
tab.requestReload();
|
||||
|
||||
expect(tab.update).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("rebuilds the catalogue after an Extra menus feature level is saved", async () => {
|
||||
const tab = createSettingsTab();
|
||||
tab.editingSettings.usePowerUserMode = true;
|
||||
|
||||
await tab.saveSettings(["usePowerUserMode"]);
|
||||
|
||||
expect(tab.update).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("rebuilds translated catalogue names when the display language changes externally", () => {
|
||||
const tab = createSettingsTab();
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
changeLog.page().display();
|
||||
tab.core.settings.displayLanguage = "ja";
|
||||
|
||||
tab.requestReload();
|
||||
|
||||
expect(tab.update).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("rebuilds the catalogue after accepting an external page-visibility setting over a dirty value", () => {
|
||||
const tab = createSettingsTab();
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
changeLog.page().display();
|
||||
tab.initialSettings!.usePowerUserMode = false;
|
||||
tab.editingSettings.usePowerUserMode = true;
|
||||
tab.core.settings.usePowerUserMode = true;
|
||||
|
||||
tab.requestReload();
|
||||
const configureAnchor = vi.mocked(tab.core.confirm.askInPopup).mock.calls[0]?.[2];
|
||||
expect(configureAnchor).toBeTypeOf("function");
|
||||
let acceptExternalSetting: (() => void) | undefined;
|
||||
configureAnchor?.({
|
||||
text: "",
|
||||
addEventListener: vi.fn((_event: string, callback: () => void) => {
|
||||
acceptExternalSetting = callback;
|
||||
}),
|
||||
} as unknown as HTMLAnchorElement);
|
||||
expect(acceptExternalSetting).toBeTypeOf("function");
|
||||
vi.mocked(tab.update).mockClear();
|
||||
acceptExternalSetting!();
|
||||
|
||||
expect(tab.update).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not reopen a custom page when a catalogue update has already hidden it", () => {
|
||||
const tab = createSettingsTab();
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
const page = changeLog.page();
|
||||
page.display();
|
||||
vi.mocked(tab.update).mockImplementation(() => page.hide());
|
||||
|
||||
tab.requestCatalogueRefresh();
|
||||
|
||||
expect(runtime.paneChangeLog).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("keeps saved-setting effects owned by the tab after a custom page closes", async () => {
|
||||
runtime.paneChangeLog.mockImplementation(function (this: ObsidianLiveSyncSettingTab) {
|
||||
this.addOnSaved("displayLanguage", runtime.savedEffect);
|
||||
});
|
||||
const tab = createSettingsTab();
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
|
||||
const page = changeLog.page();
|
||||
page.display();
|
||||
page.hide();
|
||||
tab.editingSettings.displayLanguage = "ja";
|
||||
await tab.saveSettings(["displayLanguage"]);
|
||||
|
||||
expect(runtime.savedEffect).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,8 @@
|
||||
import { App, Component, PluginSettingTab } from "@/deps.ts";
|
||||
import { App, Component, PluginSettingTab, requireApiVersion, SettingPage } from "@/deps.ts";
|
||||
import {
|
||||
type ObsidianLiveSyncSettings,
|
||||
type RemoteDBSettings,
|
||||
LOG_LEVEL_NOTICE,
|
||||
FLAGMD_REDFLAG2_HR,
|
||||
FLAGMD_REDFLAG3_HR,
|
||||
REMOTE_COUCHDB,
|
||||
REMOTE_MINIO,
|
||||
type ConfigLevel,
|
||||
@@ -33,8 +31,14 @@ import {
|
||||
import { $msg } from "@/common/translation";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import { fireAndForget, yieldNextAnimationFrame } from "octagonal-wheels/promises";
|
||||
import { EVENT_REQUEST_RELOAD_SETTING_TAB, eventHub } from "@/common/events.ts";
|
||||
import { paneChangeLog } from "./PaneChangeLog.ts";
|
||||
import {
|
||||
EVENT_ON_UNRESOLVED_ERROR,
|
||||
EVENT_REQUEST_COPY_SETUP_URI,
|
||||
EVENT_REQUEST_OPEN_SETUP_URI,
|
||||
EVENT_REQUEST_RELOAD_SETTING_TAB,
|
||||
EVENT_REQUEST_SHOW_SETUP_QR,
|
||||
eventHub,
|
||||
} from "@/common/events.ts";
|
||||
import {
|
||||
enableOnly,
|
||||
// findAttrFromParent,
|
||||
@@ -46,32 +50,47 @@ import {
|
||||
type OnSavedHandlerFunc,
|
||||
type OnUpdateFunc,
|
||||
type OnUpdateResult,
|
||||
type DeferredPageElement,
|
||||
type PageFunctions,
|
||||
type UpdateFunction,
|
||||
} from "./SettingPane.ts";
|
||||
import { paneSetup } from "./PaneSetup.ts";
|
||||
import { paneGeneral } from "./PaneGeneral.ts";
|
||||
import { paneRemoteConfig } from "./PaneRemoteConfig.ts";
|
||||
import { paneSelector } from "./PaneSelector.ts";
|
||||
import { paneSyncSettings } from "./PaneSyncSettings.ts";
|
||||
import { paneCustomisationSync } from "./PaneCustomisationSync.ts";
|
||||
import { paneHatch } from "./PaneHatch.ts";
|
||||
import { paneAdvanced } from "./PaneAdvanced.ts";
|
||||
import { panePowerUsers } from "./PanePowerUsers.ts";
|
||||
import { panePatches } from "./PanePatches.ts";
|
||||
import { paneMaintenance } from "./PaneMaintenance.ts";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { JournalSyncCore } from "@vrtmrz/livesync-commonlib/compat/replication/journal/JournalSyncCore";
|
||||
import { MinioStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/replication/journal/objectstore/MinioStorageAdapter";
|
||||
import { closeObsidianSettings } from "@/common/obsidianSettings.ts";
|
||||
import {
|
||||
createAdvancedSettingDefinitionGroups,
|
||||
createExtraMenuSettingDefinitions,
|
||||
createGeneralSettingDefinitionGroups,
|
||||
createSettingsPageCatalogue,
|
||||
getSettingsRootGroupEntry,
|
||||
type SettingsPageEntry,
|
||||
type SettingsRootGroupId,
|
||||
} from "./SettingsPageCatalogue.ts";
|
||||
import { createAdvancedSettingSpecGroups } from "./AdvancedSettingSpecs.ts";
|
||||
import { isValidSettingSpecValue, type SettingSpec } from "./SettingSpec.ts";
|
||||
import type {
|
||||
SettingDefinitionAction,
|
||||
SettingDefinitionGroup,
|
||||
SettingDefinitionItem,
|
||||
SettingDefinitionPage,
|
||||
SettingGroupItem,
|
||||
} from "obsidian";
|
||||
import { createExtraMenuSettingSpecGroup, createGeneralSettingSpecGroups } from "./GeneralSettingSpecs.ts";
|
||||
import { SetupManager } from "@/modules/features/SetupManager.ts";
|
||||
import { isP2PMainRemote } from "@/common/remoteConfiguration.ts";
|
||||
|
||||
// For creating a document
|
||||
// const toc = new Set<string>();
|
||||
|
||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
private _lifetimeComponent: Component = new Component();
|
||||
private _lifetimeComponent?: Component;
|
||||
private activePageRefresh?: () => void;
|
||||
get lifetimeComponent(): Component {
|
||||
if (!this._lifetimeComponent) {
|
||||
throw new Error("The settings page render scope has not been initialised");
|
||||
}
|
||||
return this._lifetimeComponent;
|
||||
}
|
||||
get core() {
|
||||
@@ -200,9 +219,39 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
for (const func of this.controlledElementFunc) {
|
||||
func();
|
||||
}
|
||||
if (requireApiVersion("1.13.0") && typeof this.refreshDomState === "function") {
|
||||
this.refreshDomState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Re-render the active imperative page without assuming which settings renderer owns it. */
|
||||
requestPageRefresh() {
|
||||
if (this.activePageRefresh) {
|
||||
this.activePageRefresh();
|
||||
return;
|
||||
}
|
||||
if (requireApiVersion("1.13.0") && typeof SettingPage === "function" && typeof this.update === "function") {
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
this.displayImperative();
|
||||
}
|
||||
|
||||
/** Rebuild the native page catalogue and preserve the current imperative page where possible. */
|
||||
requestCatalogueRefresh() {
|
||||
if (requireApiVersion("1.13.0") && typeof SettingPage === "function" && typeof this.update === "function") {
|
||||
const refreshPage = this.activePageRefresh;
|
||||
const owner = this._lifetimeComponent;
|
||||
this.update();
|
||||
if (owner && this._lifetimeComponent === owner) {
|
||||
refreshPage?.();
|
||||
}
|
||||
} else {
|
||||
this.displayImperative();
|
||||
}
|
||||
}
|
||||
|
||||
reloadAllLocalSettings() {
|
||||
const ret = { ...OnDialogSettingsDefault };
|
||||
ret.configPassphrase = compatGlobal.localStorage.getItem("ls-setting-passphrase") || "";
|
||||
@@ -275,8 +324,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
controlledElementFunc = [] as UpdateFunction[];
|
||||
onSavedHandlers = [] as OnSavedHandler<AllSettingItemKey>[];
|
||||
|
||||
inWizard: boolean = false;
|
||||
|
||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
@@ -284,6 +331,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
eventHub.onEvent(EVENT_REQUEST_RELOAD_SETTING_TAB, () => {
|
||||
this.requestReload();
|
||||
});
|
||||
this.addOnSaved("displayLanguage", () => this.requestCatalogueRefresh());
|
||||
this.addOnSaved("showStatusOnEditor", () => eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR));
|
||||
this.addOnSaved("networkWarningStyle", () => eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR));
|
||||
this.addOnSaved("useAdvancedMode", () => this.requestCatalogueRefresh());
|
||||
this.addOnSaved("usePowerUserMode", () => this.requestCatalogueRefresh());
|
||||
this.addOnSaved("useEdgeCaseMode", () => this.requestCatalogueRefresh());
|
||||
}
|
||||
|
||||
async testConnection(settingOverride: Partial<ObsidianLiveSyncSettings> = {}): Promise<void> {
|
||||
@@ -311,6 +364,29 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
closeObsidianSettings(this.plugin.app);
|
||||
}
|
||||
|
||||
requestOpenSetupURI(): void {
|
||||
this.closeSetting();
|
||||
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETUP_URI);
|
||||
}
|
||||
|
||||
async rerunOnboardingWizard(): Promise<void> {
|
||||
await this.core.getModule(SetupManager).startOnBoarding();
|
||||
}
|
||||
|
||||
async enableLiveSyncFromSettings(): Promise<void> {
|
||||
this.editingSettings.isConfigured = true;
|
||||
await this.saveAllDirtySettings();
|
||||
this.services.appLifecycle.askRestart();
|
||||
}
|
||||
|
||||
requestCopySetupURI(): void {
|
||||
eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI);
|
||||
}
|
||||
|
||||
requestShowSetupQRCode(): void {
|
||||
eventHub.emitEvent(EVENT_REQUEST_SHOW_SETUP_QR);
|
||||
}
|
||||
|
||||
handleElement(element: HTMLElement, func: OnUpdateFunc) {
|
||||
const updateFunc = ((element, func) => {
|
||||
const prev = {} as OnUpdateResult;
|
||||
@@ -357,7 +433,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
addOnSaved<T extends AllSettingItemKey>(key: T, func: OnSavedHandlerFunc<T>) {
|
||||
const newHandler = { key, handler: func } as OnSavedHandler<AllSettingItemKey>;
|
||||
this.onSavedHandlers.push(newHandler);
|
||||
const existing = this.onSavedHandlers.findIndex((handler) => handler.key === key);
|
||||
if (existing === -1) {
|
||||
this.onSavedHandlers.push(newHandler);
|
||||
} else {
|
||||
this.onSavedHandlers.splice(existing, 1, newHandler);
|
||||
}
|
||||
}
|
||||
resetEditingSettings() {
|
||||
this._editingSettings = undefined;
|
||||
@@ -365,17 +446,37 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
|
||||
override hide() {
|
||||
this.disposeRenderScope();
|
||||
super.hide();
|
||||
this._lifetimeComponent.unload();
|
||||
this.isShown = false;
|
||||
}
|
||||
isShown: boolean = false;
|
||||
|
||||
private changesPageCatalogue(key: AllSettingItemKey): boolean {
|
||||
return (
|
||||
key === "displayLanguage" ||
|
||||
key === "useAdvancedMode" ||
|
||||
key === "usePowerUserMode" ||
|
||||
key === "useEdgeCaseMode" ||
|
||||
key === "isConfigured" ||
|
||||
key === "liveSync" ||
|
||||
key === "periodicReplication" ||
|
||||
key === "syncOnSave" ||
|
||||
key === "syncOnEditorSave" ||
|
||||
key === "syncOnStart" ||
|
||||
key === "syncOnFileOpen" ||
|
||||
key === "syncAfterMerge"
|
||||
);
|
||||
}
|
||||
|
||||
requestReload() {
|
||||
if (this.isShown) {
|
||||
const nativeTabIsShown =
|
||||
this.supportsDeclarativeSettings() && this.containerEl !== undefined && this.containerEl.isShown();
|
||||
if (this.isShown || nativeTabIsShown) {
|
||||
const newConf = this.core.settings;
|
||||
const keys = Object.keys(newConf) as (keyof ObsidianLiveSyncSettings)[];
|
||||
let hasLoaded = false;
|
||||
let catalogueVisibilityChanged = false;
|
||||
for (const k of keys) {
|
||||
if (isObjectDifferent(newConf[k], this.initialSettings?.[k])) {
|
||||
// Something has changed
|
||||
@@ -390,7 +491,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
anchor.text = $msg("obsidianLiveSyncSettingTab.optionHere");
|
||||
anchor.addEventListener("click", () => {
|
||||
this.refreshSetting(k as AllSettingItemKey);
|
||||
this.display();
|
||||
if (this.changesPageCatalogue(k as AllSettingItemKey)) {
|
||||
this.requestCatalogueRefresh();
|
||||
} else {
|
||||
this.requestPageRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -401,11 +506,18 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
continue;
|
||||
}
|
||||
hasLoaded = true;
|
||||
if (this.changesPageCatalogue(k as AllSettingItemKey)) {
|
||||
catalogueVisibilityChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasLoaded) {
|
||||
this.display();
|
||||
if (catalogueVisibilityChanged) {
|
||||
this.requestCatalogueRefresh();
|
||||
} else {
|
||||
this.requestPageRefresh();
|
||||
}
|
||||
} else {
|
||||
this.requestUpdate();
|
||||
}
|
||||
@@ -436,20 +548,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
this.selectedScreen = screen;
|
||||
}
|
||||
async enableMinimalSetup() {
|
||||
this.editingSettings.liveSync = false;
|
||||
this.editingSettings.periodicReplication = false;
|
||||
this.editingSettings.syncOnSave = false;
|
||||
this.editingSettings.syncOnEditorSave = false;
|
||||
this.editingSettings.syncOnStart = false;
|
||||
this.editingSettings.syncOnFileOpen = false;
|
||||
this.editingSettings.syncAfterMerge = false;
|
||||
this.core.replicator.closeReplication();
|
||||
await this.saveAllDirtySettings();
|
||||
this.containerEl.addClass("isWizard");
|
||||
this.inWizard = true;
|
||||
this.changeDisplay("20");
|
||||
}
|
||||
menuEl?: HTMLElement;
|
||||
|
||||
addScreenElement(key: string, element: HTMLElement) {
|
||||
@@ -504,6 +602,340 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
return false;
|
||||
}
|
||||
|
||||
private supportsDeclarativeSettings(): boolean {
|
||||
return requireApiVersion("1.13.0") && typeof SettingPage === "function";
|
||||
}
|
||||
|
||||
private isPageVisible(level?: ConfigLevel): boolean {
|
||||
if (level === LEVEL_ADVANCED) {
|
||||
return this.isConfiguredAs("useAdvancedMode", true);
|
||||
}
|
||||
if (level === LEVEL_POWER_USER) {
|
||||
return this.isConfiguredAs("usePowerUserMode", true);
|
||||
}
|
||||
if (level === LEVEL_EDGE_CASE) {
|
||||
return this.isConfiguredAs("useEdgeCaseMode", true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private getDeclarativeSettingSpec(key: string): SettingSpec {
|
||||
const spec = [
|
||||
...createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails: () => this.isConfiguredAs("showStatusOnEditor", true),
|
||||
showVerboseLog: () => this.isConfiguredAs("lessInformationInLog", false),
|
||||
}),
|
||||
createExtraMenuSettingSpecGroup(),
|
||||
...createAdvancedSettingSpecGroups({
|
||||
isCouchDB: () => this.isConfiguredAs("remoteType", REMOTE_COUCHDB),
|
||||
}),
|
||||
]
|
||||
.flatMap((group) => group.items)
|
||||
.find((candidate) => candidate.key === key);
|
||||
if (!spec) {
|
||||
throw new Error(`Unknown declarative setting key: ${key}`);
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
override getControlValue(key: string): unknown {
|
||||
const spec = this.getDeclarativeSettingSpec(key);
|
||||
return this.editingSettings[spec.key];
|
||||
}
|
||||
|
||||
override async setControlValue(key: string, value: unknown): Promise<void> {
|
||||
const spec = this.getDeclarativeSettingSpec(key);
|
||||
if (!isValidSettingSpecValue(spec, value)) {
|
||||
throw new TypeError(`Invalid value for declarative setting ${key}`);
|
||||
}
|
||||
Reflect.set(this.editingSettings, spec.key, value);
|
||||
await this.saveSettings([spec.key]);
|
||||
}
|
||||
|
||||
private createRebuildRequiredAction(): SettingDefinitionAction {
|
||||
return {
|
||||
name: $msg("obsidianLiveSyncSettingTab.optionApply"),
|
||||
desc: $msg("obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied"),
|
||||
visible: () => this.isNeedRebuildLocal() || this.isNeedRebuildRemote(),
|
||||
action: () => fireAndForget(async () => await this.confirmRebuild()),
|
||||
};
|
||||
}
|
||||
|
||||
private renderRebuildRequiredAction(parentEl: HTMLElement): void {
|
||||
this.createEl(
|
||||
parentEl,
|
||||
"div",
|
||||
{ cls: "sls-setting-menu-buttons" },
|
||||
(el) => {
|
||||
el.createEl("label", { text: $msg("obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied") });
|
||||
void this.addEl(
|
||||
el,
|
||||
"button",
|
||||
{ text: $msg("obsidianLiveSyncSettingTab.optionApply"), cls: "mod-warning" },
|
||||
(buttonEl) => {
|
||||
buttonEl.addEventListener("click", () =>
|
||||
fireAndForget(async () => await this.confirmRebuild())
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
visibleOnly(() => this.isNeedRebuildLocal() || this.isNeedRebuildRemote())
|
||||
);
|
||||
}
|
||||
|
||||
private addPanel(
|
||||
parentEl: HTMLElement,
|
||||
title: string,
|
||||
callback?: (el: HTMLDivElement) => void,
|
||||
func?: OnUpdateFunc,
|
||||
level?: ConfigLevel
|
||||
): DeferredPageElement {
|
||||
const owner = this.lifetimeComponent;
|
||||
const el = this.createEl(parentEl, "div", { text: "" }, callback, func);
|
||||
setLevelClass(el, level);
|
||||
this.createEl(el, "h4", { text: title, cls: "sls-setting-panel-title" });
|
||||
return this.resolveWithinRenderScope(el, owner);
|
||||
}
|
||||
|
||||
/** Run delayed pane construction only while the requesting page still owns the render scope. */
|
||||
private resolveWithinRenderScope<T extends HTMLElement>(value: T, owner: Component): DeferredPageElement<T> {
|
||||
return {
|
||||
then: (callback) => {
|
||||
queueMicrotask(() => {
|
||||
if (this._lifetimeComponent === owner) {
|
||||
callback(value);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private renderCustomPage(page: SettingPage, entry: SettingsPageEntry): Component {
|
||||
if (requireApiVersion("1.13.0")) {
|
||||
const component = this.beginRenderScope(() => page.display());
|
||||
this.isShown = true;
|
||||
page.title = entry.name();
|
||||
page.containerEl.empty();
|
||||
page.containerEl.addClass("sls-setting");
|
||||
setStyle(page.containerEl, "menu-setting-poweruser", () => this.isConfiguredAs("usePowerUserMode", true));
|
||||
setStyle(page.containerEl, "menu-setting-advanced", () => this.isConfiguredAs("useAdvancedMode", true));
|
||||
setStyle(page.containerEl, "menu-setting-edgecase", () => this.isConfiguredAs("useEdgeCaseMode", true));
|
||||
this.renderRebuildRequiredAction(page.containerEl);
|
||||
|
||||
const addPane: PageFunctions["addPane"] = (parentEl, title, _icon, _order, level) => {
|
||||
const paneEl = this.createEl(parentEl, "div", { text: "" });
|
||||
setLevelClass(paneEl, level);
|
||||
new Setting(paneEl).setName(title).setHeading().setClass("sls-setting-pane-title");
|
||||
return this.resolveWithinRenderScope(paneEl, component);
|
||||
};
|
||||
entry.legacy.call(this, page.containerEl, {
|
||||
addPane,
|
||||
addPanel: this.addPanel.bind(this),
|
||||
});
|
||||
this.requestUpdate();
|
||||
return component;
|
||||
}
|
||||
throw new Error("Custom settings pages require Obsidian 1.13.0 or later");
|
||||
}
|
||||
|
||||
private createCustomSettingPage(entry: SettingsPageEntry): SettingPage {
|
||||
if (requireApiVersion("1.13.0") && typeof SettingPage === "function") {
|
||||
const renderCustomPage = this.renderCustomPage.bind(this);
|
||||
const disposeRenderScope = this.disposeRenderScope.bind(this);
|
||||
return new (class extends SettingPage {
|
||||
private scope?: Component;
|
||||
override title = entry.name();
|
||||
|
||||
override display(): void {
|
||||
this.scope = renderCustomPage(this, entry);
|
||||
}
|
||||
|
||||
override hide(): void {
|
||||
disposeRenderScope(this.scope);
|
||||
this.scope = undefined;
|
||||
super.hide();
|
||||
}
|
||||
})();
|
||||
}
|
||||
throw new Error("Custom settings pages require Obsidian 1.13.0 or later");
|
||||
}
|
||||
|
||||
private createDeclarativePage(entry: SettingsPageEntry): SettingDefinitionPage {
|
||||
const page: SettingDefinitionPage = {
|
||||
type: "page",
|
||||
name: `${entry.icon} ${entry.name()}`,
|
||||
visible: () => this.isPageVisible(entry.level),
|
||||
};
|
||||
if (entry.content === "native") {
|
||||
page.items = [
|
||||
this.createRebuildRequiredAction(),
|
||||
...createAdvancedSettingDefinitionGroups({
|
||||
isCouchDB: () => this.isConfiguredAs("remoteType", REMOTE_COUCHDB),
|
||||
}),
|
||||
];
|
||||
} else {
|
||||
page.page = () => this.createCustomSettingPage(entry);
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
private createGeneralSettingsGroup(): SettingDefinitionGroup {
|
||||
const groups = createGeneralSettingDefinitionGroups({
|
||||
showEditorStatusDetails: () => this.isConfiguredAs("showStatusOnEditor", true),
|
||||
showVerboseLog: () => this.isConfiguredAs("lessInformationInLog", false),
|
||||
});
|
||||
const [appearance, logging] = groups;
|
||||
if (!appearance || !logging) {
|
||||
throw new Error("General settings must define Appearance and Logging groups");
|
||||
}
|
||||
return this.createRootGroup("general-settings", [
|
||||
{
|
||||
type: "page",
|
||||
name: `🎨 ${appearance.heading}`,
|
||||
items: appearance.items,
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
name: `📝 ${logging.heading}`,
|
||||
items: logging.items,
|
||||
},
|
||||
this.createExtraMenusPage(),
|
||||
]);
|
||||
}
|
||||
|
||||
private createExtraMenusPage(): SettingDefinitionPage {
|
||||
return {
|
||||
type: "page",
|
||||
name: `🎚️ ${$msg("obsidianLiveSyncSettingTab.titleExtraMenus")}`,
|
||||
items: createExtraMenuSettingDefinitions(),
|
||||
};
|
||||
}
|
||||
|
||||
private createQuickSetupGroup(): SettingDefinitionGroup {
|
||||
return this.createRootGroup("quick-setup", [
|
||||
{
|
||||
name: $msg("obsidianLiveSyncSettingTab.nameConnectSetupURI"),
|
||||
desc: $msg("obsidianLiveSyncSettingTab.descConnectSetupURI"),
|
||||
action: () => this.requestOpenSetupURI(),
|
||||
},
|
||||
{
|
||||
name: $msg("Rerun Onboarding Wizard"),
|
||||
desc: $msg("Rerun the onboarding wizard to set up Self-hosted LiveSync again."),
|
||||
action: () => fireAndForget(async () => await this.rerunOnboardingWizard()),
|
||||
},
|
||||
{
|
||||
name: $msg("obsidianLiveSyncSettingTab.nameEnableLiveSync"),
|
||||
desc: $msg("obsidianLiveSyncSettingTab.descEnableLiveSync"),
|
||||
visible: () => !this.isConfiguredAs("isConfigured", true),
|
||||
action: () => fireAndForget(async () => await this.enableLiveSyncFromSettings()),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
private createRootGroup(
|
||||
id: SettingsRootGroupId,
|
||||
items: SettingGroupItem[],
|
||||
visible?: () => boolean
|
||||
): SettingDefinitionGroup {
|
||||
const { icon, name } = getSettingsRootGroupEntry(id);
|
||||
return {
|
||||
type: "group",
|
||||
heading: `${icon} ${name()}`,
|
||||
items,
|
||||
...(visible ? { visible } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
private createSetupOtherDevicesGroup(): SettingDefinitionGroup {
|
||||
return this.createRootGroup(
|
||||
"setup-other-devices",
|
||||
[
|
||||
{
|
||||
name: $msg("obsidianLiveSyncSettingTab.nameCopySetupURI"),
|
||||
desc: $msg("obsidianLiveSyncSettingTab.descCopySetupURI"),
|
||||
action: () => this.requestCopySetupURI(),
|
||||
},
|
||||
{
|
||||
name: $msg("Setup.ShowQRCode"),
|
||||
desc: $msg("Setup.ShowQRCode.Desc"),
|
||||
action: () => this.requestShowSetupQRCode(),
|
||||
},
|
||||
],
|
||||
() => this.isConfiguredAs("isConfigured", true)
|
||||
);
|
||||
}
|
||||
|
||||
override getSettingDefinitions(): SettingDefinitionItem[] {
|
||||
if (!this.supportsDeclarativeSettings()) {
|
||||
return [];
|
||||
}
|
||||
const catalogue = createSettingsPageCatalogue();
|
||||
const getPage = (id: string): SettingDefinitionPage => {
|
||||
const entry = catalogue.find((candidate) => candidate.id === id);
|
||||
if (!entry) {
|
||||
throw new Error(`Unknown settings page: ${id}`);
|
||||
}
|
||||
return this.createDeclarativePage(entry);
|
||||
};
|
||||
const synchronisation = this.createRootGroup("synchronisation", [
|
||||
getPage("remote-configuration"),
|
||||
getPage("synchronisation"),
|
||||
]);
|
||||
const generalSettings = this.createGeneralSettingsGroup();
|
||||
const quickSetup = this.createQuickSetupGroup();
|
||||
const setupOtherDevices = this.createSetupOtherDevicesGroup();
|
||||
const maintenance = this.createRootGroup("maintenance-and-recovery", [
|
||||
getPage("maintenance"),
|
||||
getPage("hatch"),
|
||||
]);
|
||||
const extraFeatures = this.createRootGroup(
|
||||
"extra-features",
|
||||
[getPage("selector"), getPage("customisation-sync")],
|
||||
() => this.isPageVisible(LEVEL_ADVANCED)
|
||||
);
|
||||
const advancedSettings = this.createRootGroup(
|
||||
"advanced-settings",
|
||||
[getPage("advanced"), getPage("power-users"), getPage("patches")],
|
||||
() =>
|
||||
this.isPageVisible(LEVEL_ADVANCED) ||
|
||||
this.isPageVisible(LEVEL_POWER_USER) ||
|
||||
this.isPageVisible(LEVEL_EDGE_CASE)
|
||||
);
|
||||
const helpAndInformation = this.createRootGroup("help-and-information", [
|
||||
getPage("help"),
|
||||
getPage("change-log"),
|
||||
]);
|
||||
const laterGroups = [setupOtherDevices, maintenance, extraFeatures, advancedSettings, helpAndInformation];
|
||||
|
||||
const pendingInitialisation = this.createRebuildRequiredAction();
|
||||
if (this.isAnySyncEnabled()) {
|
||||
return [pendingInitialisation, synchronisation, generalSettings, quickSetup, ...laterGroups];
|
||||
}
|
||||
return [pendingInitialisation, quickSetup, synchronisation, generalSettings, ...laterGroups];
|
||||
}
|
||||
|
||||
private beginRenderScope(refresh: () => void): Component {
|
||||
this.disposeRenderScope();
|
||||
const component = new Component();
|
||||
this._lifetimeComponent = component;
|
||||
this.activePageRefresh = refresh;
|
||||
this.settingComponents.length = 0;
|
||||
this.controlledElementFunc.length = 0;
|
||||
component.load();
|
||||
return component;
|
||||
}
|
||||
|
||||
private disposeRenderScope(owner?: Component): void {
|
||||
if (owner && this._lifetimeComponent !== owner) {
|
||||
return;
|
||||
}
|
||||
this._lifetimeComponent?.unload();
|
||||
this._lifetimeComponent = undefined;
|
||||
this.activePageRefresh = undefined;
|
||||
this.settingComponents.length = 0;
|
||||
this.controlledElementFunc.length = 0;
|
||||
}
|
||||
|
||||
enableOnlySyncDisabled = enableOnly(() => !this.isAnySyncEnabled());
|
||||
|
||||
onlyOnP2POrCouchDB = () =>
|
||||
@@ -600,61 +1032,66 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
Logger(`Passphrase is not valid, please fix it.`, LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
const OPTION_FETCH = $msg("obsidianLiveSyncSettingTab.optionFetchFromRemote");
|
||||
const OPTION_REBUILD_BOTH = $msg("obsidianLiveSyncSettingTab.optionRebuildBoth");
|
||||
const OPTION_ONLY_SETTING = $msg("obsidianLiveSyncSettingTab.optionSaveOnlySettings");
|
||||
const OPTION_CANCEL = $msg("obsidianLiveSyncSettingTab.optionCancel");
|
||||
const title = $msg("obsidianLiveSyncSettingTab.titleRebuildRequired");
|
||||
const note = $msg("obsidianLiveSyncSettingTab.msgRebuildRequired", {
|
||||
OPTION_REBUILD_BOTH,
|
||||
OPTION_FETCH,
|
||||
OPTION_ONLY_SETTING,
|
||||
const keepEditing = $msg("Ui.SetupWizard.ApplySettingsInitialisation.KeepEditing");
|
||||
const setupManager = this.core.getModule(SetupManager);
|
||||
const result = await setupManager.applySettingsWithInitialisationChoice({
|
||||
isP2P: isP2PMainRemote(this.editingSettings),
|
||||
validateChoice: async (mode) => {
|
||||
if (mode !== "fetch" || (await this.checkWorkingPassphrase())) {
|
||||
return true;
|
||||
}
|
||||
const continueFetch = $msg("Ui.SetupWizard.ApplySettingsInitialisation.ContinueFetch");
|
||||
return (
|
||||
(await this.core.confirm.confirmWithMessage(
|
||||
$msg("Ui.SetupWizard.ApplySettingsInitialisation.RemoteVerificationTitle"),
|
||||
$msg("Ui.SetupWizard.ApplySettingsInitialisation.RemoteVerificationGuidance"),
|
||||
[continueFetch, keepEditing],
|
||||
keepEditing
|
||||
)) === continueFetch
|
||||
);
|
||||
},
|
||||
applySettings: async () => {
|
||||
if (!this.editingSettings.encrypt) {
|
||||
this.editingSettings.passphrase = "";
|
||||
}
|
||||
await this.saveAllDirtySettings();
|
||||
},
|
||||
});
|
||||
const buttons = [
|
||||
OPTION_FETCH,
|
||||
OPTION_REBUILD_BOTH, // OPTION_REBUILD_REMOTE,
|
||||
OPTION_ONLY_SETTING,
|
||||
OPTION_CANCEL,
|
||||
];
|
||||
const result = await this.core.confirm.confirmWithMessage(title, note, buttons, OPTION_CANCEL);
|
||||
if (result == OPTION_CANCEL) return;
|
||||
if (result == OPTION_FETCH) {
|
||||
if (!(await this.checkWorkingPassphrase())) {
|
||||
if (
|
||||
(await this.core.confirm.askYesNoDialog($msg("obsidianLiveSyncSettingTab.msgAreYouSureProceed"), {
|
||||
defaultOption: "No",
|
||||
})) != "yes"
|
||||
)
|
||||
return;
|
||||
if (result.result === "scheduled") {
|
||||
this.closeSetting();
|
||||
return;
|
||||
}
|
||||
if (result.result === "failed") {
|
||||
return;
|
||||
}
|
||||
|
||||
const applyWithoutInitialisation = $msg(
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.ApplyWithoutInitialisation"
|
||||
);
|
||||
const fallback = await this.core.confirm.confirmWithMessage(
|
||||
$msg("Ui.SetupWizard.ApplySettingsInitialisation.BypassTitle"),
|
||||
$msg("Ui.SetupWizard.ApplySettingsInitialisation.BypassGuidance"),
|
||||
[applyWithoutInitialisation, keepEditing],
|
||||
keepEditing
|
||||
);
|
||||
if (fallback === applyWithoutInitialisation) {
|
||||
if (!this.editingSettings.encrypt) {
|
||||
this.editingSettings.passphrase = "";
|
||||
}
|
||||
}
|
||||
if (!this.editingSettings.encrypt) {
|
||||
this.editingSettings.passphrase = "";
|
||||
}
|
||||
await this.saveAllDirtySettings();
|
||||
await Promise.resolve(this.applyAllSettings());
|
||||
if (result == OPTION_FETCH) {
|
||||
await this.core.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, "");
|
||||
this.services.appLifecycle.scheduleRestart();
|
||||
this.closeSetting();
|
||||
// await rebuildDB("localOnly");
|
||||
} else if (result == OPTION_REBUILD_BOTH) {
|
||||
await this.core.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, "");
|
||||
this.services.appLifecycle.scheduleRestart();
|
||||
this.closeSetting();
|
||||
} else if (result == OPTION_ONLY_SETTING) {
|
||||
await this.services.setting.saveSettingData();
|
||||
await this.saveAllDirtySettings();
|
||||
}
|
||||
}
|
||||
|
||||
// The imperative renderer remains required by the declared Obsidian 1.7.2 minimum version.
|
||||
override display(): void {
|
||||
this.displayImperative();
|
||||
}
|
||||
|
||||
private displayImperative(): void {
|
||||
const changeDisplay = this.changeDisplay.bind(this);
|
||||
// Make sure lifetime component is loaded for markdown rendering in panes.
|
||||
this._lifetimeComponent.load();
|
||||
// Make sure the page-owned component is loaded for markdown rendering in panes.
|
||||
this.beginRenderScope(() => this.displayImperative());
|
||||
const { containerEl } = this;
|
||||
this.settingComponents.length = 0;
|
||||
this.controlledElementFunc.length = 0;
|
||||
this.onSavedHandlers.length = 0;
|
||||
this.screenElements = {};
|
||||
if (this._editingSettings == undefined || this.initialSettings == undefined) {
|
||||
this.reloadAllSettings();
|
||||
@@ -667,7 +1104,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.addClass("sls-setting");
|
||||
containerEl.removeClass("isWizard");
|
||||
|
||||
setStyle(containerEl, "menu-setting-poweruser", () => this.isConfiguredAs("usePowerUserMode", true));
|
||||
setStyle(containerEl, "menu-setting-advanced", () => this.isConfiguredAs("useAdvancedMode", true));
|
||||
@@ -683,87 +1119,38 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
this.menuEl.addClass("sls-setting-menu");
|
||||
const menuTabs = this.menuEl.querySelectorAll(".sls-setting-label");
|
||||
|
||||
this.createEl(
|
||||
menuWrapper,
|
||||
"div",
|
||||
{ cls: "sls-setting-menu-buttons" },
|
||||
(el) => {
|
||||
el.addClass("wizardHidden");
|
||||
el.createEl("label", { text: $msg("obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied") });
|
||||
void this.addEl(
|
||||
el,
|
||||
"button",
|
||||
{ text: $msg("obsidianLiveSyncSettingTab.optionApply"), cls: "mod-warning" },
|
||||
(buttonEl) => {
|
||||
buttonEl.addEventListener("click", () =>
|
||||
fireAndForget(async () => await this.confirmRebuild())
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
visibleOnly(() => this.isNeedRebuildLocal() || this.isNeedRebuildRemote())
|
||||
);
|
||||
this.renderRebuildRequiredAction(menuWrapper);
|
||||
|
||||
// let paneNo = 0;
|
||||
const addPane = (
|
||||
parentEl: HTMLElement,
|
||||
title: string,
|
||||
icon: string,
|
||||
order: number,
|
||||
wizardHidden: boolean,
|
||||
level?: ConfigLevel
|
||||
) => {
|
||||
const addPane = (parentEl: HTMLElement, title: string, icon: string, order: number, level?: ConfigLevel) => {
|
||||
const owner = this.lifetimeComponent;
|
||||
const el = this.createEl(parentEl, "div", { text: "" });
|
||||
|
||||
setLevelClass(el, level);
|
||||
new Setting(el).setName(title).setHeading().setClass("sls-setting-pane-title");
|
||||
if (this.menuEl) {
|
||||
this.menuEl.createEl(
|
||||
"label",
|
||||
{ cls: `sls-setting-label c-${order} ${wizardHidden ? "wizardHidden" : ""}` },
|
||||
(el) => {
|
||||
setLevelClass(el, level);
|
||||
const inputEl = el.createEl("input", {
|
||||
type: "radio",
|
||||
name: "disp",
|
||||
value: `${order}`,
|
||||
cls: "sls-setting-tab",
|
||||
} as DomElementInfo);
|
||||
el.createDiv({
|
||||
cls: "sls-setting-menu-btn",
|
||||
text: icon,
|
||||
title: title,
|
||||
});
|
||||
inputEl.addEventListener("change", (evt) => this.selectPane(evt));
|
||||
inputEl.addEventListener("click", (evt) => this.selectPane(evt));
|
||||
}
|
||||
);
|
||||
this.menuEl.createEl("label", { cls: `sls-setting-label c-${order}` }, (el) => {
|
||||
setLevelClass(el, level);
|
||||
const inputEl = el.createEl("input", {
|
||||
type: "radio",
|
||||
name: "disp",
|
||||
value: `${order}`,
|
||||
cls: "sls-setting-tab",
|
||||
} as DomElementInfo);
|
||||
el.createDiv({
|
||||
cls: "sls-setting-menu-btn",
|
||||
text: icon,
|
||||
title: title,
|
||||
});
|
||||
inputEl.addEventListener("change", (evt) => this.selectPane(evt));
|
||||
inputEl.addEventListener("click", (evt) => this.selectPane(evt));
|
||||
});
|
||||
}
|
||||
this.addScreenElement(`${order}`, el);
|
||||
const p = Promise.resolve(el);
|
||||
// fireAndForget
|
||||
// p.finally(() => {
|
||||
// // Recap at the end.
|
||||
// });
|
||||
return p;
|
||||
return this.resolveWithinRenderScope(el, owner);
|
||||
};
|
||||
// const panelNoMap = {} as { [key: string]: number };
|
||||
const addPanel = (
|
||||
parentEl: HTMLElement,
|
||||
title: string,
|
||||
callback?: (el: HTMLDivElement) => void,
|
||||
func?: OnUpdateFunc,
|
||||
level?: ConfigLevel
|
||||
) => {
|
||||
const el = this.createEl(parentEl, "div", { text: "" }, callback, func);
|
||||
setLevelClass(el, level);
|
||||
this.createEl(el, "h4", { text: title, cls: "sls-setting-panel-title" });
|
||||
const p = Promise.resolve(el);
|
||||
// p.finally(() => {
|
||||
// // Recap at the end.
|
||||
// })
|
||||
return p;
|
||||
};
|
||||
const addPanel = this.addPanel.bind(this);
|
||||
|
||||
menuTabs.forEach((element) => {
|
||||
const e = element.querySelector(".sls-setting-tab");
|
||||
@@ -791,34 +1178,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
// Add panes
|
||||
|
||||
// TODO: Refactor to new API style.
|
||||
void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.panelChangeLog"), "💬", 100, false).then(
|
||||
bindPane(paneChangeLog)
|
||||
);
|
||||
void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.panelSetup"), "🧙♂️", 110, false).then(
|
||||
bindPane(paneSetup)
|
||||
);
|
||||
void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.panelGeneralSettings"), "⚙️", 20, false).then(
|
||||
bindPane(paneGeneral)
|
||||
);
|
||||
void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.panelRemoteConfiguration"), "🛰️", 0, false).then(
|
||||
bindPane(paneRemoteConfig)
|
||||
);
|
||||
void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.titleSyncSettings"), "🔄", 30, false).then(
|
||||
bindPane(paneSyncSettings)
|
||||
);
|
||||
void addPane(containerEl, "Selector", "🚦", 33, false, LEVEL_ADVANCED).then(bindPane(paneSelector));
|
||||
void addPane(containerEl, "Customization sync", "🔌", 60, false, LEVEL_ADVANCED).then(
|
||||
bindPane(paneCustomisationSync)
|
||||
);
|
||||
|
||||
void addPane(containerEl, "Hatch", "🧰", 50, true).then(bindPane(paneHatch));
|
||||
void addPane(containerEl, "Advanced", "🔧", 46, false, LEVEL_ADVANCED).then(bindPane(paneAdvanced));
|
||||
void addPane(containerEl, "Power users", "💪", 47, true, LEVEL_POWER_USER).then(bindPane(panePowerUsers));
|
||||
|
||||
void addPane(containerEl, "Patches", "🩹", 51, false, LEVEL_EDGE_CASE).then(bindPane(panePatches));
|
||||
|
||||
void addPane(containerEl, "Maintenance", "🎛️", 70, true).then(bindPane(paneMaintenance));
|
||||
for (const entry of createSettingsPageCatalogue()) {
|
||||
void addPane(containerEl, entry.name(), entry.icon, entry.order, entry.level).then(bindPane(entry.legacy));
|
||||
}
|
||||
|
||||
void yieldNextAnimationFrame().then(() => {
|
||||
if (this.selectedScreen == "") {
|
||||
|
||||
@@ -1,20 +1,43 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_SETTINGS, REMOTE_COUCHDB } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
|
||||
const negotiationMocks = vi.hoisted(() => ({
|
||||
checkSyncInfo: vi.fn(async () => true),
|
||||
}));
|
||||
const settingsInitialisationMocks = vi.hoisted(() => ({
|
||||
applySettingsWithInitialisationChoice: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/deps.ts", () => ({
|
||||
App: class {},
|
||||
Component: class {},
|
||||
Component: class {
|
||||
load = vi.fn();
|
||||
unload = vi.fn();
|
||||
register = vi.fn();
|
||||
},
|
||||
PluginSettingTab: class {},
|
||||
SettingPage: undefined,
|
||||
requireApiVersion: vi.fn(() => false),
|
||||
}));
|
||||
vi.mock("@/main.ts", () => ({ default: class {} }));
|
||||
vi.mock("@/common/events.ts", () => ({
|
||||
EVENT_REQUEST_RELOAD_SETTING_TAB: "request-reload-setting-tab",
|
||||
eventHub: { onEvent: vi.fn() },
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions", () => ({
|
||||
getLanguage: vi.fn(() => "en"),
|
||||
compatGlobal: {
|
||||
localStorage: {
|
||||
getItem: vi.fn(() => null),
|
||||
setItem: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("@/common/events.ts", () => ({
|
||||
EVENT_ON_UNRESOLVED_ERROR: "on-unresolved-error",
|
||||
EVENT_REQUEST_COPY_SETUP_URI: "request-copy-setup-uri",
|
||||
EVENT_REQUEST_OPEN_SETUP_URI: "request-open-setup-uri",
|
||||
EVENT_REQUEST_RELOAD_SETTING_TAB: "request-reload-setting-tab",
|
||||
EVENT_REQUEST_SHOW_SETUP_QR: "request-show-setup-qr",
|
||||
eventHub: { emitEvent: vi.fn(), onEvent: vi.fn() },
|
||||
}));
|
||||
vi.mock("@/modules/features/SetupManager.ts", () => ({ SetupManager: class {} }));
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/pouchdb/negotiation", () => negotiationMocks);
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator", () => ({
|
||||
LiveSyncCouchDBReplicator: class {},
|
||||
@@ -27,7 +50,8 @@ vi.mock("./SettingPane.ts", () => ({
|
||||
visibleOnly: vi.fn(() => vi.fn()),
|
||||
}));
|
||||
vi.mock("./PaneChangeLog.ts", () => ({ paneChangeLog: vi.fn() }));
|
||||
vi.mock("./PaneSetup.ts", () => ({ paneSetup: vi.fn() }));
|
||||
vi.mock("./PaneQuickSetup.ts", () => ({ paneQuickSetup: vi.fn() }));
|
||||
vi.mock("./PaneHelp.ts", () => ({ paneHelp: vi.fn() }));
|
||||
vi.mock("./PaneGeneral.ts", () => ({ paneGeneral: vi.fn() }));
|
||||
vi.mock("./PaneRemoteConfig.ts", () => ({ paneRemoteConfig: vi.fn() }));
|
||||
vi.mock("./PaneSelector.ts", () => ({ paneSelector: vi.fn() }));
|
||||
@@ -42,6 +66,10 @@ vi.mock("./PaneMaintenance.ts", () => ({ paneMaintenance: vi.fn() }));
|
||||
import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||
|
||||
beforeEach(() => {
|
||||
settingsInitialisationMocks.applySettingsWithInitialisationChoice.mockReset();
|
||||
});
|
||||
|
||||
describe("ObsidianLiveSyncSettingTab passphrase verification", () => {
|
||||
it("closes the finite remote connection after checking synchronisation information", async () => {
|
||||
const remoteDatabase = {
|
||||
@@ -73,3 +101,211 @@ describe("ObsidianLiveSyncSettingTab passphrase verification", () => {
|
||||
expect(remoteDatabase.close).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ObsidianLiveSyncSettingTab pending-setting initialisation", () => {
|
||||
function createSettingsTab() {
|
||||
const saveSettingData = vi.fn(async () => undefined);
|
||||
const confirmWithMessage = vi.fn();
|
||||
const plugin = {
|
||||
app: {},
|
||||
core: {
|
||||
settings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
handleFilenameCaseSensitive: false,
|
||||
},
|
||||
getModule: vi.fn(() => settingsInitialisationMocks),
|
||||
confirm: {
|
||||
confirmWithMessage,
|
||||
},
|
||||
services: {
|
||||
setting: {
|
||||
saveSettingData,
|
||||
getDeviceAndVaultName: vi.fn(() => ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const tab = new ObsidianLiveSyncSettingTab({} as never, plugin as never);
|
||||
Object.assign(tab, {
|
||||
_editingSettings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
handleFilenameCaseSensitive: true,
|
||||
},
|
||||
initialSettings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
handleFilenameCaseSensitive: false,
|
||||
},
|
||||
});
|
||||
vi.spyOn(tab, "isPassphraseValid").mockResolvedValue(true);
|
||||
vi.spyOn(tab, "checkWorkingPassphrase").mockResolvedValue(true);
|
||||
const closeSetting = vi.spyOn(tab, "closeSetting").mockImplementation(() => undefined);
|
||||
return { tab, saveSettingData, confirmWithMessage, closeSetting };
|
||||
}
|
||||
|
||||
it("keeps pending settings in the editing buffer when initialisation and the fallback are cancelled", async () => {
|
||||
const { tab, saveSettingData, confirmWithMessage, closeSetting } = createSettingsTab();
|
||||
settingsInitialisationMocks.applySettingsWithInitialisationChoice.mockResolvedValueOnce({
|
||||
result: "cancelled",
|
||||
});
|
||||
confirmWithMessage.mockResolvedValueOnce("Keep Editing");
|
||||
|
||||
await tab.confirmRebuild();
|
||||
|
||||
expect(settingsInitialisationMocks.applySettingsWithInitialisationChoice).toHaveBeenCalledOnce();
|
||||
expect(confirmWithMessage).toHaveBeenCalledWith(
|
||||
"Apply Settings without Initialisation?",
|
||||
expect.any(String),
|
||||
["Apply without Initialisation", "Keep Editing"],
|
||||
"Keep Editing"
|
||||
);
|
||||
expect(saveSettingData).not.toHaveBeenCalled();
|
||||
expect(tab.editingSettings.handleFilenameCaseSensitive).toBe(true);
|
||||
expect(tab.core.settings.handleFilenameCaseSensitive).toBe(false);
|
||||
expect(closeSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("applies pending settings only after a separately confirmed initialisation bypass", async () => {
|
||||
const { tab, saveSettingData, confirmWithMessage, closeSetting } = createSettingsTab();
|
||||
settingsInitialisationMocks.applySettingsWithInitialisationChoice.mockResolvedValueOnce({
|
||||
result: "cancelled",
|
||||
});
|
||||
confirmWithMessage.mockResolvedValueOnce("Apply without Initialisation");
|
||||
|
||||
await tab.confirmRebuild();
|
||||
|
||||
expect(settingsInitialisationMocks.applySettingsWithInitialisationChoice).toHaveBeenCalledOnce();
|
||||
expect(saveSettingData).toHaveBeenCalledOnce();
|
||||
expect(tab.core.settings.handleFilenameCaseSensitive).toBe(true);
|
||||
expect(closeSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("closes settings only after initialisation has been scheduled", async () => {
|
||||
const { tab, saveSettingData, confirmWithMessage, closeSetting } = createSettingsTab();
|
||||
settingsInitialisationMocks.applySettingsWithInitialisationChoice.mockImplementationOnce(
|
||||
async ({ applySettings }: { applySettings: () => Promise<void> }) => {
|
||||
await applySettings();
|
||||
return { result: "scheduled", mode: "rebuild" };
|
||||
}
|
||||
);
|
||||
|
||||
await tab.confirmRebuild();
|
||||
|
||||
expect(saveSettingData).toHaveBeenCalledOnce();
|
||||
expect(confirmWithMessage).not.toHaveBeenCalled();
|
||||
expect(closeSetting).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not offer the settings-only fallback after an initialisation failure", async () => {
|
||||
const { tab, saveSettingData, confirmWithMessage, closeSetting } = createSettingsTab();
|
||||
settingsInitialisationMocks.applySettingsWithInitialisationChoice.mockResolvedValueOnce({
|
||||
result: "failed",
|
||||
mode: "fetch",
|
||||
});
|
||||
|
||||
await tab.confirmRebuild();
|
||||
|
||||
expect(saveSettingData).not.toHaveBeenCalled();
|
||||
expect(confirmWithMessage).not.toHaveBeenCalled();
|
||||
expect(tab.editingSettings.handleFilenameCaseSensitive).toBe(true);
|
||||
expect(tab.core.settings.handleFilenameCaseSensitive).toBe(false);
|
||||
expect(closeSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ObsidianLiveSyncSettingTab declarative settings boundary", () => {
|
||||
function createSettingsTab() {
|
||||
const saveSettingData = vi.fn(async () => undefined);
|
||||
const plugin = {
|
||||
app: {},
|
||||
core: {
|
||||
settings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
hashCacheMaxCount: 300,
|
||||
displayLanguage: "",
|
||||
},
|
||||
services: {
|
||||
setting: {
|
||||
saveSettingData,
|
||||
getDeviceAndVaultName: vi.fn(() => ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
Object.defineProperty(plugin, "settings", {
|
||||
get: () => {
|
||||
throw new Error("The declarative adapter must not use plugin.settings");
|
||||
},
|
||||
});
|
||||
const tab = new ObsidianLiveSyncSettingTab({} as never, plugin as never);
|
||||
Object.assign(tab, {
|
||||
_editingSettings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
hashCacheMaxCount: 300,
|
||||
displayLanguage: "",
|
||||
},
|
||||
initialSettings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
hashCacheMaxCount: 300,
|
||||
displayLanguage: "",
|
||||
},
|
||||
});
|
||||
return { tab, saveSettingData };
|
||||
}
|
||||
|
||||
it("loads the imperative fallback without a SettingPage runtime export", () => {
|
||||
const { tab } = createSettingsTab();
|
||||
|
||||
expect(tab.display).toBeTypeOf("function");
|
||||
expect(tab.getSettingDefinitions()).toEqual([]);
|
||||
});
|
||||
|
||||
it("reads and writes registered controls through the editing buffer and existing save owner", async () => {
|
||||
const { tab } = createSettingsTab();
|
||||
const saveSettings = vi.spyOn(tab, "saveSettings").mockResolvedValue(undefined);
|
||||
|
||||
expect(tab.getControlValue("hashCacheMaxCount")).toBe(300);
|
||||
|
||||
await tab.setControlValue("hashCacheMaxCount", 321);
|
||||
|
||||
expect(tab.editingSettings.hashCacheMaxCount).toBe(321);
|
||||
expect(saveSettings).toHaveBeenCalledOnce();
|
||||
expect(saveSettings).toHaveBeenCalledWith(["hashCacheMaxCount"]);
|
||||
});
|
||||
|
||||
it("rejects unregistered declarative control keys", async () => {
|
||||
const { tab } = createSettingsTab();
|
||||
|
||||
expect(() => tab.getControlValue("couchDB_PASSWORD")).toThrow(/Unknown declarative setting key/u);
|
||||
await expect(tab.setControlValue("couchDB_PASSWORD", "secret")).rejects.toThrow(
|
||||
/Unknown declarative setting key/u
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects declarative values outside the registered control contract", async () => {
|
||||
const { tab } = createSettingsTab();
|
||||
const saveSettings = vi.spyOn(tab, "saveSettings").mockResolvedValue(undefined);
|
||||
|
||||
await expect(tab.setControlValue("hashCacheMaxCount", 9)).rejects.toThrow(
|
||||
/Invalid value for declarative setting/u
|
||||
);
|
||||
await expect(tab.setControlValue("chunkSplitterVersion", "unknown-splitter")).rejects.toThrow(
|
||||
/Invalid value for declarative setting/u
|
||||
);
|
||||
|
||||
expect(saveSettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("replaces a saved-setting handler when a page is rendered again", async () => {
|
||||
const { tab } = createSettingsTab();
|
||||
const first = vi.fn();
|
||||
const replacement = vi.fn();
|
||||
tab.addOnSaved("displayLanguage", first);
|
||||
tab.addOnSaved("displayLanguage", replacement);
|
||||
tab.editingSettings.displayLanguage = "ja";
|
||||
|
||||
await tab.saveSettings(["displayLanguage"]);
|
||||
|
||||
expect(first).not.toHaveBeenCalled();
|
||||
expect(replacement).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,53 +1,18 @@
|
||||
import { ChunkAlgorithmNames } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import type { PageFunctions } from "./SettingPane.ts";
|
||||
import { createAdvancedSettingSpecGroups } from "./AdvancedSettingSpecs.ts";
|
||||
import { renderLegacySettingSpec } from "./SettingSpec.ts";
|
||||
|
||||
export function paneAdvanced(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void {
|
||||
void addPanel(paneEl, "Memory cache").then((paneEl) => {
|
||||
new Setting(paneEl).autoWireNumeric("hashCacheMaxCount", { clampMin: 10 });
|
||||
// new Setting(paneEl).autoWireNumeric("hashCacheMaxAmount", { clampMin: 1 });
|
||||
const groups = createAdvancedSettingSpecGroups({
|
||||
isCouchDB: () => this.onlyOnCouchDB().visibility !== false,
|
||||
});
|
||||
void addPanel(paneEl, "Local Database Tweak").then((paneEl) => {
|
||||
paneEl.addClass("wizardHidden");
|
||||
|
||||
const items = ChunkAlgorithmNames;
|
||||
new Setting(paneEl).autoWireDropDown("chunkSplitterVersion", {
|
||||
options: items,
|
||||
for (const group of groups) {
|
||||
void addPanel(paneEl, group.heading).then((panelEl) => {
|
||||
for (const spec of group.items) {
|
||||
renderLegacySettingSpec(new Setting(panelEl), spec);
|
||||
}
|
||||
});
|
||||
new Setting(paneEl).autoWireNumeric("customChunkSize", { clampMin: 0, acceptZero: true });
|
||||
});
|
||||
|
||||
void addPanel(paneEl, "Transfer Tweak").then((paneEl) => {
|
||||
new Setting(paneEl)
|
||||
.setClass("wizardHidden")
|
||||
.autoWireToggle("readChunksOnline", { onUpdate: this.onlyOnCouchDB });
|
||||
new Setting(paneEl)
|
||||
.setClass("wizardHidden")
|
||||
.autoWireToggle("useOnlyLocalChunk", { onUpdate: this.onlyOnCouchDB });
|
||||
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("concurrencyOfReadChunksOnline", {
|
||||
clampMin: 10,
|
||||
onUpdate: this.onlyOnCouchDB,
|
||||
});
|
||||
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("minimumIntervalOfReadChunksOnline", {
|
||||
clampMin: 10,
|
||||
onUpdate: this.onlyOnCouchDB,
|
||||
});
|
||||
new Setting(paneEl)
|
||||
.setClass("wizardHidden")
|
||||
.autoWireToggle("autoAcceptCompatibleTweak", { defaultToggleValue: true });
|
||||
// new Setting(paneEl)
|
||||
// .setClass("wizardHidden")
|
||||
// .autoWireToggle("sendChunksBulk", { onUpdate: onlyOnCouchDB })
|
||||
// new Setting(paneEl)
|
||||
// .setClass("wizardHidden")
|
||||
// .autoWireNumeric("sendChunksBulkMaxSize", {
|
||||
// clampMax: 100, clampMin: 1, onUpdate: onlyOnCouchDB
|
||||
// })
|
||||
});
|
||||
void addPanel(paneEl, "Remote Database Tweak").then((paneEl) => {
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("enableCompression");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { paneAdvanced } from "./PaneAdvanced.ts";
|
||||
|
||||
const settingHarness = vi.hoisted(() => ({
|
||||
createdIn: [] as HTMLElement[],
|
||||
rendered: [] as unknown[],
|
||||
}));
|
||||
|
||||
vi.mock("./LiveSyncSetting.ts", () => ({
|
||||
LiveSyncSetting: class LiveSyncSetting {
|
||||
constructor(containerEl: HTMLElement) {
|
||||
settingHarness.createdIn.push(containerEl);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./SettingSpec.ts", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import("./SettingSpec.ts")>();
|
||||
return {
|
||||
...original,
|
||||
renderLegacySettingSpec: vi.fn((_renderer: unknown, spec: unknown) => {
|
||||
settingHarness.rendered.push(spec);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
settingHarness.createdIn.length = 0;
|
||||
settingHarness.rendered.length = 0;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("paneAdvanced", () => {
|
||||
it("renders the shared specifications into the four existing panels", async () => {
|
||||
const panelElements = new Map<string, HTMLElement>();
|
||||
const addPanel = vi.fn((_parent: HTMLElement, heading: string) => {
|
||||
const panel = { heading } as unknown as HTMLElement;
|
||||
panelElements.set(heading, panel);
|
||||
return Promise.resolve(panel);
|
||||
});
|
||||
const host = {
|
||||
onlyOnCouchDB: vi.fn(() => ({ visibility: false })),
|
||||
};
|
||||
|
||||
paneAdvanced.call(host as never, {} as HTMLElement, { addPanel } as never);
|
||||
await vi.waitFor(() => expect(settingHarness.rendered).toHaveLength(9));
|
||||
|
||||
expect(addPanel.mock.calls.map(([, heading]) => heading)).toEqual([
|
||||
"Memory cache",
|
||||
"Local Database Tweak",
|
||||
"Transfer Tweak",
|
||||
"Remote Database Tweak",
|
||||
]);
|
||||
expect(settingHarness.createdIn).toEqual([
|
||||
panelElements.get("Memory cache"),
|
||||
panelElements.get("Local Database Tweak"),
|
||||
panelElements.get("Local Database Tweak"),
|
||||
panelElements.get("Transfer Tweak"),
|
||||
panelElements.get("Transfer Tweak"),
|
||||
panelElements.get("Transfer Tweak"),
|
||||
panelElements.get("Transfer Tweak"),
|
||||
panelElements.get("Transfer Tweak"),
|
||||
panelElements.get("Remote Database Tweak"),
|
||||
]);
|
||||
expect(settingHarness.rendered.map((spec) => (spec as { key: string }).key)).toEqual([
|
||||
"hashCacheMaxCount",
|
||||
"chunkSplitterVersion",
|
||||
"customChunkSize",
|
||||
"readChunksOnline",
|
||||
"useOnlyLocalChunk",
|
||||
"concurrencyOfReadChunksOnline",
|
||||
"minimumIntervalOfReadChunksOnline",
|
||||
"autoAcceptCompatibleTweak",
|
||||
"enableCompression",
|
||||
]);
|
||||
|
||||
const readChunksOnline = settingHarness.rendered.find(
|
||||
(spec) => (spec as { key: string }).key === "readChunksOnline"
|
||||
) as { visible: () => boolean };
|
||||
expect(readChunksOnline.visible()).toBe(false);
|
||||
expect(host.onlyOnCouchDB).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,8 @@ const updateInformation: string = UPDATE_INFO || "";
|
||||
|
||||
export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement): void {
|
||||
const informationDivEl = this.createEl(paneEl, "div", { text: "" });
|
||||
const lifetimeComponent = this.lifetimeComponent;
|
||||
fireAndForget(() =>
|
||||
MarkdownRenderer.render(this.plugin.app, updateInformation, informationDivEl, "/", this.lifetimeComponent)
|
||||
MarkdownRenderer.render(this.plugin.app, updateInformation, informationDivEl, "/", lifetimeComponent)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,60 +1,22 @@
|
||||
import { $msg, $t } from "@/common/translation";
|
||||
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@/common/rosetta";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import type { PageFunctions } from "./SettingPane.ts";
|
||||
import { visibleOnly } from "./SettingPane.ts";
|
||||
import { EVENT_ON_UNRESOLVED_ERROR, eventHub } from "@/common/events.ts";
|
||||
import { NetworkWarningStyles } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const";
|
||||
export function paneGeneral(
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
paneEl: HTMLElement,
|
||||
{ addPanel, addPane }: PageFunctions
|
||||
): void {
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleAppearance")).then((paneEl) => {
|
||||
const languages = Object.fromEntries([
|
||||
// ["", $msg("obsidianLiveSyncSettingTab.defaultLanguage")],
|
||||
...SUPPORTED_I18N_LANGS.map((e) => [e, $t(`lang-${e}`)]),
|
||||
]) as Record<I18N_LANGS, string>;
|
||||
new Setting(paneEl).autoWireDropDown("displayLanguage", {
|
||||
options: languages,
|
||||
});
|
||||
this.addOnSaved("displayLanguage", () => this.display());
|
||||
new Setting(paneEl).autoWireToggle("showStatusOnEditor");
|
||||
this.addOnSaved("showStatusOnEditor", () => {
|
||||
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
|
||||
});
|
||||
new Setting(paneEl).autoWireToggle("showOnlyIconsOnEditor", {
|
||||
onUpdate: visibleOnly(() => this.isConfiguredAs("showStatusOnEditor", true)),
|
||||
});
|
||||
new Setting(paneEl).autoWireToggle("showStatusOnStatusbar");
|
||||
new Setting(paneEl).autoWireToggle("hideFileWarningNotice");
|
||||
new Setting(paneEl).autoWireDropDown("networkWarningStyle", {
|
||||
options: {
|
||||
[NetworkWarningStyles.BANNER]: "Show full banner",
|
||||
[NetworkWarningStyles.ICON]: "Show icon only",
|
||||
[NetworkWarningStyles.HIDDEN]: "Hide completely",
|
||||
},
|
||||
});
|
||||
this.addOnSaved("networkWarningStyle", () => {
|
||||
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
|
||||
});
|
||||
});
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleLogging")).then((paneEl) => {
|
||||
paneEl.addClass("wizardHidden");
|
||||
import { createExtraMenuSettingSpecGroup, createGeneralSettingSpecGroups } from "./GeneralSettingSpecs.ts";
|
||||
import { renderLegacySettingSpec } from "./SettingSpec.ts";
|
||||
|
||||
new Setting(paneEl).autoWireToggle("lessInformationInLog");
|
||||
|
||||
new Setting(paneEl).autoWireToggle("showVerboseLog", {
|
||||
onUpdate: visibleOnly(() => this.isConfiguredAs("lessInformationInLog", false)),
|
||||
export function paneGeneral(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void {
|
||||
const groups = [
|
||||
...createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails: () => this.isConfiguredAs("showStatusOnEditor", true),
|
||||
showVerboseLog: () => this.isConfiguredAs("lessInformationInLog", false),
|
||||
}),
|
||||
createExtraMenuSettingSpecGroup(),
|
||||
];
|
||||
for (const group of groups) {
|
||||
void addPanel(paneEl, group.heading).then((panelEl) => {
|
||||
for (const spec of group.items) {
|
||||
renderLegacySettingSpec(new Setting(panelEl), spec);
|
||||
}
|
||||
});
|
||||
});
|
||||
new Setting(paneEl).setClass("wizardOnly").addButton((button) =>
|
||||
button
|
||||
.setButtonText($msg("obsidianLiveSyncSettingTab.btnNext"))
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.changeDisplay("0");
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
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 { setButtonDestructiveState, type PageFunctions } from "./SettingPane.ts";
|
||||
import { isNotFoundError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
|
||||
import {
|
||||
chooseAndCopyFileDatabaseInfo,
|
||||
@@ -1097,10 +1097,9 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
|
||||
.setName("Check and convert non-path-obfuscated files")
|
||||
.setDesc("")
|
||||
.addButton((button) =>
|
||||
button
|
||||
setButtonDestructiveState(button)
|
||||
.setButtonText("Perform")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
for await (const docName of this.core.localDatabase.findAllDocNames()) {
|
||||
if (!docName.startsWith("f:")) {
|
||||
@@ -1185,10 +1184,9 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
|
||||
);
|
||||
|
||||
new Setting(paneEl).setName("Delete all customization sync data").addButton((button) =>
|
||||
button
|
||||
setButtonDestructiveState(button)
|
||||
.setButtonText("Delete")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
Logger(`Deleting customization sync data`, LOG_LEVEL_NOTICE);
|
||||
const entriesToDelete = await this.core.localDatabase.allDocsRaw({
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { MarkdownRenderer, request } from "@/deps.ts";
|
||||
import { $msg } from "@/common/translation";
|
||||
import { LiveSyncError } from "@vrtmrz/livesync-commonlib/compat/common/LSError";
|
||||
import { fireAndForget } from "octagonal-wheels/promises";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import type { PageFunctions } from "./SettingPane.ts";
|
||||
|
||||
/** Render the online help and troubleshooting browser. */
|
||||
export function paneHelp(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void {
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleOnlineTips")).then((panelEl) => {
|
||||
const lifetimeComponent = this.lifetimeComponent;
|
||||
let pageDisposed = false;
|
||||
lifetimeComponent.register(() => {
|
||||
pageDisposed = true;
|
||||
});
|
||||
const repo = "vrtmrz/obsidian-livesync";
|
||||
const topPath = $msg("obsidianLiveSyncSettingTab.linkTroubleshooting");
|
||||
const rawRepoURI = `https://raw.githubusercontent.com/${repo}/main`;
|
||||
this.createEl(panelEl, "div", "", (el) => {
|
||||
el.createEl("a", { text: $msg("obsidianLiveSyncSettingTab.linkOpenInBrowser") }, (anchor) => {
|
||||
anchor.href = `https://github.com/${repo}/blob/main${topPath}`;
|
||||
anchor.target = "_blank";
|
||||
anchor.rel = "noopener";
|
||||
});
|
||||
});
|
||||
const troubleShootEl = this.createEl(panelEl, "div", {
|
||||
text: "",
|
||||
cls: "sls-troubleshoot-preview",
|
||||
});
|
||||
const loadMarkdownPage = async (pathAll: string, basePathParam: string = "") => {
|
||||
troubleShootEl.setCssStyles({ minHeight: troubleShootEl.clientHeight + "px" });
|
||||
troubleShootEl.empty();
|
||||
const fullPath = pathAll.startsWith("/") ? pathAll : `${basePathParam}/${pathAll}`;
|
||||
|
||||
const directoryArr = fullPath.split("/");
|
||||
const filename = directoryArr.pop();
|
||||
const basePath = directoryArr.join("/");
|
||||
|
||||
let remoteTroubleShootMDSrc = "";
|
||||
try {
|
||||
remoteTroubleShootMDSrc = await request(`${rawRepoURI}${basePath}/${filename}`);
|
||||
} catch (ex) {
|
||||
const err = LiveSyncError.fromError(ex);
|
||||
remoteTroubleShootMDSrc = `${$msg("obsidianLiveSyncSettingTab.logErrorOccurred")}\n${err.toString()}`;
|
||||
}
|
||||
if (pageDisposed) return;
|
||||
const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace(
|
||||
/\((.*?(.png)|(.jpg))\)/g,
|
||||
`(${rawRepoURI}${basePath}/$1)`
|
||||
);
|
||||
await MarkdownRenderer.render(
|
||||
this.plugin.app,
|
||||
`<a class='sls-troubleshoot-anchor'></a> [${$msg("obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting")}](${topPath}) [${$msg("obsidianLiveSyncSettingTab.linkPageTop")}](${filename})\n\n${remoteTroubleShootMD}`,
|
||||
troubleShootEl,
|
||||
`${rawRepoURI}`,
|
||||
lifetimeComponent
|
||||
);
|
||||
if (pageDisposed) return;
|
||||
troubleShootEl.querySelector<HTMLAnchorElement>(".sls-troubleshoot-anchor")?.parentElement?.setCssStyles({
|
||||
position: "sticky",
|
||||
top: "-1em",
|
||||
backgroundColor: "var(--modal-background)",
|
||||
});
|
||||
troubleShootEl.querySelectorAll<HTMLAnchorElement>("a.internal-link").forEach((anchorEl) => {
|
||||
anchorEl.addEventListener("click", (evt) => {
|
||||
fireAndForget(async () => {
|
||||
const uri = anchorEl.getAttr("data-href");
|
||||
if (!uri) return;
|
||||
if (uri.startsWith("#")) {
|
||||
evt.preventDefault();
|
||||
const elements = Array.from(
|
||||
troubleShootEl.querySelectorAll<HTMLHeadingElement>("[data-heading]")
|
||||
);
|
||||
const target = elements.find(
|
||||
(element) =>
|
||||
element.getAttr("data-heading")?.toLowerCase().split(" ").join("-") ===
|
||||
uri.substring(1).toLowerCase()
|
||||
);
|
||||
if (target) {
|
||||
target.setCssStyles({ scrollMargin: "3em" });
|
||||
target.scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
evt.preventDefault();
|
||||
await loadMarkdownPage(uri, basePath);
|
||||
troubleShootEl.setCssStyles({ scrollMargin: "1em" });
|
||||
troubleShootEl.scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
troubleShootEl.setCssStyles({ minHeight: "" });
|
||||
};
|
||||
void loadMarkdownPage(topPath);
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import { EVENT_REQUEST_PERFORM_GC_V3, eventHub } from "@/common/events.ts";
|
||||
import { $msg } from "@/common/translation";
|
||||
import {
|
||||
createCoreSettingsAfterFullReset,
|
||||
createEditingSettingsAfterFullReset,
|
||||
} from "@/serviceFeatures/setupObsidian/settingsReset.ts";
|
||||
import { LOG_LEVEL_NOTICE, Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
|
||||
import { FlagFilesHumanReadable, FLAGMD_REDFLAG } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { FlagFilesHumanReadable, FlagFilesOriginal } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||
import { visibleOnly, type PageFunctions } from "./SettingPane";
|
||||
import { setButtonDestructiveState, visibleOnly, type PageFunctions } from "./SettingPane";
|
||||
export function paneMaintenance(
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
paneEl: HTMLElement,
|
||||
@@ -33,7 +38,7 @@ export function paneMaintenance(
|
||||
e.addEventListener("click", () => {
|
||||
fireAndForget(async () => {
|
||||
await this.services.replication.markResolved();
|
||||
this.display();
|
||||
this.requestPageRefresh();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -60,7 +65,7 @@ export function paneMaintenance(
|
||||
e.addEventListener("click", () => {
|
||||
fireAndForget(async () => {
|
||||
await this.services.replication.markUnlocked();
|
||||
this.display();
|
||||
this.requestPageRefresh();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -73,10 +78,9 @@ export function paneMaintenance(
|
||||
.setName("Lock Server")
|
||||
.setDesc("Lock the remote server to prevent synchronization with other devices.")
|
||||
.addButton((button) =>
|
||||
button
|
||||
setButtonDestructiveState(button)
|
||||
.setButtonText("Lock")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
await this.services.replication.markLocked();
|
||||
})
|
||||
@@ -87,12 +91,11 @@ export function paneMaintenance(
|
||||
.setName("Emergency restart")
|
||||
.setDesc("Disables all synchronization and restart.")
|
||||
.addButton((button) =>
|
||||
button
|
||||
setButtonDestructiveState(button)
|
||||
.setButtonText("Flag and restart")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
await this.core.storageAccess.writeFileAuto(FLAGMD_REDFLAG, "");
|
||||
await this.core.storageAccess.writeFileAuto(FlagFilesOriginal.SUSPEND_ALL, "");
|
||||
this.services.appLifecycle.performRestart();
|
||||
})
|
||||
);
|
||||
@@ -132,9 +135,8 @@ export function paneMaintenance(
|
||||
.setName("Resend")
|
||||
.setDesc("Resend all chunks to the remote.")
|
||||
.addButton((button) =>
|
||||
button
|
||||
setButtonDestructiveState(button)
|
||||
.setButtonText("Send chunks")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
if (this.core.replicator instanceof LiveSyncCouchDBReplicator) {
|
||||
@@ -150,9 +152,8 @@ export function paneMaintenance(
|
||||
"Initialise journal received history. On the next sync, every item except this device sent will be downloaded again."
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
setButtonDestructiveState(button)
|
||||
.setButtonText("Reset received")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({
|
||||
@@ -171,9 +172,8 @@ export function paneMaintenance(
|
||||
"Initialise journal sent history. On the next sync, every item except this device received will be sent again."
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
setButtonDestructiveState(button)
|
||||
.setButtonText("Reset sent history")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({
|
||||
@@ -314,9 +314,8 @@ export function paneMaintenance(
|
||||
.setName("Overwrite remote")
|
||||
.setDesc("Overwrite remote with local DB and passphrase.")
|
||||
.addButton((button) =>
|
||||
button
|
||||
setButtonDestructiveState(button)
|
||||
.setButtonText("Send")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await this.rebuildDB("remoteOnly");
|
||||
@@ -327,9 +326,8 @@ export function paneMaintenance(
|
||||
.setName("Reset all journal counter")
|
||||
.setDesc("Initialise all journal history, On the next sync, every item will be received and sent.")
|
||||
.addButton((button) =>
|
||||
button
|
||||
setButtonDestructiveState(button)
|
||||
.setButtonText("Reset all")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await this.getMinioJournalSyncClient().resetCheckpointInfo();
|
||||
@@ -342,9 +340,8 @@ export function paneMaintenance(
|
||||
.setName("Purge all journal counter")
|
||||
.setDesc("Purge all download/upload cache.")
|
||||
.addButton((button) =>
|
||||
button
|
||||
setButtonDestructiveState(button)
|
||||
.setButtonText("Reset all")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(() => {
|
||||
this.getMinioJournalSyncClient().resetAllCaches();
|
||||
@@ -357,9 +354,8 @@ export function paneMaintenance(
|
||||
.setName("Fresh Start Wipe")
|
||||
.setDesc("Delete all data on the remote server.")
|
||||
.addButton((button) =>
|
||||
button
|
||||
setButtonDestructiveState(button)
|
||||
.setButtonText("Delete")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({
|
||||
@@ -378,12 +374,35 @@ export function paneMaintenance(
|
||||
});
|
||||
|
||||
void addPanel(paneEl, "Reset").then((paneEl) => {
|
||||
new Setting(paneEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameDiscardSettings"))
|
||||
.addButton((button) => {
|
||||
setButtonDestructiveState(button)
|
||||
.setButtonText($msg("obsidianLiveSyncSettingTab.btnDiscard"))
|
||||
.onClick(async () => {
|
||||
if (
|
||||
(await this.core.confirm.askYesNoDialog(
|
||||
$msg("obsidianLiveSyncSettingTab.msgDiscardConfirmation"),
|
||||
{ defaultOption: "No" }
|
||||
)) !== "yes"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.editingSettings = createEditingSettingsAfterFullReset(this.editingSettings);
|
||||
await this.saveAllDirtySettings();
|
||||
this.core.settings = createCoreSettingsAfterFullReset();
|
||||
await this.services.setting.saveSettingData();
|
||||
await this.services.database.resetDatabase();
|
||||
this.services.appLifecycle.askRestart();
|
||||
});
|
||||
})
|
||||
.addOnUpdate(visibleOnly(() => this.isConfiguredAs("isConfigured", true)));
|
||||
|
||||
new Setting(paneEl)
|
||||
.setName("Delete local database to reset or uninstall Self-hosted LiveSync")
|
||||
.addButton((button) =>
|
||||
button
|
||||
setButtonDestructiveState(button)
|
||||
.setButtonText("Delete")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await this.services.database.resetDatabase();
|
||||
|
||||
@@ -13,20 +13,20 @@ import { visibleOnly } from "./SettingPane.ts";
|
||||
import { PouchDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/pouchdb-browser";
|
||||
import { ExtraSuffixIndexedDB } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { migrateDatabases } from "./settingUtils.ts";
|
||||
import { usesLegacyIndexedDBAdapter } from "@/common/compatibilitySettings.ts";
|
||||
|
||||
export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void {
|
||||
void addPanel(paneEl, "Compatibility (Metadata)").then((paneEl) => {
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("deleteMetadataOfDeletedFiles");
|
||||
new Setting(paneEl).autoWireToggle("deleteMetadataOfDeletedFiles");
|
||||
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("automaticallyDeleteMetadataOfDeletedFiles", {
|
||||
new Setting(paneEl).autoWireNumeric("automaticallyDeleteMetadataOfDeletedFiles", {
|
||||
onUpdate: visibleOnly(() => this.isConfiguredAs("deleteMetadataOfDeletedFiles", true)),
|
||||
});
|
||||
});
|
||||
|
||||
void addPanel(paneEl, "Compatibility (Conflict Behaviour)").then((paneEl) => {
|
||||
paneEl.addClass("wizardHidden");
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("disableMarkdownAutoMerge");
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("writeDocumentsIfConflicted");
|
||||
new Setting(paneEl).autoWireToggle("disableMarkdownAutoMerge");
|
||||
new Setting(paneEl).autoWireToggle("writeDocumentsIfConflicted");
|
||||
});
|
||||
|
||||
void addPanel(paneEl, "Compatibility (Database structure)").then((paneEl) => {
|
||||
@@ -75,7 +75,8 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
|
||||
}
|
||||
};
|
||||
{
|
||||
const infoClass = this.editingSettings.useIndexedDBAdapter ? "op-warn" : "op-warn-info";
|
||||
const useIndexedDBAdapter = usesLegacyIndexedDBAdapter(this.editingSettings);
|
||||
const infoClass = useIndexedDBAdapter ? "op-warn" : "op-warn-info";
|
||||
paneEl.createDiv({
|
||||
text: "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.",
|
||||
cls: infoClass,
|
||||
@@ -88,8 +89,8 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
|
||||
.setName("Database Adapter")
|
||||
.setDesc("Select the database adapter to use. ");
|
||||
const el = setting.controlEl.createDiv({});
|
||||
el.setText(`Current adapter: ${this.editingSettings.useIndexedDBAdapter ? "IndexedDB" : "IDB"}`);
|
||||
if (!this.editingSettings.useIndexedDBAdapter) {
|
||||
el.setText(`Current adapter: ${useIndexedDBAdapter ? "IndexedDB" : "IDB"}`);
|
||||
if (!useIndexedDBAdapter) {
|
||||
setting.addButton((button) => {
|
||||
button.setButtonText("Switch to IndexedDB").onClick(async () => {
|
||||
Logger("Migrating all data to IndexedDB...", LOG_LEVEL_NOTICE);
|
||||
@@ -113,7 +114,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
|
||||
});
|
||||
}
|
||||
}
|
||||
new Setting(paneEl).autoWireToggle("handleFilenameCaseSensitive", { holdValue: true }).setClass("wizardHidden");
|
||||
new Setting(paneEl).autoWireToggle("handleFilenameCaseSensitive", { holdValue: true });
|
||||
});
|
||||
|
||||
void addPanel(paneEl, "Compatibility (Internal API Usage)").then((paneEl) => {
|
||||
@@ -158,7 +159,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
|
||||
});
|
||||
void addPanel(paneEl, "Edge case addressing (Behaviour)").then((paneEl) => {
|
||||
new Setting(paneEl).autoWireToggle("doNotSuspendOnFetching");
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("doNotDeleteFolder");
|
||||
new Setting(paneEl).autoWireToggle("doNotDeleteFolder");
|
||||
new Setting(paneEl).autoWireToggle("processSizeMismatchedFiles");
|
||||
});
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ export function panePowerUsers(
|
||||
{ addPanel }: PageFunctions
|
||||
): void {
|
||||
void addPanel(paneEl, "CouchDB Connection Tweak", undefined, this.onlyOnCouchDB).then((paneEl) => {
|
||||
paneEl.addClass("wizardHidden");
|
||||
|
||||
this.createEl(
|
||||
paneEl,
|
||||
"div",
|
||||
@@ -19,16 +17,14 @@ export function panePowerUsers(
|
||||
},
|
||||
undefined,
|
||||
this.onlyOnCouchDB
|
||||
).addClass("wizardHidden");
|
||||
);
|
||||
|
||||
new Setting(paneEl)
|
||||
.setClass("wizardHidden")
|
||||
.autoWireNumeric("batch_size", { clampMin: 2, onUpdate: this.onlyOnCouchDB });
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batches_limit", {
|
||||
new Setting(paneEl).autoWireNumeric("batch_size", { clampMin: 2, onUpdate: this.onlyOnCouchDB });
|
||||
new Setting(paneEl).autoWireNumeric("batches_limit", {
|
||||
clampMin: 2,
|
||||
onUpdate: this.onlyOnCouchDB,
|
||||
});
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("useTimeouts", { onUpdate: this.onlyOnCouchDB });
|
||||
new Setting(paneEl).autoWireToggle("useTimeouts", { onUpdate: this.onlyOnCouchDB });
|
||||
});
|
||||
void addPanel(paneEl, "Configuration Encryption").then((paneEl) => {
|
||||
const passphrase_options: Record<ConfigPassphraseStore, string> = {
|
||||
@@ -42,18 +38,14 @@ export function panePowerUsers(
|
||||
.autoWireDropDown("configPassphraseStore", {
|
||||
options: passphrase_options,
|
||||
holdValue: true,
|
||||
})
|
||||
.setClass("wizardHidden");
|
||||
});
|
||||
|
||||
new Setting(paneEl)
|
||||
.autoWireText("configPassphrase", { isPassword: true, holdValue: true })
|
||||
.setClass("wizardHidden")
|
||||
.addOnUpdate(() => ({
|
||||
disabled: !this.isConfiguredAs("configPassphraseStore", "LOCALSTORAGE"),
|
||||
}));
|
||||
new Setting(paneEl).addApplyButton(["configPassphrase", "configPassphraseStore"]).setClass("wizardHidden");
|
||||
new Setting(paneEl).autoWireText("configPassphrase", { isPassword: true, holdValue: true }).addOnUpdate(() => ({
|
||||
disabled: !this.isConfiguredAs("configPassphraseStore", "LOCALSTORAGE"),
|
||||
}));
|
||||
new Setting(paneEl).addApplyButton(["configPassphrase", "configPassphraseStore"]);
|
||||
});
|
||||
void addPanel(paneEl, "Developer").then((paneEl) => {
|
||||
new Setting(paneEl).autoWireToggle("enableDebugTools").setClass("wizardHidden");
|
||||
new Setting(paneEl).autoWireToggle("enableDebugTools");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { $msg } from "@/common/translation";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import { visibleOnly, type PageFunctions } from "./SettingPane.ts";
|
||||
|
||||
/** Render setup actions in the pane-based settings interface used before Obsidian 1.13. */
|
||||
export function paneQuickSetup(
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
paneEl: HTMLElement,
|
||||
{ addPanel }: PageFunctions
|
||||
): void {
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleQuickSetup")).then((panelEl) => {
|
||||
new Setting(panelEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameConnectSetupURI"))
|
||||
.setDesc($msg("obsidianLiveSyncSettingTab.descConnectSetupURI"))
|
||||
.addButton((button) => {
|
||||
button.setButtonText($msg("obsidianLiveSyncSettingTab.btnUse")).onClick(() => {
|
||||
this.requestOpenSetupURI();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(panelEl)
|
||||
.setName($msg("Rerun Onboarding Wizard"))
|
||||
.setDesc($msg("Rerun the onboarding wizard to set up Self-hosted LiveSync again."))
|
||||
.addButton((button) => {
|
||||
button.setButtonText($msg("Rerun Wizard")).onClick(async () => {
|
||||
await this.rerunOnboardingWizard();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(panelEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameEnableLiveSync"))
|
||||
.setDesc($msg("obsidianLiveSyncSettingTab.descEnableLiveSync"))
|
||||
.addOnUpdate(visibleOnly(() => !this.isConfiguredAs("isConfigured", true)))
|
||||
.addButton((button) => {
|
||||
button.setButtonText($msg("obsidianLiveSyncSettingTab.btnEnable")).onClick(async () => {
|
||||
await this.enableLiveSyncFromSettings();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
void addPanel(
|
||||
paneEl,
|
||||
`📲 ${$msg("obsidianLiveSyncSettingTab.titleSetupOtherDevices")}`,
|
||||
undefined,
|
||||
visibleOnly(() => this.isConfiguredAs("isConfigured", true))
|
||||
).then((panelEl) => {
|
||||
new Setting(panelEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameCopySetupURI"))
|
||||
.setDesc($msg("obsidianLiveSyncSettingTab.descCopySetupURI"))
|
||||
.addButton((button) => {
|
||||
button.setButtonText($msg("obsidianLiveSyncSettingTab.btnCopy")).onClick(() => {
|
||||
this.requestCopySetupURI();
|
||||
});
|
||||
});
|
||||
new Setting(panelEl)
|
||||
.setName($msg("Setup.ShowQRCode"))
|
||||
.setDesc($msg("Setup.ShowQRCode.Desc"))
|
||||
.addButton((button) => {
|
||||
button.setButtonText($msg("Setup.ShowQRCode")).onClick(() => {
|
||||
this.requestShowSetupQRCode();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { Menu, type ButtonComponent } from "@/deps.ts";
|
||||
import { $msg } from "@/common/translation";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import type { PageFunctions } from "./SettingPane.ts";
|
||||
import { setButtonDestructiveState, type PageFunctions } from "./SettingPane.ts";
|
||||
// import { visibleOnly } from "./SettingPane.ts";
|
||||
import InfoPanel from "./InfoPanel.svelte";
|
||||
import { writable } from "svelte/store";
|
||||
@@ -89,7 +89,7 @@ function suggestRemoteConfigurationName(parsed: RemoteConfigurationResult): stri
|
||||
export function paneRemoteConfig(
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
paneEl: HTMLElement,
|
||||
{ addPanel, addPane }: PageFunctions
|
||||
{ addPanel }: PageFunctions
|
||||
): void {
|
||||
{
|
||||
/* E2EE */
|
||||
@@ -103,11 +103,12 @@ export function paneRemoteConfig(
|
||||
});
|
||||
};
|
||||
void addPanel(paneEl, "E2EE Configuration", () => {}).then((paneEl) => {
|
||||
new SveltePanel(InfoPanel, paneEl, E2EESummaryWritable);
|
||||
const infoPanel = new SveltePanel(InfoPanel, paneEl, E2EESummaryWritable);
|
||||
this.lifetimeComponent.register(() => infoPanel.destroy());
|
||||
const setupButton = new Setting(paneEl).setName("Configure E2EE");
|
||||
setupButton
|
||||
.addButton((button) =>
|
||||
button
|
||||
setButtonDestructiveState(button)
|
||||
.onClick(async () => {
|
||||
const setupManager = this.core.getModule(SetupManager);
|
||||
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
|
||||
@@ -115,10 +116,9 @@ export function paneRemoteConfig(
|
||||
updateE2EESummary();
|
||||
})
|
||||
.setButtonText("Configure")
|
||||
.setWarning()
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
setButtonDestructiveState(button)
|
||||
.onClick(async () => {
|
||||
const setupManager = this.core.getModule(SetupManager);
|
||||
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
|
||||
@@ -126,7 +126,6 @@ export function paneRemoteConfig(
|
||||
updateE2EESummary();
|
||||
})
|
||||
.setButtonText("Configure And Change Remote")
|
||||
.setWarning()
|
||||
);
|
||||
updateE2EESummary();
|
||||
});
|
||||
@@ -553,83 +552,6 @@ export function paneRemoteConfig(
|
||||
// );
|
||||
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleNotification"), () => {}).then((paneEl) => {
|
||||
paneEl.addClass("wizardHidden");
|
||||
new Setting(paneEl).autoWireNumeric("notifyThresholdOfRemoteStorageSize", {}).setClass("wizardHidden");
|
||||
new Setting(paneEl).autoWireNumeric("notifyThresholdOfRemoteStorageSize", {});
|
||||
});
|
||||
|
||||
// new Setting(paneEl).setClass("wizardOnly").addButton((button) =>
|
||||
// button
|
||||
// .setButtonText($msg("obsidianLiveSyncSettingTab.buttonNext"))
|
||||
// .setCta()
|
||||
// .setDisabled(false)
|
||||
// .onClick(async () => {
|
||||
// if (!(await checkConfig(checkResultDiv))) {
|
||||
// if (
|
||||
// (await this.plugin.confirm.askYesNoDialog(
|
||||
// $msg("obsidianLiveSyncSettingTab.msgConfigCheckFailed"),
|
||||
// {
|
||||
// defaultOption: "No",
|
||||
// title: $msg("obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed"),
|
||||
// }
|
||||
// )) == "no"
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// const isEncryptionFullyEnabled =
|
||||
// !this.editingSettings.encrypt || !this.editingSettings.usePathObfuscation;
|
||||
// if (isEncryptionFullyEnabled) {
|
||||
// if (
|
||||
// (await this.plugin.confirm.askYesNoDialog(
|
||||
// $msg("obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation"),
|
||||
// {
|
||||
// defaultOption: "No",
|
||||
// title: $msg("obsidianLiveSyncSettingTab.titleEncryptionNotEnabled"),
|
||||
// }
|
||||
// )) == "no"
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// if (!this.editingSettings.encrypt) {
|
||||
// this.editingSettings.passphrase = "";
|
||||
// }
|
||||
// if (!(await this.isPassphraseValid())) {
|
||||
// if (
|
||||
// (await this.plugin.confirm.askYesNoDialog(
|
||||
// $msg("obsidianLiveSyncSettingTab.msgInvalidPassphrase"),
|
||||
// {
|
||||
// defaultOption: "No",
|
||||
// title: $msg("obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid"),
|
||||
// }
|
||||
// )) == "no"
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// if (isCloudantURI(this.editingSettings.couchDB_URI)) {
|
||||
// this.editingSettings = { ...this.editingSettings, ...PREFERRED_SETTING_CLOUDANT };
|
||||
// } else if (this.editingSettings.remoteType == REMOTE_MINIO) {
|
||||
// this.editingSettings = { ...this.editingSettings, ...PREFERRED_JOURNAL_SYNC };
|
||||
// } else {
|
||||
// this.editingSettings = { ...this.editingSettings, ...PREFERRED_SETTING_SELF_HOSTED };
|
||||
// }
|
||||
// if (
|
||||
// (await this.plugin.confirm.askYesNoDialog(
|
||||
// $msg("obsidianLiveSyncSettingTab.msgFetchConfigFromRemote"),
|
||||
// { defaultOption: "Yes", title: $msg("obsidianLiveSyncSettingTab.titleFetchConfig") }
|
||||
// )) == "yes"
|
||||
// ) {
|
||||
// const trialSetting = { ...this.initialSettings, ...this.editingSettings };
|
||||
// const newTweaks = await this.services.tweakValue.checkAndAskUseRemoteConfiguration(trialSetting);
|
||||
// if (newTweaks.result !== false) {
|
||||
// this.editingSettings = { ...this.editingSettings, ...newTweaks.result };
|
||||
// this.requestUpdate();
|
||||
// } else {
|
||||
// // Messages should be already shown.
|
||||
// }
|
||||
// }
|
||||
// this.changeDisplay("30");
|
||||
// })
|
||||
// );
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runtime = vi.hoisted(() => ({
|
||||
panels: [] as Array<{ destroy: ReturnType<typeof vi.fn> }>,
|
||||
}));
|
||||
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/common/types", () => ({
|
||||
DEFAULT_SETTINGS: {},
|
||||
LOG_LEVEL_NOTICE: 1,
|
||||
LOG_LEVEL_VERBOSE: 2,
|
||||
REMOTE_COUCHDB: "couchdb",
|
||||
REMOTE_MINIO: "minio",
|
||||
REMOTE_P2P: "p2p",
|
||||
}));
|
||||
vi.mock("@/deps.ts", () => ({
|
||||
Menu: class {},
|
||||
}));
|
||||
vi.mock("@/common/translation", () => ({
|
||||
$msg: (message: string) => message,
|
||||
}));
|
||||
vi.mock("./LiveSyncSetting.ts", () => ({
|
||||
LiveSyncSetting: class {
|
||||
nameEl = { addClass: vi.fn(), appendText: vi.fn() };
|
||||
|
||||
setName() {
|
||||
return this;
|
||||
}
|
||||
|
||||
setDesc() {
|
||||
return this;
|
||||
}
|
||||
|
||||
addButton() {
|
||||
return this;
|
||||
}
|
||||
|
||||
autoWireNumeric() {
|
||||
return this;
|
||||
}
|
||||
},
|
||||
}));
|
||||
vi.mock("./InfoPanel.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SveltePanel.ts", () => ({
|
||||
SveltePanel: class {
|
||||
destroy = vi.fn();
|
||||
|
||||
constructor() {
|
||||
runtime.panels.push(this);
|
||||
}
|
||||
},
|
||||
}));
|
||||
vi.mock("./settingUtils.ts", () => ({
|
||||
getE2EEConfigSummary: vi.fn(() => ({ summary: "summary" })),
|
||||
}));
|
||||
vi.mock("@/modules/features/SetupManager.ts", () => ({
|
||||
SetupManager: class {},
|
||||
UserMode: { Update: "update" },
|
||||
}));
|
||||
vi.mock("./settingConstants.ts", () => ({
|
||||
OnDialogSettingsDefault: {},
|
||||
}));
|
||||
vi.mock("@vrtmrz/livesync-commonlib/remote-configurations", () => ({
|
||||
activateRemoteConfiguration: vi.fn(),
|
||||
}));
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/common/ConnectionString", () => ({
|
||||
ConnectionStringParser: {
|
||||
parse: vi.fn(),
|
||||
serialize: vi.fn(() => ""),
|
||||
},
|
||||
}));
|
||||
vi.mock("@/modules/features/SetupWizard/dialogs/SetupRemote.svelte", () => ({ default: {} }));
|
||||
vi.mock("@/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte", () => ({ default: {} }));
|
||||
vi.mock("@/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte", () => ({ default: {} }));
|
||||
vi.mock("@/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte", () => ({ default: {} }));
|
||||
vi.mock("./remoteConfigBuffer.ts", () => ({
|
||||
syncActivatedRemoteSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
import { paneRemoteConfig } from "./PaneRemoteConfig.ts";
|
||||
|
||||
function createPanelElement(): HTMLElement {
|
||||
return {
|
||||
createDiv: vi.fn(() => ({ empty: vi.fn() })),
|
||||
} as unknown as HTMLElement;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
runtime.panels.length = 0;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("paneRemoteConfig", () => {
|
||||
it("destroys the E2EE info panel when the settings page lifetime unloads", async () => {
|
||||
const callbacks: Array<() => unknown> = [];
|
||||
const lifetimeComponent = {
|
||||
register: vi.fn((callback: () => unknown) => callbacks.push(callback)),
|
||||
unload: vi.fn(() => callbacks.splice(0).forEach((callback) => callback())),
|
||||
};
|
||||
const addPanel = vi.fn((_parent: HTMLElement, _heading: string) => Promise.resolve(createPanelElement()));
|
||||
const host = {
|
||||
editingSettings: { remoteConfigurations: {} },
|
||||
core: { settings: { remoteConfigurations: {} } },
|
||||
lifetimeComponent,
|
||||
};
|
||||
|
||||
paneRemoteConfig.call(host as never, {} as HTMLElement, { addPanel } as never);
|
||||
await vi.waitFor(() => expect(runtime.panels).toHaveLength(1));
|
||||
|
||||
lifetimeComponent.unload();
|
||||
|
||||
expect(runtime.panels[0].destroy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -2,21 +2,18 @@ import { LEVEL_ADVANCED, type CustomRegExpSource } from "@vrtmrz/livesync-common
|
||||
import { constructCustomRegExpList, splitCustomRegExpList } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import MultipleRegExpControl from "./MultipleRegExpControl.svelte";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import { mount } from "svelte";
|
||||
import { mount, unmount } from "svelte";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import type { PageFunctions } from "./SettingPane.ts";
|
||||
import { visibleOnly } from "./SettingPane.ts";
|
||||
export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void {
|
||||
void addPanel(paneEl, "Normal Files").then((paneEl) => {
|
||||
paneEl.addClass("wizardHidden");
|
||||
|
||||
const syncFilesSetting = new Setting(paneEl)
|
||||
.setName("Synchronising files")
|
||||
.setDesc(
|
||||
"(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files."
|
||||
)
|
||||
.setClass("wizardHidden");
|
||||
mount(MultipleRegExpControl, {
|
||||
);
|
||||
const syncFilesControl = mount(MultipleRegExpControl, {
|
||||
target: syncFilesSetting.controlEl,
|
||||
props: {
|
||||
patterns: splitCustomRegExpList(this.editingSettings.syncOnlyRegEx, "|[]|"),
|
||||
@@ -24,17 +21,17 @@ export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
|
||||
apply: async (newPatterns: CustomRegExpSource[]) => {
|
||||
this.editingSettings.syncOnlyRegEx = constructCustomRegExpList(newPatterns, "|[]|");
|
||||
await this.saveAllDirtySettings();
|
||||
this.display();
|
||||
this.requestPageRefresh();
|
||||
},
|
||||
},
|
||||
});
|
||||
this.lifetimeComponent.register(() => void unmount(syncFilesControl));
|
||||
|
||||
const nonSyncFilesSetting = new Setting(paneEl)
|
||||
.setName("Non-Synchronising files")
|
||||
.setDesc("(RegExp) If this is set, any changes to local and remote files that match this will be skipped.")
|
||||
.setClass("wizardHidden");
|
||||
.setDesc("(RegExp) If this is set, any changes to local and remote files that match this will be skipped.");
|
||||
|
||||
mount(MultipleRegExpControl, {
|
||||
const nonSyncFilesControl = mount(MultipleRegExpControl, {
|
||||
target: nonSyncFilesSetting.controlEl,
|
||||
props: {
|
||||
patterns: splitCustomRegExpList(this.editingSettings.syncIgnoreRegEx, "|[]|"),
|
||||
@@ -42,24 +39,24 @@ export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
|
||||
apply: async (newPatterns: CustomRegExpSource[]) => {
|
||||
this.editingSettings.syncIgnoreRegEx = constructCustomRegExpList(newPatterns, "|[]|");
|
||||
await this.saveAllDirtySettings();
|
||||
this.display();
|
||||
this.requestPageRefresh();
|
||||
},
|
||||
},
|
||||
});
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("syncMaxSizeInMB", { clampMin: 0 });
|
||||
this.lifetimeComponent.register(() => void unmount(nonSyncFilesControl));
|
||||
new Setting(paneEl).autoWireNumeric("syncMaxSizeInMB", { clampMin: 0 });
|
||||
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("useIgnoreFiles");
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireTextArea("ignoreFiles", {
|
||||
new Setting(paneEl).autoWireToggle("useIgnoreFiles");
|
||||
new Setting(paneEl).autoWireTextArea("ignoreFiles", {
|
||||
onUpdate: visibleOnly(() => this.isConfiguredAs("useIgnoreFiles", true)),
|
||||
});
|
||||
});
|
||||
void addPanel(paneEl, "Hidden Files", undefined, undefined, LEVEL_ADVANCED).then((paneEl) => {
|
||||
const targetPatternSetting = new Setting(paneEl)
|
||||
.setName("Target patterns")
|
||||
.setClass("wizardHidden")
|
||||
.setDesc("Patterns to match files for syncing");
|
||||
const patTarget = splitCustomRegExpList(this.editingSettings.syncInternalFilesTargetPatterns, ",");
|
||||
mount(MultipleRegExpControl, {
|
||||
const targetPatternControl = mount(MultipleRegExpControl, {
|
||||
target: targetPatternSetting.controlEl,
|
||||
props: {
|
||||
patterns: patTarget,
|
||||
@@ -67,19 +64,20 @@ export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
|
||||
apply: async (newPatterns: CustomRegExpSource[]) => {
|
||||
this.editingSettings.syncInternalFilesTargetPatterns = constructCustomRegExpList(newPatterns, ",");
|
||||
await this.saveAllDirtySettings();
|
||||
this.display();
|
||||
this.requestPageRefresh();
|
||||
},
|
||||
},
|
||||
});
|
||||
this.lifetimeComponent.register(() => void unmount(targetPatternControl));
|
||||
|
||||
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, ^\\.git\\/, \\/obsidian-livesync\\/";
|
||||
const defaultSkipPatternXPlat =
|
||||
defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$,\\/workspace-mobile.json$";
|
||||
|
||||
const pat = splitCustomRegExpList(this.editingSettings.syncInternalFilesIgnorePatterns, ",");
|
||||
const patSetting = new Setting(paneEl).setName("Ignore patterns").setClass("wizardHidden").setDesc("");
|
||||
const patSetting = new Setting(paneEl).setName("Ignore patterns").setDesc("");
|
||||
|
||||
mount(MultipleRegExpControl, {
|
||||
const ignorePatternControl = mount(MultipleRegExpControl, {
|
||||
target: patSetting.controlEl,
|
||||
props: {
|
||||
patterns: pat,
|
||||
@@ -87,10 +85,11 @@ export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
|
||||
apply: async (newPatterns: CustomRegExpSource[]) => {
|
||||
this.editingSettings.syncInternalFilesIgnorePatterns = constructCustomRegExpList(newPatterns, ",");
|
||||
await this.saveAllDirtySettings();
|
||||
this.display();
|
||||
this.requestPageRefresh();
|
||||
},
|
||||
},
|
||||
});
|
||||
this.lifetimeComponent.register(() => void unmount(ignorePatternControl));
|
||||
|
||||
const addDefaultPatterns = async (patterns: string) => {
|
||||
const oldList = splitCustomRegExpList(this.editingSettings.syncInternalFilesIgnorePatterns, ",");
|
||||
@@ -101,12 +100,11 @@ export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
|
||||
const allSet = new Set<CustomRegExpSource>([...oldList, ...newList]);
|
||||
this.editingSettings.syncInternalFilesIgnorePatterns = constructCustomRegExpList([...allSet], ",");
|
||||
await this.saveAllDirtySettings();
|
||||
this.display();
|
||||
this.requestPageRefresh();
|
||||
};
|
||||
|
||||
new Setting(paneEl)
|
||||
.setName("Add default patterns")
|
||||
.setClass("wizardHidden")
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Default").onClick(async () => {
|
||||
await addDefaultPatterns(defaultSkipPattern);
|
||||
@@ -120,10 +118,9 @@ export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
|
||||
|
||||
const overwritePatterns = new Setting(paneEl)
|
||||
.setName("Overwrite patterns")
|
||||
.setClass("wizardHidden")
|
||||
.setDesc("Patterns to match files for overwriting instead of merging");
|
||||
const patTarget2 = splitCustomRegExpList(this.editingSettings.syncInternalFileOverwritePatterns, ",");
|
||||
mount(MultipleRegExpControl, {
|
||||
const overwritePatternControl = mount(MultipleRegExpControl, {
|
||||
target: overwritePatterns.controlEl,
|
||||
props: {
|
||||
patterns: patTarget2,
|
||||
@@ -134,9 +131,10 @@ export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
|
||||
","
|
||||
);
|
||||
await this.saveAllDirtySettings();
|
||||
this.display();
|
||||
this.requestPageRefresh();
|
||||
},
|
||||
},
|
||||
});
|
||||
this.lifetimeComponent.register(() => void unmount(overwritePatternControl));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
import { MarkdownRenderer } from "@/deps.ts";
|
||||
import { $msg } from "@/common/translation";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import { fireAndForget } from "octagonal-wheels/promises";
|
||||
import {
|
||||
EVENT_REQUEST_COPY_SETUP_URI,
|
||||
EVENT_REQUEST_OPEN_SETUP_URI,
|
||||
EVENT_REQUEST_SHOW_SETUP_QR,
|
||||
eventHub,
|
||||
} from "@/common/events.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import type { PageFunctions } from "./SettingPane.ts";
|
||||
import { visibleOnly } from "./SettingPane.ts";
|
||||
import { request } from "@/deps.ts";
|
||||
import { SetupManager } from "@/modules/features/SetupManager.ts";
|
||||
import { LiveSyncError } from "@vrtmrz/livesync-commonlib/compat/common/LSError";
|
||||
import {
|
||||
createCoreSettingsAfterFullReset,
|
||||
createEditingSettingsAfterFullReset,
|
||||
} from "@/serviceFeatures/setupObsidian/settingsReset.ts";
|
||||
export function paneSetup(
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
paneEl: HTMLElement,
|
||||
{ addPanel, addPane }: PageFunctions
|
||||
): void {
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleQuickSetup")).then((paneEl) => {
|
||||
new Setting(paneEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameConnectSetupURI"))
|
||||
.setDesc($msg("obsidianLiveSyncSettingTab.descConnectSetupURI"))
|
||||
.addButton((text) => {
|
||||
text.setButtonText($msg("obsidianLiveSyncSettingTab.btnUse")).onClick(() => {
|
||||
this.closeSetting();
|
||||
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETUP_URI);
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(paneEl)
|
||||
.setName($msg("Rerun Onboarding Wizard"))
|
||||
.setDesc($msg("Rerun the onboarding wizard to set up Self-hosted LiveSync again."))
|
||||
.addButton((text) => {
|
||||
text.setButtonText($msg("Rerun Wizard")).onClick(async () => {
|
||||
const setupManager = this.core.getModule(SetupManager);
|
||||
await setupManager.startOnBoarding();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(paneEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameEnableLiveSync"))
|
||||
.setDesc($msg("obsidianLiveSyncSettingTab.descEnableLiveSync"))
|
||||
.addOnUpdate(visibleOnly(() => !this.isConfiguredAs("isConfigured", true)))
|
||||
.addButton((text) => {
|
||||
text.setButtonText($msg("obsidianLiveSyncSettingTab.btnEnable")).onClick(async () => {
|
||||
this.editingSettings.isConfigured = true;
|
||||
await this.saveAllDirtySettings();
|
||||
this.services.appLifecycle.askRestart();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
void addPanel(
|
||||
paneEl,
|
||||
$msg("obsidianLiveSyncSettingTab.titleSetupOtherDevices"),
|
||||
undefined,
|
||||
visibleOnly(() => this.isConfiguredAs("isConfigured", true))
|
||||
).then((paneEl) => {
|
||||
new Setting(paneEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameCopySetupURI"))
|
||||
.setDesc($msg("obsidianLiveSyncSettingTab.descCopySetupURI"))
|
||||
.addButton((text) => {
|
||||
text.setButtonText($msg("obsidianLiveSyncSettingTab.btnCopy")).onClick(() => {
|
||||
// await this.plugin.addOnSetup.command_copySetupURI();
|
||||
eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI);
|
||||
});
|
||||
});
|
||||
new Setting(paneEl)
|
||||
.setName($msg("Setup.ShowQRCode"))
|
||||
.setDesc($msg("Setup.ShowQRCode.Desc"))
|
||||
.addButton((text) => {
|
||||
text.setButtonText($msg("Setup.ShowQRCode")).onClick(() => {
|
||||
eventHub.emitEvent(EVENT_REQUEST_SHOW_SETUP_QR);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleReset")).then((paneEl) => {
|
||||
new Setting(paneEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameDiscardSettings"))
|
||||
.addButton((text) => {
|
||||
text.setButtonText($msg("obsidianLiveSyncSettingTab.btnDiscard"))
|
||||
.onClick(async () => {
|
||||
if (
|
||||
(await this.core.confirm.askYesNoDialog(
|
||||
$msg("obsidianLiveSyncSettingTab.msgDiscardConfirmation"),
|
||||
{ defaultOption: "No" }
|
||||
)) == "yes"
|
||||
) {
|
||||
this.editingSettings = createEditingSettingsAfterFullReset(this.editingSettings);
|
||||
await this.saveAllDirtySettings();
|
||||
this.core.settings = createCoreSettingsAfterFullReset();
|
||||
await this.services.setting.saveSettingData();
|
||||
await this.services.database.resetDatabase();
|
||||
// await this.plugin.initializeDatabase();
|
||||
this.services.appLifecycle.askRestart();
|
||||
}
|
||||
})
|
||||
.setWarning();
|
||||
})
|
||||
.addOnUpdate(visibleOnly(() => this.isConfiguredAs("isConfigured", true)));
|
||||
});
|
||||
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleExtraFeatures")).then((paneEl) => {
|
||||
new Setting(paneEl).autoWireToggle("useAdvancedMode");
|
||||
|
||||
new Setting(paneEl).autoWireToggle("usePowerUserMode");
|
||||
new Setting(paneEl).autoWireToggle("useEdgeCaseMode");
|
||||
|
||||
this.addOnSaved("useAdvancedMode", () => this.display());
|
||||
this.addOnSaved("usePowerUserMode", () => this.display());
|
||||
this.addOnSaved("useEdgeCaseMode", () => this.display());
|
||||
});
|
||||
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleOnlineTips")).then((paneEl) => {
|
||||
// this.createEl(paneEl, "h3", { text: $msg("obsidianLiveSyncSettingTab.titleOnlineTips") });
|
||||
const repo = "vrtmrz/obsidian-livesync";
|
||||
const topPath = $msg("obsidianLiveSyncSettingTab.linkTroubleshooting");
|
||||
const rawRepoURI = `https://raw.githubusercontent.com/${repo}/main`;
|
||||
this.createEl(paneEl, "div", "", (el) => {
|
||||
el.createEl("a", { text: $msg("obsidianLiveSyncSettingTab.linkOpenInBrowser") }, (anchor) => {
|
||||
anchor.href = `https://github.com/${repo}/blob/main${topPath}`;
|
||||
anchor.target = "_blank";
|
||||
anchor.rel = "noopener";
|
||||
});
|
||||
});
|
||||
const troubleShootEl = this.createEl(paneEl, "div", {
|
||||
text: "",
|
||||
cls: "sls-troubleshoot-preview",
|
||||
});
|
||||
const loadMarkdownPage = async (pathAll: string, basePathParam: string = "") => {
|
||||
troubleShootEl.setCssStyles({ minHeight: troubleShootEl.clientHeight + "px" });
|
||||
troubleShootEl.empty();
|
||||
const fullPath = pathAll.startsWith("/") ? pathAll : `${basePathParam}/${pathAll}`;
|
||||
|
||||
const directoryArr = fullPath.split("/");
|
||||
const filename = directoryArr.pop();
|
||||
const directly = directoryArr.join("/");
|
||||
const basePath = directly;
|
||||
|
||||
let remoteTroubleShootMDSrc = "";
|
||||
try {
|
||||
remoteTroubleShootMDSrc = await request(`${rawRepoURI}${basePath}/${filename}`);
|
||||
} catch (ex) {
|
||||
const err = LiveSyncError.fromError(ex);
|
||||
remoteTroubleShootMDSrc = `${$msg("obsidianLiveSyncSettingTab.logErrorOccurred")}\n${err.toString()}`;
|
||||
}
|
||||
const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace(
|
||||
/\((.*?(.png)|(.jpg))\)/g,
|
||||
`(${rawRepoURI}${basePath}/$1)`
|
||||
);
|
||||
// Render markdown
|
||||
await MarkdownRenderer.render(
|
||||
this.plugin.app,
|
||||
`<a class='sls-troubleshoot-anchor'></a> [${$msg("obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting")}](${topPath}) [${$msg("obsidianLiveSyncSettingTab.linkPageTop")}](${filename})\n\n${remoteTroubleShootMD}`,
|
||||
troubleShootEl,
|
||||
`${rawRepoURI}`,
|
||||
this.lifetimeComponent
|
||||
);
|
||||
// Menu
|
||||
troubleShootEl.querySelector<HTMLAnchorElement>(".sls-troubleshoot-anchor")?.parentElement?.setCssStyles({
|
||||
position: "sticky",
|
||||
top: "-1em",
|
||||
backgroundColor: "var(--modal-background)",
|
||||
});
|
||||
// Trap internal links.
|
||||
troubleShootEl.querySelectorAll<HTMLAnchorElement>("a.internal-link").forEach((anchorEl) => {
|
||||
anchorEl.addEventListener("click", (evt) => {
|
||||
fireAndForget(async () => {
|
||||
const uri = anchorEl.getAttr("data-href");
|
||||
if (!uri) return;
|
||||
if (uri.startsWith("#")) {
|
||||
evt.preventDefault();
|
||||
const elements = Array.from(
|
||||
troubleShootEl.querySelectorAll<HTMLHeadingElement>("[data-heading]")
|
||||
);
|
||||
const p = elements.find(
|
||||
(e) =>
|
||||
e.getAttr("data-heading")?.toLowerCase().split(" ").join("-") ==
|
||||
uri.substring(1).toLowerCase()
|
||||
);
|
||||
if (p) {
|
||||
p.setCssStyles({ scrollMargin: "3em" });
|
||||
p.scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
evt.preventDefault();
|
||||
await loadMarkdownPage(uri, basePath);
|
||||
troubleShootEl.setCssStyles({ scrollMargin: "1em" });
|
||||
troubleShootEl.scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
troubleShootEl.setCssStyles({ minHeight: "" });
|
||||
};
|
||||
void loadMarkdownPage(topPath);
|
||||
});
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
import { type ObsidianLiveSyncSettings, LOG_LEVEL_NOTICE, REMOTE_COUCHDB, LEVEL_ADVANCED } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import {
|
||||
type ObsidianLiveSyncSettings,
|
||||
LOG_LEVEL_NOTICE,
|
||||
REMOTE_COUCHDB,
|
||||
LEVEL_ADVANCED,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
|
||||
import { $msg } from "@/common/translation";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import { EVENT_REQUEST_COPY_SETUP_URI, eventHub } from "@/common/events.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import type { PageFunctions } from "./SettingPane.ts";
|
||||
import { visibleOnly } from "./SettingPane.ts";
|
||||
export function paneSyncSettings(
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
paneEl: HTMLElement,
|
||||
{ addPanel, addPane }: PageFunctions
|
||||
{ addPanel }: PageFunctions
|
||||
): void {
|
||||
this.createEl(paneEl, "div", {
|
||||
text: $msg("obsidianLiveSyncSettingTab.msgSelectAndApplyPreset"),
|
||||
cls: "wizardOnly",
|
||||
}).addClasses(["op-warn-info"]);
|
||||
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleSynchronizationPreset")).then((paneEl) => {
|
||||
const options: Record<string, string> =
|
||||
this.editingSettings.remoteType == REMOTE_COUCHDB
|
||||
@@ -94,44 +93,12 @@ export function paneSyncSettings(
|
||||
};
|
||||
}
|
||||
|
||||
if (this.inWizard) {
|
||||
this.closeSetting();
|
||||
this.inWizard = false;
|
||||
if (!this.editingSettings.isConfigured) {
|
||||
this.editingSettings.isConfigured = true;
|
||||
await this.saveAllDirtySettings();
|
||||
await this.services.control.applySettings();
|
||||
await this.rebuildDB("localOnly");
|
||||
// this.resetEditingSettings();
|
||||
if (
|
||||
(await this.core.confirm.askYesNoDialog(
|
||||
$msg("obsidianLiveSyncSettingTab.msgGenerateSetupURI"),
|
||||
{
|
||||
defaultOption: "Yes",
|
||||
title: $msg("obsidianLiveSyncSettingTab.titleCongratulations"),
|
||||
}
|
||||
)) == "yes"
|
||||
) {
|
||||
eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI);
|
||||
}
|
||||
} else {
|
||||
if (this.isNeedRebuildLocal() || this.isNeedRebuildRemote()) {
|
||||
await this.confirmRebuild();
|
||||
} else {
|
||||
await this.saveAllDirtySettings();
|
||||
await this.services.control.applySettings();
|
||||
this.services.appLifecycle.askRestart();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await this.saveAllDirtySettings();
|
||||
await this.services.control.applySettings();
|
||||
}
|
||||
await this.saveAllDirtySettings();
|
||||
await this.services.control.applySettings();
|
||||
this.requestCatalogueRefresh();
|
||||
});
|
||||
});
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleSynchronizationMethod")).then((paneEl) => {
|
||||
paneEl.addClass("wizardHidden");
|
||||
|
||||
// const onlyOnLiveSync = visibleOnly(() => this.isConfiguredAs("syncMode", "LIVESYNC"));
|
||||
const onlyOnNonLiveSync = visibleOnly(() => !this.isConfiguredAs("syncMode", "LIVESYNC"));
|
||||
const onlyOnPeriodic = visibleOnly(() => this.isConfiguredAs("syncMode", "PERIODIC"));
|
||||
@@ -148,12 +115,10 @@ export function paneSyncSettings(
|
||||
PERIODIC: $msg("obsidianLiveSyncSettingTab.optionPeriodicAndEvents"),
|
||||
};
|
||||
|
||||
new Setting(paneEl)
|
||||
.autoWireDropDown("syncMode", {
|
||||
//@ts-ignore
|
||||
options: optionsSyncMode,
|
||||
})
|
||||
.setClass("wizardHidden");
|
||||
new Setting(paneEl).autoWireDropDown("syncMode", {
|
||||
//@ts-ignore
|
||||
options: optionsSyncMode,
|
||||
});
|
||||
this.addOnSaved("syncMode", async (value) => {
|
||||
this.editingSettings.liveSync = false;
|
||||
this.editingSettings.periodicReplication = false;
|
||||
@@ -165,38 +130,44 @@ export function paneSyncSettings(
|
||||
await this.saveSettings(["liveSync", "periodicReplication"]);
|
||||
|
||||
await this.services.control.applySettings();
|
||||
this.requestCatalogueRefresh();
|
||||
});
|
||||
|
||||
new Setting(paneEl)
|
||||
.autoWireNumeric("periodicReplicationInterval", {
|
||||
clampMax: 5000,
|
||||
onUpdate: onlyOnPeriodic,
|
||||
})
|
||||
.setClass("wizardHidden");
|
||||
new Setting(paneEl).autoWireNumeric("periodicReplicationInterval", {
|
||||
clampMax: 5000,
|
||||
onUpdate: onlyOnPeriodic,
|
||||
});
|
||||
|
||||
new Setting(paneEl).autoWireNumeric("syncMinimumInterval", {
|
||||
onUpdate: onlyOnNonLiveSync,
|
||||
});
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncOnSave", { onUpdate: onlyOnNonLiveSync });
|
||||
new Setting(paneEl)
|
||||
.setClass("wizardHidden")
|
||||
.autoWireToggle("syncOnEditorSave", { onUpdate: onlyOnNonLiveSync });
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncOnFileOpen", { onUpdate: onlyOnNonLiveSync });
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncOnStart", { onUpdate: onlyOnNonLiveSync });
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncAfterMerge", { onUpdate: onlyOnNonLiveSync });
|
||||
new Setting(paneEl).autoWireToggle("syncOnSave", { onUpdate: onlyOnNonLiveSync });
|
||||
new Setting(paneEl).autoWireToggle("syncOnEditorSave", { onUpdate: onlyOnNonLiveSync });
|
||||
new Setting(paneEl).autoWireToggle("syncOnFileOpen", { onUpdate: onlyOnNonLiveSync });
|
||||
new Setting(paneEl).autoWireToggle("syncOnStart", { onUpdate: onlyOnNonLiveSync });
|
||||
new Setting(paneEl).autoWireToggle("syncAfterMerge", { onUpdate: onlyOnNonLiveSync });
|
||||
for (const key of [
|
||||
"syncOnSave",
|
||||
"syncOnEditorSave",
|
||||
"syncOnFileOpen",
|
||||
"syncOnStart",
|
||||
"syncAfterMerge",
|
||||
] as const) {
|
||||
this.addOnSaved(key, () => this.requestCatalogueRefresh());
|
||||
}
|
||||
// Desktop app only, and only for the sync modes that keep a background replication channel
|
||||
// (LiveSync and Periodic). Ignored on mobile, where suspending preserves battery. The
|
||||
// visibility predicate mirrors the runtime guard in ModuleObsidianEvents.
|
||||
if (!this.services.API.isMobile()) {
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("keepReplicationActiveInBackground", {
|
||||
new Setting(paneEl).autoWireToggle("keepReplicationActiveInBackground", {
|
||||
onUpdate: visibleOnly(
|
||||
() => this.isConfiguredAs("syncMode", "LIVESYNC") || this.isConfiguredAs("syncMode", "PERIODIC")
|
||||
),
|
||||
});
|
||||
}
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("allowSleepDuringSynchronisation");
|
||||
new Setting(paneEl).autoWireToggle("allowSleepDuringSynchronisation");
|
||||
if (!this.services.API.isMobile()) {
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("allowSleepDuringSynchronisationOnDesktop");
|
||||
new Setting(paneEl).autoWireToggle("allowSleepDuringSynchronisationOnDesktop");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -206,13 +177,12 @@ export function paneSyncSettings(
|
||||
undefined,
|
||||
visibleOnly(() => !this.isConfiguredAs("syncMode", "LIVESYNC"))
|
||||
).then((paneEl) => {
|
||||
paneEl.addClass("wizardHidden");
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("batchSave");
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batchSaveMinimumDelay", {
|
||||
new Setting(paneEl).autoWireToggle("batchSave");
|
||||
new Setting(paneEl).autoWireNumeric("batchSaveMinimumDelay", {
|
||||
acceptZero: true,
|
||||
onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)),
|
||||
});
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batchSaveMaximumDelay", {
|
||||
new Setting(paneEl).autoWireNumeric("batchSaveMaximumDelay", {
|
||||
acceptZero: true,
|
||||
onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)),
|
||||
});
|
||||
@@ -225,8 +195,7 @@ export function paneSyncSettings(
|
||||
undefined,
|
||||
LEVEL_ADVANCED
|
||||
).then((paneEl) => {
|
||||
paneEl.addClass("wizardHidden");
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("doNotDeleteFolder");
|
||||
new Setting(paneEl).autoWireToggle("doNotDeleteFolder");
|
||||
});
|
||||
void addPanel(
|
||||
paneEl,
|
||||
@@ -235,13 +204,11 @@ export function paneSyncSettings(
|
||||
undefined,
|
||||
LEVEL_ADVANCED
|
||||
).then((paneEl) => {
|
||||
paneEl.addClass("wizardHidden");
|
||||
new Setting(paneEl).autoWireToggle("resolveConflictsByNewerFile");
|
||||
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("resolveConflictsByNewerFile");
|
||||
new Setting(paneEl).autoWireToggle("checkConflictOnlyOnOpen");
|
||||
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("checkConflictOnlyOnOpen");
|
||||
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("showMergeDialogOnlyOnActive");
|
||||
new Setting(paneEl).autoWireToggle("showMergeDialogOnlyOnActive");
|
||||
});
|
||||
|
||||
void addPanel(
|
||||
@@ -251,7 +218,6 @@ export function paneSyncSettings(
|
||||
undefined,
|
||||
LEVEL_ADVANCED
|
||||
).then((paneEl) => {
|
||||
paneEl.addClass("wizardHidden");
|
||||
new Setting(paneEl).autoWireText("settingSyncFile", { holdValue: true }).addApplyButton(["settingSyncFile"]);
|
||||
|
||||
new Setting(paneEl).autoWireToggle("writeCredentialsForSettingSync");
|
||||
@@ -266,32 +232,28 @@ export function paneSyncSettings(
|
||||
undefined,
|
||||
LEVEL_ADVANCED
|
||||
).then((paneEl) => {
|
||||
paneEl.addClass("wizardHidden");
|
||||
|
||||
const LABEL_ENABLED = $msg("obsidianLiveSyncSettingTab.labelEnabled");
|
||||
const LABEL_DISABLED = $msg("obsidianLiveSyncSettingTab.labelDisabled");
|
||||
|
||||
const hiddenFileSyncSetting = new Setting(paneEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameHiddenFileSynchronization"))
|
||||
.setClass("wizardHidden");
|
||||
const hiddenFileSyncSetting = new Setting(paneEl).setName(
|
||||
$msg("obsidianLiveSyncSettingTab.nameHiddenFileSynchronization")
|
||||
);
|
||||
const hiddenFileSyncSettingEl = hiddenFileSyncSetting.settingEl;
|
||||
const hiddenFileSyncSettingDiv = hiddenFileSyncSettingEl.createDiv("");
|
||||
hiddenFileSyncSettingDiv.innerText = this.editingSettings.syncInternalFiles ? LABEL_ENABLED : LABEL_DISABLED;
|
||||
if (this.editingSettings.syncInternalFiles) {
|
||||
new Setting(paneEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameDisableHiddenFileSync"))
|
||||
.setClass("wizardHidden")
|
||||
.addButton((button) => {
|
||||
button.setButtonText($msg("obsidianLiveSyncSettingTab.btnDisable")).onClick(async () => {
|
||||
this.editingSettings.syncInternalFiles = false;
|
||||
await this.saveAllDirtySettings();
|
||||
this.display();
|
||||
this.requestPageRefresh();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
new Setting(paneEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameEnableHiddenFileSync"))
|
||||
.setClass("wizardHidden")
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Merge").onClick(async () => {
|
||||
this.closeSetting();
|
||||
@@ -315,12 +277,12 @@ export function paneSyncSettings(
|
||||
});
|
||||
}
|
||||
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("suppressNotifyHiddenFilesChange", {});
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncInternalFilesBeforeReplication", {
|
||||
new Setting(paneEl).autoWireToggle("suppressNotifyHiddenFilesChange", {});
|
||||
new Setting(paneEl).autoWireToggle("syncInternalFilesBeforeReplication", {
|
||||
onUpdate: visibleOnly(() => this.isConfiguredAs("watchInternalFileChanges", true)),
|
||||
});
|
||||
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("syncInternalFilesInterval", {
|
||||
new Setting(paneEl).autoWireNumeric("syncInternalFilesInterval", {
|
||||
clampMin: 10,
|
||||
acceptZero: true,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { $msg } from "@/common/translation";
|
||||
import { LEVEL_ADVANCED, LEVEL_EDGE_CASE, LEVEL_POWER_USER, type ConfigLevel } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import {
|
||||
LEVEL_ADVANCED,
|
||||
LEVEL_EDGE_CASE,
|
||||
LEVEL_POWER_USER,
|
||||
type ConfigLevel,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { AllSettingItemKey, AllSettings } from "./settingConstants";
|
||||
import type { ButtonComponent } from "@/deps.ts";
|
||||
|
||||
export const combineOnUpdate = (func1: OnUpdateFunc, func2: OnUpdateFunc): OnUpdateFunc => {
|
||||
return () => ({
|
||||
@@ -33,6 +39,25 @@ export function setStyle(el: HTMLElement, styleHead: string, condition: () => bo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies destructive-action styling without requiring Obsidian 1.13 at
|
||||
* runtime. Older supported versions used the `mod-warning` class for the same
|
||||
* presentation.
|
||||
*/
|
||||
export function setButtonDestructiveState(button: ButtonComponent, isDestructive = true): ButtonComponent {
|
||||
const compatibleButton = button as unknown as {
|
||||
setDestructive?: () => ButtonComponent;
|
||||
removeDestructive?: () => ButtonComponent;
|
||||
};
|
||||
const updateNativeStyle = isDestructive ? compatibleButton.setDestructive : compatibleButton.removeDestructive;
|
||||
if (typeof updateNativeStyle === "function") {
|
||||
updateNativeStyle.call(button);
|
||||
} else {
|
||||
button.buttonEl.classList.toggle("mod-warning", isDestructive);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
export function visibleOnly(cond: () => boolean): OnUpdateFunc {
|
||||
return () => ({
|
||||
visibility: cond(),
|
||||
@@ -101,20 +126,28 @@ export function wrapMemo<T>(func: (arg: T) => void) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Defers pane construction until the owning settings page can confirm that its
|
||||
* render scope is still active.
|
||||
*/
|
||||
export type DeferredPageElement<T extends HTMLElement = HTMLDivElement> = {
|
||||
then(callback: (value: T) => unknown): void;
|
||||
};
|
||||
|
||||
export type PageFunctions = {
|
||||
addPane: (
|
||||
parentEl: HTMLElement,
|
||||
title: string,
|
||||
icon: string,
|
||||
order: number,
|
||||
wizardHidden: boolean,
|
||||
level?: ConfigLevel
|
||||
) => Promise<HTMLDivElement>;
|
||||
) => DeferredPageElement;
|
||||
addPanel: (
|
||||
parentEl: HTMLElement,
|
||||
title: string,
|
||||
callback?: (el: HTMLDivElement) => void,
|
||||
func?: OnUpdateFunc,
|
||||
level?: ConfigLevel
|
||||
) => Promise<HTMLDivElement>;
|
||||
) => DeferredPageElement;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { ButtonComponent } from "@/deps.ts";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { setButtonDestructiveState } from "./SettingPane.ts";
|
||||
|
||||
type CompatibleButton = ButtonComponent & {
|
||||
setDestructive?: () => ButtonComponent;
|
||||
removeDestructive?: () => ButtonComponent;
|
||||
};
|
||||
|
||||
function createButton(overrides: Partial<CompatibleButton> = {}): CompatibleButton {
|
||||
return {
|
||||
buttonEl: {
|
||||
classList: {
|
||||
toggle: vi.fn(),
|
||||
},
|
||||
},
|
||||
...overrides,
|
||||
} as unknown as CompatibleButton;
|
||||
}
|
||||
|
||||
describe("setButtonDestructiveState", () => {
|
||||
it("uses the native destructive-button API when it is available", () => {
|
||||
const setDestructive = vi.fn();
|
||||
const removeDestructive = vi.fn();
|
||||
const button = createButton({ setDestructive, removeDestructive });
|
||||
|
||||
expect(setButtonDestructiveState(button, true)).toBe(button);
|
||||
expect(setButtonDestructiveState(button, false)).toBe(button);
|
||||
|
||||
expect(setDestructive).toHaveBeenCalledOnce();
|
||||
expect(removeDestructive).toHaveBeenCalledOnce();
|
||||
expect(button.buttonEl.classList.toggle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the legacy warning class when the native API is unavailable", () => {
|
||||
const button = createButton();
|
||||
|
||||
setButtonDestructiveState(button, true);
|
||||
setButtonDestructiveState(button, false);
|
||||
|
||||
expect(button.buttonEl.classList.toggle).toHaveBeenNthCalledWith(1, "mod-warning", true);
|
||||
expect(button.buttonEl.classList.toggle).toHaveBeenNthCalledWith(2, "mod-warning", false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,249 @@
|
||||
import { statusDisplay, type ConfigurationItem } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { SettingControl, SettingDefinitionControl } from "obsidian";
|
||||
import type { AutoWireOption, OnUpdateResult } from "./SettingPane.ts";
|
||||
import type { AllBooleanItemKey, AllNumericItemKey, AllStringItemKey, OnDialogSettings } from "./settingConstants.ts";
|
||||
|
||||
export type PersistedBooleanSettingKey = Exclude<AllBooleanItemKey, keyof OnDialogSettings>;
|
||||
export type PersistedStringSettingKey = Exclude<AllStringItemKey, keyof OnDialogSettings>;
|
||||
export type PersistedNumericSettingKey = Exclude<AllNumericItemKey, keyof OnDialogSettings>;
|
||||
export type PersistedSettingKey = PersistedBooleanSettingKey | PersistedStringSettingKey | PersistedNumericSettingKey;
|
||||
|
||||
type SettingSpecBase<K extends PersistedSettingKey, C> = {
|
||||
key: K;
|
||||
control: C;
|
||||
visible?: () => boolean;
|
||||
disabled?: () => boolean;
|
||||
aliases?: string[];
|
||||
};
|
||||
|
||||
type ToggleSettingSpec = SettingSpecBase<
|
||||
PersistedBooleanSettingKey,
|
||||
{
|
||||
type: "toggle";
|
||||
defaultValue?: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
type NumberSettingSpec = SettingSpecBase<
|
||||
PersistedNumericSettingKey,
|
||||
{
|
||||
type: "number";
|
||||
min?: number;
|
||||
max?: number;
|
||||
allowZero?: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
type DropdownSettingSpec = SettingSpecBase<
|
||||
PersistedStringSettingKey,
|
||||
{
|
||||
type: "dropdown";
|
||||
options: () => Record<string, string>;
|
||||
}
|
||||
>;
|
||||
|
||||
export type SettingSpec = ToggleSettingSpec | NumberSettingSpec | DropdownSettingSpec;
|
||||
|
||||
/** An existing Advanced-page panel and the standard controls rendered within it. */
|
||||
export type SettingSpecGroup = {
|
||||
heading: string;
|
||||
items: readonly SettingSpec[];
|
||||
};
|
||||
|
||||
export type SettingSpecMessages = {
|
||||
valueShouldBeInRange: (range: { min?: number; max?: number }) => string;
|
||||
};
|
||||
|
||||
type LegacySettingOptions = Pick<AutoWireOption, "onUpdate">;
|
||||
|
||||
export type LegacySettingBinding =
|
||||
| {
|
||||
type: "toggle";
|
||||
key: PersistedBooleanSettingKey;
|
||||
options: LegacySettingOptions & Pick<AutoWireOption, "defaultToggleValue">;
|
||||
}
|
||||
| {
|
||||
type: "number";
|
||||
key: PersistedNumericSettingKey;
|
||||
options: LegacySettingOptions & {
|
||||
clampMin?: number;
|
||||
clampMax?: number;
|
||||
acceptZero?: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "dropdown";
|
||||
key: PersistedStringSettingKey;
|
||||
options: LegacySettingOptions & { options: Record<string, string> };
|
||||
};
|
||||
|
||||
export interface LegacySettingSpecRenderer {
|
||||
autoWireToggle(key: AllBooleanItemKey, options?: AutoWireOption): unknown;
|
||||
autoWireNumeric(
|
||||
key: AllNumericItemKey,
|
||||
options: AutoWireOption & { clampMin?: number; clampMax?: number; acceptZero?: boolean }
|
||||
): unknown;
|
||||
autoWireDropDown(key: AllStringItemKey, options: AutoWireOption & { options: Record<string, string> }): unknown;
|
||||
}
|
||||
|
||||
function isToggleSettingSpec(spec: SettingSpec): spec is ToggleSettingSpec {
|
||||
return spec.control.type === "toggle";
|
||||
}
|
||||
|
||||
function isNumberSettingSpec(spec: SettingSpec): spec is NumberSettingSpec {
|
||||
return spec.control.type === "number";
|
||||
}
|
||||
|
||||
function toLegacyUpdateOptions(spec: SettingSpec): LegacySettingOptions {
|
||||
if (!spec.visible && !spec.disabled) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
onUpdate: (): OnUpdateResult => ({
|
||||
...(spec.visible ? { visibility: spec.visible() } : {}),
|
||||
...(spec.disabled ? { disabled: spec.disabled() } : {}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Convert a shared specification to the arguments accepted by the current AutoWire renderer. */
|
||||
export function toLegacySettingBinding(spec: SettingSpec): LegacySettingBinding {
|
||||
const updateOptions = toLegacyUpdateOptions(spec);
|
||||
if (isToggleSettingSpec(spec)) {
|
||||
return {
|
||||
type: "toggle",
|
||||
key: spec.key,
|
||||
options: {
|
||||
...updateOptions,
|
||||
...(spec.control.defaultValue === undefined ? {} : { defaultToggleValue: spec.control.defaultValue }),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (isNumberSettingSpec(spec)) {
|
||||
return {
|
||||
type: "number",
|
||||
key: spec.key,
|
||||
options: {
|
||||
...updateOptions,
|
||||
...(spec.control.min === undefined ? {} : { clampMin: spec.control.min }),
|
||||
...(spec.control.max === undefined ? {} : { clampMax: spec.control.max }),
|
||||
...(spec.control.allowZero === undefined ? {} : { acceptZero: spec.control.allowZero }),
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: "dropdown",
|
||||
key: spec.key,
|
||||
options: {
|
||||
...updateOptions,
|
||||
options: spec.control.options(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Render one shared specification without moving value or persistence ownership out of the settings tab. */
|
||||
export function renderLegacySettingSpec(renderer: LegacySettingSpecRenderer, spec: SettingSpec): void {
|
||||
const binding = toLegacySettingBinding(spec);
|
||||
switch (binding.type) {
|
||||
case "toggle":
|
||||
renderer.autoWireToggle(binding.key, binding.options);
|
||||
return;
|
||||
case "number":
|
||||
renderer.autoWireNumeric(binding.key, binding.options);
|
||||
return;
|
||||
case "dropdown":
|
||||
renderer.autoWireDropDown(binding.key, binding.options);
|
||||
}
|
||||
}
|
||||
|
||||
function numberIsOutOfRange(value: number, control: NumberSettingSpec["control"]) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return true;
|
||||
}
|
||||
if (control.max !== undefined && value > control.max) {
|
||||
return true;
|
||||
}
|
||||
if (control.allowZero && value === 0) {
|
||||
return false;
|
||||
}
|
||||
return control.min !== undefined && value < control.min;
|
||||
}
|
||||
|
||||
/** Check a value at the settings-tab persistence boundary against its shared control specification. */
|
||||
export function isValidSettingSpecValue(spec: SettingSpec, value: unknown): boolean {
|
||||
switch (spec.control.type) {
|
||||
case "toggle":
|
||||
return typeof value === "boolean";
|
||||
case "number":
|
||||
return typeof value === "number" && !numberIsOutOfRange(value, spec.control);
|
||||
case "dropdown":
|
||||
return typeof value === "string" && Object.prototype.hasOwnProperty.call(spec.control.options(), value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert one shared specification to a declarative Obsidian control.
|
||||
*
|
||||
* This function returns a plain object and only refers to Obsidian through erased types, so Stage B does not load or
|
||||
* activate the 1.13 runtime API. Translated metadata and validation messages are explicit inputs to keep conversion
|
||||
* side-effect free.
|
||||
*/
|
||||
export function toObsidianSettingDefinition(
|
||||
spec: SettingSpec,
|
||||
metadata: Pick<ConfigurationItem, "name" | "desc" | "placeHolder" | "status">,
|
||||
messages: SettingSpecMessages
|
||||
): SettingDefinitionControl<PersistedSettingKey> {
|
||||
let control: SettingControl<PersistedSettingKey>;
|
||||
switch (spec.control.type) {
|
||||
case "toggle":
|
||||
control = {
|
||||
type: "toggle",
|
||||
key: spec.key,
|
||||
...(spec.control.defaultValue === undefined ? {} : { defaultValue: spec.control.defaultValue }),
|
||||
...(spec.disabled ? { disabled: spec.disabled } : {}),
|
||||
};
|
||||
break;
|
||||
case "number": {
|
||||
const numericControl = spec.control;
|
||||
const hasRange = numericControl.min !== undefined || numericControl.max !== undefined;
|
||||
control = {
|
||||
type: "number",
|
||||
key: spec.key,
|
||||
...(numericControl.min === undefined
|
||||
? {}
|
||||
: { min: numericControl.allowZero ? Math.min(0, numericControl.min) : numericControl.min }),
|
||||
...(numericControl.max === undefined ? {} : { max: numericControl.max }),
|
||||
...(metadata.placeHolder ? { placeholder: metadata.placeHolder } : {}),
|
||||
...(hasRange
|
||||
? {
|
||||
validate: (value: number) =>
|
||||
numberIsOutOfRange(value, numericControl)
|
||||
? messages.valueShouldBeInRange({
|
||||
min: numericControl.min,
|
||||
max: numericControl.max,
|
||||
})
|
||||
: undefined,
|
||||
}
|
||||
: {}),
|
||||
...(spec.disabled ? { disabled: spec.disabled } : {}),
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "dropdown":
|
||||
control = {
|
||||
type: "dropdown",
|
||||
key: spec.key,
|
||||
options: spec.control.options(),
|
||||
...(spec.disabled ? { disabled: spec.disabled } : {}),
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
name: `${metadata.name}${statusDisplay(metadata.status)}`,
|
||||
...(metadata.desc ? { desc: metadata.desc } : {}),
|
||||
...(spec.aliases ? { aliases: spec.aliases } : {}),
|
||||
...(spec.visible ? { visible: spec.visible } : {}),
|
||||
control,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import { describe, expect, expectTypeOf, it, vi } from "vitest";
|
||||
import { statusDisplay, type ConfigurationItem } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { getConfig, type OnDialogSettings } from "./settingConstants.ts";
|
||||
import {
|
||||
renderLegacySettingSpec,
|
||||
toLegacySettingBinding,
|
||||
toObsidianSettingDefinition,
|
||||
type PersistedSettingKey,
|
||||
type SettingSpec,
|
||||
} from "./SettingSpec.ts";
|
||||
import { createAdvancedSettingSpecGroups } from "./AdvancedSettingSpecs.ts";
|
||||
import { createExtraMenuSettingSpecGroup, createGeneralSettingSpecGroups } from "./GeneralSettingSpecs.ts";
|
||||
|
||||
const rangeMessages = {
|
||||
valueShouldBeInRange: ({ min, max }: { min?: number; max?: number }) => `${min ?? "~"}..${max ?? "~"}`,
|
||||
};
|
||||
|
||||
describe("Advanced setting specifications", () => {
|
||||
it("lists only the nine controls currently exposed by the Advanced page", () => {
|
||||
const groups = createAdvancedSettingSpecGroups({ isCouchDB: () => true });
|
||||
|
||||
expect(groups.map(({ heading }) => heading)).toEqual([
|
||||
"Memory cache",
|
||||
"Local Database Tweak",
|
||||
"Transfer Tweak",
|
||||
"Remote Database Tweak",
|
||||
]);
|
||||
expect(groups.flatMap(({ items }) => items.map(({ key }) => key))).toEqual([
|
||||
"hashCacheMaxCount",
|
||||
"chunkSplitterVersion",
|
||||
"customChunkSize",
|
||||
"readChunksOnline",
|
||||
"useOnlyLocalChunk",
|
||||
"concurrencyOfReadChunksOnline",
|
||||
"minimumIntervalOfReadChunksOnline",
|
||||
"autoAcceptCompatibleTweak",
|
||||
"enableCompression",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps CouchDB visibility on the four remote chunk controls", () => {
|
||||
let couchDB = false;
|
||||
const groups = createAdvancedSettingSpecGroups({ isCouchDB: () => couchDB });
|
||||
const conditional = groups.flatMap(({ items }) => items).filter(({ visible }) => visible !== undefined);
|
||||
|
||||
expect(conditional.map(({ key }) => key)).toEqual([
|
||||
"readChunksOnline",
|
||||
"useOnlyLocalChunk",
|
||||
"concurrencyOfReadChunksOnline",
|
||||
"minimumIntervalOfReadChunksOnline",
|
||||
]);
|
||||
expect(conditional.every(({ visible }) => visible?.() === false)).toBe(true);
|
||||
|
||||
couchDB = true;
|
||||
expect(conditional.every(({ visible }) => visible?.() === true)).toBe(true);
|
||||
});
|
||||
|
||||
it("excludes transient settings-dialogue keys from the persisted key type", () => {
|
||||
type OnDialogOverlap = Extract<keyof OnDialogSettings, PersistedSettingKey>;
|
||||
expectTypeOf<OnDialogOverlap>().toEqualTypeOf<never>();
|
||||
});
|
||||
|
||||
it("resolves translated Commonlib metadata for every explicitly exposed key", () => {
|
||||
const specs = createAdvancedSettingSpecGroups({ isCouchDB: () => true }).flatMap(({ items }) => items);
|
||||
|
||||
for (const spec of specs) {
|
||||
const metadata = getConfig(spec.key);
|
||||
expect(metadata, spec.key).not.toBe(false);
|
||||
if (!metadata) {
|
||||
throw new Error(`Missing setting metadata for ${spec.key}`);
|
||||
}
|
||||
const definition = toObsidianSettingDefinition(spec, metadata, rangeMessages);
|
||||
expect(definition.name).toBe(`${metadata.name}${statusDisplay(metadata.status)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("General setting specifications", () => {
|
||||
it("shares every General and Logging control between the imperative and declarative renderers", () => {
|
||||
const groups = createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails: () => true,
|
||||
showVerboseLog: () => true,
|
||||
});
|
||||
|
||||
expect(groups.map(({ heading }) => heading)).toEqual(["Appearance", "Logging"]);
|
||||
expect(groups.flatMap(({ items }) => items.map(({ key }) => key))).toEqual([
|
||||
"displayLanguage",
|
||||
"showStatusOnEditor",
|
||||
"showOnlyIconsOnEditor",
|
||||
"showStatusOnStatusbar",
|
||||
"hideFileWarningNotice",
|
||||
"networkWarningStyle",
|
||||
"lessInformationInLog",
|
||||
"showVerboseLog",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the three feature-level controls together under Extra menus", () => {
|
||||
const group = createExtraMenuSettingSpecGroup();
|
||||
|
||||
expect(group.heading).toBe("Extra menus");
|
||||
expect(group.items.map(({ key }) => key)).toEqual(["useAdvancedMode", "usePowerUserMode", "useEdgeCaseMode"]);
|
||||
});
|
||||
|
||||
it("retains the two conditional visibility rules", () => {
|
||||
let editorDetails = false;
|
||||
let verboseLog = false;
|
||||
const specs = createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails: () => editorDetails,
|
||||
showVerboseLog: () => verboseLog,
|
||||
}).flatMap(({ items }) => items);
|
||||
const editorIcons = specs.find(({ key }) => key === "showOnlyIconsOnEditor");
|
||||
const verbose = specs.find(({ key }) => key === "showVerboseLog");
|
||||
|
||||
expect(editorIcons?.visible?.()).toBe(false);
|
||||
expect(verbose?.visible?.()).toBe(false);
|
||||
editorDetails = true;
|
||||
verboseLog = true;
|
||||
expect(editorIcons?.visible?.()).toBe(true);
|
||||
expect(verbose?.visible?.()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SettingSpec conversion", () => {
|
||||
it("maps metadata and a toggle to an Obsidian definition without importing the runtime API", () => {
|
||||
const visible = vi.fn(() => true);
|
||||
const disabled = vi.fn(() => false);
|
||||
const spec: SettingSpec = {
|
||||
key: "autoAcceptCompatibleTweak",
|
||||
control: { type: "toggle", defaultValue: true },
|
||||
aliases: ["compatible tweaks"],
|
||||
visible,
|
||||
disabled,
|
||||
};
|
||||
const metadata = {
|
||||
name: "Automatic compatibility",
|
||||
desc: "Accept compatible values.",
|
||||
status: "BETA",
|
||||
} satisfies ConfigurationItem;
|
||||
|
||||
expect(toObsidianSettingDefinition(spec, metadata, rangeMessages)).toEqual({
|
||||
name: `Automatic compatibility${statusDisplay("BETA")}`,
|
||||
desc: "Accept compatible values.",
|
||||
aliases: ["compatible tweaks"],
|
||||
visible,
|
||||
control: {
|
||||
type: "toggle",
|
||||
key: "autoAcceptCompatibleTweak",
|
||||
defaultValue: true,
|
||||
disabled,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("maps dropdown options to both native and legacy representations", () => {
|
||||
const options = {
|
||||
"v3-rabin-karp": "V3",
|
||||
legacy: "Legacy",
|
||||
};
|
||||
const spec: SettingSpec = {
|
||||
key: "chunkSplitterVersion",
|
||||
control: { type: "dropdown", options: () => options },
|
||||
};
|
||||
const metadata = {
|
||||
name: "Chunk splitter",
|
||||
placeHolder: "Select a splitter",
|
||||
} satisfies ConfigurationItem;
|
||||
|
||||
expect(toObsidianSettingDefinition(spec, metadata, rangeMessages).control).toEqual({
|
||||
type: "dropdown",
|
||||
key: "chunkSplitterVersion",
|
||||
options,
|
||||
});
|
||||
expect(toLegacySettingBinding(spec)).toEqual({
|
||||
type: "dropdown",
|
||||
key: "chunkSplitterVersion",
|
||||
options: { options },
|
||||
});
|
||||
});
|
||||
|
||||
it("maps number limits and zero exceptions to equivalent validation", () => {
|
||||
const spec: SettingSpec = {
|
||||
key: "hashCacheMaxCount",
|
||||
control: { type: "number", min: 10, max: 100, allowZero: true },
|
||||
};
|
||||
const metadata = { name: "Cache size" } satisfies ConfigurationItem;
|
||||
const definition = toObsidianSettingDefinition(spec, metadata, rangeMessages);
|
||||
const control = definition.control;
|
||||
|
||||
expect(control).toMatchObject({
|
||||
type: "number",
|
||||
key: "hashCacheMaxCount",
|
||||
min: 0,
|
||||
max: 100,
|
||||
});
|
||||
if (control.type !== "number") {
|
||||
throw new Error("Expected a number control");
|
||||
}
|
||||
expect(control.validate?.(0)).toBeUndefined();
|
||||
expect(control.validate?.(10)).toBeUndefined();
|
||||
expect(control.validate?.(5)).toBe("10..100");
|
||||
expect(control.validate?.(101)).toBe("10..100");
|
||||
expect(toLegacySettingBinding(spec)).toEqual({
|
||||
type: "number",
|
||||
key: "hashCacheMaxCount",
|
||||
options: { clampMin: 10, clampMax: 100, acceptZero: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("checks the maximum before applying a zero exception, as the legacy renderer does", () => {
|
||||
const spec: SettingSpec = {
|
||||
key: "hashCacheMaxCount",
|
||||
control: { type: "number", max: -1, allowZero: true },
|
||||
};
|
||||
const definition = toObsidianSettingDefinition(spec, { name: "Cache size" }, rangeMessages);
|
||||
const control = definition.control;
|
||||
if (control.type !== "number") {
|
||||
throw new Error("Expected a number control");
|
||||
}
|
||||
|
||||
expect(control.validate?.(0)).toBe("~..-1");
|
||||
});
|
||||
|
||||
it("dispatches a specification through the existing AutoWire renderer", () => {
|
||||
const renderer = {
|
||||
autoWireToggle: vi.fn(),
|
||||
autoWireNumeric: vi.fn(),
|
||||
autoWireDropDown: vi.fn(),
|
||||
};
|
||||
const visible = vi.fn(() => false);
|
||||
const spec: SettingSpec = {
|
||||
key: "readChunksOnline",
|
||||
control: { type: "toggle" },
|
||||
visible,
|
||||
};
|
||||
|
||||
renderLegacySettingSpec(renderer, spec);
|
||||
|
||||
expect(renderer.autoWireToggle).toHaveBeenCalledOnce();
|
||||
const [, options] = renderer.autoWireToggle.mock.calls[0];
|
||||
expect(options.onUpdate()).toEqual({ visibility: false });
|
||||
expect(visible).toHaveBeenCalledOnce();
|
||||
expect(renderer.autoWireNumeric).not.toHaveBeenCalled();
|
||||
expect(renderer.autoWireDropDown).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,270 @@
|
||||
import { $msg } from "@/common/translation";
|
||||
import {
|
||||
LEVEL_ADVANCED,
|
||||
LEVEL_EDGE_CASE,
|
||||
LEVEL_POWER_USER,
|
||||
type ConfigLevel,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { SettingDefinitionGroup } from "obsidian";
|
||||
import { createAdvancedSettingSpecGroups, type AdvancedSettingSpecContext } from "./AdvancedSettingSpecs.ts";
|
||||
import { toObsidianSettingDefinition, type PersistedSettingKey, type SettingSpec } from "./SettingSpec.ts";
|
||||
import { getConfig } from "./settingConstants.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import type { PageFunctions } from "./SettingPane.ts";
|
||||
import {
|
||||
createExtraMenuSettingSpecGroup,
|
||||
createGeneralSettingSpecGroups,
|
||||
type GeneralSettingSpecContext,
|
||||
} from "./GeneralSettingSpecs.ts";
|
||||
import { paneAdvanced } from "./PaneAdvanced.ts";
|
||||
import { paneChangeLog } from "./PaneChangeLog.ts";
|
||||
import { paneCustomisationSync } from "./PaneCustomisationSync.ts";
|
||||
import { paneGeneral } from "./PaneGeneral.ts";
|
||||
import { paneHatch } from "./PaneHatch.ts";
|
||||
import { paneMaintenance } from "./PaneMaintenance.ts";
|
||||
import { paneHelp } from "./PaneHelp.ts";
|
||||
import { panePatches } from "./PanePatches.ts";
|
||||
import { panePowerUsers } from "./PanePowerUsers.ts";
|
||||
import { paneRemoteConfig } from "./PaneRemoteConfig.ts";
|
||||
import { paneSelector } from "./PaneSelector.ts";
|
||||
import { paneQuickSetup } from "./PaneQuickSetup.ts";
|
||||
import { paneSyncSettings } from "./PaneSyncSettings.ts";
|
||||
|
||||
/** The existing pane renderer used by the imperative settings tab and custom pages. */
|
||||
export type SettingsPaneRenderer = (
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
paneEl: HTMLElement,
|
||||
functions: PageFunctions
|
||||
) => void;
|
||||
|
||||
export type SettingsPageContent = "native" | "custom";
|
||||
|
||||
/** One established settings page, shared by the imperative and declarative renderers. */
|
||||
export type SettingsPageEntry = {
|
||||
id: string;
|
||||
name: () => string;
|
||||
icon: string;
|
||||
order: number;
|
||||
level?: ConfigLevel;
|
||||
content: SettingsPageContent;
|
||||
legacy: SettingsPaneRenderer;
|
||||
};
|
||||
|
||||
export type SettingsRootGroupEntry = {
|
||||
icon: string;
|
||||
name: () => string;
|
||||
};
|
||||
|
||||
/** Root groups used only by Obsidian's declarative settings landing page. */
|
||||
const SETTINGS_ROOT_GROUP_CATALOGUE = {
|
||||
"quick-setup": {
|
||||
icon: "🧙♂️",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.titleQuickSetup"),
|
||||
},
|
||||
synchronisation: {
|
||||
icon: "🔄",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.titleSynchronisation"),
|
||||
},
|
||||
"general-settings": {
|
||||
icon: "⚙️",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.panelGeneralSettings"),
|
||||
},
|
||||
"setup-other-devices": {
|
||||
icon: "📲",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.titleSetupOtherDevices"),
|
||||
},
|
||||
"maintenance-and-recovery": {
|
||||
icon: "🛠️",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.titleMaintenanceAndRecovery"),
|
||||
},
|
||||
"extra-features": {
|
||||
icon: "🧩",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.titleExtraFeaturesGroup"),
|
||||
},
|
||||
"advanced-settings": {
|
||||
icon: "🔧",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.titleAdvancedSettings"),
|
||||
},
|
||||
"help-and-information": {
|
||||
icon: "ℹ️",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.titleHelpAndInformation"),
|
||||
},
|
||||
} as const satisfies Record<string, SettingsRootGroupEntry>;
|
||||
|
||||
export type SettingsRootGroupId = keyof typeof SETTINGS_ROOT_GROUP_CATALOGUE;
|
||||
|
||||
/** Resolve a late-translated root-group label while keeping its icon separate until the Obsidian API boundary. */
|
||||
export function getSettingsRootGroupEntry(id: SettingsRootGroupId): SettingsRootGroupEntry {
|
||||
return SETTINGS_ROOT_GROUP_CATALOGUE[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the explicit page list in the same order as the existing settings tab.
|
||||
*
|
||||
* Names remain functions so a catalogue refresh observes the current language,
|
||||
* while constructing the catalogue itself performs no rendering or persistence.
|
||||
*/
|
||||
export function createSettingsPageCatalogue(): SettingsPageEntry[] {
|
||||
return [
|
||||
{
|
||||
id: "change-log",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.panelChangeLog"),
|
||||
icon: "💬",
|
||||
order: 100,
|
||||
level: undefined,
|
||||
content: "custom",
|
||||
legacy: paneChangeLog,
|
||||
},
|
||||
{
|
||||
id: "quick-setup",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.titleQuickSetup"),
|
||||
icon: "🧙♂️",
|
||||
order: 110,
|
||||
level: undefined,
|
||||
content: "custom",
|
||||
legacy: paneQuickSetup,
|
||||
},
|
||||
{
|
||||
id: "general",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.panelGeneralSettings"),
|
||||
icon: "⚙️",
|
||||
order: 20,
|
||||
level: undefined,
|
||||
content: "custom",
|
||||
legacy: paneGeneral,
|
||||
},
|
||||
{
|
||||
id: "remote-configuration",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.panelRemoteConfiguration"),
|
||||
icon: "🛰️",
|
||||
order: 0,
|
||||
level: undefined,
|
||||
content: "custom",
|
||||
legacy: paneRemoteConfig,
|
||||
},
|
||||
{
|
||||
id: "synchronisation",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.titleSyncSettings"),
|
||||
icon: "🔄",
|
||||
order: 30,
|
||||
level: undefined,
|
||||
content: "custom",
|
||||
legacy: paneSyncSettings,
|
||||
},
|
||||
{
|
||||
id: "selector",
|
||||
name: () => "Selector",
|
||||
icon: "🚦",
|
||||
order: 33,
|
||||
level: LEVEL_ADVANCED,
|
||||
content: "custom",
|
||||
legacy: paneSelector,
|
||||
},
|
||||
{
|
||||
id: "customisation-sync",
|
||||
name: () => "Customisation sync",
|
||||
icon: "🔌",
|
||||
order: 60,
|
||||
level: LEVEL_ADVANCED,
|
||||
content: "custom",
|
||||
legacy: paneCustomisationSync,
|
||||
},
|
||||
{
|
||||
id: "hatch",
|
||||
name: () => "Hatch",
|
||||
icon: "🧰",
|
||||
order: 50,
|
||||
level: undefined,
|
||||
content: "custom",
|
||||
legacy: paneHatch,
|
||||
},
|
||||
{
|
||||
id: "advanced",
|
||||
name: () => "Advanced",
|
||||
icon: "🔧",
|
||||
order: 46,
|
||||
level: LEVEL_ADVANCED,
|
||||
content: "native",
|
||||
legacy: paneAdvanced,
|
||||
},
|
||||
{
|
||||
id: "power-users",
|
||||
name: () => "Power users",
|
||||
icon: "💪",
|
||||
order: 47,
|
||||
level: LEVEL_POWER_USER,
|
||||
content: "custom",
|
||||
legacy: panePowerUsers,
|
||||
},
|
||||
{
|
||||
id: "patches",
|
||||
name: () => "Patches",
|
||||
icon: "🩹",
|
||||
order: 51,
|
||||
level: LEVEL_EDGE_CASE,
|
||||
content: "custom",
|
||||
legacy: panePatches,
|
||||
},
|
||||
{
|
||||
id: "maintenance",
|
||||
name: () => "Maintenance",
|
||||
icon: "🎛️",
|
||||
order: 70,
|
||||
level: undefined,
|
||||
content: "custom",
|
||||
legacy: paneMaintenance,
|
||||
},
|
||||
{
|
||||
id: "help",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.titleHelpAndTroubleshooting"),
|
||||
icon: "❓",
|
||||
order: 90,
|
||||
level: undefined,
|
||||
content: "custom",
|
||||
legacy: paneHelp,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const numberRangeMessage = ({ min, max }: { min?: number; max?: number }): string =>
|
||||
$msg("liveSyncSetting.valueShouldBeInRange", {
|
||||
min: min === undefined ? "~" : `${min}`,
|
||||
max: max === undefined ? "~" : `${max}`,
|
||||
});
|
||||
|
||||
function toSettingDefinition(spec: SettingSpec): ReturnType<typeof toObsidianSettingDefinition> {
|
||||
const metadata = getConfig(spec.key);
|
||||
if (!metadata) {
|
||||
throw new Error(`Missing translated setting metadata for ${spec.key}`);
|
||||
}
|
||||
return toObsidianSettingDefinition(spec, metadata, {
|
||||
valueShouldBeInRange: numberRangeMessage,
|
||||
});
|
||||
}
|
||||
|
||||
/** Convert the existing Advanced specifications to native Obsidian groups. */
|
||||
export function createAdvancedSettingDefinitionGroups(
|
||||
context: AdvancedSettingSpecContext
|
||||
): SettingDefinitionGroup<PersistedSettingKey>[] {
|
||||
return createAdvancedSettingSpecGroups(context).map((group) => ({
|
||||
type: "group",
|
||||
heading: group.heading,
|
||||
items: group.items.map(toSettingDefinition),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Convert the shared General specifications to native Obsidian groups. */
|
||||
export function createGeneralSettingDefinitionGroups(
|
||||
context: GeneralSettingSpecContext
|
||||
): SettingDefinitionGroup<PersistedSettingKey>[] {
|
||||
return createGeneralSettingSpecGroups(context).map((group) => ({
|
||||
type: "group",
|
||||
heading: group.heading,
|
||||
items: group.items.map(toSettingDefinition),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Convert the Extra menus feature-level controls to native Obsidian settings. */
|
||||
export function createExtraMenuSettingDefinitions(): ReturnType<typeof toObsidianSettingDefinition>[] {
|
||||
return createExtraMenuSettingSpecGroup().items.map(toSettingDefinition);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/common/translation", () => ({
|
||||
$msg: (key: string) => key,
|
||||
translateLiveSyncMessage: (key: string) => key,
|
||||
}));
|
||||
vi.mock("./PaneChangeLog.ts", () => ({ paneChangeLog: vi.fn() }));
|
||||
vi.mock("./PaneQuickSetup.ts", () => ({ paneQuickSetup: vi.fn() }));
|
||||
vi.mock("./PaneHelp.ts", () => ({ paneHelp: vi.fn() }));
|
||||
vi.mock("./PaneGeneral.ts", () => ({ paneGeneral: vi.fn() }));
|
||||
vi.mock("./PaneRemoteConfig.ts", () => ({ paneRemoteConfig: vi.fn() }));
|
||||
vi.mock("./PaneSelector.ts", () => ({ paneSelector: vi.fn() }));
|
||||
vi.mock("./PaneSyncSettings.ts", () => ({ paneSyncSettings: vi.fn() }));
|
||||
vi.mock("./PaneCustomisationSync.ts", () => ({ paneCustomisationSync: vi.fn() }));
|
||||
vi.mock("./PaneHatch.ts", () => ({ paneHatch: vi.fn() }));
|
||||
vi.mock("./PaneAdvanced.ts", () => ({ paneAdvanced: vi.fn() }));
|
||||
vi.mock("./PanePowerUsers.ts", () => ({ panePowerUsers: vi.fn() }));
|
||||
vi.mock("./PanePatches.ts", () => ({ panePatches: vi.fn() }));
|
||||
vi.mock("./PaneMaintenance.ts", () => ({ paneMaintenance: vi.fn() }));
|
||||
|
||||
import {
|
||||
createAdvancedSettingDefinitionGroups,
|
||||
createSettingsPageCatalogue,
|
||||
getSettingsRootGroupEntry,
|
||||
type SettingsRootGroupId,
|
||||
} from "./SettingsPageCatalogue.ts";
|
||||
|
||||
const EXPECTED_ROOT_GROUPS = [
|
||||
["quick-setup", "🧙♂️", "obsidianLiveSyncSettingTab.titleQuickSetup"],
|
||||
["synchronisation", "🔄", "obsidianLiveSyncSettingTab.titleSynchronisation"],
|
||||
["general-settings", "⚙️", "obsidianLiveSyncSettingTab.panelGeneralSettings"],
|
||||
["setup-other-devices", "📲", "obsidianLiveSyncSettingTab.titleSetupOtherDevices"],
|
||||
["maintenance-and-recovery", "🛠️", "obsidianLiveSyncSettingTab.titleMaintenanceAndRecovery"],
|
||||
["extra-features", "🧩", "obsidianLiveSyncSettingTab.titleExtraFeaturesGroup"],
|
||||
["advanced-settings", "🔧", "obsidianLiveSyncSettingTab.titleAdvancedSettings"],
|
||||
["help-and-information", "ℹ️", "obsidianLiveSyncSettingTab.titleHelpAndInformation"],
|
||||
] as const satisfies readonly (readonly [SettingsRootGroupId, string, string])[];
|
||||
|
||||
describe("settings page catalogue", () => {
|
||||
it("keeps each declarative root-group icon separate from its late-translated name", () => {
|
||||
expect(
|
||||
EXPECTED_ROOT_GROUPS.map(([id]) => {
|
||||
const { icon, name } = getSettingsRootGroupEntry(id);
|
||||
return [id, icon, name()];
|
||||
})
|
||||
).toEqual(EXPECTED_ROOT_GROUPS);
|
||||
});
|
||||
|
||||
it("registers every existing page once and keeps only Advanced native", () => {
|
||||
const catalogue = createSettingsPageCatalogue();
|
||||
|
||||
expect(catalogue.map(({ id }) => id)).toEqual([
|
||||
"change-log",
|
||||
"quick-setup",
|
||||
"general",
|
||||
"remote-configuration",
|
||||
"synchronisation",
|
||||
"selector",
|
||||
"customisation-sync",
|
||||
"hatch",
|
||||
"advanced",
|
||||
"power-users",
|
||||
"patches",
|
||||
"maintenance",
|
||||
"help",
|
||||
]);
|
||||
expect(new Set(catalogue.map(({ id }) => id)).size).toBe(catalogue.length);
|
||||
expect(new Set(catalogue.map(({ name }) => name())).size).toBe(catalogue.length);
|
||||
expect(catalogue.filter(({ content }) => content === "native").map(({ id }) => id)).toEqual(["advanced"]);
|
||||
expect(catalogue.filter(({ content }) => content === "custom")).toHaveLength(12);
|
||||
expect(catalogue.find(({ id }) => id === "quick-setup")?.name()).toBe(
|
||||
"obsidianLiveSyncSettingTab.titleQuickSetup"
|
||||
);
|
||||
expect(catalogue.find(({ id }) => id === "help")?.name()).toBe(
|
||||
"obsidianLiveSyncSettingTab.titleHelpAndTroubleshooting"
|
||||
);
|
||||
});
|
||||
|
||||
it("registers each Advanced control key exactly once", () => {
|
||||
const groups = createAdvancedSettingDefinitionGroups({ isCouchDB: () => true });
|
||||
const keys = groups.flatMap(({ items = [] }) =>
|
||||
items.flatMap((item) => ("control" in item && item.control ? [item.control.key] : []))
|
||||
);
|
||||
|
||||
expect(keys).toEqual([
|
||||
"hashCacheMaxCount",
|
||||
"chunkSplitterVersion",
|
||||
"customChunkSize",
|
||||
"readChunksOnline",
|
||||
"useOnlyLocalChunk",
|
||||
"concurrencyOfReadChunksOnline",
|
||||
"minimumIntervalOfReadChunksOnline",
|
||||
"autoAcceptCompatibleTweak",
|
||||
"enableCompression",
|
||||
]);
|
||||
expect(new Set(keys).size).toBe(keys.length);
|
||||
});
|
||||
});
|
||||
@@ -1 +1,37 @@
|
||||
export * from "@vrtmrz/livesync-commonlib/compat/common/settingConstants";
|
||||
export {
|
||||
AllSettingDefault,
|
||||
OnDialogSettingsDefault,
|
||||
SettingInformation,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/settingConstants";
|
||||
export type {
|
||||
AllSettings,
|
||||
AllSettingItemKey,
|
||||
AllStringItemKey,
|
||||
AllNumericItemKey,
|
||||
AllBooleanItemKey,
|
||||
OnDialogSettings,
|
||||
ValueOf,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/settingConstants";
|
||||
|
||||
import {
|
||||
getConfig as getCommonlibConfig,
|
||||
getConfName as getCommonlibConfName,
|
||||
type AllSettingItemKey,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/settingConstants";
|
||||
import type { MessageTranslator } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { translateLiveSyncMessage } from "@/common/translation";
|
||||
|
||||
// Commonlib defaults `translate` to its English-only translator, so every caller which omits
|
||||
// it silently renders English regardless of `displayLanguage`. Default it to the LiveSync
|
||||
// catalogue instead, and re-export these wrappers under the original names so that no call
|
||||
// site has to remember the second argument.
|
||||
|
||||
/** `getConfig` with the LiveSync catalogue applied by default. */
|
||||
export function getConfig(key: AllSettingItemKey, translate: MessageTranslator = translateLiveSyncMessage) {
|
||||
return getCommonlibConfig(key, translate);
|
||||
}
|
||||
|
||||
/** `getConfName` with the LiveSync catalogue applied by default. See `getConfig`. */
|
||||
export function getConfName(key: AllSettingItemKey, translate: MessageTranslator = translateLiveSyncMessage) {
|
||||
return getCommonlibConfName(key, translate);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { setLang } from "@/common/translation";
|
||||
import { getConfig, getConfName } from "./settingConstants";
|
||||
|
||||
describe("setting manifest labels", () => {
|
||||
afterEach(() => setLang("def"));
|
||||
|
||||
it("renders names and descriptions in the selected display language", () => {
|
||||
setLang("zh-tw");
|
||||
|
||||
expect(getConfName("liveSync")).toBe("同步模式");
|
||||
expect(getConfig("couchDB_URI")).toMatchObject({ name: "伺服器 URI" });
|
||||
expect(getConfig("encrypt")).toMatchObject({
|
||||
name: "端對端加密",
|
||||
desc: "加密遠端資料庫中的內容。如果你使用外掛的同步功能,建議啟用此選項。",
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves English untouched, so that the catalogue key and its English value stay interchangeable", () => {
|
||||
expect(getConfName("liveSync")).toBe("Sync Mode");
|
||||
expect(getConfig("encrypt")).toMatchObject({
|
||||
name: "End-to-End Encryption",
|
||||
desc: "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes through labels which Commonlib owns but the catalogue does not carry", () => {
|
||||
setLang("zh-tw");
|
||||
|
||||
expect(getConfName("chunkSplitterVersion")).toBe("Chunk Splitter");
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,7 @@ import UseSetupURI from "./SetupWizard/dialogs/UseSetupURI.svelte";
|
||||
import OutroNewUser from "./SetupWizard/dialogs/OutroNewUser.svelte";
|
||||
import OutroExistingUser from "./SetupWizard/dialogs/OutroExistingUser.svelte";
|
||||
import OutroAskUserMode from "./SetupWizard/dialogs/OutroAskUserMode.svelte";
|
||||
import ApplySettingsInitialisation from "./SetupWizard/dialogs/ApplySettingsInitialisation.svelte";
|
||||
import SetupRemote from "./SetupWizard/dialogs/SetupRemote.svelte";
|
||||
import SetupRemoteCouchDB from "./SetupWizard/dialogs/SetupRemoteCouchDB.svelte";
|
||||
import SetupRemoteBucket from "./SetupWizard/dialogs/SetupRemoteBucket.svelte";
|
||||
@@ -38,10 +39,13 @@ import type {
|
||||
SetupRemoteP2PResultType,
|
||||
SetupRemoteResultType,
|
||||
UseSetupURIResultType,
|
||||
ApplySettingsInitialisationResultType,
|
||||
ApplySettingsInitialisationInitialData,
|
||||
} from "./SetupWizard/dialogs/setupDialogTypes.ts";
|
||||
import {
|
||||
applySettingsAndFetchOnActivation,
|
||||
applySettingsWithScheduledInitialisation,
|
||||
type SetupInitialisationMode,
|
||||
} from "@/serviceFeatures/setupObsidian/setupActivationLifecycle.ts";
|
||||
import { isP2PMainRemote } from "@/common/remoteConfiguration.ts";
|
||||
|
||||
@@ -75,6 +79,17 @@ export const enum UserMode {
|
||||
Update = "unknown", // Alias for Unknown for better readability
|
||||
}
|
||||
|
||||
export type SettingsInitialisationApplicationResult =
|
||||
| { result: "scheduled"; mode: SetupInitialisationMode }
|
||||
| { result: "cancelled" }
|
||||
| { result: "failed"; mode: SetupInitialisationMode };
|
||||
|
||||
export type ApplySettingsWithInitialisationChoiceOptions = {
|
||||
applySettings: () => Promise<void>;
|
||||
isP2P: boolean;
|
||||
validateChoice?: (mode: SetupInitialisationMode) => Promise<boolean>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup Manager to handle onboarding and configuration setup
|
||||
*/
|
||||
@@ -87,6 +102,32 @@ export class SetupManager extends AbstractModule {
|
||||
return this.services.UI.dialogManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask which existing data should be authoritative for pending setting changes,
|
||||
* then reserve the matching next-start operation before applying them.
|
||||
*
|
||||
* Cancellation and reservation failure remain distinct so the caller may
|
||||
* offer an explicit settings-only fallback only after a user cancellation.
|
||||
*/
|
||||
async applySettingsWithInitialisationChoice({
|
||||
applySettings,
|
||||
isP2P,
|
||||
validateChoice = () => Promise.resolve(true),
|
||||
}: ApplySettingsWithInitialisationChoiceOptions): Promise<SettingsInitialisationApplicationResult> {
|
||||
const mode = await this.dialogManager.openWithExplicitCancel<
|
||||
ApplySettingsInitialisationResultType,
|
||||
ApplySettingsInitialisationInitialData
|
||||
>(ApplySettingsInitialisation, { isP2P });
|
||||
if (mode === "cancelled") {
|
||||
return { result: "cancelled" };
|
||||
}
|
||||
if (!(await validateChoice(mode))) {
|
||||
return { result: "failed", mode };
|
||||
}
|
||||
const scheduled = await applySettingsWithScheduledInitialisation(this.core.rebuilder, mode, applySettings);
|
||||
return scheduled ? { result: "scheduled", mode } : { result: "failed", mode };
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the onboarding process
|
||||
* @returns Promise that resolves to true if onboarding completed successfully, false otherwise
|
||||
|
||||
@@ -17,6 +17,7 @@ vi.mock("./SetupWizard/dialogs/UseSetupURI.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/OutroNewUser.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/OutroExistingUser.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/OutroAskUserMode.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/ApplySettingsInitialisation.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/SetupRemote.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/SetupRemoteCouchDB.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/SetupRemoteBucket.svelte", () => ({ default: {} }));
|
||||
@@ -273,6 +274,64 @@ describe("SetupManager", () => {
|
||||
expect(setting.currentSettings().isConfigured).toBe(false);
|
||||
});
|
||||
|
||||
it("reports cancellation separately when applying settings which require initialisation", async () => {
|
||||
const { manager, dialogManager, core } = createSetupManager();
|
||||
const applySettings = vi.fn(async () => undefined);
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValueOnce("cancelled");
|
||||
|
||||
const result = await manager.applySettingsWithInitialisationChoice({ applySettings, isP2P: true });
|
||||
|
||||
expect(result).toEqual({ result: "cancelled" });
|
||||
expect(dialogManager.openWithExplicitCancel).toHaveBeenCalledWith(expect.anything(), { isP2P: true });
|
||||
expect(core.rebuilder.scheduleFetch).not.toHaveBeenCalled();
|
||||
expect(core.rebuilder.scheduleRebuild).not.toHaveBeenCalled();
|
||||
expect(applySettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reserves the selected initialisation before applying pending settings", async () => {
|
||||
const { manager, dialogManager, core } = createSetupManager();
|
||||
const applySettings = vi.fn(async () => undefined);
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValueOnce("fetch");
|
||||
|
||||
const result = await manager.applySettingsWithInitialisationChoice({ applySettings, isP2P: false });
|
||||
|
||||
expect(result).toEqual({ result: "scheduled", mode: "fetch" });
|
||||
expect(core.rebuilder.scheduleFetch).toHaveBeenCalledWith(expect.any(Function));
|
||||
expect(core.rebuilder.scheduleFetch.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
applySettings.mock.invocationCallOrder[0]
|
||||
);
|
||||
});
|
||||
|
||||
it("reports a failed reservation without applying pending settings", async () => {
|
||||
const { manager, dialogManager, core } = createSetupManager();
|
||||
const applySettings = vi.fn(async () => undefined);
|
||||
core.rebuilder.scheduleRebuild.mockResolvedValueOnce(false);
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValueOnce("rebuild");
|
||||
|
||||
const result = await manager.applySettingsWithInitialisationChoice({ applySettings, isP2P: false });
|
||||
|
||||
expect(result).toEqual({ result: "failed", mode: "rebuild" });
|
||||
expect(applySettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not reserve initialisation when the selected source cannot be validated", async () => {
|
||||
const { manager, dialogManager, core } = createSetupManager();
|
||||
const applySettings = vi.fn(async () => undefined);
|
||||
const validateChoice = vi.fn(async () => false);
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValueOnce("fetch");
|
||||
|
||||
const result = await manager.applySettingsWithInitialisationChoice({
|
||||
applySettings,
|
||||
isP2P: false,
|
||||
validateChoice,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ result: "failed", mode: "fetch" });
|
||||
expect(validateChoice).toHaveBeenCalledWith("fetch");
|
||||
expect(core.rebuilder.scheduleFetch).not.toHaveBeenCalled();
|
||||
expect(applySettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves modern profiles, display names, and the active selection from a Setup URI", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
const imported = {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import DialogHeader from "@/modules/services/LiveSyncUI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/modules/services/LiveSyncUI/components/Guidance.svelte";
|
||||
import Decision from "@/modules/services/LiveSyncUI/components/Decision.svelte";
|
||||
import Question from "@/modules/services/LiveSyncUI/components/Question.svelte";
|
||||
import Option from "@/modules/services/LiveSyncUI/components/Option.svelte";
|
||||
import Instruction from "@/modules/services/LiveSyncUI/components/Instruction.svelte";
|
||||
import UserDecisions from "@/modules/services/LiveSyncUI/components/UserDecisions.svelte";
|
||||
import InfoNote from "@/modules/services/LiveSyncUI/components/InfoNote.svelte";
|
||||
import { $msg as msg } from "@/common/translation";
|
||||
import {
|
||||
type ApplySettingsInitialisationInitialData,
|
||||
type ApplySettingsInitialisationResultType,
|
||||
TYPE_CANCELLED,
|
||||
TYPE_FETCH,
|
||||
TYPE_REBUILD,
|
||||
} from "./setupDialogTypes";
|
||||
|
||||
type Props = {
|
||||
setResult: (result: ApplySettingsInitialisationResultType) => void;
|
||||
getInitialData?: () => ApplySettingsInitialisationInitialData | undefined;
|
||||
};
|
||||
const { setResult, getInitialData }: Props = $props();
|
||||
const isP2P = $derived(getInitialData?.()?.isP2P === true);
|
||||
let selectedMode = $state<ApplySettingsInitialisationResultType>(TYPE_CANCELLED);
|
||||
const canProceed = $derived(selectedMode === TYPE_FETCH || selectedMode === TYPE_REBUILD);
|
||||
const proceedMessage = $derived.by(() => {
|
||||
if (selectedMode === TYPE_FETCH) {
|
||||
return isP2P
|
||||
? msg("Ui.SetupWizard.ApplySettingsInitialisation.ProceedFetchP2P")
|
||||
: msg("Ui.SetupWizard.ApplySettingsInitialisation.ProceedFetch");
|
||||
}
|
||||
if (selectedMode === TYPE_REBUILD) {
|
||||
return isP2P
|
||||
? msg("Ui.SetupWizard.ApplySettingsInitialisation.ProceedRebuildP2P")
|
||||
: msg("Ui.SetupWizard.ApplySettingsInitialisation.ProceedRebuild");
|
||||
}
|
||||
return msg("Ui.SetupWizard.Common.ProceedSelectOption");
|
||||
});
|
||||
</script>
|
||||
|
||||
<DialogHeader title={msg("Ui.SetupWizard.ApplySettingsInitialisation.Title")} />
|
||||
<Guidance>
|
||||
<p>{msg("Ui.SetupWizard.ApplySettingsInitialisation.Guidance")}</p>
|
||||
</Guidance>
|
||||
<Instruction>
|
||||
<Question>{msg("Ui.SetupWizard.ApplySettingsInitialisation.Question")}</Question>
|
||||
<Option
|
||||
title={msg("Ui.SetupWizard.ApplySettingsInitialisation.FetchOption")}
|
||||
bind:value={selectedMode}
|
||||
selectedValue={TYPE_FETCH}
|
||||
>
|
||||
<InfoNote notice>
|
||||
{isP2P
|
||||
? msg("Ui.SetupWizard.ApplySettingsInitialisation.FetchOptionP2PDesc")
|
||||
: msg("Ui.SetupWizard.ApplySettingsInitialisation.FetchOptionDesc")}
|
||||
</InfoNote>
|
||||
</Option>
|
||||
<Option
|
||||
title={isP2P
|
||||
? msg("Ui.SetupWizard.ApplySettingsInitialisation.RebuildOptionP2P")
|
||||
: msg("Ui.SetupWizard.ApplySettingsInitialisation.RebuildOption")}
|
||||
bind:value={selectedMode}
|
||||
selectedValue={TYPE_REBUILD}
|
||||
>
|
||||
<InfoNote warning={!isP2P} notice={isP2P}>
|
||||
{isP2P
|
||||
? msg("Ui.SetupWizard.ApplySettingsInitialisation.RebuildOptionP2PDesc")
|
||||
: msg("Ui.SetupWizard.ApplySettingsInitialisation.RebuildOptionDesc")}
|
||||
</InfoNote>
|
||||
</Option>
|
||||
</Instruction>
|
||||
<UserDecisions>
|
||||
<Decision
|
||||
title={proceedMessage}
|
||||
important={selectedMode !== TYPE_REBUILD || isP2P}
|
||||
destructive={selectedMode === TYPE_REBUILD && !isP2P}
|
||||
disabled={!canProceed}
|
||||
commit={() => setResult(selectedMode)}
|
||||
/>
|
||||
<Decision
|
||||
title={msg("Ui.SetupWizard.ApplySettingsInitialisation.Back")}
|
||||
commit={() => setResult(TYPE_CANCELLED)}
|
||||
/>
|
||||
</UserDecisions>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user