- Setting dialogue very slightly refined.
  - The hodgepodge inside the `Hatch` pane has been sorted into more explicit categorised panes.
  - Applying the settings will now be more informative.

New features:
- Word-segmented chunk building on users language.

Fixed:
- Sending chunks on `Send chunk in bulk` are now buffered to avoid the out-of-memory error.
- `Send chunk in bulk` is back to default disabled.
- Merging conflicts of JSON files are now works fine even if it contains `null`.
Development:
- Implemented the logic for automatically generating the stub of document for the setting dialogue.
This commit is contained in:
vorotamoroz
2024-09-24 14:00:44 +01:00
parent 48e4d57278
commit b73ca73776
15 changed files with 3274 additions and 2315 deletions

View File

@@ -23,12 +23,14 @@
"args": "none"
}
],
"no-unused-labels": "off",
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off",
"require-await": "warn",
"no-async-promise-executor": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "error"
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"no-constant-condition": ["error", { "checkLoops": false }]
}
}
}

View File

@@ -1,267 +1,750 @@
NOTE: This document surely became outdated. I'll improve this doc in a while. but your contributions are always welcome.
NOTE: This document not completed. I'll improve this doc in a while. but your contributions are always welcome.
# Settings of this plugin
# Settings of Self-hosted LiveSync
The settings dialog has been quite long, so I split each configuration into tabs.
If you feel something, please feel free to inform me.
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:
| icon | description |
| :---: | ----------------------------------------------------------------- |
| 🛰️ | [Remote Database Configurations](#remote-database-configurations) |
| 📦 | [Local Database Configurations](#local-database-configurations) |
| ⚙️ | [General Settings](#general-settings) |
| 🔁 | [Sync Settings](#sync-settings) |
| 🔧 | [Miscellaneous](#miscellaneous) |
| 🧰 | [Hatch](#miscellaneous) |
| 🔌 | [Plugin and its settings](#plugin-and-its-settings) |
| 🚑 | [Corrupted data](#corrupted-data) |
| Icon | Description |
| :--: | ------------------------------------------------------------------ |
| 💬 | [0. Update Information](#0-update-information) |
| 🧙‍♂️ | [1. Setup](#1-setup) |
| ⚙️ | [2. General Settings](#2-general-settings) |
| 🛰️ | [3. Remote Configuration](#3-remote-configuration) |
| 🔄 | [4. Sync Settings](#4-sync-settings) |
| 🚦 | [5. Selector (Advanced)](#5-selector-advanced) |
| 🔌 | [6. Customization sync (Advanced)](#6-customization-sync-advanced) |
| 🧰 | [7. Hatch](#7-hatch) |
| 🔧 | [8. Advanced (Advanced)](#8-advanced-advanced) |
| 💪 | [9. Power users (Power User)](#9-power-users-power-user) |
| 🩹 | [10. Patches (Edge Case)](#10-patches-edge-case) |
| 🎛️ | [11. Maintenance](#11-maintenance) |
## Remote Database Configurations
Configure the settings of synchronize server. If any synchronization is enabled, you can't edit this section. Please disable all synchronization to change.
## 0. Update Information
### URI
URI of CouchDB. In the case of Cloudant, It's "External Endpoint(preferred)".
**Do not end it up with a slash** when it doesn't contain the database name.
This pane shows version up information. You can check what has been changed in recent versions.
### Username
Your CouchDB's Username. Administrator's privilege is preferred.
## 1. Setup
### Password
Your CouchDB's Password.
Note: This password is saved into your Obsidian's vault in plain text.
This pane is used for setting up Self-hosted LiveSync. There are several options to set up Self-hosted LiveSync.
### Database Name
The Database name to synchronize.
If not exist, created automatically.
### 1. Quick Setup
Most preferred method to setup Self-hosted LiveSync. You can setup Self-hosted LiveSync with a few clicks.
### End to End Encryption
Encrypt your database. It affects only the database, your files are left as plain.
#### Use the copied setup URI
The encryption algorithm is AES-GCM.
Setup the Self-hosted LiveSync with the `setup URI` which is [copied from another device](#copy-current-settings-as-a-new-setup-uri) or the setup script.
Note: If you want to use "Plugins and their settings", you have to enable this.
#### Minimal setup
### Passphrase
The passphrase to used as the key of encryption. Please use the long text.
Step-by-step setup for Self-hosted LiveSync. You can setup Self-hosted LiveSync manually with Minimal setting items.
### Apply
Set the End to End encryption enabled and its passphrase for use in replication.
If you change the passphrase of an existing database, overwriting the remote database is strongly recommended.
#### Enable LiveSync on this device as the setup was completed manually
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.
### Overwrite remote database
Overwrite the remote database with the local database using the passphrase you applied.
### 2. To setup the other devices
#### Copy current settings as a new setup URI
### Rebuild
Rebuild remote and local databases with local files. It will delete all document history and retained chunks, and shrink the database.
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).
### Test Database connection
You can check the connection by clicking this button.
### 3. Reset
### Check database configuration
You can check and modify your CouchDB configuration from here directly.
#### Discard existing settings and databases
### Lock remote database.
Other devices are banned from the database when you have locked the database.
If you have something troubled with other devices, you can protect the vault and remote database with your device.
Reset the Self-hosted LiveSync settings and databases.
**Hazardous operation. Please be careful when using this.**
## Local Database Configurations
"Local Database" is created inside your obsidian.
### 4. Enable extra and advanced features
### Batch database update
Delay database update until raise replication, open another file, window visibility changes, or file events except for file modification.
This option can not be used with LiveSync at the same time.
To keep the set-up dialogue simple, some panes are hidden in default. You can enable them here.
#### Enable advanced features
### Fetch rebuilt DB.
If one device rebuilds or locks the remote database, every other device will be locked out from the remote database until it fetches rebuilt DB.
Setting key: useAdvancedMode
### minimum chunk size and LongLine threshold
The configuration of chunk splitting.
Following panes will be shown when you enable this setting.
| Icon | Description |
| :--: | ------------------------------------------------------------------ |
| 🚦 | [5. Selector (Advanced)](#5-selector-advanced) |
| 🔌 | [6. Customization sync (Advanced)](#6-customization-sync-advanced) |
| 🔧 | [8. Advanced (Advanced)](#8-advanced-advanced) |
Self-hosted LiveSync splits the note into chunks for efficient synchronization. This chunk should be longer than the "Minimum chunk size".
#### Enable power user features
Specifically, the length of the chunk is determined by the following orders.
Setting key: usePowerUserMode
1. Find the nearest newline character, and if it is farther than LongLineThreshold, this piece becomes an independent chunk.
Following panes will be shown when you enable this setting.
| Icon | Description |
| :--: | ------------------------------------------------------------------ |
| 💪 | [9. Power users (Power User)](#9-power-users-power-user) |
2. If not, find the nearest to these items.
1. A newline character
2. An empty line (Windows style)
3. An empty line (non-Windows style)
3. Compare the farther in these 3 positions and the next "newline\]#" position, and pick a shorter piece as a chunk.
#### Enable edge case treatment features
This rule was made empirically from my dataset. If this rule acts as badly on your data. Please give me the information.
Setting key: useEdgeCaseMode
You can dump saved note structure to `Dump informations of this doc`. Replace every character with x except newline and "#" when sending information to me.
Following panes will be shown when you enable this setting.
| Icon | Description |
| :--: | ------------------------------------------------------------------ |
| 🩹 | [10. Patches (Edge Case)](#10-patches-edge-case) |
The default values are 20 letters and 250 letters.
## 2. General Settings
## General Settings
### 1. Appearance
### Do not show low-priority log
If you enable this option, log only the entries with the popup.
#### Display Language
### Verbose log
Setting key: displayLanguage
## Sync Settings
You can change the display language. It is independent of the system language and/or Obsidian's language.
Note: Not all messages have been translated. And, please revert to "Default" when reporting errors. Of course, your contribution to translation is always welcome!
### LiveSync
Do LiveSync.
#### Show status inside the editor
It is the one of raison d'être of this plugin.
Setting key: showStatusOnEditor
Useful, but this method drains many batteries on the mobile and uses not the ignorable amount of data transfer.
We can show the status of synchronisation inside the editor.
This method is exclusive to other synchronization methods.
Reflected after reboot
### Periodic Sync
Synchronize periodically.
#### Show status as icons only
### Periodic Sync Interval
Unit is seconds.
Setting key: showOnlyIconsOnEditor
### Sync on Save
Synchronize when the note has been modified or created.
Show status as icons only. This is useful when you want to save space on the status bar.
### Sync on File Open
Synchronize when the note is opened.
#### Show status on the status bar
### Sync on Start
Synchronize when Obsidian started.
Setting key: showStatusOnStatusbar
### Use Trash for deleted files
When the file has been deleted on remote devices, deletion will be replicated to the local device and the file will be deleted.
We can show the status of synchronisation on the status bar. (Default: On)
If this option is enabled, move deleted files into the trash instead delete actually.
### 2. Logging
### Do not delete empty folder
Self-hosted LiveSync will delete the folder when the folder becomes empty. If this option is enabled, leave it as an empty folder.
#### Show only notifications
### Use newer file if conflicted (beta)
Always use the newer file to resolve and overwrite when conflict has occurred.
Setting key: lessInformationInLog
Prevent logging and show only notification. Please disable when you report the logs
### Experimental.
### Sync hidden files
#### Verbose Log
Synchronize hidden files.
Setting key: showVerboseLog
- Scan hidden files before replication.
If you enable this option, all hidden files are scanned once before replication.
Show verbose log. Please enable when you report the logs
- Scan hidden files periodicaly.
If you enable this option, all hidden files will be scanned each [n] seconds.
## 3. Remote Configuration
Hidden files are not actively detected, so we need scanning.
### 1. Remote Server
Each scan stores the file with their modification time. And if the file has been disappeared, the fact is also stored. Then, When the entry of the hidden file has been replicated, it will be reflected in the storage if the entry is newer than storage.
#### Remote Type
Therefore, the clock must be adjusted. If the modification time is determined to be older, the changeset will be skipped or cancelled (It means, **deleted**), even if the file spawned in a hidden folder.
Setting key: remoteType
### Advanced settings
Self-hosted LiveSync using PouchDB and synchronizes with the remote by [this protocol](https://docs.couchdb.org/en/stable/replication/protocol.html).
So, it splits every entry into chunks to be acceptable by the database with limited payload size and document size.
Remote server type
However, it was not enough.
According to [2.4.2.5.2. Upload Batch of Changed Documents](https://docs.couchdb.org/en/stable/replication/protocol.html#upload-batch-of-changed-documents) in [Replicate Changes](https://docs.couchdb.org/en/stable/replication/protocol.html#replicate-changes), it might become a bigger request.
### 2. Notification
Unfortunately, there is no way to deal with this automatically by size for every request.
Therefore, I made it possible to configure this.
#### Notify when the estimated remote storage size exceeds on start up
Note: If you set these values lower number, the number of requests will increase.
Therefore, if you are far from the server, the total throughput will be low, and the traffic will increase.
Setting key: notifyThresholdOfRemoteStorageSize
### Batch size
Number of change feed items to process at a time. Defaults to 250.
MB (0 to disable). We can get a notification when the estimated remote storage size exceeds this value.
### Batch limit
Number of batches to process at a time. Defaults to 40. This along with batch size controls how many docs are kept in memory at a time.
### 3. Confidentiality
## Miscellaneous
#### End-to-End Encryption
### Show status inside editor
Show information inside the editor pane.
It would be useful for mobile.
Setting key: encrypt
### Check integrity on saving
Check all chunks are correctly saved on saving.
Enable end-to-end encryption. enabling this is recommend. If you change the passphrase, you need to rebuild databases (You will be informed).
### Presets
You can set synchronization method at once as these pattern:
- LiveSync
- LiveSync : enabled
- Batch database update : disabled
- Periodic Sync : disabled
- Sync on Save : disabled
- Sync on File Open : disabled
- Sync on Start : disabled
- Periodic w/ batch
- LiveSync : disabled
- Batch database update : enabled
- Periodic Sync : enabled
- Sync on Save : disabled
- Sync on File Open : enabled
- Sync on Start : enabled
- Disable all sync
- LiveSync : disabled
- Batch database update : disabled
- Periodic Sync : disabled
- Sync on Save : disabled
- Sync on File Open : disabled
- Sync on Start : disabled
#### Passphrase
Setting key: passphrase
## Hatch
From here, everything is under the hood. Please handle it with care.
Encrypting passphrase. If you change the passphrase, you need to rebuild databases (You will be informed).
When there are problems with synchronization, the warning message is shown Under this section header.
#### Path Obfuscation
- Pattern 1
![CorruptedData](../images/lock_pattern1.png)
This message is shown when the remote database is locked and your device is not marked as "resolved".
Almost it is happened by enabling End-to-End encryption or History has been dropped.
If you enabled End-to-End encryption, you can unlock the remote database by "Apply and receive" automatically. Or "Drop and receive" when you dropped. If you want to unlock manually, click "mark this device as resolved".
Setting key: usePathObfuscation
- Pattern 2
![CorruptedData](../images/lock_pattern2.png)
The remote database indicates that has been unlocked Pattern 1.
When you mark all devices as resolved, you can unlock the database.
But, there's no problem even if you leave it as it is.
In default, the path of the file is not obfuscated to improve the performance. If you enable this, the path of the file will be obfuscated. This is useful when you want to hide the path of the file.
### Verify and repair all files
read all files in the vault, and update them into the database if there's diff or could not read from the database.
#### Use dynamic iteration count (Experimental)
### Suspend file watching
If enable this option, Self-hosted LiveSync dismisses every file change or deletes the event.
Setting key: useDynamicIterationCount
From here, these commands are used inside applying encryption passphrases or dropping histories.
This is an experimental feature and not recommended. If you enable this, the iteration count of the encryption will be dynamically determined. This is useful when you want to improve the performance.
Usually, doesn't use it so much. But sometimes it could be handy.
---
## Plugins and settings (beta)
**now writing from here onwards, sorry**
### Enable plugin synchronization
If you want to use this feature, you have to activate this feature by this switch.
---
### Sweep plugins automatically
Plugin sweep will run before replication automatically.
### 4. Minio,S3,R2
### Sweep plugins periodically
Plugin sweep will run each 1 minute.
#### Endpoint URL
### Notify updates
When replication is complete, a message will be notified if a newer version of the plugin applied to this device is configured on another device.
Setting key: endpoint
### Device and Vault name
To save the plugins, you have to set a unique name every each device.
#### Access Key
### Open
Open the "Plugins and their settings" dialog.
Setting key: accessKey
### Corrupted or missing data
![CorruptedData](../images/corrupted_data.png)
#### Secret Key
When Self-hosted LiveSync could not write to the file on the storage, the files are shown here. If you have the old data in your vault, change it once, it will be cured. Or you can use the "File History" plugin.
Setting key: secretKey
#### Region
Setting key: region
#### Bucket Name
Setting key: bucket
#### Use Custom HTTP Handler
Setting key: useCustomRequestHandler
If your Object Storage could not configured accepting CORS, enable this.
#### Test Connection
#### Apply Settings
### 5. CouchDB
#### URI
Setting key: couchDB_URI
#### Username
Setting key: couchDB_USER
username
#### Password
Setting key: couchDB_PASSWORD
password
#### Database name
Setting key: couchDB_DBNAME
#### Test Database Connection
Open database connection. If the remote database is not found and you have the privilege to create a database, the database will be created.
#### Check and fix database configuration
Check the database configuration, and fix if there are any problems.
#### Apply Settings
## 4. Sync Settings
### 1. Synchronization Preset
#### Presets
Setting key: preset
Apply preset configuration
### 2. Synchronization Methods
#### Sync Mode
Setting key: syncMode
#### Periodic Sync interval
Setting key: periodicReplicationInterval
Interval (sec)
#### Sync on Save
Setting key: syncOnSave
When you save a file, sync automatically
#### Sync on Editor Save
Setting key: syncOnEditorSave
When you save a file in the editor, sync automatically
#### Sync on File Open
Setting key: syncOnFileOpen
When you open a file, sync automatically
#### Sync on Start
Setting key: syncOnStart
Start synchronization after launching Obsidian.
#### Sync after merging file
Setting key: syncAfterMerge
Sync automatically after merging files
### 3. Update thinning
#### Batch database update
Setting key: batchSave
Reducing the frequency with which on-disk changes are reflected into the DB
#### Minimum delay for batch database updating
Setting key: batchSaveMinimumDelay
Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.
#### Maximum delay for batch database updating
Setting key: batchSaveMaximumDelay
Saving will be performed forcefully after this number of seconds.
### 4. Deletion Propagation (Advanced)
#### Use the trash bin
Setting key: trashInsteadDelete
Do not delete files that are deleted in remote, just move to trash.
#### Keep empty folder
Setting key: doNotDeleteFolder
Normally, a folder is deleted when it becomes empty after a synchronization. Enabling this will prevent it from getting deleted
### 5. Conflict resolution (Advanced)
#### Always overwrite with a newer file (beta)
Setting key: resolveConflictsByNewerFile
(Def off) Resolve conflicts by newer files automatically.
#### Postpone resolution of inactive files
Setting key: checkConflictOnlyOnOpen
#### Postpone manual resolution of inactive files
Setting key: showMergeDialogOnlyOnActive
### 6. Sync settings via markdown (Advanced)
#### Filename
Setting key: settingSyncFile
If you set this, all settings are saved in a markdown file. You will be notified when new settings arrive. You can set different files by the platform.
#### Write credentials in the file
Setting key: writeCredentialsForSettingSync
(Not recommended) If set, credentials will be stored in the file.
#### Notify all setting files
Setting key: notifyAllSettingSyncFile
### 7. Hidden files (Advanced)
#### Hidden file synchronization
#### Enable Hidden files sync
#### Scan for hidden files before replication
Setting key: syncInternalFilesBeforeReplication
#### Scan hidden files periodically
Setting key: syncInternalFilesInterval
Seconds, 0 to disable
## 5. Selector (Advanced)
### 1. Normal Files
#### Synchronising files
(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.
#### Non-Synchronising files
(RegExp) If this is set, any changes to local and remote files that match this will be skipped.
#### Maximum file size
Setting key: syncMaxSizeInMB
(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.
#### (Beta) Use ignore files
Setting key: useIgnoreFiles
If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.
#### Ignore files
Setting key: ignoreFiles
We can use multiple ignore files, e.g.) `.gitignore, .dockerignore`
### 2. Hidden Files (Advanced)
#### Ignore patterns
#### Add default patterns
## 6. Customization sync (Advanced)
### 1. Customization Sync
#### Device name
Setting key: deviceAndVaultName
Unique name between all synchronized devices. To edit this setting, please disable customization sync once.
#### Per-file-saved customization sync
Setting key: usePluginSyncV2
If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.
#### Enable customization sync
Setting key: usePluginSync
#### Scan customization automatically
Setting key: autoSweepPlugins
Scan customization before replicating.
#### Scan customization periodically
Setting key: autoSweepPluginsPeriodic
Scan customization every 1 minute.
#### Notify customized
Setting key: notifyPluginOrSettingUpdated
Notify when other device has newly customized.
#### Open
Open the dialog
## 7. Hatch
### 1. Reporting Issue
#### Make report to inform the issue
#### Write logs into the file
Setting key: writeLogToTheFile
Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.
### 2. Scram Switches
#### Suspend file watching
Setting key: suspendFileWatching
Stop watching for file change.
#### Suspend database reflecting
Setting key: suspendParseReplicationResult
Stop reflecting database changes to storage files.
### 3. Recovery and Repair
#### Recreate missing chunks for all files
This will recreate chunks for all files. If there were missing chunks, this may fix the errors.
#### Verify and repair all files
Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.
#### Check and convert non-path-obfuscated files
### 4. Reset
#### Back to non-configured
#### Delete all customization sync data
## 8. Advanced (Advanced)
### 1. Memory cache
#### Memory cache size (by total items)
Setting key: hashCacheMaxCount
#### Memory cache size (by total characters)
Setting key: hashCacheMaxAmount
(Mega chars)
### 2. Local Database Tweak
#### Enhance chunk size
Setting key: customChunkSize
#### Use splitting-limit-capped chunk splitter
Setting key: enableChunkSplitterV2
If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.
#### Use Segmented-splitter
Setting key: useSegmenter
If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.
### 3. Transfer Tweak
#### Fetch chunks on demand
Setting key: readChunksOnline
(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.
#### Batch size of on-demand fetching
Setting key: concurrencyOfReadChunksOnline
#### The delay for consecutive on-demand fetches
Setting key: minimumIntervalOfReadChunksOnline
#### Send chunks in bulk
Setting key: sendChunksBulk
If this enabled, all chunks will be sent in bulk. This is useful for the environment that has a high latency.
#### Maximum size of chunks to send in one request
Setting key: sendChunksBulkMaxSize
MB
## 9. Power users (Power User)
### 1. Remote Database Tweak
#### Incubate Chunks in Document (Beta)
Setting key: useEden
If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.
#### Maximum Incubating Chunks
Setting key: maxChunksInEden
The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.
#### Maximum Incubating Chunk Size
Setting key: maxTotalLengthInEden
The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.
#### Maximum Incubation Period
Setting key: maxAgeInEden
The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.
#### Data Compression (Experimental)
Setting key: enableCompression
### 2. CouchDB Connection Tweak
#### Batch size
Setting key: batch_size
Number of change feed items to process at a time. Defaults to 50. Minimum is 2.
#### Batch limit
Setting key: batches_limit
Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.
#### Use timeouts instead of heartbeats
Setting key: useTimeouts
If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.
### 3. Configuration Encryption
#### Encrypting sensitive configuration items
Setting key: configPassphraseStore
#### Passphrase of sensitive configuration items
Setting key: configPassphrase
This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.
## 10. Patches (Edge Case)
### 1. Compatibility (Metadata)
#### Do not keep metadata of deleted files.
Setting key: deleteMetadataOfDeletedFiles
#### Delete old metadata of deleted files on start-up
Setting key: automaticallyDeleteMetadataOfDeletedFiles
(Days passed, 0 to disable automatic-deletion)
### 2. Compatibility (Conflict Behaviour)
#### Always resolve conflicts manually
Setting key: disableMarkdownAutoMerge
If this switch is turned on, a merge dialog will be displayed, even if the sensible-merge is possible automatically. (Turn on to previous behavior)
#### Always reflect synchronized changes even if the note has a conflict
Setting key: writeDocumentsIfConflicted
Turn on to previous behavior
### 3. Compatibility (Database structure)
#### (Obsolete) Use an old adapter for compatibility (obsolete)
Setting key: useIndexedDBAdapter
Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.
#### Compute revisions for chunks (Previous behaviour)
Setting key: doNotUseFixedRevisionForChunks
If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)
#### Handle files as Case-Sensitive
Setting key: handleFilenameCaseSensitive
If this enabled, All files are handled as case-Sensitive (Previous behaviour).
### 4. Compatibility (Internal API Usage)
#### Scan changes on customization sync
Setting key: watchInternalFileChanges
Do not use internal API
### 5. Edge case addressing (Database)
#### Database suffix
Setting key: additionalSuffixOfDatabaseName
LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.
#### The Hash algorithm for chunk IDs (Experimental)
Setting key: hashAlg
### 6. Edge case addressing (Behaviour)
#### Fetch database with previous behaviour
Setting key: doNotSuspendOnFetching
#### Keep empty folder
Setting key: doNotDeleteFolder
Normally, a folder is deleted when it becomes empty after a synchronization. Enabling this will prevent it from getting deleted
### 7. Edge case addressing (Processing)
#### Do not split chunks in the background
Setting key: disableWorkerForGeneratingChunks
If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).
#### Process small files in the foreground
Setting key: processSmallFilesInUIThread
If enabled, the file under 1kb will be processed in the UI thread.
### 8. Compatibility (Trouble addressed)
#### Do not check configuration mismatch before replication
Setting key: disableCheckingConfigMismatch
## 11. Maintenance
### 1. Scram!
#### Lock remote
Lock remote to prevent synchronization with other devices.
#### Emergency restart
place the flag file to prevent all operation and restart.
### 2. Data-complementary Operations
#### Resend
Resend all chunks to the remote.
#### Reset journal received history
Initialise journal received history. On the next sync, every item except this device sent will be downloaded again.
#### Reset journal sent history
Initialise journal sent history. On the next sync, every item except this device received will be sent again.
### 3. Rebuilding Operations (Local)
#### Fetch from remote
Restore or reconstruct local database from remote.
#### Fetch rebuilt DB (Save local documents before)
Restore or reconstruct local database from remote database but use local chunks.
### 4. Total Overhaul
#### Rebuild everything
Rebuild local and remote database with local files.
### 5. Rebuilding Operations (Remote Only)
#### Perform compaction
Compaction discards all of Eden in the non-latest revisions, reducing the storage usage. However, this operation requires the same free space on the remote as the current database.
#### Overwrite remote
Overwrite remote with local DB and passphrase.
#### Reset all journal counter
Initialise all journal history, On the next sync, every item will be received and sent.
#### Purge all journal counter
Purge all sending and downloading cache.
#### Make empty the bucket
Delete all data on the remote.
### 6. Niches
#### (Obsolete) Clean up databases
Delete unused chunks to shrink the database. However, this feature could be not effective in some cases. Please use rebuild everything instead.
### 7. Reset
#### Discard local database to reset or uninstall Self-hosted LiveSync

View File

@@ -9,67 +9,10 @@ import fs from "node:fs";
// import terser from "terser";
import { minify } from "terser";
import inlineWorkerPlugin from "esbuild-plugin-inline-worker";
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD AND TERSER
if you want to view the source, please visit the github repository of this plugin
*/
`;
import { terserOption } from "./terser.config.mjs";
const prod = process.argv[2] === "production";
const dev = process.argv[2] === "dev";
const keepTest = !prod || dev;
const terserOpt = {
sourceMap: !prod
? {
url: "inline",
}
: {},
format: {
indent_level: 2,
beautify: true,
comments: "some",
ecma: 2018,
preamble: banner,
webkit: true,
},
parse: {
// parse options
},
compress: {
// compress options
defaults: false,
evaluate: true,
dead_code: true,
inline: 3,
join_vars: true,
loops: true,
passes: prod ? 4 : 1,
reduce_vars: true,
reduce_funcs: true,
arrows: true,
collapse_vars: true,
comparisons: true,
lhs_constants: true,
hoist_props: true,
side_effects: true,
if_return: true,
ecma: 2018,
unused: true,
},
// mangle: false,
ecma: 2018, // specify one of: 5, 2015, 2016, etc.
enclose: false, // or specify true, or "args:values"
keep_classnames: true,
keep_fnames: true,
ie8: false,
module: false,
// nameCache: null, // or specify a name cache object
safari10: false,
toplevel: false,
};
const keepTest = !prod;
const manifestJson = JSON.parse(fs.readFileSync("./manifest.json") + "");
const packageJson = JSON.parse(fs.readFileSync("./package.json") + "");
@@ -91,7 +34,7 @@ const plugins = [
console.log("Performing terser");
const src = fs.readFileSync("./main_org.js").toString();
// @ts-ignore
const ret = await minify(src, terserOpt);
const ret = await minify(src, terserOption);
if (ret && ret.code) {
fs.writeFileSync("./main.js", ret.code);
}
@@ -107,7 +50,7 @@ const plugins = [
const externals = ["obsidian", "electron", "crypto", "@codemirror/autocomplete", "@codemirror/collab", "@codemirror/commands", "@codemirror/language", "@codemirror/lint", "@codemirror/search", "@codemirror/state", "@codemirror/view", "@lezer/common", "@lezer/highlight", "@lezer/lr"];
const context = await esbuild.context({
banner: {
js: banner,
js: "// Leave it all to terser",
},
entryPoints: ["src/main.ts"],
bundle: true,
@@ -146,7 +89,7 @@ const context = await esbuild.context({
],
});
if (prod || dev) {
if (prod) {
await context.rebuild();
process.exit(0);
} else {

15
package-lock.json generated
View File

@@ -18,7 +18,7 @@
"fflate": "^0.8.2",
"idb": "^8.0.0",
"minimatch": "^10.0.1",
"octagonal-wheels": "^0.1.14",
"octagonal-wheels": "^0.1.15",
"xxhash-wasm": "0.4.2",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
},
@@ -5099,10 +5099,9 @@
}
},
"node_modules/octagonal-wheels": {
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.14.tgz",
"integrity": "sha512-W0DQL5YNL7oJH2Dcb7mOE/P7MNrM7PXWJZHJpPGzbrKHAT14OtklbBFNJvM26v8nizwlb5WHQA+W/Tg1CIDSGQ==",
"license": "MIT",
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.15.tgz",
"integrity": "sha512-rl6y/3/+tqeiDmBmW2RgYQh85D4xVHQIGg14DyC4/sUnO6UDLuVUULIO+00E2jfA/o4coKnPOPEB8Tt6BE9SEA==",
"dependencies": {
"idb": "^8.0.0",
"xxhash-wasm": "0.4.2",
@@ -10234,9 +10233,9 @@
}
},
"octagonal-wheels": {
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.14.tgz",
"integrity": "sha512-W0DQL5YNL7oJH2Dcb7mOE/P7MNrM7PXWJZHJpPGzbrKHAT14OtklbBFNJvM26v8nizwlb5WHQA+W/Tg1CIDSGQ==",
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.15.tgz",
"integrity": "sha512-rl6y/3/+tqeiDmBmW2RgYQh85D4xVHQIGg14DyC4/sUnO6UDLuVUULIO+00E2jfA/o4coKnPOPEB8Tt6BE9SEA==",
"requires": {
"idb": "^8.0.0",
"xxhash-wasm": "0.4.2",

View File

@@ -65,7 +65,7 @@
"fflate": "^0.8.2",
"idb": "^8.0.0",
"minimatch": "^10.0.1",
"octagonal-wheels": "^0.1.14",
"octagonal-wheels": "^0.1.15",
"xxhash-wasm": "0.4.2",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
}

View File

@@ -134,7 +134,13 @@ export function generatePatchObj(from: Record<string | number | symbol, any>, to
//if type is not match, replace completely.
ret[key] = { [MARK_SWAPPED]: value };
} else {
if (typeof (v) == "object" && typeof (value) == "object" && !Array.isArray(v) && !Array.isArray(value)) {
if (v === null && value === null) {
// NO OP.
} else if (v === null && value !== null) {
ret[key] = { [MARK_SWAPPED]: value };
} else if (v !== null && value === null) {
ret[key] = { [MARK_SWAPPED]: value };
} else if (typeof (v) == "object" && typeof (value) == "object" && !Array.isArray(v) && !Array.isArray(value)) {
const wk = generatePatchObj(v, value);
if (Object.keys(wk).length > 0) ret[key] = wk;
} else if (typeof (v) == "object" && typeof (value) == "object" && Array.isArray(v) && Array.isArray(value)) {
@@ -169,6 +175,10 @@ export function applyPatch(from: Record<string | number | symbol, any>, patch: R
delete ret[key];
continue;
}
if (value === null) {
ret[key] = null;
continue;
}
if (typeof (value) == "object") {
if (MARK_SWAPPED in value) {
ret[key] = value[MARK_SWAPPED];
@@ -251,6 +261,7 @@ export function mergeObject(
export function flattenObject(obj: Record<string | number | symbol, any>, path: string[] = []): [string, any][] {
if (typeof (obj) != "object") return [[path.join("."), obj]];
if (obj === null) return [[path.join("."), null]];
if (Array.isArray(obj)) return [[path.join("."), JSON.stringify(obj)]];
const e = Object.entries(obj);
const ret = []
@@ -489,6 +500,35 @@ export function useMemo<T>({ key, forceUpdate, validator }: MemoOption, updateFu
return value;
}
// const _static = new Map<string, any>();
const _staticObj = new Map<string, {
value: any
}>();
export function useStatic<T>(key: string): { value: (T | undefined) };
export function useStatic<T>(key: string, initial: T): { value: T };
export function useStatic<T>(key: string, initial?: T) {
// if (!_static.has(key) && initial) {
// _static.set(key, initial);
// }
const obj = _staticObj.get(key);
if (obj !== undefined) {
return obj;
} else {
// let buf = initial;
const obj = {
_buf: initial,
get value() {
return this._buf as T;
},
set value(value: T) {
this._buf = value
}
}
_staticObj.set(key, obj);
return obj;
}
}
export function disposeMemo(key: string) {
_cached.delete(key);
}

View File

@@ -129,7 +129,7 @@ export class LogAddOn extends LiveSyncCommands {
this.statusBarLabels = statusBarLabels;
const applyToDisplay = throttle((label: typeof statusBarLabels.value) => {
const v = label;
// const v = label;
this.applyStatusBarText();
}, 20);
@@ -161,10 +161,10 @@ export class LogAddOn extends LiveSyncCommands {
}
onload(): void | Promise<void> {
eventHub.on(EVENT_FILE_RENAMED, (evt: CustomEvent<{ oldPath: string, newPath: string }>) => {
eventHub.onEvent(EVENT_FILE_RENAMED, (evt: CustomEvent<{ oldPath: string, newPath: string }>) => {
this.setFileStatus();
});
eventHub.on(EVENT_LEAF_ACTIVE_CHANGED, () => this.onActiveLeafChange());
eventHub.onEvent(EVENT_LEAF_ACTIVE_CHANGED, () => this.onActiveLeafChange());
const w = document.querySelectorAll(`.livesync-status`);
w.forEach(e => e.remove());
@@ -175,7 +175,7 @@ export class LogAddOn extends LiveSyncCommands {
this.messageArea = this.statusDiv.createDiv({ cls: "livesync-status-messagearea" });
this.logMessage = this.statusDiv.createDiv({ cls: "livesync-status-logmessage" });
this.logHistory = this.statusDiv.createDiv({ cls: "livesync-status-loghistory" });
eventHub.on(EVENT_LAYOUT_READY, () => this.adjustStatusDivPosition());
eventHub.onEvent(EVENT_LAYOUT_READY, () => this.adjustStatusDivPosition());
if (this.settings.showStatusOnStatusbar) {
this.statusBar = this.plugin.addStatusBarItem();
this.statusBar.addClass("syncstatusbar");

Submodule src/lib updated: 633af447d2...3108e3e3db

View File

@@ -46,7 +46,7 @@ import { LogAddOn } from "./features/CmdStatusInsideEditor.ts";
import { eventHub } from "./lib/src/hub/hub.ts";
import { EVENT_FILE_RENAMED, EVENT_LAYOUT_READY, EVENT_LEAF_ACTIVE_CHANGED, EVENT_PLUGIN_LOADED, EVENT_PLUGIN_UNLOADED, EVENT_SETTING_SAVED } from "./common/events.ts";
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
import { yieldMicrotask } from "octagonal-wheels/promises";
setNoticeClass(Notice);
@@ -354,10 +354,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
return new PouchDB(name, optionPass);
}
beforeOnUnload(db: LiveSyncLocalDB): void {
this.kvDB.close();
if (this.kvDB) this.kvDB.close();
}
onClose(db: LiveSyncLocalDB): void {
this.kvDB.close();
if (this.kvDB) this.kvDB.close();
}
getNewReplicator(settingOverride: Partial<ObsidianLiveSyncSettings> = {}): LiveSyncAbstractReplicator {
const settings = { ...this.settings, ...settingOverride };
@@ -367,8 +367,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
return new LiveSyncCouchDBReplicator(this);
}
async onInitializeDatabase(db: LiveSyncLocalDB): Promise<void> {
await delay(10);
this.kvDB = await OpenKeyValueDatabase(db.dbname + "-livesync-kv");
// this.trench = new Trench(this.simpleStore);
this.replicator = this.getNewReplicator();
}
async onResetDatabase(db: LiveSyncLocalDB): Promise<void> {
@@ -376,8 +376,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin
this.kvDB.del(kvDBKey);
// localStorage.removeItem(lsKey);
await this.kvDB.destroy();
await yieldMicrotask();
this.kvDB = await OpenKeyValueDatabase(db.dbname + "-livesync-kv");
// this.trench = new Trench(this.simpleStore);
await yieldMicrotask();
this.replicator = this.getNewReplicator()
}
getReplicator() {
@@ -710,7 +711,7 @@ ___However, to enable either of these changes, both remote and local databases n
}
async onLayoutReady() {
eventHub.emit(EVENT_LAYOUT_READY);
eventHub.emitEvent(EVENT_LAYOUT_READY);
this.registerFileWatchEvents();
if (!this.localDatabase.isReady) {
Logger(`Something went wrong! The local database is not ready`, LOG_LEVEL_NOTICE);
@@ -1160,34 +1161,34 @@ Note: We can always able to read V1 format. It will be progressively converted.
}
wireUpEvents() {
eventHub.on(EVENT_SETTING_SAVED, (evt: CustomEvent<ObsidianLiveSyncSettings>) => {
eventHub.onEvent(EVENT_SETTING_SAVED, (evt: CustomEvent<ObsidianLiveSyncSettings>) => {
const settings = evt.detail;
this.localDatabase.settings = settings;
setLang(settings.displayLanguage);
this.settingTab.requestReload();
this.ignoreFiles = settings.ignoreFiles.split(",").map(e => e.trim());
});
eventHub.on(EVENT_SETTING_SAVED, (evt: CustomEvent<ObsidianLiveSyncSettings>) => {
eventHub.onEvent(EVENT_SETTING_SAVED, (evt: CustomEvent<ObsidianLiveSyncSettings>) => {
const settings = evt.detail;
if (settings.settingSyncFile != "") {
fireAndForget(() => this.saveSettingToMarkdown(settings.settingSyncFile));
}
})
eventHub.on(EVENT_SETTING_SAVED, (evt: CustomEvent<ObsidianLiveSyncSettings>) => {
eventHub.onEvent(EVENT_SETTING_SAVED, (evt: CustomEvent<ObsidianLiveSyncSettings>) => {
fireAndForget(() => this.realizeSettingSyncMode());
})
}
connectObsidianEvents() {
// this.registerEvent(this.app.workspace.on("editor-change", ));
this.registerEvent(this.app.vault.on("rename", (file, oldPath) => {
eventHub.emit(EVENT_FILE_RENAMED, { newPath: file.path, old: oldPath });
eventHub.emitEvent(EVENT_FILE_RENAMED, { newPath: file.path, old: oldPath });
}));
this.registerEvent(this.app.workspace.on("active-leaf-change", () => eventHub.emit(EVENT_LEAF_ACTIVE_CHANGED)));
this.registerEvent(this.app.workspace.on("active-leaf-change", () => eventHub.emitEvent(EVENT_LEAF_ACTIVE_CHANGED)));
}
async onload() {
this.wireUpEvents();
this.connectObsidianEvents();
eventHub.emit(EVENT_PLUGIN_LOADED, this);
eventHub.emitEvent(EVENT_PLUGIN_LOADED, this);
logStore.pipeTo(new QueueProcessor(logs => logs.forEach(e => this.addLog(e.message, e.level, e.key)), { suspended: false, batchSize: 20, concurrentLimit: 1, delay: 0 })).startPipeline();
Logger("loading plugin");
__onMissingTranslation(() => { });
@@ -1214,6 +1215,36 @@ Note: We can always able to read V1 format. It will be progressively converted.
}
});
})
type STUB = {
toc: Set<string>,
stub: { [key: string]: { [key: string]: Map<string, Record<string, string>> } }
};
eventHub.onEvent("document-stub-created", async (e: CustomEvent<STUB>) => {
const stub = e.detail.stub;
const toc = e.detail.toc;
const stubDocX =
Object.entries(stub).map(([key, value]) => {
return [`## ${key}`, Object.entries(value).
map(([key2, value2]) => {
return [`### ${key2}`,
([...(value2.entries())].map(([key3, value3]) => {
// return `#### ${key3}` + "\n" + JSON.stringify(value3);
const isObsolete = value3["is_obsolete"] ? " (obsolete)" : "";
const desc = value3["desc"] ?? "";
const key = value3["key"] ? "Setting key: " + value3["key"] + "\n" : "";
return `#### ${key3}${isObsolete}\n${key}${desc}\n`
}))].flat()
}).flat()].flat()
}).flat();
const stubDocMD = `
| Icon | Description |
| :---: | ----------------------------------------------------------------- |
` +
[...toc.values()].map(e => `${e}`).join("\n") + "\n\n" +
stubDocX.join("\n");
await this.vaultAccess.adapterWrite(this.app.vault.configDir + "/ls-debug/stub-doc.md", stubDocMD);
})
}
this.settingTab = new ObsidianLiveSyncSettingTab(this.app, this);
this.addSettingTab(this.settingTab);
@@ -1292,7 +1323,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
}
onunload() {
eventHub.emit(EVENT_PLUGIN_UNLOADED);
eventHub.emitEvent(EVENT_PLUGIN_UNLOADED);
cancelAllPeriodicTask();
cancelAllTasks();
stopAllRunningProcessors();
@@ -1305,7 +1336,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
}
this.periodicSyncProcessor?.disable();
if (this.localDatabase != null) {
this.replicator.closeReplication();
this.replicator?.closeReplication();
this.localDatabase.close();
}
Logger($f`unloading plugin`);
@@ -1496,7 +1527,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
}
await this.saveData(settings);
eventHub.emit(EVENT_SETTING_SAVED, settings);
eventHub.emitEvent(EVENT_SETTING_SAVED, settings);
}
extractSettingFromWholeText(data: string): { preamble: string, body: string, postscript: string } {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,339 @@
import { Setting, TextComponent, type ToggleComponent, type DropdownComponent, ButtonComponent, type TextAreaComponent, type ValueComponent } from "obsidian";
import { unique } from "octagonal-wheels/collection";
import { LEVEL_ADVANCED, LEVEL_POWER_USER, statusDisplay, type ConfigurationItem } from "../../lib/src/common/types";
import { type ObsidianLiveSyncSettingTab, type AutoWireOption, wrapMemo, type OnUpdateResult, createStub, findAttrFromParent } from "../ObsidianLiveSyncSettingTab";
import { type AllSettingItemKey, getConfig, type AllSettings, type AllStringItemKey, type AllNumericItemKey, type AllBooleanItemKey } from "../settingConstants";
export class LiveSyncSetting extends Setting {
autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent;
applyButtonComponent?: ButtonComponent;
selfKey?: AllSettingItemKey;
watchDirtyKeys = [] as AllSettingItemKey[];
holdValue: boolean = false;
static env: ObsidianLiveSyncSettingTab;
descBuf: string | DocumentFragment = "";
nameBuf: string | DocumentFragment = "";
placeHolderBuf: string = "";
hasPassword: boolean = false;
invalidateValue?: () => void;
setValue?: (value: any) => void;
constructor(containerEl: HTMLElement) {
super(containerEl);
LiveSyncSetting.env.settingComponents.push(this);
}
_createDocStub(key: string, value: string | DocumentFragment) {
DEV: {
const paneName = findAttrFromParent(this.settingEl, "data-pane");
const panelName = findAttrFromParent(this.settingEl, "data-panel");
const itemName = typeof this.nameBuf == "string" ? this.nameBuf : this.nameBuf.textContent?.toString() ?? "";
const strValue = typeof value == "string" ? value : value.textContent?.toString() ?? "";
createStub(itemName, key, strValue, panelName, paneName);
}
}
setDesc(desc: string | DocumentFragment): this {
this.descBuf = desc;
DEV: {
this._createDocStub("desc", desc);
}
super.setDesc(desc);
return this;
}
setName(name: string | DocumentFragment): this {
this.nameBuf = name;
DEV: {
this._createDocStub("name", name);
}
super.setName(name);
return this;
}
setAuto(key: AllSettingItemKey, opt?: AutoWireOption) {
this.autoWireSetting(key, opt);
return this;
}
autoWireSetting(key: AllSettingItemKey, opt?: AutoWireOption) {
const conf = getConfig(key);
if (!conf) {
// throw new Error(`No such setting item :${key}`)
return;
}
const name = `${conf.name}${statusDisplay(conf.status)}`;
this.setName(name);
if (conf.desc) {
this.setDesc(conf.desc);
}
DEV: {
this._createDocStub("key", key);
if (conf.obsolete) this._createDocStub("is_obsolete", "true");
if (conf.level) this._createDocStub("level", conf.level);
}
this.holdValue = opt?.holdValue || this.holdValue;
this.selfKey = key;
if (conf.obsolete || opt?.obsolete) {
this.settingEl.toggleClass("sls-setting-obsolete", true);
}
if (opt?.onUpdate) this.addOnUpdate(opt.onUpdate);
const stat = this._getComputedStatus();
if (stat.visibility === false) {
this.settingEl.toggleClass("sls-setting-hidden", !stat.visibility);
}
return conf;
}
autoWireComponent(component: ValueComponent<any>, conf?: ConfigurationItem, opt?: AutoWireOption) {
this.placeHolderBuf = conf?.placeHolder || opt?.placeHolder || "";
if (conf?.level == LEVEL_ADVANCED) {
this.settingEl.toggleClass("sls-setting-advanced", true);
} else if (conf?.level == LEVEL_POWER_USER) {
this.settingEl.toggleClass("sls-setting-poweruser", true);
}
if (this.placeHolderBuf && component instanceof TextComponent) {
component.setPlaceholder(this.placeHolderBuf);
}
if (opt?.onUpdate) this.addOnUpdate(opt.onUpdate);
}
async commitValue<T extends AllSettingItemKey>(value: AllSettings[T]) {
const key = this.selfKey as T;
if (key !== undefined) {
if (value != LiveSyncSetting.env.editingSettings[key]) {
LiveSyncSetting.env.editingSettings[key] = value;
if (!this.holdValue) {
await LiveSyncSetting.env.saveSettings([key]);
}
}
}
LiveSyncSetting.env.requestUpdate();
}
autoWireText(key: AllStringItemKey, opt?: AutoWireOption) {
const conf = this.autoWireSetting(key, opt);
this.addText(text => {
this.autoWiredComponent = text;
const setValue = wrapMemo((value: string) => text.setValue(value));
this.invalidateValue = () => setValue(`${LiveSyncSetting.env.editingSettings[key]}`);
this.invalidateValue();
text.onChange(async (value) => {
await this.commitValue(value);
});
if (opt?.isPassword) {
text.inputEl.setAttribute("type", "password");
this.hasPassword = true;
}
this.autoWireComponent(this.autoWiredComponent, conf, opt);
});
return this;
}
autoWireTextArea(key: AllStringItemKey, opt?: AutoWireOption) {
const conf = this.autoWireSetting(key, opt);
this.addTextArea(text => {
this.autoWiredComponent = text;
const setValue = wrapMemo((value: string) => text.setValue(value));
this.invalidateValue = () => setValue(`${LiveSyncSetting.env.editingSettings[key]}`);
this.invalidateValue();
text.onChange(async (value) => {
await this.commitValue(value);
});
if (opt?.isPassword) {
text.inputEl.setAttribute("type", "password");
this.hasPassword = true;
}
this.autoWireComponent(this.autoWiredComponent, conf, opt);
});
return this;
}
autoWireNumeric(key: AllNumericItemKey, opt: AutoWireOption & { clampMin?: number; clampMax?: number; acceptZero?: boolean; }) {
const conf = this.autoWireSetting(key, opt);
this.addText(text => {
this.autoWiredComponent = text;
if (opt.clampMin) {
text.inputEl.setAttribute("min", `${opt.clampMin}`);
}
if (opt.clampMax) {
text.inputEl.setAttribute("max", `${opt.clampMax}`);
}
let lastError = false;
const setValue = wrapMemo((value: string) => text.setValue(value));
this.invalidateValue = () => {
if (!lastError) setValue(`${LiveSyncSetting.env.editingSettings[key]}`);
};
this.invalidateValue();
text.onChange(async (TextValue) => {
const parsedValue = Number(TextValue);
const value = parsedValue;
let hasError = false;
if (isNaN(value)) hasError = true;
if (opt.clampMax && opt.clampMax < value) hasError = true;
if (opt.clampMin && opt.clampMin > value) {
if (opt.acceptZero && value == 0) {
// This is ok.
} else {
hasError = true;
}
}
if (!hasError) {
lastError = false;
this.setTooltip(``);
text.inputEl.toggleClass("sls-item-invalid-value", false);
await this.commitValue(value);
} else {
this.setTooltip(`The value should ${opt.clampMin || "~"} < value < ${opt.clampMax || "~"}`);
text.inputEl.toggleClass("sls-item-invalid-value", true);
lastError = true;
return false;
}
});
text.inputEl.setAttr("type", "number");
this.autoWireComponent(this.autoWiredComponent, conf, opt);
});
return this;
}
autoWireToggle(key: AllBooleanItemKey, opt?: AutoWireOption) {
const conf = this.autoWireSetting(key, opt);
this.addToggle(toggle => {
this.autoWiredComponent = toggle;
const setValue = wrapMemo((value: boolean) => toggle.setValue(opt?.invert ? !value : value));
this.invalidateValue = () => setValue(LiveSyncSetting.env.editingSettings[key] ?? false);
this.invalidateValue();
toggle.onChange(async (value) => {
await this.commitValue(opt?.invert ? !value : value);
});
this.autoWireComponent(this.autoWiredComponent, conf, opt);
});
return this;
}
autoWireDropDown<T extends string>(key: AllStringItemKey, opt: AutoWireOption & { options: Record<T, string>; }) {
const conf = this.autoWireSetting(key, opt);
this.addDropdown(dropdown => {
this.autoWiredComponent = dropdown;
const setValue = wrapMemo((value: string) => {
dropdown.setValue(value);
});
dropdown
.addOptions(opt.options);
this.invalidateValue = () => setValue(LiveSyncSetting.env.editingSettings[key] || "");
this.invalidateValue();
dropdown.onChange(async (value) => {
await this.commitValue(value);
});
this.autoWireComponent(this.autoWiredComponent, conf, opt);
});
return this;
}
addApplyButton(keys: AllSettingItemKey[], text?: string) {
this.addButton((button) => {
this.applyButtonComponent = button;
this.watchDirtyKeys = unique([...keys, ...this.watchDirtyKeys]);
button.setButtonText(text ?? "Apply");
button.onClick(async () => {
await LiveSyncSetting.env.saveSettings(keys);
LiveSyncSetting.env.reloadAllSettings();
});
LiveSyncSetting.env.requestUpdate();
});
return this;
}
addOnUpdate(func: () => OnUpdateResult) {
this.updateHandlers.add(func);
// this._applyOnUpdateHandlers();
return this;
}
updateHandlers = new Set<() => OnUpdateResult>();
prevStatus: OnUpdateResult = {};
_getComputedStatus() {
let newConf = {} as OnUpdateResult;
for (const handler of this.updateHandlers) {
newConf = {
...newConf,
...handler(),
};
}
return newConf;
}
_applyOnUpdateHandlers() {
if (this.updateHandlers.size > 0) {
const newConf = this._getComputedStatus();
const keys = Object.keys(newConf) as [keyof OnUpdateResult];
for (const k of keys) {
if (k in this.prevStatus && this.prevStatus[k] == newConf[k]) {
continue;
}
// const newValue = newConf[k];
switch (k) {
case "visibility":
this.settingEl.toggleClass("sls-setting-hidden", !(newConf[k] || false));
this.prevStatus[k] = newConf[k];
break;
case "classes":
break;
case "disabled":
this.setDisabled((newConf[k] || false));
this.settingEl.toggleClass("sls-setting-disabled", (newConf[k] || false));
this.prevStatus[k] = newConf[k];
break;
case "isCta":
{
const component = this.autoWiredComponent;
if (component instanceof ButtonComponent) {
if (newConf[k]) {
component.setCta();
} else {
component.removeCta();
}
}
this.prevStatus[k] = newConf[k];
}
break;
case "isWarning":
{
const component = this.autoWiredComponent;
if (component instanceof ButtonComponent) {
if (newConf[k]) {
component.setWarning();
} else {
//TODO:IMPLEMENT
// component.removeCta();
}
}
this.prevStatus[k] = newConf[k];
}
break;
}
}
}
}
_onUpdate() {
if (this.applyButtonComponent) {
const isDirty = LiveSyncSetting.env.isSomeDirty(this.watchDirtyKeys);
this.applyButtonComponent.setDisabled(!isDirty);
if (isDirty) {
this.applyButtonComponent.setCta();
} else {
this.applyButtonComponent.removeCta();
}
}
if (this.selfKey && !LiveSyncSetting.env.isDirty(this.selfKey) && this.invalidateValue) {
this.invalidateValue();
}
if (this.holdValue && this.selfKey) {
const isDirty = LiveSyncSetting.env.isDirty(this.selfKey);
const alt = isDirty ? `Original: ${LiveSyncSetting.env.initialSettings![this.selfKey]}` : "";
this.controlEl.toggleClass("sls-item-dirty", isDirty);
if (!this.hasPassword) {
this.nameEl.toggleClass("sls-item-dirty-help", isDirty);
this.setTooltip(alt, { delay: 10, placement: "right" });
}
}
this._applyOnUpdateHandlers();
}
}

View File

@@ -198,8 +198,9 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
"name": "Do not keep metadata of deleted files."
},
"useIndexedDBAdapter": {
"name": "Use an old adapter for compatibility",
"desc": "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this."
"name": "(Obsolete) Use an old adapter for compatibility",
"desc": "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.",
"obsolete": true
},
"watchInternalFileChanges": {
"name": "Scan changes on customization sync",
@@ -341,6 +342,18 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
name: "Maximum size of chunks to send in one request",
desc: "MB"
},
"useAdvancedMode": {
name: "Enable advanced features",
// desc: "Enable advanced mode"
},
usePowerUserMode: {
name: "Enable power user features",
// desc: "Enable power user mode",
// level: LEVEL_ADVANCED
},
useEdgeCaseMode: {
name: "Enable edge case treatment features",
},
}
function translateInfo(infoSrc: ConfigurationItem | undefined | false) {
if (!infoSrc) return false;

View File

@@ -128,13 +128,67 @@ div.sls-setting-menu-btn {
color: var(--text-on-accent);
}
.sls-setting-menu-wrapper {
display: flex;
flex-direction: column;
flex-grow: 1;
position: sticky;
top: 0;
background-color: rgba(var(--background-primary), 0.3);
backdrop-filter: blur(4px);
border-radius: 4px;
z-index: 10;
}
.sls-setting-menu {
display: flex;
flex-direction: row;
/* flex-wrap: wrap; */
overflow-x: auto;
}
body {
--sls-col-transparent: transparent;
--sls-col-warn: rgba(var(--background-modifier-error-rgb), 0.1);
--sls-col-warn-stripe1: var(--sls-col-transparent);
--sls-col-warn-stripe2: var(--sls-col-warn);
}
.sls-setting-menu-buttons {
border: 1px solid var(--sls-col-warn);
padding: 2px;
margin: 1px;
border-radius: 4px;
background-image: linear-gradient(-45deg,
var(--sls-col-warn-stripe1) 25%, var(--sls-col-warn-stripe2) 25%, var(--sls-col-warn-stripe2) 50%,
var(--sls-col-warn-stripe1) 50%, var(--sls-col-warn-stripe1) 75%, var(--sls-col-warn-stripe2) 75%, var(--sls-col-warn-stripe2));
background-size: 30px 30px;
display: flex;
flex-direction: row;
justify-content: flex-end;
padding: 0.5em 0.25em;
justify-content: center;
align-items: center;
/* transition: background-position 1s; */
animation: sls-scroll-warn 1s linear 0s infinite;
}
@keyframes sls-scroll-warn {
0% {
background-position: 0 0;
}
100% {
background-position: 30px 0;
}
}
.sls-setting-menu-buttons label {
margin-right: auto;
flex-grow: 1;
color: var(--text-warning);
}
.sls-setting-label {
flex-grow: 1;
display: inline-flex;
@@ -242,6 +296,17 @@ div.sls-setting-menu-btn {
display: none;
}
.sls-setting-obsolete {
background-image: linear-gradient(-45deg,
var(--sls-col-warn-stripe1) 25%, var(--sls-col-warn-stripe2) 25%, var(--sls-col-warn-stripe2) 50%,
var(--sls-col-warn-stripe1) 50%, var(--sls-col-warn-stripe1) 75%, var(--sls-col-warn-stripe2) 75%, var(--sls-col-warn-stripe2));
background-image: linear-gradient(-45deg,
transparent 25%, rgba(var(--background-secondary), 0.1) 25%, rgba(var(--background-secondary), 0.1) 50%, transparent 50%, transparent 75%, rgba(var(--background-secondary), 0.1) 75%, rgba(var(--background-secondary), 0.1));
background-size: 60px 60px;
}
.password-input>.setting-item-control>input {
-webkit-text-security: disc;
}
@@ -338,4 +403,27 @@ span.ls-mark-cr::after {
background: var(--background-modifier-error);
-webkit-filter: unset;
filter: unset;
}
.menu-setting-poweruser-disabled .sls-setting-poweruser {
display: none;
}
.menu-setting-advanced-disabled .sls-setting-advanced {
display: none;
}
.menu-setting-edgecase-disabled .sls-setting-edgecase {
display: none;
}
.sls-setting-panel-title {
position: sticky;
}
.sls-setting-panel-title {
top: 2em;
background-color: rgba(var(--background-primary), 0.3);
backdrop-filter: blur(4px);
border-radius: 30%;
}

61
terser.config.mjs Normal file
View File

@@ -0,0 +1,61 @@
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD AND TERSER
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = process.argv[2] === "production";
const terserOption = {
sourceMap: !prod
? {
url: "inline",
}
: {},
format: {
indent_level: 2,
beautify: true,
comments: "some",
ecma: 2018,
preamble: banner,
webkit: true,
},
parse: {
// parse options
},
compress: {
// compress options
defaults: false,
evaluate: true,
dead_code: true,
// directives: true,
// conditionals: true,
inline: 3,
join_vars: true,
loops: true,
passes: 4,
reduce_vars: true,
reduce_funcs: true,
arrows: true,
collapse_vars: true,
comparisons: true,
lhs_constants: true,
hoist_props: true,
side_effects: true,
ecma: 2018,
if_return: true,
unused: true,
},
mangle: false,
ecma: 2018, // specify one of: 5, 2015, 2016, etc.
enclose: false, // or specify true, or "args:values"
keep_classnames: true,
keep_fnames: true,
ie8: false,
module: false,
// nameCache: null, // or specify a name cache object
safari10: false,
toplevel: false,
};
export { terserOption };

View File

@@ -25,6 +25,7 @@
"ES7",
"es2019.array",
"ES2020.BigInt",
"ESNext.Intl"
]
},
"include": [