diff --git a/docs/adr/2026_07_common_library_package_boundary.md b/docs/adr/2026_07_common_library_package_boundary.md index 43be51da..24c90cea 100644 --- a/docs/adr/2026_07_common_library_package_boundary.md +++ b/docs/adr/2026_07_common_library_package_boundary.md @@ -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. diff --git a/docs/adr/2026_07_feature_maturity_for_1_0.md b/docs/adr/2026_07_feature_maturity_for_1_0.md index c1668947..a4117d7c 100644 --- a/docs/adr/2026_07_feature_maturity_for_1_0.md +++ b/docs/adr/2026_07_feature_maturity_for_1_0.md @@ -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 diff --git a/docs/adr/2026_08_declarative_settings_adapter.md b/docs/adr/2026_08_declarative_settings_adapter.md new file mode 100644 index 00000000..7d06dfcc --- /dev/null +++ b/docs/adr/2026_08_declarative_settings_adapter.md @@ -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; +type PersistedStringSettingKey = Exclude; +type PersistedNumericSettingKey = Exclude; +type PersistedSettingKey = PersistedBooleanSettingKey | PersistedStringSettingKey | PersistedNumericSettingKey; + +type SettingSpecBase = { + key: K; + control: C; + visible?: () => boolean; + disabled?: () => boolean; + aliases?: string[]; +}; + +type SettingSpec = + | SettingSpecBase + | SettingSpecBase< + PersistedNumericSettingKey, + { + type: "number"; + min?: number; + max?: number; + allowZero?: boolean; + } + > + | SettingSpecBase< + PersistedStringSettingKey, + { + type: "dropdown"; + options: () => Record; + } + >; +``` + +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 diff --git a/docs/settings.md b/docs/settings.md index 830c7e66..3bfe759a 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -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 @@ -1085,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 diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a87c0e93..14d40344 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -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. diff --git a/package-lock.json b/package-lock.json index 874dbfa7..dc7a66d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", "@vrtmrz/livesync-commonlib": "0.1.19", - "@vrtmrz/obsidian-plugin-kit": "0.1.3", + "@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", @@ -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", @@ -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", diff --git a/package.json b/package.json index 7c1ee602..6ce3a4c0 100644 --- a/package.json +++ b/package.json @@ -179,7 +179,7 @@ "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", "@vrtmrz/livesync-commonlib": "0.1.19", - "@vrtmrz/obsidian-plugin-kit": "0.1.3", + "@vrtmrz/obsidian-plugin-kit": "0.1.4", "@vrtmrz/ui-interactions": "0.1.2", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", diff --git a/src/apps/cli/package.json b/src/apps/cli/package.json index caf1143b..7bb12afc 100644 --- a/src/apps/cli/package.json +++ b/src/apps/cli/package.json @@ -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", diff --git a/src/common/compatibilitySettings.ts b/src/common/compatibilitySettings.ts new file mode 100644 index 00000000..5197ade5 --- /dev/null +++ b/src/common/compatibilitySettings.ts @@ -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; +} diff --git a/src/common/compatibilitySettings.unit.spec.ts b/src/common/compatibilitySettings.unit.spec.ts new file mode 100644 index 00000000..8482ff56 --- /dev/null +++ b/src/common/compatibilitySettings.unit.spec.ts @@ -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 }); + }); +}); diff --git a/src/common/events.ts b/src/common/events.ts index 0f68a9ac..911cc2ca 100644 --- a/src/common/events.ts +++ b/src/common/events.ts @@ -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; diff --git a/src/common/messages/combinedMessages.prod.ts b/src/common/messages/combinedMessages.prod.ts index 7cc161e3..968bb4c1 100644 --- a/src/common/messages/combinedMessages.prod.ts +++ b/src/common/messages/combinedMessages.prod.ts @@ -4206,16 +4206,6 @@ export const allMessages: Readonly\n圖䞋\n\n| įŦĻ號 | åĢ意 |\n|: ------ :| ------- |\n| ⇔ | åˇ˛æ˜¯æœ€æ–° |\n| ⇄ | 同æ­ĨäģĨå–åž—ä¸€č‡´ |\n| ⇐,⇒ | 傺čŧ¸ä¸Ļčφå¯Ģ |\n| ⇠,â‡ĸ | åžžåĻ一į̝傺čŧ¸ä¸Ļčφå¯Ģ |\n\n\n\n## ${OPTION_REBUILD_BOTH}\næĻ‚čĻŊīŧš 📄 ⇒¹ đŸ’ģ ⇒² đŸ›°ī¸ â‡ĸâŋ đŸ’ģ ⇄âŋâēš 📄\näģĨæ­¤čŖįŊŽä¸Šįžæœ‰įš„æĒ”æĄˆé‡åģ翜ŦæŠŸčˆ‡é įĢ¯čŗ‡æ–™åēĢ。\n這會鎖厚å…ļäģ–čŖįŊŽīŧŒåŽƒå€‘åŋ…é ˆåŸˇčĄŒæŠ“å–ã€‚\n## ${OPTION_FETCH}\næĻ‚čĻŊīŧš 📄 ⇄² đŸ’ģ ⇐¹ đŸ›°ī¸ ⇔ đŸ’ģ ⇔ 📄\n初始化æœŦæŠŸčŗ‡æ–™åēĢīŧŒä¸ĻäģĨ垞遠įĢ¯čŗ‡æ–™åēĢæŠ“å–įš„čŗ‡æ–™é‡åģē。\n這也æļĩ蓋äŊ åˇ˛é‡åģē遠įĢ¯čŗ‡æ–™åēĢįš„æƒ…æŗã€‚\n## ${OPTION_ONLY_SETTING}\nåƒ…å„˛å­˜č¨­åŽšã€‚**æŗ¨æ„īŧšé€™å¯čƒŊå°Žč‡´čŗ‡æ–™ææ¯€**īŧ›é€šå¸¸äģéœ€čĻé‡åģēčŗ‡æ–™åēĢ。", }, - "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\nLegends\n\n| Symbol | Meaning |\n|: ------ :| ------- |\n| ⇔ | Up to Date |\n| ⇄ | Synchronise to balance |\n| ⇐,⇒ | Transfer to overwrite |\n| ⇠,â‡ĸ | Transfer to overwrite from other side |\n\n\n\n## ${OPTION_REBUILD_BOTH}\nAt a glance: 📄 ⇒¹ đŸ’ģ ⇒² đŸ›°ī¸ â‡ĸâŋ đŸ’ģ ⇄âŋâēš 📄\nReconstruct both the local and remote databases using existing files from this device.\nThis causes a lockout other devices, and they need to perform fetching.\n## ${OPTION_FETCH}\nAt a glance: 📄 ⇄² đŸ’ģ ⇐¹ đŸ›°ī¸ ⇔ đŸ’ģ ⇔ 📄\nInitialise the local database and reconstruct it using data fetched from the remote database.\nThis case includes the case which you have rebuilt the remote database.\n## ${OPTION_ONLY_SETTING}\nStore only the settings. **Caution: This may lead to data corruption**; database reconstruction is generally necessary.", - "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", diff --git a/src/common/messagesJson/es.json b/src/common/messagesJson/es.json index 957b1b60..ea7ea6d4 100644 --- a/src/common/messagesJson/es.json +++ b/src/common/messagesJson/es.json @@ -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
\nLegendas\n\n| Símbolo | Significado |\n|: ------ :| ------- |\n| ⇔ | Actualizado |\n| ⇄ | Sincronizar para equilibrar |\n| ⇐,⇒ | Transferir para sobrescribir |\n| ⇠,â‡ĸ | Transferir para sobrescribir desde otro lado |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\nA simple vista: 📄 ⇒¹ đŸ’ģ ⇒² đŸ›°ī¸ â‡ĸâŋ đŸ’ģ ⇄âŋâēš 📄\nReconstruir tanto la base de datos local como la remota utilizando los archivos existentes de este dispositivo.\nEsto bloquea a otros dispositivos, y necesitan realizar la obtenciÃŗn.\n## ${OPTION_FETCH}\nA simple vista: 📄 ⇄² đŸ’ģ ⇐¹ đŸ›°ī¸ ⇔ đŸ’ģ ⇔ 📄\nInicializa la base de datos local y la reconstruye utilizando los datos obtenidos de la base de datos remota.\nEste caso incluye el caso en el que has reconstruido la base de datos remota.\n## ${OPTION_ONLY_SETTING}\nAlmacena solo la configuraciÃŗn. **PrecauciÃŗn: esto puede provocar corrupciÃŗn de datos**; generalmente es necesario reconstruir la base de datos.", - "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", diff --git a/src/common/messagesJson/fr.json b/src/common/messagesJson/fr.json index 72351b2e..9893c2e3 100644 --- a/src/common/messagesJson/fr.json +++ b/src/common/messagesJson/fr.json @@ -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
\nLÊgende\n\n| Symbole | Signification |\n|: ------ :| ------- |\n| ⇔ | À jour |\n| ⇄ | Synchroniser pour Êquilibrer |\n| ⇐,⇒ | TransfÊrer pour Êcraser |\n| ⇠,â‡ĸ | TransfÊrer pour Êcraser depuis l'autre côtÊ |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\nEn bref : 📄 ⇒¹ đŸ’ģ ⇒² đŸ›°ī¸ â‡ĸâŋ đŸ’ģ ⇄âŋâēš 📄\nReconstruit les bases locale et distante à partir des fichiers existants de cet appareil.\nCeci provoque un verrouillage des autres appareils, qui devront effectuer une rÊcupÊration.\n## ${OPTION_FETCH}\nEn bref : 📄 ⇄² đŸ’ģ ⇐¹ đŸ›°ī¸ ⇔ đŸ’ģ ⇔ 📄\nInitialise la base locale et la reconstruit à partir des donnÊes rÊcupÊrÊes depuis la base distante.\nCe cas inclut Êgalement celui oÚ vous avez reconstruit la base distante.\n## ${OPTION_ONLY_SETTING}\nNe stocker que les paramètres. **Attention : cela peut entraÃŽner une corruption des donnÊes** ; une reconstruction de la base est gÊnÊralement nÊcessaire.", - "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Ê", diff --git a/src/common/messagesJson/he.json b/src/common/messagesJson/he.json index b95b9b8d..0d70dea6 100644 --- a/src/common/messagesJson/he.json +++ b/src/common/messagesJson/he.json @@ -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
\nמקרא\n\n| סמל | מ׊מ×ĸו×Ē |\n|: ------ :| ------- |\n| ⇔ | מ×ĸודכן |\n| ⇄ | סנכרן לאיזון |\n| ⇐,⇒ | ה×ĸבר לדריסה |\n| ⇠,â‡ĸ | ה×ĸבר לדריסה מה×Ļד השני |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\nבמבט: 📄 ⇒¹ đŸ’ģ ⇒² đŸ›°ī¸ â‡ĸâŋ đŸ’ģ ⇄âŋâēš 📄\nבנה מחדש גם א×Ē ×ž×Ą×“ הנ×Ēונים המקומי וגם המרוחד ×Ēוך שימוש בקב×Ļים קיימים ממכשיר זה.\nפ×ĸולה זו ×Ē× ×ĸל מכשירים אחרים שי×Ļטרכו לב×Ļ×ĸ משיכה.\n## ${OPTION_FETCH}\nבמבט: 📄 ⇄² đŸ’ģ ⇐¹ đŸ›°ī¸ ⇔ đŸ’ģ ⇔ 📄\nא×Ēחל א×Ē ×ž×Ą×“ הנ×Ēונים המקומי ובנה או×Ēו מחדש ×Ēוך שימוש בנ×Ēונים שנמשכו ממסד הנ×Ēונים המרוחד.\nכולל א×Ē ×”×ž×§×¨×” שבו בני×Ē ×ž×—×“×Š א×Ē ×ž×Ą×“ הנ×Ēונים המרוחד.\n## ${OPTION_ONLY_SETTING}\nשמור רק א×Ē ×”×”×’×“×¨×•×Ē. **זהירו×Ē: ×ĸלול לגרום לפגי×ĸה בנ×Ēונים**; בנייה מחדש של מסד הנ×Ēונים נדרש×Ē ×‘×“×¨×š כלל.", - "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": "הה×Ļפנה אינה מופ×ĸל×Ē", diff --git a/src/common/messagesJson/ja.json b/src/common/messagesJson/ja.json index d2aff276..e3a06dcd 100644 --- a/src/common/messagesJson/ja.json +++ b/src/common/messagesJson/ja.json @@ -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
\nå‡Ąäž‹\n\n| č¨˜åˇ | æ„å‘ŗ |\n|: ------ :| ------- |\n| ⇔ | 最新 |\n| ⇄ | 同期しãĻãƒãƒŠãƒŗã‚šã‚’å–ã‚‹ |\n| ⇐,⇒ | 上書きするためčģĸ送 |\n| ⇠,â‡ĸ | 反寞側から上書きするためčģĸ送 |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\næĻ‚čρ: 📄 ⇒¹ đŸ’ģ ⇒² đŸ›°ī¸ â‡ĸâŋ đŸ’ģ ⇄âŋâēš 📄\nこぎデバイ゚ぎæ—ĸå­˜ãƒ•ã‚Ąã‚¤ãƒĢをäŊŋį”¨ã—ãĻロãƒŧã‚ĢãƒĢとãƒĒãƒĸãƒŧãƒˆãŽä¸Ąæ–šãŽãƒ‡ãƒŧã‚ŋベãƒŧã‚šã‚’å†æ§‹į¯‰ã—ãžã™ã€‚\näģ–ぎデバイ゚はロックã‚ĸã‚Ļトされ、フェッチがåŋ…čĻã§ã™ã€‚\n## ${OPTION_FETCH}\næĻ‚čρ: 📄 ⇄² đŸ’ģ ⇐¹ đŸ›°ī¸ ⇔ đŸ’ģ ⇔ 📄\nロãƒŧã‚ĢãƒĢデãƒŧã‚ŋベãƒŧ゚を初期化し、ãƒĒãƒĸãƒŧトデãƒŧã‚ŋベãƒŧ゚から取垗したデãƒŧã‚ŋをäŊŋį”¨ã—ãĻå†æ§‹į¯‰ã—ãžã™ã€‚\nãƒĒãƒĸãƒŧトデãƒŧã‚ŋベãƒŧã‚šã‚’å†æ§‹į¯‰ã—ãŸå ´åˆã‚‚åĢぞれぞす。\n## ${OPTION_ONLY_SETTING}\nč¨­åŽšãŽãŋをäŋå­˜ã—ぞす。**æŗ¨æ„: デãƒŧã‚ŋį ´æãĢつãĒがる可čƒŊ性がありぞす**。通常、デãƒŧã‚ŋベãƒŧã‚šãŽå†æ§‹į¯‰ãŒåŋ…čĻã§ã™ã€‚", - "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": "æš—åˇåŒ–ãŒæœ‰åŠšãĢãĒãŖãĻいぞせん", diff --git a/src/common/messagesJson/ko.json b/src/common/messagesJson/ko.json index a838b374..e07771f1 100644 --- a/src/common/messagesJson/ko.json +++ b/src/common/messagesJson/ko.json @@ -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___다만 ė´ ëŗ€ę˛Ŋ 뤑 ė–´ëŠ 하나ëŧ도 ė ėšŠí•˜ë ¤ëŠ´ ė›ę˛Šęŗŧ 로ėģŦ ë°ė´í„°ë˛ ė´ėŠ¤ëĨŧ ëĒ¨ë‘ ėžŦęĩŦėļ•í•´ė•ŧ 합니다. ė´ ęŗŧė •ė€ ëLJ ëļ„ė´ 깸ëĻŦë¯€ëĄœ ė‹œę°„ė´ ėļŠëļ„í•  때 ė§„í–‰í•˜ė‹œę¸°ëĨŧ ęļŒėžĨ합니다.___\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
\n범례\n\n| 기호 | ė˜ë¯¸ |\n|: ------ :| ------- |\n| ⇔ | ėĩœė‹  ėƒíƒœ |\n| ⇄ | ė–‘ėĒŊė„ 맞ėļ”는 동기화 |\n| ⇐,⇒ | ëŽė–´ė“°ę¸° ė „ė†Ą |\n| ⇠,â‡ĸ | ë°˜ëŒ€íŽ¸ė—ė„œ ëŽė–´ė“°ę¸° ė „ė†Ą |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\ní•œëˆˆė— ëŗ´ę¸°: 📄 ⇒¹ đŸ’ģ ⇒² đŸ›°ī¸ â‡ĸâŋ đŸ’ģ ⇄âŋâēš 📄\nė´ ę¸°ę¸°ė˜ ę¸°ėĄ´ 파ėŧė„ ė‚ŦėšŠí•´ 로ėģŦęŗŧ ė›ę˛Š ë°ė´í„°ë˛ ė´ėŠ¤ëĨŧ ëĒ¨ë‘ ėžŦęĩŦėļ•핊니다.\nė´ ę˛Ŋ뚰 다ëĨ¸ 기기는 ėž ę¸°ëŠ°, ę°€ė ¸ė˜¤ę¸°ëĨŧ ėˆ˜í–‰í•´ė•ŧ 합니다.\n## ${OPTION_FETCH}\ní•œëˆˆė— ëŗ´ę¸°: 📄 ⇄² đŸ’ģ ⇐¹ đŸ›°ī¸ ⇔ đŸ’ģ ⇔ 📄\n로ėģŦ ë°ė´í„°ë˛ ė´ėŠ¤ëĨŧ ė´ˆę¸°í™”í•œ 뒤, ė›ę˛Š ë°ė´í„°ë˛ ė´ėŠ¤ė—ė„œ ę°€ė ¸ė˜¨ ë°ė´í„°ëĄœ ėžŦęĩŦėļ•핊니다.\nė›ę˛Š ë°ė´í„°ë˛ ė´ėŠ¤ëĨŧ ė´ë¯¸ ėžŦęĩŦėļ•한 ę˛Ŋėš°ë„ ė—Ŧ揰뗐 해당합니다.\n## ${OPTION_ONLY_SETTING}\nė„¤ė •ë§Œ ė €ėžĨ합니다. **ėŖŧė˜: ë°ė´í„°ę°€ ė†ėƒë  눘 ėžˆėŠĩ니다.** ėŧë°˜ė ėœŧ로는 ë°ė´í„°ë˛ ė´ėŠ¤ ėžŦęĩŦėļ•ė´ í•„ėš”í•Šë‹ˆë‹¤.", - "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": "ė•”í˜¸í™”ę°€ í™œė„ąí™”ë˜ė§€ ė•ŠėŒ", diff --git a/src/common/messagesJson/ru.json b/src/common/messagesJson/ru.json index 6712d66d..f1035446 100644 --- a/src/common/messagesJson/ru.json +++ b/src/common/messagesJson/ru.json @@ -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": "Đ¨Đ¸Ņ„Ņ€ĐžĐ˛Đ°ĐŊиĐĩ ĐŊĐĩ вĐēĐģŅŽŅ‡ĐĩĐŊĐž", diff --git a/src/common/messagesJson/zh-tw.json b/src/common/messagesJson/zh-tw.json index ee4d0bac..cca440d1 100644 --- a/src/common/messagesJson/zh-tw.json +++ b/src/common/messagesJson/zh-tw.json @@ -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
\n圖䞋\n\n| įŦĻ號 | åĢ意 |\n|: ------ :| ------- |\n| ⇔ | åˇ˛æ˜¯æœ€æ–° |\n| ⇄ | 同æ­ĨäģĨå–åž—ä¸€č‡´ |\n| ⇐,⇒ | 傺čŧ¸ä¸Ļčφå¯Ģ |\n| ⇠,â‡ĸ | åžžåĻ一į̝傺čŧ¸ä¸Ļčφå¯Ģ |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\næĻ‚čĻŊīŧš 📄 ⇒¹ đŸ’ģ ⇒² đŸ›°ī¸ â‡ĸâŋ đŸ’ģ ⇄âŋâēš 📄\näģĨæ­¤čŖįŊŽä¸Šįžæœ‰įš„æĒ”æĄˆé‡åģ翜ŦæŠŸčˆ‡é įĢ¯čŗ‡æ–™åēĢ。\n這會鎖厚å…ļäģ–čŖįŊŽīŧŒåŽƒå€‘åŋ…é ˆåŸˇčĄŒæŠ“å–ã€‚\n## ${OPTION_FETCH}\næĻ‚čĻŊīŧš 📄 ⇄² đŸ’ģ ⇐¹ đŸ›°ī¸ ⇔ đŸ’ģ ⇔ 📄\n初始化æœŦæŠŸčŗ‡æ–™åēĢīŧŒä¸ĻäģĨ垞遠įĢ¯čŗ‡æ–™åēĢæŠ“å–įš„čŗ‡æ–™é‡åģē。\n這也æļĩ蓋äŊ åˇ˛é‡åģē遠įĢ¯čŗ‡æ–™åēĢįš„æƒ…æŗã€‚\n## ${OPTION_ONLY_SETTING}\nåƒ…å„˛å­˜č¨­åŽšã€‚**æŗ¨æ„īŧšé€™å¯čƒŊå°Žč‡´čŗ‡æ–™ææ¯€**īŧ›é€šå¸¸äģéœ€čĻé‡åģēčŗ‡æ–™åēĢ。", - "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": "尚æœĒå•Ÿį”¨åŠ å¯†", diff --git a/src/common/messagesJson/zh.json b/src/common/messagesJson/zh.json index 0a238406..6dd85520 100644 --- a/src/common/messagesJson/zh.json +++ b/src/common/messagesJson/zh.json @@ -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
\n回䞋\n\n| įŦĻåˇ | åĢ义 |\n|: ------ :| ------- |\n| ⇔ | 最新 |\n| ⇄ | 同æ­ĨäģĨåšŗčĄĄ |\n| ⇐,⇒ | äŧ čž“äģĨčφᛖ |\n| ⇠,â‡ĸ | äģŽåĻ一侧äŧ čž“äģĨčφᛖ |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\næĻ‚č§ˆ:📄 ⇒¹ đŸ’ģ ⇒² đŸ›°ī¸ â‡ĸâŋ đŸ’ģ ⇄âŋâēš 📄\näŊŋį”¨æ­¤čŽžå¤‡įš„įŽ°æœ‰æ–‡äģļ重åģ翜Ŧ地和čŋœį¨‹æ•°æŽåē“。\nčŋ™å°†å¯ŧ致å…ļäģ–čŽžå¤‡čĸĢ锁厚,åšļ且厃äģŦ需čĻæ‰§čĄŒčŽˇå–æ“äŊœã€‚\n## ${OPTION_FETCH}\næĻ‚č§ˆ:📄 ⇄² đŸ’ģ ⇐¹ đŸ›°ī¸ ⇔ đŸ’ģ ⇔ 📄\n初始化æœŦ地数捎åē“åšļäŊŋᔍäģŽčŋœį¨‹æ•°æŽåē“čŽˇå–įš„æ•°æŽé‡åģē厃。\nčŋ™į§æƒ…å†ĩ包æ‹Ŧæ‚¨åˇ˛įģé‡åģēäē†čŋœį¨‹æ•°æŽåē“įš„æƒ…å†ĩ。\n## ${OPTION_ONLY_SETTING}\näģ…å­˜å‚¨čŽžįŊŽã€‚**æŗ¨æ„īŧščŋ™å¯čƒŊå¯ŧč‡´æ•°æŽæŸå**īŧ›é€šå¸¸éœ€čĻé‡åģēæ•°æŽåē“", - "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": "尚æœĒå¯į”¨åŠ å¯†", diff --git a/src/common/messagesYAML/de.yaml b/src/common/messagesYAML/de.yaml index 5472df4b..58620d54 100644 --- a/src/common/messagesYAML/de.yaml +++ b/src/common/messagesYAML/de.yaml @@ -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 diff --git a/src/common/messagesYAML/en.yaml b/src/common/messagesYAML/en.yaml index 844842c5..e6ad4908 100644 --- a/src/common/messagesYAML/en.yaml +++ b/src/common/messagesYAML/en.yaml @@ -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 diff --git a/src/common/messagesYAML/es.yaml b/src/common/messagesYAML/es.yaml index 6b79fddc..8d0eaa64 100644 --- a/src/common/messagesYAML/es.yaml +++ b/src/common/messagesYAML/es.yaml @@ -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 diff --git a/src/common/messagesYAML/fr.yaml b/src/common/messagesYAML/fr.yaml index 09d5b990..ee3901d8 100644 --- a/src/common/messagesYAML/fr.yaml +++ b/src/common/messagesYAML/fr.yaml @@ -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Ê diff --git a/src/common/messagesYAML/he.yaml b/src/common/messagesYAML/he.yaml index e25cdd2b..63f22bc2 100644 --- a/src/common/messagesYAML/he.yaml +++ b/src/common/messagesYAML/he.yaml @@ -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: הה×Ļפנה אינה מופ×ĸל×Ē diff --git a/src/common/messagesYAML/ja.yaml b/src/common/messagesYAML/ja.yaml index fb6eb738..edd5c97c 100644 --- a/src/common/messagesYAML/ja.yaml +++ b/src/common/messagesYAML/ja.yaml @@ -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: æš—åˇåŒ–ãŒæœ‰åŠšãĢãĒãŖãĻいぞせん diff --git a/src/common/messagesYAML/ko.yaml b/src/common/messagesYAML/ko.yaml index 3058a1e0..096de218 100644 --- a/src/common/messagesYAML/ko.yaml +++ b/src/common/messagesYAML/ko.yaml @@ -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: ė•”í˜¸í™”ę°€ í™œė„ąí™”ë˜ė§€ ė•ŠėŒ diff --git a/src/common/messagesYAML/ru.yaml b/src/common/messagesYAML/ru.yaml index 665f84b9..09333033 100644 --- a/src/common/messagesYAML/ru.yaml +++ b/src/common/messagesYAML/ru.yaml @@ -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: Đ¨Đ¸Ņ„Ņ€ĐžĐ˛Đ°ĐŊиĐĩ ĐŊĐĩ вĐēĐģŅŽŅ‡ĐĩĐŊĐž diff --git a/src/common/messagesYAML/zh-tw.yaml b/src/common/messagesYAML/zh-tw.yaml index 36a9ba89..14b5a53b 100644 --- a/src/common/messagesYAML/zh-tw.yaml +++ b/src/common/messagesYAML/zh-tw.yaml @@ -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: 尚æœĒå•Ÿį”¨åŠ å¯† diff --git a/src/common/messagesYAML/zh.yaml b/src/common/messagesYAML/zh.yaml index 8510a62b..0b345fc6 100644 --- a/src/common/messagesYAML/zh.yaml +++ b/src/common/messagesYAML/zh.yaml @@ -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: 尚æœĒå¯į”¨åŠ å¯† diff --git a/src/common/translation.unit.spec.ts b/src/common/translation.unit.spec.ts index f2da8cee..00ce480f 100644 --- a/src/common/translation.unit.spec.ts +++ b/src/common/translation.unit.spec.ts @@ -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"); diff --git a/src/deps.ts b/src/deps.ts index 0e4eb819..bac2545c 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -16,6 +16,7 @@ export { requestUrl, sanitizeHTMLToDom, Setting, + SettingPage, stringifyYaml, TAbstractFile, TextAreaComponent, diff --git a/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts b/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts index e09efe85..0237360c 100644 --- a/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts +++ b/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts @@ -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(); + }); }); diff --git a/src/features/ConfigSync/CmdConfigSync.ts b/src/features/ConfigSync/CmdConfigSync.ts index d88e8bd8..4201385f 100644 --- a/src/features/ConfigSync/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -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(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, diff --git a/src/features/LiveSyncCommands.ts b/src/features/LiveSyncCommands.ts index ed7a13f6..3f1ec783 100644 --- a/src/features/LiveSyncCommands.ts +++ b/src/features/LiveSyncCommands.ts @@ -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 { 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); diff --git a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts index 932436c2..75b00dcd 100644 --- a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts +++ b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts @@ -35,7 +35,7 @@ export class LocalDatabaseMaintenance extends LiveSyncCommands { } onload(): void | Promise { // 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", diff --git a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts index 6e64bb02..0d337484 100644 --- a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts +++ b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts @@ -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), }); diff --git a/src/main.ts b/src/main.ts index 8f5f2d6c..cb68bd79 100644 --- a/src/main.ts +++ b/src/main.ts @@ -170,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) => { diff --git a/src/modules/AbstractObsidianModule.ts b/src/modules/AbstractObsidianModule.ts index 0dcfb13f..3de7a5fc 100644 --- a/src/modules/AbstractObsidianModule.ts +++ b/src/modules/AbstractObsidianModule.ts @@ -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 { + override get services() { + return this.core.services; + } + get app() { return this.plugin.app; } diff --git a/src/modules/core/ModuleReplicator.ts b/src/modules/core/ModuleReplicator.ts index f7aed4cc..4d2b352a 100644 --- a/src/modules/core/ModuleReplicator.ts +++ b/src/modules/core/ModuleReplicator.ts @@ -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"); diff --git a/src/modules/essential/ModuleMigration.ts b/src/modules/essential/ModuleMigration.ts index 280140ef..a878c301 100644 --- a/src/modules/essential/ModuleMigration.ts +++ b/src/modules/essential/ModuleMigration.ts @@ -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 { } 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 { 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 { diff --git a/src/modules/essential/ModuleMigration.unit.spec.ts b/src/modules/essential/ModuleMigration.unit.spec.ts index b51665c7..5e2f3e99 100644 --- a/src/modules/essential/ModuleMigration.unit.spec.ts +++ b/src/modules/essential/ModuleMigration.unit.spec.ts @@ -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(); diff --git a/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts b/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts index 4f911e7b..bc7082ea 100644 --- a/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts +++ b/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts @@ -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"; diff --git a/src/modules/extras/ModuleDev.ts b/src/modules/extras/ModuleDev.ts index b74b94ec..0a03db8d 100644 --- a/src/modules/extras/ModuleDev.ts +++ b/src/modules/extras/ModuleDev.ts @@ -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 { __onMissingTranslation(() => {}); @@ -35,25 +32,8 @@ export class ModuleDev extends AbstractObsidianModule { } } - private _everyOnloadAfterLoadSettings(): Promise { - 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 { 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)); } diff --git a/src/modules/extras/devUtil/TestPane.svelte b/src/modules/extras/devUtil/TestPane.svelte deleted file mode 100644 index 09918409..00000000 --- a/src/modules/extras/devUtil/TestPane.svelte +++ /dev/null @@ -1,118 +0,0 @@ - - -

TESTING BENCH: Self-hosted LiveSync

- -

Module Checks

- - - - - -{#each resultLines as [result, line, message]} -
- [{result ? "PASS" : "FAILED"}] {line} -
{message}
-
-{/each} - -

Synchronisation Result Status

-
{syncStatus.join("\n")}
- -

Performance test

- - - -
- - diff --git a/src/modules/extras/devUtil/TestPaneView.ts b/src/modules/extras/devUtil/TestPaneView.ts deleted file mode 100644 index 56c81772..00000000 --- a/src/modules/extras/devUtil/TestPaneView.ts +++ /dev/null @@ -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(); - } -} diff --git a/src/modules/extras/devUtil/tests.ts b/src/modules/extras/devUtil/tests.ts deleted file mode 100644 index 60e3ec0d..00000000 --- a/src/modules/extras/devUtil/tests.ts +++ /dev/null @@ -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(); - -function clearResult(name: string) { - measures.set(name, [0, 0]); -} -async function measureEach(name: string, proc: () => void | Promise) { - 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, - times: number = 10000, - duration: number = 1000 -): Promise { - 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); -} diff --git a/src/modules/features/ModuleLog.ts b/src/modules/features/ModuleLog.ts index b44334ba..485014b1 100644 --- a/src/modules/features/ModuleLog.ts +++ b/src/modules/features/ModuleLog.ts @@ -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 { + 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 - } }); } } diff --git a/src/modules/features/ModuleObsidianSettingTab.ts b/src/modules/features/ModuleObsidianSettingTab.ts index 983372ba..09a82824 100644 --- a/src/modules/features/ModuleObsidianSettingTab.ts +++ b/src/modules/features/ModuleObsidianSettingTab.ts @@ -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); } diff --git a/src/modules/features/SettingDialogue/AdvancedSettingSpecs.ts b/src/modules/features/SettingDialogue/AdvancedSettingSpecs.ts new file mode 100644 index 00000000..3dbbe96d --- /dev/null +++ b/src/modules/features/SettingDialogue/AdvancedSettingSpecs.ts @@ -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" } }], + }, + ]; +} diff --git a/src/modules/features/SettingDialogue/GeneralSettingSpecs.ts b/src/modules/features/SettingDialogue/GeneralSettingSpecs.ts new file mode 100644 index 00000000..a839a2e1 --- /dev/null +++ b/src/modules/features/SettingDialogue/GeneralSettingSpecs.ts @@ -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" } }, + ], + }; +} diff --git a/src/modules/features/SettingDialogue/LiveSyncSetting.ts b/src/modules/features/SettingDialogue/LiveSyncSetting.ts index b7876678..adaf66cb 100644 --- a/src/modules/features/SettingDialogue/LiveSyncSetting.ts +++ b/src/modules/features/SettingDialogue/LiveSyncSetting.ts @@ -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]; } diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.declarative.unit.spec.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.declarative.unit.spec.ts new file mode 100644 index 00000000..bac9214e --- /dev/null +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.declarative.unit.spec.ts @@ -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; + unload: ReturnType; + 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 { + 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 + ) { + 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 + ) { + 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(); + }); +}); diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts index dbb7b239..1f564928 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts @@ -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(); 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[]; - 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 = {}): Promise { @@ -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 { + await this.core.getModule(SetupManager).startOnBoarding(); + } + + async enableLiveSyncFromSettings(): Promise { + 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(key: T, func: OnSavedHandlerFunc) { const newHandler = { key, handler: func } as OnSavedHandler; - 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 { + 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(value: T, owner: Component): DeferredPageElement { + 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 == "") { diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.unit.spec.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.unit.spec.ts index 9b79a865..92955f45 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.unit.spec.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.unit.spec.ts @@ -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 }) => { + 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(); + }); +}); diff --git a/src/modules/features/SettingDialogue/PaneAdvanced.ts b/src/modules/features/SettingDialogue/PaneAdvanced.ts index bfa0d02e..fee6b3f3 100644 --- a/src/modules/features/SettingDialogue/PaneAdvanced.ts +++ b/src/modules/features/SettingDialogue/PaneAdvanced.ts @@ -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"); - }); + } } diff --git a/src/modules/features/SettingDialogue/PaneAdvanced.unit.spec.ts b/src/modules/features/SettingDialogue/PaneAdvanced.unit.spec.ts new file mode 100644 index 00000000..e33b3429 --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneAdvanced.unit.spec.ts @@ -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(); + 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(); + 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(); + }); +}); diff --git a/src/modules/features/SettingDialogue/PaneChangeLog.ts b/src/modules/features/SettingDialogue/PaneChangeLog.ts index 18808754..c3139b43 100644 --- a/src/modules/features/SettingDialogue/PaneChangeLog.ts +++ b/src/modules/features/SettingDialogue/PaneChangeLog.ts @@ -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) ); } diff --git a/src/modules/features/SettingDialogue/PaneGeneral.ts b/src/modules/features/SettingDialogue/PaneGeneral.ts index cb826100..fc595312 100644 --- a/src/modules/features/SettingDialogue/PaneGeneral.ts +++ b/src/modules/features/SettingDialogue/PaneGeneral.ts @@ -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; - 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"); - }) - ); + } } diff --git a/src/modules/features/SettingDialogue/PaneHatch.ts b/src/modules/features/SettingDialogue/PaneHatch.ts index 16118413..376157ec 100644 --- a/src/modules/features/SettingDialogue/PaneHatch.ts +++ b/src/modules/features/SettingDialogue/PaneHatch.ts @@ -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({ diff --git a/src/modules/features/SettingDialogue/PaneHelp.ts b/src/modules/features/SettingDialogue/PaneHelp.ts new file mode 100644 index 00000000..c0923752 --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneHelp.ts @@ -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, + ` [${$msg("obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting")}](${topPath}) [${$msg("obsidianLiveSyncSettingTab.linkPageTop")}](${filename})\n\n${remoteTroubleShootMD}`, + troubleShootEl, + `${rawRepoURI}`, + lifetimeComponent + ); + if (pageDisposed) return; + troubleShootEl.querySelector(".sls-troubleshoot-anchor")?.parentElement?.setCssStyles({ + position: "sticky", + top: "-1em", + backgroundColor: "var(--modal-background)", + }); + troubleShootEl.querySelectorAll("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("[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); + }); +} diff --git a/src/modules/features/SettingDialogue/PaneMaintenance.ts b/src/modules/features/SettingDialogue/PaneMaintenance.ts index 47fd1424..d79a2b8c 100644 --- a/src/modules/features/SettingDialogue/PaneMaintenance.ts +++ b/src/modules/features/SettingDialogue/PaneMaintenance.ts @@ -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(); diff --git a/src/modules/features/SettingDialogue/PanePatches.ts b/src/modules/features/SettingDialogue/PanePatches.ts index 8f03e061..1816d6d5 100644 --- a/src/modules/features/SettingDialogue/PanePatches.ts +++ b/src/modules/features/SettingDialogue/PanePatches.ts @@ -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"); }); diff --git a/src/modules/features/SettingDialogue/PanePowerUsers.ts b/src/modules/features/SettingDialogue/PanePowerUsers.ts index c5542475..976ca827 100644 --- a/src/modules/features/SettingDialogue/PanePowerUsers.ts +++ b/src/modules/features/SettingDialogue/PanePowerUsers.ts @@ -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 = { @@ -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"); }); } diff --git a/src/modules/features/SettingDialogue/PaneQuickSetup.ts b/src/modules/features/SettingDialogue/PaneQuickSetup.ts new file mode 100644 index 00000000..72b1fb61 --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneQuickSetup.ts @@ -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(); + }); + }); + }); +} diff --git a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts index 9ec37c56..bd2143ef 100644 --- a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts +++ b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts @@ -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"); - // }) - // ); } diff --git a/src/modules/features/SettingDialogue/PaneRemoteConfig.unit.spec.ts b/src/modules/features/SettingDialogue/PaneRemoteConfig.unit.spec.ts new file mode 100644 index 00000000..a11f20f7 --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneRemoteConfig.unit.spec.ts @@ -0,0 +1,113 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const runtime = vi.hoisted(() => ({ + panels: [] as Array<{ destroy: ReturnType }>, +})); + +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(); + }); +}); diff --git a/src/modules/features/SettingDialogue/PaneSelector.ts b/src/modules/features/SettingDialogue/PaneSelector.ts index c2f247b1..3a6f8e4b 100644 --- a/src/modules/features/SettingDialogue/PaneSelector.ts +++ b/src/modules/features/SettingDialogue/PaneSelector.ts @@ -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([...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)); }); } diff --git a/src/modules/features/SettingDialogue/PaneSetup.ts b/src/modules/features/SettingDialogue/PaneSetup.ts deleted file mode 100644 index 1fd66675..00000000 --- a/src/modules/features/SettingDialogue/PaneSetup.ts +++ /dev/null @@ -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, - ` [${$msg("obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting")}](${topPath}) [${$msg("obsidianLiveSyncSettingTab.linkPageTop")}](${filename})\n\n${remoteTroubleShootMD}`, - troubleShootEl, - `${rawRepoURI}`, - this.lifetimeComponent - ); - // Menu - troubleShootEl.querySelector(".sls-troubleshoot-anchor")?.parentElement?.setCssStyles({ - position: "sticky", - top: "-1em", - backgroundColor: "var(--modal-background)", - }); - // Trap internal links. - troubleShootEl.querySelectorAll("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("[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); - }); -} diff --git a/src/modules/features/SettingDialogue/PaneSyncSettings.ts b/src/modules/features/SettingDialogue/PaneSyncSettings.ts index 1f9edc52..70a4b566 100644 --- a/src/modules/features/SettingDialogue/PaneSyncSettings.ts +++ b/src/modules/features/SettingDialogue/PaneSyncSettings.ts @@ -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 = 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, }); diff --git a/src/modules/features/SettingDialogue/SettingPane.ts b/src/modules/features/SettingDialogue/SettingPane.ts index b3cd4e1b..a21a7e4c 100644 --- a/src/modules/features/SettingDialogue/SettingPane.ts +++ b/src/modules/features/SettingDialogue/SettingPane.ts @@ -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(func: (arg: T) => void) { } }; } + +/** + * Defers pane construction until the owning settings page can confirm that its + * render scope is still active. + */ +export type DeferredPageElement = { + then(callback: (value: T) => unknown): void; +}; + export type PageFunctions = { addPane: ( parentEl: HTMLElement, title: string, icon: string, order: number, - wizardHidden: boolean, level?: ConfigLevel - ) => Promise; + ) => DeferredPageElement; addPanel: ( parentEl: HTMLElement, title: string, callback?: (el: HTMLDivElement) => void, func?: OnUpdateFunc, level?: ConfigLevel - ) => Promise; + ) => DeferredPageElement; }; diff --git a/src/modules/features/SettingDialogue/SettingPane.unit.spec.ts b/src/modules/features/SettingDialogue/SettingPane.unit.spec.ts new file mode 100644 index 00000000..be208eb8 --- /dev/null +++ b/src/modules/features/SettingDialogue/SettingPane.unit.spec.ts @@ -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 { + 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); + }); +}); diff --git a/src/modules/features/SettingDialogue/SettingSpec.ts b/src/modules/features/SettingDialogue/SettingSpec.ts new file mode 100644 index 00000000..a14c6adb --- /dev/null +++ b/src/modules/features/SettingDialogue/SettingSpec.ts @@ -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; +export type PersistedStringSettingKey = Exclude; +export type PersistedNumericSettingKey = Exclude; +export type PersistedSettingKey = PersistedBooleanSettingKey | PersistedStringSettingKey | PersistedNumericSettingKey; + +type SettingSpecBase = { + 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; + } +>; + +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; + +export type LegacySettingBinding = + | { + type: "toggle"; + key: PersistedBooleanSettingKey; + options: LegacySettingOptions & Pick; + } + | { + type: "number"; + key: PersistedNumericSettingKey; + options: LegacySettingOptions & { + clampMin?: number; + clampMax?: number; + acceptZero?: boolean; + }; + } + | { + type: "dropdown"; + key: PersistedStringSettingKey; + options: LegacySettingOptions & { options: Record }; + }; + +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 }): 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, + messages: SettingSpecMessages +): SettingDefinitionControl { + let control: SettingControl; + 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, + }; +} diff --git a/src/modules/features/SettingDialogue/SettingSpec.unit.spec.ts b/src/modules/features/SettingDialogue/SettingSpec.unit.spec.ts new file mode 100644 index 00000000..5c1c029f --- /dev/null +++ b/src/modules/features/SettingDialogue/SettingSpec.unit.spec.ts @@ -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; + expectTypeOf().toEqualTypeOf(); + }); + + 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(); + }); +}); diff --git a/src/modules/features/SettingDialogue/SettingsPageCatalogue.ts b/src/modules/features/SettingDialogue/SettingsPageCatalogue.ts new file mode 100644 index 00000000..c25fa19e --- /dev/null +++ b/src/modules/features/SettingDialogue/SettingsPageCatalogue.ts @@ -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; + +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 { + 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[] { + 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[] { + 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[] { + return createExtraMenuSettingSpecGroup().items.map(toSettingDefinition); +} diff --git a/src/modules/features/SettingDialogue/SettingsPageCatalogue.unit.spec.ts b/src/modules/features/SettingDialogue/SettingsPageCatalogue.unit.spec.ts new file mode 100644 index 00000000..9de8ff93 --- /dev/null +++ b/src/modules/features/SettingDialogue/SettingsPageCatalogue.unit.spec.ts @@ -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); + }); +}); diff --git a/src/modules/features/SetupManager.ts b/src/modules/features/SetupManager.ts index b37f3ec0..71d639fe 100644 --- a/src/modules/features/SetupManager.ts +++ b/src/modules/features/SetupManager.ts @@ -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; + isP2P: boolean; + validateChoice?: (mode: SetupInitialisationMode) => Promise; +}; + /** * 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 { + 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 diff --git a/src/modules/features/SetupManager.unit.spec.ts b/src/modules/features/SetupManager.unit.spec.ts index b9eb45b6..7bf889c8 100644 --- a/src/modules/features/SetupManager.unit.spec.ts +++ b/src/modules/features/SetupManager.unit.spec.ts @@ -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 = { diff --git a/src/modules/features/SetupWizard/dialogs/ApplySettingsInitialisation.svelte b/src/modules/features/SetupWizard/dialogs/ApplySettingsInitialisation.svelte new file mode 100644 index 00000000..639b5a36 --- /dev/null +++ b/src/modules/features/SetupWizard/dialogs/ApplySettingsInitialisation.svelte @@ -0,0 +1,85 @@ + + + + +

{msg("Ui.SetupWizard.ApplySettingsInitialisation.Guidance")}

+
+ + {msg("Ui.SetupWizard.ApplySettingsInitialisation.Question")} + + + + + setResult(selectedMode)} + /> + setResult(TYPE_CANCELLED)} + /> + diff --git a/src/modules/features/SetupWizard/dialogs/setupDialogTypes.ts b/src/modules/features/SetupWizard/dialogs/setupDialogTypes.ts index 8ad4a97b..66850c16 100644 --- a/src/modules/features/SetupWizard/dialogs/setupDialogTypes.ts +++ b/src/modules/features/SetupWizard/dialogs/setupDialogTypes.ts @@ -28,6 +28,10 @@ export const TYPE_COMPATIBLE_EXISTING = "compatible-existing-user"; // OutroExistingUser export const TYPE_APPLY = "apply"; +// Applying pending settings which require database initialisation +export const TYPE_FETCH = "fetch"; +export const TYPE_REBUILD = "rebuild"; + // Select methods export const TYPE_USE_SETUP_URI = "use-setup-uri"; export const TYPE_SCAN_QR_CODE = "scan-qr-code"; @@ -82,6 +86,12 @@ export type OutroExistingUserResultType = typeof TYPE_APPLY | typeof TYPE_CANCEL export type OutroNewUserResultType = typeof TYPE_APPLY | typeof TYPE_CANCELLED; +export type ApplySettingsInitialisationResultType = typeof TYPE_FETCH | typeof TYPE_REBUILD | typeof TYPE_CANCELLED; + +export type ApplySettingsInitialisationInitialData = { + isP2P: boolean; +}; + export type SelectMethodNewUserResultType = | typeof TYPE_USE_SETUP_URI | typeof TYPE_CONFIGURE_MANUALLY diff --git a/src/modules/services/ObsidianConfirm.ts b/src/modules/services/ObsidianConfirm.ts index f9197e02..a2e3c493 100644 --- a/src/modules/services/ObsidianConfirm.ts +++ b/src/modules/services/ObsidianConfirm.ts @@ -1,5 +1,5 @@ -import { type App, type Plugin, Notice } from "@/deps"; -import { scheduleTask, memoIfNotExist, memoObject, retrieveMemoObject, disposeMemoObject } from "@/common/utils"; +import { type App, type Plugin } from "@/deps"; +import { scheduleTask } from "@/common/utils"; import { EVENT_PLUGIN_UNLOADED } from "@/common/events"; import { $msg } from "@/common/translation"; import type { Confirm, ConfirmActionLayout } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm"; @@ -180,30 +180,20 @@ export class ObsidianConfirm { + scheduleTask(popupKey, 1000, () => { if (this.dialogueController.signal.aborted) { this.popupKeys.delete(popupKey); return; } - const popup = await memoIfNotExist(popupKey, () => new Notice(fragment, 0)); - const isShown = popup?.noticeEl?.isShown(); - if (!isShown) { - memoObject(popupKey, new Notice(fragment, 0)); - } - scheduleTask(popupKey + "-close", durationMs, () => this.closePopup(popupKey)); + this._context.notices.show(popupKey, fragment, { durationMs }); + scheduleTask(`${popupKey}-forget`, durationMs, () => { + this.popupKeys.delete(popupKey); + }); }); } private closePopup(popupKey: string) { - const popup = retrieveMemoObject(popupKey); - if (!popup) { - this.popupKeys.delete(popupKey); - return; - } - if (popup.noticeEl?.isShown()) { - popup.hide(); - } - disposeMemoObject(popupKey); + this._context.notices.hide(popupKey); this.popupKeys.delete(popupKey); } diff --git a/src/modules/services/ObsidianConfirm.unit.spec.ts b/src/modules/services/ObsidianConfirm.unit.spec.ts index afb7e3a0..18fe29b0 100644 --- a/src/modules/services/ObsidianConfirm.unit.spec.ts +++ b/src/modules/services/ObsidianConfirm.unit.spec.ts @@ -32,7 +32,6 @@ vi.mock("@/deps", () => ({ })); import { EVENT_PLUGIN_UNLOADED } from "@/common/events"; -import { memoObject, retrieveMemoObject } from "@/common/utils"; import { createLiveSyncEventHub } from "@vrtmrz/livesync-commonlib/context"; import { ObsidianConfirm } from "./ObsidianConfirm"; import type { ObsidianServiceContext } from "./ObsidianServiceContext"; @@ -41,8 +40,12 @@ function createConfirm() { const app = { id: "app" }; const plugin = { app }; const events = createLiveSyncEventHub(); - const context = { app, plugin, events } as unknown as ObsidianServiceContext; - return { confirm: new ObsidianConfirm(context), events, app, plugin }; + const notices = { + show: vi.fn(), + hide: vi.fn(), + }; + const context = { app, plugin, events, notices } as unknown as ObsidianServiceContext; + return { confirm: new ObsidianConfirm(context), events, app, plugin, notices }; } beforeEach(() => { @@ -259,19 +262,46 @@ describe("ObsidianConfirm Fancy Kit adapter", () => { expect(observedSignal?.aborted).toBe(true); }); - it("closes an active Notice when the plug-in unload event is emitted", () => { - const { confirm, events } = createConfirm(); + it("routes popup display and expiry through the context-owned keyed Notice manager", async () => { + vi.useFakeTimers(); const popupKey = "popup-remote-size-exceeded"; - const popup = { - hide: vi.fn(), - noticeEl: { isShown: vi.fn(() => true) }, + const fragment = {} as DocumentFragment; + const anchor = { addEventListener: vi.fn() } as unknown as HTMLAnchorElement; + const span = { + appendText: vi.fn(), + appendChild: vi.fn(), + createEl: vi.fn((_tag, _options, callback: (element: HTMLAnchorElement) => void) => { + callback(anchor); + return anchor; + }), }; - memoObject(popupKey, popup); + vi.stubGlobal("createFragment", (callback: (document: unknown) => void) => { + callback({ + createSpan: (_options: unknown, build: (element: typeof span) => void) => build(span), + }); + return fragment; + }); + const { confirm, notices } = createConfirm(); + + try { + confirm.askInPopup("remote-size-exceeded", "Review {HERE} details", vi.fn(), 20_000); + await vi.advanceTimersByTimeAsync(1_000); + + expect(notices.show).toHaveBeenCalledWith(popupKey, fragment, { durationMs: 20_000 }); + } finally { + await vi.runAllTimersAsync(); + vi.useRealTimers(); + vi.unstubAllGlobals(); + } + }); + + it("closes an owned keyed Notice when the plug-in unload event is emitted", () => { + const { confirm, events, notices } = createConfirm(); + const popupKey = "popup-remote-size-exceeded"; (confirm as unknown as { popupKeys: Set }).popupKeys.add(popupKey); events.emitEvent(EVENT_PLUGIN_UNLOADED); - expect(popup.hide).toHaveBeenCalledOnce(); - expect(retrieveMemoObject(popupKey)).toBe(false); + expect(notices.hide).toHaveBeenCalledWith(popupKey); }); }); diff --git a/src/modules/services/ObsidianServiceContext.ts b/src/modules/services/ObsidianServiceContext.ts index 694b336e..f31fbea7 100644 --- a/src/modules/services/ObsidianServiceContext.ts +++ b/src/modules/services/ObsidianServiceContext.ts @@ -4,19 +4,28 @@ import { ServiceContext } from "@vrtmrz/livesync-commonlib/context"; import { eventHub } from "@/common/events"; import { translateLiveSyncMessage } from "@/common/translation"; import type { ObsidianNoticeGroups } from "./ObsidianNoticeGroups"; +import type { KeyedNoticeManager } from "@vrtmrz/obsidian-plugin-kit/notice"; /** Host capabilities owned by one Self-hosted LiveSync plug-in instance. */ export class ObsidianServiceContext extends ServiceContext { app: App; plugin: Plugin; liveSyncPlugin: ObsidianLiveSyncPlugin; + readonly notices: KeyedNoticeManager; readonly noticeGroups: ObsidianNoticeGroups; - constructor(app: App, plugin: Plugin, liveSyncPlugin: ObsidianLiveSyncPlugin, noticeGroups: ObsidianNoticeGroups) { + constructor( + app: App, + plugin: Plugin, + liveSyncPlugin: ObsidianLiveSyncPlugin, + notices: KeyedNoticeManager, + noticeGroups: ObsidianNoticeGroups + ) { super({ events: eventHub, translate: translateLiveSyncMessage }); this.app = app; this.plugin = plugin; this.liveSyncPlugin = liveSyncPlugin; + this.notices = notices; this.noticeGroups = noticeGroups; } } diff --git a/src/modules/services/ObsidianServiceContext.unit.spec.ts b/src/modules/services/ObsidianServiceContext.unit.spec.ts index d95f9306..32d74a57 100644 --- a/src/modules/services/ObsidianServiceContext.unit.spec.ts +++ b/src/modules/services/ObsidianServiceContext.unit.spec.ts @@ -13,8 +13,9 @@ describe("ObsidianServiceContext contract", () => { const app = {} as Parameters[0]; const plugin = {} as Parameters[1]; const liveSyncPlugin = {} as Parameters[2]; - const noticeGroups = {} as Parameters[3]; - const context = new ObsidianServiceContext(app, plugin, liveSyncPlugin, noticeGroups); + const notices = {} as Parameters[3]; + const noticeGroups = {} as Parameters[4]; + const context = new ObsidianServiceContext(app, plugin, liveSyncPlugin, notices, noticeGroups); expect(observeServiceContext(context, TRANSLATION_KEY)).toEqual({ translation: translateLiveSyncMessage(TRANSLATION_KEY), @@ -24,6 +25,7 @@ describe("ObsidianServiceContext contract", () => { expect(context.app).toBe(app); expect(context.plugin).toBe(plugin); expect(context.liveSyncPlugin).toBe(liveSyncPlugin); + expect(context.notices).toBe(notices); expect(context.noticeGroups).toBe(noticeGroups); }); }); diff --git a/src/modules/services/ObsidianServiceHub.ts b/src/modules/services/ObsidianServiceHub.ts index a5d25801..3613ea27 100644 --- a/src/modules/services/ObsidianServiceHub.ts +++ b/src/modules/services/ObsidianServiceHub.ts @@ -26,14 +26,16 @@ import { createScreenWakeLockManager } from "octagonal-wheels/browser/wakeLock"; import { PouchDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/pouchdb-browser"; import { OpenKeyValueDatabase } from "@/common/KeyValueDB"; import { ObsidianNoticeGroupManager } from "./ObsidianNoticeGroups"; +import { KeyedNoticeManager } from "@vrtmrz/obsidian-plugin-kit/notice"; import { setLang } from "@/common/translation"; // InjectableServiceHub export class ObsidianServiceHub extends InjectableServiceHub { constructor(plugin: ObsidianLiveSyncPlugin) { + const notices = new KeyedNoticeManager(); const noticeGroups = new ObsidianNoticeGroupManager(); - const context = new ObsidianServiceContext(plugin.app, plugin, plugin, noticeGroups); + const context = new ObsidianServiceContext(plugin.app, plugin, plugin, notices, noticeGroups); const API = new ObsidianAPIService(context); const conflict = new ObsidianConflictService(context); @@ -66,6 +68,7 @@ export class ObsidianServiceHub extends InjectableServiceHub { await screenWakeLock.dispose(); + notices.dispose(); noticeGroups.dispose(); return true; }); diff --git a/src/serviceFeatures/fileDatabaseInfo.ts b/src/serviceFeatures/fileDatabaseInfo.ts index 40fc01e3..300ab4e3 100644 --- a/src/serviceFeatures/fileDatabaseInfo.ts +++ b/src/serviceFeatures/fileDatabaseInfo.ts @@ -1,4 +1,5 @@ import { $msg } from "@/common/translation"; +import type PouchDB from "pouchdb-core"; import type { FilePath, FilePathWithPrefix, diff --git a/styles.css b/styles.css index b5bf7ac6..241176e0 100644 --- a/styles.css +++ b/styles.css @@ -299,14 +299,6 @@ body { border-radius: 4px; } -.isWizard .wizardHidden { - display: none; -} - -.sls-setting:not(.isWizard) .wizardOnly { - display: none; -} - .sls-item-dirty::before { content: "✏"; } diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index bb4ea65f..9f9ca382 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -102,11 +102,22 @@ The underlying `test:e2e:obsidian:` scripts remain available for an im `test:contract:context:cli` builds the Node CLI and runs its existing Deno setup, put, read, list, information, remove, conflict-resolution, and revision workflow. `test:contract:context:obsidian` builds the plug-in and runs the real-Obsidian smoke test, including the Context inspection. These runtime scripts are local validation entry points and are not added to the default CI gate by this change. -`test:e2e:obsidian:onboarding-invitation` starts an unconfigured temporary Vault with no plug-in data and verifies that startup selects Commonlib's new-Vault recommendations, offers the setup wizard without opening it, and does not scan Vault files automatically. It checks the invitation action and introduction in mobile test mode, then reopens the wizard from **Self-hosted LiveSync settings** → **Setup** on the desktop. This scenario owns the unconfigured-startup boundary only; configured compatibility review remains covered by `settings-ui`, and the setup workflows remain covered by their dedicated scenarios. +`test:e2e:obsidian:onboarding-invitation` starts an unconfigured temporary Vault with no plug-in data and verifies that startup selects Commonlib's new-Vault recommendations, offers the setup wizard without opening it, and does not scan Vault files automatically. It checks the invitation action and introduction in mobile test mode, then reopens the wizard from Quick Setup in either settings interface. This scenario owns the unconfigured-startup boundary only; configured compatibility review remains covered by `settings-ui`, and the setup workflows remain covered by their dedicated scenarios. `test:e2e:obsidian:dialog-mounts` starts a temporary real Obsidian session and exercises remote selection and CouchDB settings through `SetupManager`, plus Setup URI entry through the registered command. It verifies the compatibility pause and remote-size review, the distinction between a central data-storage server and P2P signalling, the explicit tested and untested CouchDB save actions, the internal-API warning, the Setup URI controls, automatic adjustment when differences are limited to compatible chunk settings, and both manual configuration-mismatch routes. The same session opens the live log and generated full report, reaches the `Hatch` recovery controls, writes and removes its own persistent log, and runs the missing-chunk recreation and file-verification actions against the empty disposable Vault. It captures representative desktop and mobile dialogues, checks the mobile layout and vertically stacked actions, closes each route through its normal controls, and verifies that each mounted operation settles without an error. It does not apply a remote configuration, contact a remote service, or claim to repair a deliberately damaged database. -`test:e2e:obsidian:settings-ui` starts with a pending compatibility review and verifies the dedicated pause summary, its detailed explanation, and the explicit resume action in a temporary real Obsidian session. It captures the desktop summary and the iPhone-sized summary and detail dialogues; the mobile checks cover viewport containment, horizontal overflow, safe-area containment, and the close control's touch target. It confirms that the acknowledged internal version advances only after the review is accepted, and checks that the Change Log contains no acknowledgement control. It then selects the Synchronisation Settings pane and verifies that the deletion panel still exposes the effective 'Keep empty folder' setting without presenting the legacy `trashInsteadDelete` control, whose value no longer changes Obsidian deletion behaviour. +`test:e2e:obsidian:settings-ui` starts with a pending compatibility review and verifies the dedicated pause summary, its detailed explanation, and the explicit resume action in a temporary real Obsidian session. It captures the desktop summary and the iPhone-sized summary and detail dialogues; the mobile checks cover viewport containment, horizontal overflow, safe-area containment, and the close control's touch target. It confirms that the acknowledged internal version advances only after the review is accepted, and checks that the Change Log contains no acknowledgement control. It then enables Advanced mode and persists one numeric Advanced setting. On Obsidian before 1.13, this covers the imperative settings fallback used by the `SettingSpec` proof. On Obsidian 1.13 or later, it verifies the task-oriented landing order, the separate Synchronisation group containing Remote Configuration and Sync Settings, and the General Settings group containing Appearance, Logging, and Extra menus. It reopens and cancels onboarding from Quick Setup, opens all 14 nested settings pages, searches globally for the Advanced control, captures the landing page, search result, Advanced page, and mobile landing page, and verifies that the value remains after the settings dialogue is closed and reopened. The mobile landing check confirms that Remote Configuration is reachable without initial scrolling. Finally, it selects the Synchronisation Settings page and verifies that the deletion panel still exposes the effective 'Keep empty folder' setting without presenting the legacy `trashInsteadDelete` control, whose value no longer changes Obsidian deletion behaviour. + +The default runner uses the maintained pre-1.13 Obsidian fixture and therefore exercises the complete compatibility-review, mobile-layout, and imperative-settings path. To exercise only the native settings contract against an additional Obsidian 1.13-or-later installation, supply its executable and companion CLI explicitly: + +```bash +OBSIDIAN_BINARY=/path/to/obsidian \ +OBSIDIAN_CLI=/path/to/obsidian-cli \ +E2E_OBSIDIAN_SETTINGS_ONLY=true \ +npm run test:e2e:obsidian:settings-ui +``` + +The native run writes `settings-declarative-landing.png`, `settings-declarative-search.png`, `settings-declarative-advanced.png`, and `settings-declarative-landing-mobile.png` to `E2E_OBSIDIAN_DIAGNOSTICS_DIR`. All settings E2E scenarios open pages through the shared navigator in `runner/ui.ts`; scenario code must not select the legacy tab menu directly. The mobile pass uses Obsidian's `app.emulateMobile(true)`, a 390 by 844 CSS-pixel viewport, and explicit iPhone-style safe-area insets of 47 pixels at the top and 34 pixels at the bottom. The public `@vrtmrz/obsidian-test-session` layout assertions require each modal to remain within the viewport and safe area without horizontal overflow. They also require the Obsidian Close control to remain within the safe area and provide at least a 44 by 44 CSS-pixel touch target. The runner clicks that control to verify actionability, then completes the explicit cancellation path. These simulated checks cover deterministic layout and interaction boundaries; they do not claim to reproduce a native operating-system overlay. @@ -224,6 +235,7 @@ Useful environment variables: - `E2E_OBSIDIAN_DIALOG_TIMEOUT_MS`: timeout for a representative Svelte dialogue to mount, expose its principal controls, and close; default is 10 seconds. - `E2E_OBSIDIAN_REVISION_REPAIR_TIMEOUT_MS`: timeout for each visible revision-repair control and result; default is 15 seconds. - `E2E_OBSIDIAN_SETTINGS_TIMEOUT_MS`: timeout for the settings pane and its deletion controls to become visible; default is 10 seconds. +- `E2E_OBSIDIAN_SETTINGS_ONLY=true`: skip compatibility-review and its dialogue-layout coverage when running `settings-ui` against an additional Obsidian 1.13-or-later installation; the native settings mobile-landing check still runs. - `E2E_OBSIDIAN_REVIEW_HARNESS_TIMEOUT_MS`: timeout for Review Harness view and action boundaries; default is 15 seconds. - `E2E_OBSIDIAN_P2P_PANE_TIMEOUT_MS`: timeout for the P2P status pane and its principal connection control; default is 10 seconds. - `E2E_OBSIDIAN_P2P_WORKFLOW_TIMEOUT_MS`: timeout for each visible P2P Setup URI, peer-discovery, approval, and replication control; default is 60 seconds. diff --git a/test/e2e-obsidian/runner/mobileUi.ts b/test/e2e-obsidian/runner/mobileUi.ts index d492092d..bd80ede3 100644 --- a/test/e2e-obsidian/runner/mobileUi.ts +++ b/test/e2e-obsidian/runner/mobileUi.ts @@ -70,9 +70,7 @@ async function applyObsidianMobileTestMode( await mkdir(outputDirectory, { recursive: true }); const screenshotPath = join( outputDirectory, - waitForLiveSync - ? "mobile-mode-transition.failure.png" - : "mobile-mode-before-plugin-start.failure.png" + waitForLiveSync ? "mobile-mode-transition.failure.png" : "mobile-mode-before-plugin-start.failure.png" ); await page.screenshot({ path: screenshotPath, fullPage: true }); const detail = error instanceof Error ? error.message : String(error); @@ -108,7 +106,8 @@ export async function setObsidianMobileTestMode(port: number, enabled: boolean, export async function assertMobileDialogueLayout(page: Page, container: Locator, label: string): Promise { const dialogue = container.locator(".modal").last(); - const closeButton = dialogue.locator(".modal-close-button"); + // Obsidian 1.13 moved and renamed the mobile close control; both selectors remain supported runtime contracts. + const closeButton = container.locator(".modal-close-button, .modal-header-button").last(); await assertLocatorWithinViewport(page, dialogue, { label }); await assertNoHorizontalOverflow(page, dialogue, { label }); await assertLocatorWithinSafeArea(page, dialogue, { diff --git a/test/e2e-obsidian/runner/ui.ts b/test/e2e-obsidian/runner/ui.ts index 27efa226..89a6c792 100644 --- a/test/e2e-obsidian/runner/ui.ts +++ b/test/e2e-obsidian/runner/ui.ts @@ -3,6 +3,31 @@ import { dirname, join } from "node:path"; import { withObsidianPage } from "@vrtmrz/obsidian-test-session"; import type { Locator, Page } from "playwright"; +type ObsidianSettingsHost = typeof globalThis & { + app?: { + setting?: { + open(): void; + openTabById(tabId: string): void; + close(): void; + }; + vault?: { + adapter?: { getBasePath?: () => string; basePath?: string }; + }; + }; +}; + +export type LiveSyncSettingsRenderer = "imperative" | "declarative"; + +export type LiveSyncSettingsNavigator = { + dialogue: Locator; + page: Page; + renderer: LiveSyncSettingsRenderer; + close(): Promise; + openPage(name: string): Promise; + returnToCatalogue(): Promise; + isPageListed(name: string): Promise; +}; + export { obsidianRemoteDebuggingPort, preseedTrustedVaultState, @@ -42,6 +67,206 @@ export async function captureObsidianDialogue( return await captureObsidianPage(port, filename, assertReady); } +/** Wait for a visible Obsidian dialogue, including one opened outside the settings window. */ +export async function waitForVisibleObsidianDialogue(page: Page, text: string, timeoutMs = 10_000): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + for (const candidate of page.context().pages()) { + const dialogue = candidate.locator(".modal-container").filter({ hasText: text }).last(); + if (await dialogue.isVisible().catch(() => false)) return dialogue; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + throw new Error(`Obsidian did not open the expected dialogue: ${text}`); +} + +function declarativePageEntry(dialogue: Locator, name: string): Locator { + return dialogue + .locator(".setting-item.mod-navigable") + .filter({ hasText: new RegExp(`${escapeRegExp(name)}\\s*$`, "u") }); +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +} + +/** + * Open the Self-hosted LiveSync settings tab through the renderer selected by + * the running Obsidian version. + * + * Obsidian 1.13 may open settings in a separate window. The returned navigator + * owns that renderer difference for both supported settings implementations. + */ +export async function openLiveSyncSettings(page: Page, timeoutMs = 10_000): Promise { + let hostPage: Page | undefined; + for (const candidate of page.context().pages()) { + const hasSettingsHost = await candidate + .evaluate(() => (globalThis as ObsidianSettingsHost).app?.setting !== undefined) + .catch(() => false); + if (hasSettingsHost) { + hostPage = candidate; + break; + } + } + if (hostPage === undefined) throw new Error("Obsidian settings are unavailable"); + + await hostPage.evaluate(() => { + const host = globalThis as ObsidianSettingsHost; + const setting = host.app?.setting; + if (setting === undefined) throw new Error("Obsidian settings are unavailable"); + setting.open(); + setting.openTabById("obsidian-livesync"); + }); + + const deadline = Date.now() + timeoutMs; + let settingsPage: Page | undefined; + while (Date.now() < deadline && settingsPage === undefined) { + for (const candidate of page.context().pages()) { + if ( + await candidate + .locator(".modal.mod-settings:visible") + .last() + .isVisible() + .catch(() => false) + ) { + settingsPage = candidate; + break; + } + } + if (settingsPage === undefined) await new Promise((resolve) => setTimeout(resolve, 100)); + } + if (settingsPage === undefined) throw new Error("Obsidian did not open its settings interface"); + + const dialogue = settingsPage.locator(".modal.mod-settings:visible").last(); + const imperativeRoot = dialogue.locator(".sls-setting:visible").last(); + const firstDeclarativeEntry = declarativePageEntry(dialogue, "Change Log"); + await settingsPage.waitForFunction( + () => + document.querySelector(".modal.mod-settings .sls-setting") !== null || + Array.from(document.querySelectorAll(".modal.mod-settings .setting-item-name")).some( + (element) => element.textContent?.trim().endsWith("Change Log") === true + ), + undefined, + { timeout: timeoutMs } + ); + const renderer: LiveSyncSettingsRenderer = (await imperativeRoot.isVisible()) ? "imperative" : "declarative"; + + const returnToCatalogue = async (): Promise => { + if (renderer === "imperative" || (await firstDeclarativeEntry.isVisible())) return; + const backButton = dialogue.locator(".setting-page-back-button:visible, .modal-setting-back-button:visible"); + await backButton.last().click({ timeout: timeoutMs }); + await firstDeclarativeEntry.waitFor({ state: "visible", timeout: timeoutMs }); + }; + + const openPage = async (name: string): Promise => { + if (renderer === "imperative") { + const root = dialogue.locator(".sls-setting:visible").last(); + await root.locator(`.sls-setting-menu-btn[title="${name}"]`).click({ timeout: timeoutMs }); + return root; + } + + await returnToCatalogue(); + const entry = declarativePageEntry(dialogue, name); + await entry.waitFor({ state: "visible", timeout: timeoutMs }); + await entry.click({ timeout: timeoutMs }); + await entry.waitFor({ state: "hidden", timeout: timeoutMs }); + const content = dialogue.locator(".vertical-tab-content:visible").last(); + await content.waitFor({ state: "visible", timeout: timeoutMs }); + return content; + }; + + const isPageListed = async (name: string): Promise => { + if (renderer === "imperative") { + return await dialogue + .locator(`.sls-setting-menu-btn[title="${name}"]`) + .isVisible() + .catch(() => false); + } + await returnToCatalogue(); + return await declarativePageEntry(dialogue, name) + .isVisible() + .catch(() => false); + }; + + const close = async (): Promise => { + await hostPage + .evaluate(() => { + const setting = (globalThis as ObsidianSettingsHost).app?.setting; + if (setting === undefined) throw new Error("Obsidian settings are unavailable"); + setTimeout(() => setting.close(), 0); + }) + .catch((error: unknown) => { + if (!hostPage.isClosed() && !settingsPage.isClosed()) throw error; + }); + await Promise.race([ + dialogue.waitFor({ state: "hidden", timeout: timeoutMs }), + settingsPage.waitForEvent("close", { timeout: timeoutMs }), + ]).catch((error: unknown) => { + if (!settingsPage.isClosed()) throw error; + }); + }; + + return { + dialogue, + page: settingsPage, + renderer, + close, + openPage, + returnToCatalogue, + isPageListed, + }; +} + +/** Allow only the isolated E2E Vault path in Obsidian's external-action prompt. */ +export async function allowPendingObsidianTestVaultOpenAction( + port: number, + expectedVaultPath: string, + timeoutMs = 10_000 +): Promise { + await withObsidianPage(port, async (page) => { + const context = page.context(); + const action = page.locator(".modal.mod-uri-action:visible").last(); + const visible = await action + .waitFor({ state: "visible", timeout: Math.min(timeoutMs, 2_000) }) + .then(() => true) + .catch(() => false); + if (visible) { + const actionText = await action.innerText(); + if (!actionText.includes(expectedVaultPath)) { + throw new Error(`Refusing an unexpected Obsidian URI action: ${actionText}`); + } + await action.locator(".mod-checkbox").click({ timeout: timeoutMs }); + await action.getByRole("button", { name: "Continue" }).click({ timeout: timeoutMs }); + await action.waitFor({ state: "hidden", timeout: timeoutMs }); + } + + const deadline = Date.now() + timeoutMs; + let vaultPage: Page | undefined; + while (Date.now() < deadline && vaultPage === undefined) { + for (const candidate of context.pages()) { + const activePath = await candidate + .evaluate(() => { + const host = globalThis as ObsidianSettingsHost; + const adapter = host.app?.vault?.adapter; + return adapter?.getBasePath?.() ?? adapter?.basePath ?? null; + }) + .catch(() => null); + if (activePath === expectedVaultPath) { + vaultPage = candidate; + break; + } + } + if (vaultPage === undefined) await new Promise((resolve) => setTimeout(resolve, 100)); + } + if (vaultPage === undefined) { + throw new Error(`Obsidian did not open the approved E2E Vault: ${expectedVaultPath}`); + } + for (const candidate of context.pages()) { + if (candidate !== vaultPage && !candidate.isClosed()) await candidate.close(); + } + }); +} + export async function captureObsidianElement( port: number, filename: string, diff --git a/test/e2e-obsidian/scripts/dialog-mounts.ts b/test/e2e-obsidian/scripts/dialog-mounts.ts index 25e9f882..17d10d45 100644 --- a/test/e2e-obsidian/scripts/dialog-mounts.ts +++ b/test/e2e-obsidian/scripts/dialog-mounts.ts @@ -9,6 +9,7 @@ import { captureObsidianElement, captureObsidianPage, obsidianRemoteDebuggingPort, + openLiveSyncSettings, withObsidianPage, } from "../runner/ui.ts"; import { createTemporaryVault } from "../runner/vault.ts"; @@ -46,11 +47,6 @@ type LiveSyncTestPlugin = { }; }; -type ObsidianSettingsController = { - open(): void; - openTabById(tabId: string): void; -}; - type ObsidianVaultFile = { path: string; }; @@ -58,7 +54,6 @@ type ObsidianVaultFile = { type ObsidianTestApp = { commands?: { executeCommandById(commandId: string): boolean }; plugins?: { plugins: Record }; - setting?: ObsidianSettingsController; vault?: { delete(file: ObsidianVaultFile, force: boolean): Promise; getFiles(): ObsidianVaultFile[]; @@ -748,17 +743,10 @@ async function verifyCompatibleAlignmentSettingDefault(): Promise { ); } - await page.evaluate(() => { - const setting = (globalThis as ObsidianTestGlobal).app?.setting; - if (setting === undefined) throw new Error("Obsidian settings are unavailable"); - setting.open(); - setting.openTabById("obsidian-livesync"); - }); - const liveSyncSettings = page.locator(".sls-setting"); - await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs }); - await liveSyncSettings.locator('.sls-setting-menu-btn[title="Advanced"]').click({ timeout: uiTimeoutMs }); + const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs); + const liveSyncSettings = await settingsNavigator.openPage("Advanced"); const settingItem = liveSyncSettings.locator(".setting-item").filter({ - has: page.getByText("Auto-accept compatible tweak mismatches", { exact: true }), + has: settingsNavigator.page.getByText("Auto-accept compatible tweak mismatches", { exact: true }), }); await settingItem.waitFor({ state: "visible", timeout: uiTimeoutMs }); const toggle = settingItem.locator(".checkbox-container"); @@ -944,15 +932,8 @@ async function verifyHatchSurfacesAndSafeActions(): Promise { obsidianRemoteDebuggingPort(), "troubleshooting-hatch.png", async (page) => { - await page.evaluate(() => { - const setting = (globalThis as ObsidianTestGlobal).app?.setting; - if (setting === undefined) throw new Error("Obsidian settings are unavailable"); - setting.open(); - setting.openTabById("obsidian-livesync"); - }); - const liveSyncSettings = page.locator(".sls-setting"); - await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs }); - await liveSyncSettings.locator('.sls-setting-menu-btn[title="Hatch"]').click({ timeout: uiTimeoutMs }); + const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs); + const liveSyncSettings = await settingsNavigator.openPage("Hatch"); for (const label of [ "Write logs into the file", "Recreate chunks for current Vault files", diff --git a/test/e2e-obsidian/scripts/onboarding-invitation.ts b/test/e2e-obsidian/scripts/onboarding-invitation.ts index fbc8189a..17f2b044 100644 --- a/test/e2e-obsidian/scripts/onboarding-invitation.ts +++ b/test/e2e-obsidian/scripts/onboarding-invitation.ts @@ -9,7 +9,13 @@ import { evalObsidianJson } from "../runner/cli.ts"; import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; import { assertMobileDialogueLayout, iPhoneSafeArea, setObsidianMobileTestMode } from "../runner/mobileUi.ts"; import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; -import { captureObsidianDialogue, obsidianRemoteDebuggingPort, withObsidianPage } from "../runner/ui.ts"; +import { + captureObsidianDialogue, + obsidianRemoteDebuggingPort, + openLiveSyncSettings, + waitForVisibleObsidianDialogue, + withObsidianPage, +} from "../runner/ui.ts"; import { createTemporaryVault } from "../runner/vault.ts"; const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_ONBOARDING_TIMEOUT_MS ?? 15000); @@ -25,15 +31,6 @@ type UnconfiguredStartupEvidence = { }; }; -type ObsidianTestApp = { - setting?: { - open(): void; - openTabById(tabId: string): void; - }; -}; - -type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp }; - async function writeMarker(vaultPath: string): Promise { const fullPath = join(vaultPath, markerPath); await mkdir(dirname(fullPath), { recursive: true }); @@ -158,25 +155,24 @@ async function captureAndCloseIntro(filename: string, mobile: boolean): Promise< async function openOnboardingFromSettings(): Promise { await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { - await page.evaluate(() => { - const setting = (globalThis as ObsidianTestGlobal).app?.setting; - if (setting === undefined) throw new Error("Obsidian settings are unavailable"); - setting.open(); - setting.openTabById("obsidian-livesync"); - }); - - const liveSyncSettings = page.locator(".sls-setting"); - await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs }); - await liveSyncSettings.locator('.sls-setting-menu-btn[title="Setup"]').click({ timeout: uiTimeoutMs }); + const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs); + const liveSyncSettings = + settingsNavigator.renderer === "declarative" + ? (await settingsNavigator.returnToCatalogue(), settingsNavigator.dialogue) + : await settingsNavigator.openPage("Quick Setup"); const onboardingSetting = liveSyncSettings.locator(".setting-item").filter({ - has: page.locator(".setting-item-name").filter({ hasText: "Rerun Onboarding Wizard" }), + has: settingsNavigator.page.locator(".setting-item-name").filter({ hasText: "Rerun Onboarding Wizard" }), }); await onboardingSetting.waitFor({ state: "visible", timeout: uiTimeoutMs }); - await onboardingSetting - .getByRole("button", { name: "Rerun Wizard", exact: true }) - .click({ timeout: uiTimeoutMs }); - await onboardingDialogue(page).waitFor({ state: "visible", timeout: uiTimeoutMs }); + if (settingsNavigator.renderer === "declarative") { + await onboardingSetting.click({ timeout: uiTimeoutMs }); + } else { + await onboardingSetting + .getByRole("button", { name: "Rerun Wizard", exact: true }) + .click({ timeout: uiTimeoutMs }); + } + await waitForVisibleObsidianDialogue(settingsNavigator.page, "Welcome to Self-hosted LiveSync", uiTimeoutMs); }); } @@ -197,11 +193,8 @@ async function dismissVisibleNotices(): Promise { async function closeSettings(): Promise { await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { - const settingsContainer = page.locator(".modal-container").filter({ - has: page.locator(".sls-setting"), - }); - await settingsContainer.locator(".modal-close-button").click({ timeout: uiTimeoutMs }); - await settingsContainer.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs); + await settingsNavigator.close(); }); } diff --git a/test/e2e-obsidian/scripts/revision-repair.ts b/test/e2e-obsidian/scripts/revision-repair.ts index 8dbab659..87586b2b 100644 --- a/test/e2e-obsidian/scripts/revision-repair.ts +++ b/test/e2e-obsidian/scripts/revision-repair.ts @@ -6,7 +6,7 @@ import { waitForLocalDatabaseEntry, } from "../runner/liveSyncWorkflow.ts"; import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; -import { captureObsidianElement, withObsidianPage } from "../runner/ui.ts"; +import { captureObsidianElement, openLiveSyncSettings, withObsidianPage } from "../runner/ui.ts"; import { createTemporaryVault } from "../runner/vault.ts"; import type { Locator, Page } from "playwright"; @@ -35,17 +35,6 @@ type VaultWinnerState = { winnerRevision: string; }; -type ObsidianSettingsController = { - open(): void; - openTabById(tabId: string): void; -}; - -type ObsidianTestGlobal = typeof globalThis & { - app?: { - setting?: ObsidianSettingsController; - }; -}; - async function createAndOpenBaseFile(cliBinary: string, env: NodeJS.ProcessEnv): Promise { await evalObsidianJson( cliBinary, @@ -359,17 +348,10 @@ async function main(): Promise { } await withObsidianPage(session.remoteDebuggingPort, async (page) => { - await page.evaluate(() => { - const setting = (globalThis as ObsidianTestGlobal).app?.setting; - if (setting === undefined) throw new Error("Obsidian settings are unavailable"); - setting.open(); - setting.openTabById("obsidian-livesync"); - }); - const settings = page.locator(".sls-setting"); - await settings.waitFor({ state: "visible", timeout: uiTimeoutMs }); - await settings.locator('.sls-setting-menu-btn[title="Hatch"]').click({ timeout: uiTimeoutMs }); + const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs); + const settings = await settingsNavigator.openPage("Hatch"); const verifySetting = settings.locator(".setting-item").filter({ - has: page.getByText("Inspect conflicts and file/database differences", { + has: settingsNavigator.page.getByText("Inspect conflicts and file/database differences", { exact: true, }), }); diff --git a/test/e2e-obsidian/scripts/settings-ui.ts b/test/e2e-obsidian/scripts/settings-ui.ts index 3afe2010..091533f7 100644 --- a/test/e2e-obsidian/scripts/settings-ui.ts +++ b/test/e2e-obsidian/scripts/settings-ui.ts @@ -1,21 +1,43 @@ +import { mkdir } from "node:fs/promises"; import { VER } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; import { waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts"; import { assertMobileDialogueLayout, setObsidianMobileTestMode } from "../runner/mobileUi.ts"; import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; -import { captureObsidianDialogue, obsidianRemoteDebuggingPort, withObsidianPage } from "../runner/ui.ts"; +import { + allowPendingObsidianTestVaultOpenAction, + captureObsidianDialogue, + obsidianRemoteDebuggingPort, + openLiveSyncSettings, + preseedTrustedVaultState, + waitForVisibleObsidianDialogue, + withObsidianPage, +} from "../runner/ui.ts"; import { createTemporaryVault } from "../runner/vault.ts"; +import type { Locator } from "playwright"; const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETTINGS_TIMEOUT_MS ?? 10000); -const compatibilityReviewMessage = "Review the internal database compatibility change before synchronisation resumes."; - -type ObsidianSettingsController = { - open(): void; - openTabById(tabId: string): void; +const settingsOnly = process.env.E2E_OBSIDIAN_SETTINGS_ONLY === "true"; +const diagnosticsDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e"; +const settingsInitialisationRunStateKey = "__livesyncE2ESettingsInitialisation"; +const settingsScreenshotOptions = { + animations: "disabled" as const, + style: ".notice-container { visibility: hidden !important; }", }; +const compatibilityReviewMessage = "Review the internal database compatibility change before synchronisation resumes."; type LiveSyncTestPlugin = { core: { + modules: { + constructor: { name: string }; + applySettingsWithInitialisationChoice?: (options: { + applySettings: () => Promise; + isP2P: boolean; + }) => Promise; + }[]; + settings: { + handleFilenameCaseSensitive: boolean; + }; services: { setting: { currentSettings(): { @@ -25,6 +47,7 @@ type LiveSyncTestPlugin = { useAdvancedMode: boolean; usePowerUserMode: boolean; useEdgeCaseMode: boolean; + hashCacheMaxCount: number; }; getSmallConfig(key: string): string | null; }; @@ -32,13 +55,226 @@ type LiveSyncTestPlugin = { }; }; +type SettingsInitialisationRunState = { + done: boolean; + error?: string; + result?: unknown; +}; + type ObsidianTestApp = { - setting?: ObsidianSettingsController; plugins?: { plugins: Record }; }; type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp }; +const settingsPageNames = [ + "Appearance", + "Logging", + "Extra menus", + "Remote Configuration", + "Sync Settings", + "Maintenance", + "Hatch", + "Selector", + "Customisation sync", + "Advanced", + "Power users", + "Patches", + "Help and troubleshooting", + "Change Log", +] as const; + +async function assertDeclarativeLandingOrder(root: Locator): Promise { + const labels = [ + "Quick Setup", + "Synchronisation", + "Remote Configuration", + "Sync Settings", + "General Settings", + "Appearance", + "Logging", + "Extra menus", + "📲 Set up other devices", + "Maintenance and recovery", + "Maintenance", + "Hatch", + "Extra features", + "Selector", + "Customisation sync", + "Advanced settings", + "Advanced", + "Power users", + "Patches", + "Help and information", + "Help and troubleshooting", + "Change Log", + ]; + await root + .locator(".vertical-tab-content:visible") + .last() + .evaluate((container, expectedLabels) => { + const labelledElements = Array.from( + container.querySelectorAll(".setting-item-heading, .setting-item-name, h1, h2, h3, h4") + ); + const matchingElements = expectedLabels.map((label) => { + const match = labelledElements.find((element) => element.textContent?.trim().endsWith(label)); + if (!match) throw new Error(`The settings landing page did not contain '${label}'.`); + return match; + }); + for (let index = 1; index < matchingElements.length; index++) { + const previous = matchingElements[index - 1]; + const current = matchingElements[index]; + if (!(previous.compareDocumentPosition(current) & Node.DOCUMENT_POSITION_FOLLOWING)) { + throw new Error(`The settings landing page was not ordered as ${expectedLabels.join(" -> ")}.`); + } + } + }, labels); +} + +async function scrollDeclarativeLandingToTop(root: Locator): Promise { + const quickSetupHeading = root.locator(".setting-item-heading").filter({ hasText: "Quick Setup" }).first(); + await quickSetupHeading.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await quickSetupHeading.scrollIntoViewIfNeeded(); +} + +async function captureDeclarativeMobileLanding(): Promise { + const port = obsidianRemoteDebuggingPort(); + await setObsidianMobileTestMode(port, true, uiTimeoutMs); + try { + return await withObsidianPage(port, async (page) => { + const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs); + if (settingsNavigator.renderer !== "declarative") { + await settingsNavigator.close(); + return undefined; + } + await settingsNavigator.returnToCatalogue(); + await scrollDeclarativeLandingToTop(settingsNavigator.dialogue); + await assertDeclarativeLandingOrder(settingsNavigator.dialogue); + const remoteConfiguration = settingsNavigator.dialogue + .locator(".setting-item-name") + .filter({ hasText: "Remote Configuration" }) + .first(); + await remoteConfiguration.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const path = `${diagnosticsDirectory}/settings-declarative-landing-mobile.png`; + await settingsNavigator.dialogue.screenshot({ ...settingsScreenshotOptions, path }); + const remotePosition = await remoteConfiguration.evaluate((element) => { + const bounds = element.getBoundingClientRect(); + return { top: bounds.top, bottom: bounds.bottom, viewportHeight: window.innerHeight }; + }); + if (remotePosition.top < 0 || remotePosition.bottom > remotePosition.viewportHeight) { + throw new Error("Remote Configuration was not visible at the top of the mobile settings landing page."); + } + await settingsNavigator.close(); + return path; + }); + } finally { + await setObsidianMobileTestMode(port, false, uiTimeoutMs); + } +} + +async function openSettingsInitialisationDialogueForInspection(isP2P: boolean): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.evaluate( + ({ stateKey, isP2P }) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is unavailable"); + const manager = plugin.core.modules.find((module) => module.constructor.name === "SetupManager"); + if (typeof manager?.applySettingsWithInitialisationChoice !== "function") { + throw new Error("Could not find the pending-settings initialisation workflow"); + } + const state: SettingsInitialisationRunState = { done: false }; + (globalThis as unknown as Record)[stateKey] = state; + void manager + .applySettingsWithInitialisationChoice({ + isP2P, + applySettings: Promise.resolve.bind(Promise), + }) + .then( + (result) => { + state.result = result; + state.done = true; + }, + (error: unknown) => { + state.error = error instanceof Error ? error.message : String(error); + state.done = true; + } + ); + }, + { stateKey: settingsInitialisationRunStateKey, isP2P } + ); + }); +} + +async function assertSettingsInitialisationRunCancelled(): Promise { + const state = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.waitForFunction( + (stateKey) => + (globalThis as unknown as Record)[stateKey] + ?.done === true, + settingsInitialisationRunStateKey, + { timeout: uiTimeoutMs } + ); + return await page.evaluate( + (stateKey) => + (globalThis as unknown as Record)[stateKey], + settingsInitialisationRunStateKey + ); + }); + if (!state) throw new Error("The pending-settings initialisation dialogue did not record its result."); + if (state.error) throw new Error(`The pending-settings initialisation dialogue failed: ${state.error}`); + if (JSON.stringify(state.result) !== JSON.stringify({ result: "cancelled" })) { + throw new Error(`The pending-settings initialisation dialogue returned ${JSON.stringify(state.result)}.`); + } +} + +async function captureP2PSettingsInitialisationDialogue(): Promise { + await openSettingsInitialisationDialogueForInspection(true); + const path = `${diagnosticsDirectory}/settings-initialisation-p2p.png`; + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const dialogue = await waitForVisibleObsidianDialogue( + page, + "Apply Settings and Reinitialise Synchronisation", + uiTimeoutMs + ); + await dialogue.getByText("Prepare This Device from This Vault", { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await dialogue.getByText("Reset Synchronisation on This Device", { exact: true }).click({ + timeout: uiTimeoutMs, + }); + await dialogue.getByRole("button", { name: "Restart and Select a Source Device", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + if ( + (await dialogue.getByText("Overwrite Server Data with This Device's Files", { exact: true }).count()) !== 0 + ) { + throw new Error("The P2P initialisation dialogue exposed the central-server overwrite operation."); + } + await dialogue.screenshot({ ...settingsScreenshotOptions, path }); + await dialogue + .getByRole("button", { name: "Review another way to apply these settings", exact: true }) + .click({ timeout: uiTimeoutMs }); + await dialogue.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + await assertSettingsInitialisationRunCancelled(); + return path; +} + +async function resumePendingCompatibilityReviewForSettings(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const review = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Synchronisation paused for compatibility review", + }), + }); + if (!(await review.isVisible())) return; + await review.getByRole("button", { name: "Resume synchronisation" }).click({ timeout: uiTimeoutMs }); + await review.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); +} + async function verifyCompatibilityReview(): Promise { const port = obsidianRemoteDebuggingPort(); const summaryScreenshot = await captureObsidianDialogue(port, "compatibility-review-summary.png", async (page) => { @@ -222,41 +458,26 @@ async function verifyEffectiveSettings(): Promise { sleepPreferences.powerUserMode !== false || sleepPreferences.edgeCaseMode !== false ) { - throw new Error( - `Unexpected effective sleep preferences: ${JSON.stringify(sleepPreferences)}` - ); + throw new Error(`Unexpected effective sleep preferences: ${JSON.stringify(sleepPreferences)}`); } - await page.evaluate(() => { - const setting = (globalThis as ObsidianTestGlobal).app?.setting; - if (setting === undefined) throw new Error("Obsidian settings are unavailable"); - setting.open(); - setting.openTabById("obsidian-livesync"); - }); - - const liveSyncSettings = page.locator(".sls-setting"); - await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs }); - const settingsClass = await liveSyncSettings.getAttribute("class"); - for (const modeClass of [ - "menu-setting-advanced-disabled", - "menu-setting-poweruser-disabled", - "menu-setting-edgecase-disabled", - ]) { - if (!settingsClass?.split(/\s+/u).includes(modeClass)) { - throw new Error(`The settings UI did not disable ${modeClass}.`); + let settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs); + for (const hiddenPage of ["Selector", "Customisation sync", "Advanced", "Power users", "Patches"]) { + if (await settingsNavigator.isPageListed(hiddenPage)) { + throw new Error(`${hiddenPage} was visible before its feature level was enabled.`); } } - await liveSyncSettings.locator('.sls-setting-menu-btn[title="Change Log"]').click(); - const removedAcknowledgements = liveSyncSettings.getByRole("button", { + let settingsPage = await settingsNavigator.openPage("Change Log"); + const removedAcknowledgements = settingsPage.getByRole("button", { name: /I got it and updated|OK, I have read everything/u, }); if ((await removedAcknowledgements.count()) !== 0) { throw new Error("The Change Log still contains a compatibility or release-note acknowledgement control."); } - await liveSyncSettings.locator('.sls-setting-menu-btn[title="Remote Configuration"]').click(); - const connectionPanel = liveSyncSettings + settingsPage = await settingsNavigator.openPage("Remote Configuration"); + const connectionPanel = settingsPage .locator("h4.sls-setting-panel-title") .filter({ hasText: "Connection settings" }) .locator(".."); @@ -266,37 +487,135 @@ async function verifyEffectiveSettings(): Promise { timeout: uiTimeoutMs, }); - await liveSyncSettings.locator('.sls-setting-menu-btn[title="Sync Settings"]').click(); - const generalSleepSetting = liveSyncSettings.locator(".setting-item").filter({ - has: page.getByText("Allow sleep during synchronisation", { exact: true }), + settingsPage = await settingsNavigator.openPage("Sync Settings"); + const generalSleepSetting = settingsPage.locator(".setting-item").filter({ + has: settingsNavigator.page.getByText("Allow sleep during synchronisation", { exact: true }), }); await generalSleepSetting.waitFor({ state: "visible", timeout: uiTimeoutMs }); if ((await generalSleepSetting.locator(".checkbox-container.is-enabled").count()) !== 0) { throw new Error("The general sleep preference must be disabled by default."); } - const desktopSleepSetting = liveSyncSettings.locator(".setting-item").filter({ - has: page.getByText("Allow sleep during synchronisation on the desktop", { exact: true }), + const desktopSleepSetting = settingsPage.locator(".setting-item").filter({ + has: settingsNavigator.page.getByText("Allow sleep during synchronisation on the desktop", { + exact: true, + }), }); await desktopSleepSetting.waitFor({ state: "visible", timeout: uiTimeoutMs }); if ((await desktopSleepSetting.locator(".checkbox-container.is-enabled").count()) !== 1) { throw new Error("The desktop sleep preference must be enabled by default."); } - await liveSyncSettings.locator('.sls-setting-menu-btn[title="Setup"]').click(); - const advancedModeSetting = liveSyncSettings.locator(".setting-item").filter({ - has: page.getByText("Enable advanced features", { exact: true }), + settingsPage = await settingsNavigator.openPage( + settingsNavigator.renderer === "declarative" ? "Extra menus" : "General Settings" + ); + const advancedModeSetting = settingsPage.locator(".setting-item").filter({ + has: settingsNavigator.page.getByText("Enable advanced features", { exact: true }), }); await advancedModeSetting.waitFor({ state: "visible", timeout: uiTimeoutMs }); await advancedModeSetting.locator(".checkbox-container").click(); await page.waitForFunction( - () => document.querySelector(".sls-setting")?.classList.contains("menu-setting-advanced-enabled") === true, + () => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + return plugin?.core.services.setting.currentSettings().useAdvancedMode === true; + }, undefined, { timeout: uiTimeoutMs } ); - await liveSyncSettings.locator('.sls-setting-menu-btn[title="Sync Settings"]').click(); - const deletionPanel = liveSyncSettings + if (settingsNavigator.renderer === "declarative") { + for (const mode of [ + { label: "Enable poweruser features", key: "usePowerUserMode" }, + { label: "Enable edge case treatment features", key: "useEdgeCaseMode" }, + ] as const) { + const modeSetting = settingsPage.locator(".setting-item").filter({ + has: settingsNavigator.page.getByText(mode.label, { exact: true }), + }); + await modeSetting.locator(".checkbox-container").click({ timeout: uiTimeoutMs }); + await page.waitForFunction( + (key) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + return plugin?.core.services.setting.currentSettings()[key] === true; + }, + mode.key, + { timeout: uiTimeoutMs } + ); + } + } + + settingsPage = await settingsNavigator.openPage("Advanced"); + const cacheSizeSetting = settingsPage.locator(".setting-item").filter({ + has: settingsNavigator.page.getByText("Memory cache size (by total items)", { exact: true }), + }); + await cacheSizeSetting.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const cacheSizeInput = cacheSizeSetting.locator('input[type="number"]'); + if (settingsNavigator.renderer === "declarative") { + await cacheSizeInput.click(); + await cacheSizeInput.press("ControlOrMeta+A"); + await cacheSizeInput.pressSequentially("321"); + await cacheSizeInput.press("Enter"); + } else { + await cacheSizeInput.fill("321"); + } + await page.waitForFunction( + () => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + return plugin?.core.services.setting.currentSettings().hashCacheMaxCount === 321; + }, + undefined, + { timeout: uiTimeoutMs } + ); + + if (settingsNavigator.renderer === "declarative") { + await settingsNavigator.returnToCatalogue(); + await scrollDeclarativeLandingToTop(settingsNavigator.dialogue); + await settingsNavigator.dialogue.screenshot({ + ...settingsScreenshotOptions, + path: `${diagnosticsDirectory}/settings-declarative-landing.png`, + }); + await assertDeclarativeLandingOrder(settingsNavigator.dialogue); + const rerunOnboarding = settingsNavigator.dialogue + .locator(".setting-item-name") + .filter({ hasText: "Rerun Onboarding Wizard" }) + .first(); + await rerunOnboarding + .locator("xpath=ancestor::*[contains(concat(' ', normalize-space(@class), ' '), ' setting-item ')][1]") + .click({ timeout: uiTimeoutMs }); + const onboarding = await waitForVisibleObsidianDialogue( + page, + "Welcome to Self-hosted LiveSync", + uiTimeoutMs + ); + await onboarding + .getByRole("button", { name: "No, please take me back", exact: true }) + .click({ timeout: uiTimeoutMs }); + await onboarding.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + const search = settingsNavigator.dialogue.locator(".setting-search-container input"); + await search.fill("Memory cache size (by total items)"); + const searchResult = settingsNavigator.dialogue.locator(".setting-search-result-item").filter({ + has: settingsNavigator.page.getByText("Memory cache size (by total items)", { exact: true }), + }); + await settingsNavigator.dialogue.screenshot({ + ...settingsScreenshotOptions, + path: `${diagnosticsDirectory}/settings-declarative-search.png`, + }); + await searchResult.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await search.fill(""); + + for (const pageName of settingsPageNames) { + const pageRoot = await settingsNavigator.openPage(pageName); + await pageRoot.waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + settingsPage = await settingsNavigator.openPage("Advanced"); + await settingsNavigator.dialogue.screenshot({ + ...settingsScreenshotOptions, + path: `${diagnosticsDirectory}/settings-declarative-advanced.png`, + }); + } + + settingsPage = await settingsNavigator.openPage("Sync Settings"); + + const deletionPanel = settingsPage .locator("h4.sls-setting-panel-title") .filter({ hasText: "Deletion Propagation" }) .locator(".."); @@ -311,6 +630,123 @@ async function verifyEffectiveSettings(): Promise { `The obsolete LiveSync trash toggle is still present in the settings UI (${obsoleteToggleCount} found).` ); } + + if (settingsNavigator.renderer === "declarative") { + await settingsNavigator.close(); + settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs); + settingsPage = await settingsNavigator.openPage("Advanced"); + const restoredValue = await settingsPage + .locator(".setting-item") + .filter({ + has: settingsNavigator.page.getByText("Memory cache size (by total items)", { exact: true }), + }) + .locator('input[type="number"]') + .inputValue(); + if (restoredValue !== "321") { + throw new Error(`The declarative Advanced value was not restored after reopening: ${restoredValue}`); + } + } + + await settingsNavigator.close(); + }); +} + +async function verifyPendingSettingsInitialisationFlow(): Promise<{ choice: string; fallback: string }> { + return await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs); + if (settingsNavigator.renderer === "imperative") { + const general = await settingsNavigator.openPage("General Settings"); + const edgeCaseMode = general.locator(".setting-item").filter({ + has: settingsNavigator.page.getByText("Enable edge case treatment features", { exact: true }), + }); + await edgeCaseMode.locator(".checkbox-container").click({ timeout: uiTimeoutMs }); + await page.waitForFunction( + () => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + return plugin?.core.services.setting.currentSettings().useEdgeCaseMode === true; + }, + undefined, + { timeout: uiTimeoutMs } + ); + } + + const patches = await settingsNavigator.openPage("Patches"); + const caseSensitiveSetting = patches.locator(".setting-item").filter({ + has: settingsNavigator.page.getByText("Handle files as Case-Sensitive", { exact: true }), + }); + await caseSensitiveSetting.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const toggle = caseSensitiveSetting.locator(".checkbox-container"); + if ((await toggle.evaluate((element) => element.classList.contains("is-enabled"))) === true) { + throw new Error("The pending-settings fixture expected case-sensitive file handling to be disabled."); + } + await toggle.click({ timeout: uiTimeoutMs }); + + if (settingsNavigator.renderer === "declarative") { + await settingsNavigator.returnToCatalogue(); + } + const applySetting = settingsNavigator.dialogue + .locator(settingsNavigator.renderer === "declarative" ? ".setting-item" : ".sls-setting-menu-buttons") + .filter({ + has: settingsNavigator.page.getByText("Changes need to be applied!", { exact: true }), + }); + await applySetting.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await applySetting.getByRole("button", { name: "Apply", exact: true }).click({ timeout: uiTimeoutMs }); + + const choiceDialogue = await waitForVisibleObsidianDialogue( + settingsNavigator.page, + "Apply Settings and Reinitialise Synchronisation", + uiTimeoutMs + ); + await choiceDialogue.getByText("Reset Synchronisation on This Device", { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await choiceDialogue + .getByText("Overwrite Server Data with This Device's Files", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await choiceDialogue.getByText("Reset Synchronisation on This Device", { exact: true }).click({ + timeout: uiTimeoutMs, + }); + await choiceDialogue + .getByRole("button", { name: "Restart and Fetch Synchronisation Data", exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + const choice = `${diagnosticsDirectory}/settings-initialisation-choice.png`; + await choiceDialogue.screenshot({ ...settingsScreenshotOptions, path: choice }); + await choiceDialogue + .getByRole("button", { name: "Review another way to apply these settings", exact: true }) + .click({ timeout: uiTimeoutMs }); + await choiceDialogue.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + + const fallbackDialogue = await waitForVisibleObsidianDialogue( + settingsNavigator.page, + "Apply Settings without Initialisation?", + uiTimeoutMs + ); + await fallbackDialogue + .getByRole("button", { name: "Apply without Initialisation", exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await fallbackDialogue.getByRole("button", { name: "Keep Editing", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + const fallback = `${diagnosticsDirectory}/settings-initialisation-fallback.png`; + await fallbackDialogue.screenshot({ ...settingsScreenshotOptions, path: fallback }); + await fallbackDialogue.getByRole("button", { name: "Keep Editing", exact: true }).click({ + timeout: uiTimeoutMs, + }); + await fallbackDialogue.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + + const persistedCaseSensitivity = await page.evaluate(() => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is unavailable"); + return plugin.core.settings.handleFilenameCaseSensitive; + }); + if (persistedCaseSensitivity !== false) { + throw new Error("Cancelling initialisation unexpectedly persisted the pending case-sensitivity setting."); + } + await applySetting.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await settingsNavigator.close(); + return { choice, fallback }; }); } @@ -321,6 +757,7 @@ async function main(): Promise { throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); } const vault = await createTemporaryVault(); + await mkdir(diagnosticsDirectory, { recursive: true }); let session: ObsidianLiveSyncSession | undefined; try { session = await startObsidianLiveSyncSession({ @@ -329,10 +766,10 @@ async function main(): Promise { vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), pluginData: { - doctorProcessedVersion: "0.25.27", + doctorProcessedVersion: settingsOnly ? "1.0.0" : "0.25.27", isConfigured: true, liveSync: false, - versionUpFlash: compatibilityReviewMessage, + versionUpFlash: settingsOnly ? "" : compatibilityReviewMessage, notifyThresholdOfRemoteStorageSize: 0, syncOnStart: false, syncOnSave: false, @@ -345,11 +782,30 @@ async function main(): Promise { usePowerUserMode: false, useEdgeCaseMode: false, }, + lifecycle: settingsOnly + ? { + afterLaunch: async ({ remoteDebuggingPort }) => { + await preseedTrustedVaultState(remoteDebuggingPort, vault.id); + await allowPendingObsidianTestVaultOpenAction(remoteDebuggingPort, vault.path, uiTimeoutMs); + }, + } + : undefined, }); await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); - await verifyCompatibilityReview(); - await verifyConfigDoctorFollowsCompatibilityReview(); + if (settingsOnly) { + await resumePendingCompatibilityReviewForSettings(); + } else { + await verifyCompatibilityReview(); + await verifyConfigDoctorFollowsCompatibilityReview(); + } await verifyEffectiveSettings(); + const initialisation = await verifyPendingSettingsInitialisationFlow(); + const p2pInitialisation = await captureP2PSettingsInitialisationDialogue(); + const mobileLanding = await captureDeclarativeMobileLanding(); + if (mobileLanding) console.log(`Declarative mobile settings landing page: ${mobileLanding}`); + console.log( + `Pending-settings initialisation screenshots: ${initialisation.choice}, ${initialisation.fallback}, ${p2pInitialisation}` + ); console.log("Compatibility review and settings expose only effective user controls."); } finally { if (session) { diff --git a/test/e2e-obsidian/scripts/setup-uri-workflow.ts b/test/e2e-obsidian/scripts/setup-uri-workflow.ts index 3a42db44..f482b7df 100644 --- a/test/e2e-obsidian/scripts/setup-uri-workflow.ts +++ b/test/e2e-obsidian/scripts/setup-uri-workflow.ts @@ -31,6 +31,7 @@ import { captureObsidianDialogue, captureObsidianElement, captureObsidianPage, + openLiveSyncSettings, withObsidianPage, } from "../runner/ui.ts"; import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts"; @@ -90,12 +91,10 @@ function modalByTitle(page: Page, title: string): Locator { }); } -function settingPanelByTitle(page: Page, title: string): Locator { - return page - .locator(".sls-setting") - .locator("h4.sls-setting-panel-title:visible") - .filter({ hasText: title }) - .locator(".."); +async function liveSyncSettingPanelByTitle(page: Page, pageName: string, title: string): Promise { + const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs); + const settingsPage = await settingsNavigator.openPage(pageName); + return settingsPage.locator("h4.sls-setting-panel-title:visible").filter({ hasText: title }).locator(".."); } async function captureGuideDialogue(port: number, filename: string, title: string): Promise { @@ -535,58 +534,27 @@ async function captureHiddenFileGuideSettings( environment ); - await withObsidianPage(port, async (page) => { - await page.evaluate(() => { - const obsidian = globalThis as typeof globalThis & { - app?: { - setting?: { - open(): void; - openTabById(tabId: string): void; - }; - }; - }; - const setting = obsidian.app?.setting; - if (!setting) throw new Error("Obsidian settings are unavailable"); - setting.open(); - setting.openTabById("obsidian-livesync"); - }); - const settings = page.locator(".sls-setting"); - await settings.waitFor({ state: "visible", timeout: uiTimeoutMs }); - await settings.locator('.sls-setting-menu-btn[title="Setup"]').click({ timeout: uiTimeoutMs }); - }); - const screenshots = [ await captureObsidianElement(port, "guide-hidden-file-advanced-features.png", (page) => - settingPanelByTitle(page, "Enable extra and advanced features") + liveSyncSettingPanelByTitle(page, "Setup", "Enable extra and advanced features") ), ]; - await withObsidianPage(port, async (page) => { - await page - .locator(".sls-setting") - .locator('.sls-setting-menu-btn[title="Selector"]') - .click({ timeout: uiTimeoutMs }); - }); screenshots.push( await captureObsidianElement(port, "guide-hidden-file-selector.png", (page) => - settingPanelByTitle(page, "Hidden Files") + liveSyncSettingPanelByTitle(page, "Selector", "Hidden Files") ) ); - await withObsidianPage(port, async (page) => { - await page - .locator(".sls-setting") - .locator('.sls-setting-menu-btn[title="Sync Settings"]') - .click({ timeout: uiTimeoutMs }); - }); screenshots.push( await captureObsidianElement(port, "guide-hidden-file-enable.png", (page) => - settingPanelByTitle(page, "Hidden Files") + liveSyncSettingPanelByTitle(page, "Sync Settings", "Hidden Files") ) ); await withObsidianPage(port, async (page) => { - await page.keyboard.press("Escape"); + const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs); + await settingsNavigator.close(); }); return screenshots; } diff --git a/updates.md b/updates.md index 815023a2..bcb5a9e2 100644 --- a/updates.md +++ b/updates.md @@ -12,6 +12,17 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi ## Unreleased +### Interface and translation + +#### Fixed + +- Compatibility pause warnings now direct you to the dedicated compatibility review instead of the Change Log. + +#### Improved + +- Settings page names, General controls, Quick Setup actions, and Advanced controls now use Obsidian 1.13's native settings interface and global search, while retaining their familiar icons. The landing page keeps Remote Configuration and Sync Settings together, places Appearance, Logging, and Extra menus under General Settings, and groups maintenance, optional features, advanced settings, and help by purpose. Earlier supported Obsidian versions continue to use the pane-based interface. +- Settings changes which require database initialisation now use a focused Setup Manager dialogue to choose between existing synchronisation data and the files in the current Vault. The selected reset or rebuild is reserved before the settings are saved, while cancelling offers a separate, explicit settings-only fallback. + ## 1.0.18 24th August, 2026