Merge branch 'main' into per-file-sync-grammar

This commit is contained in:
vorotamoroz
2024-10-18 11:42:49 +09:00
committed by GitHub
25 changed files with 5926 additions and 3822 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

@@ -106,6 +106,8 @@ Now `https://tiles-photograph-routine-groundwater.trycloudflare.com` is our serv
$ export hostname=https://tiles-photograph-routine-groundwater.trycloudflare.com #Point to your vault
$ export database=obsidiannotes #Please change as you like
$ export passphrase=dfsapkdjaskdjasdas #Please change as you like
$ export username=johndoe
$ export password=abc123
$ deno run -A https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/flyio/generate_setupuri.ts
obsidian://setuplivesync?settings=%5B%22tm2DpsOE74nJAryprZO2M93wF%2Fvg.......4b26ed33230729%22%5D
@@ -206,4 +208,4 @@ entryPoints:
address: ":443"
...
```
```

View File

@@ -9,65 +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,
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,
},
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") + "");
@@ -89,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);
}
@@ -105,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,
@@ -122,7 +67,7 @@ const context = await esbuild.context({
logLevel: "info",
platform: "browser",
sourcemap: prod ? false : "inline",
treeShaking: true,
treeShaking: false,
outfile: "main_org.js",
mainFields: ["browser", "module", "main"],
minifyWhitespace: false,
@@ -144,7 +89,7 @@ const context = await esbuild.context({
],
});
if (prod || dev) {
if (prod) {
await context.rebuild();
process.exit(0);
} else {

10
manifest-beta.json Normal file
View File

@@ -0,0 +1,10 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.24.0.dev-rc3",
"minAppVersion": "0.9.12",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz",
"authorUrl": "https://github.com/vrtmrz",
"isDesktopOnly": false
}

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.23.18",
"version": "0.23.23",
"minAppVersion": "0.9.12",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz",

2799
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "0.23.18",
"version": "0.23.23",
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"main": "main.js",
"type": "module",
@@ -14,28 +14,29 @@
"author": "vorotamoroz",
"license": "MIT",
"devDependencies": {
"@chialab/esbuild-plugin-worker": "^0.18.1",
"@tsconfig/svelte": "^5.0.4",
"@types/diff-match-patch": "^1.0.36",
"@types/node": "^20.14.10",
"@types/node": "^22.5.4",
"@types/pouchdb": "^6.4.2",
"@types/pouchdb-adapter-http": "^6.1.6",
"@types/pouchdb-adapter-idb": "^6.1.7",
"@types/pouchdb-browser": "^6.1.5",
"@types/pouchdb-core": "^7.0.14",
"@types/pouchdb-core": "^7.0.15",
"@types/pouchdb-mapreduce": "^6.1.10",
"@types/pouchdb-replication": "^6.4.7",
"@types/transform-pouch": "^1.0.6",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"@typescript-eslint/eslint-plugin": "^8.4.0",
"@typescript-eslint/parser": "^8.4.0",
"builtin-modules": "^4.0.0",
"esbuild": "0.23.0",
"esbuild": "0.23.1",
"esbuild-svelte": "^0.8.1",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-import": "^2.30.0",
"events": "^3.3.0",
"obsidian": "^1.5.7",
"postcss": "^8.4.39",
"obsidian": "^1.6.6",
"postcss": "^8.4.45",
"postcss-load-config": "^6.0.1",
"pouchdb-adapter-http": "^9.0.0",
"pouchdb-adapter-idb": "^9.0.0",
@@ -47,25 +48,25 @@
"pouchdb-merge": "^9.0.0",
"pouchdb-replication": "^9.0.0",
"pouchdb-utils": "^9.0.0",
"svelte": "^4.2.18",
"svelte": "^4.2.19",
"svelte-preprocess": "^6.0.2",
"terser": "^5.31.2",
"terser": "^5.31.6",
"transform-pouch": "^2.0.0",
"tslib": "^2.6.3",
"typescript": "^5.5.3"
"tslib": "^2.7.0",
"typescript": "^5.5.4"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.614.0",
"@smithy/fetch-http-handler": "^3.2.1",
"@smithy/protocol-http": "^4.0.3",
"@aws-sdk/client-s3": "^3.645.0",
"@smithy/fetch-http-handler": "^3.2.4",
"@smithy/protocol-http": "^4.1.0",
"@smithy/querystring-builder": "^3.0.3",
"diff-match-patch": "^1.0.5",
"esbuild-plugin-inline-worker": "^0.1.1",
"fflate": "^0.8.2",
"idb": "^8.0.0",
"minimatch": "^10.0.1",
"octagonal-wheels": "^0.1.13",
"octagonal-wheels": "^0.1.15",
"xxhash-wasm": "0.4.2",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
}
}
}

16
src/common/events.ts Normal file
View File

@@ -0,0 +1,16 @@
export const EVENT_LAYOUT_READY = "layout-ready";
export const EVENT_PLUGIN_LOADED = "plugin-loaded";
export const EVENT_PLUGIN_UNLOADED = "plugin-unloaded";
export const EVENT_SETTING_SAVED = "setting-saved";
export const EVENT_FILE_RENAMED = "file-renamed";
export const EVENT_LEAF_ACTIVE_CHANGED = "leaf-active-changed";
// export const EVENT_FILE_CHANGED = "file-changed";
import { eventHub } from "../lib/src/hub/hub";
// TODO: Add overloads for the emit method to allow for type checking
export { eventHub };

View File

@@ -15,14 +15,14 @@ export { scheduleTask, setPeriodicTask, cancelTask, cancelAllTasks, cancelPeriod
// For backward compatibility, using the path for determining id.
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
// The first slash will be deleted when the path is normalized.
export async function path2id(filename: FilePathWithPrefix | FilePath, obfuscatePassphrase: string | false): Promise<DocumentID> {
export async function path2id(filename: FilePathWithPrefix | FilePath, obfuscatePassphrase: string | false, caseInsensitive: boolean): Promise<DocumentID> {
const temp = filename.split(":");
const path = temp.pop();
const normalizedPath = normalizePath(path as FilePath);
temp.push(normalizedPath);
const fixedPath = temp.join(":") as FilePathWithPrefix;
const out = await path2id_base(fixedPath, obfuscatePassphrase);
const out = await path2id_base(fixedPath, obfuscatePassphrase, caseInsensitive);
return out;
}
export function id2path(id: DocumentID, entry?: EntryHasPath): FilePathWithPrefix {
@@ -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 = []
@@ -465,3 +476,63 @@ export function compareFileFreshness(baseFile: TFile | AnyEntry | undefined, che
return compareMTime(modifiedBase, modifiedTarget);
}
const _cached = new Map<string, {
value: any;
context: Map<string, any>;
}>();
export type MemoOption = {
key: string;
forceUpdate?: boolean;
validator?: () => boolean;
}
export function useMemo<T>({ key, forceUpdate, validator }: MemoOption, updateFunc: (context: Map<string, any>, prev: T) => T): T {
const cached = _cached.get(key);
if (cached && !forceUpdate && (!validator || validator && !validator())) {
return cached.value;
}
const context = cached?.context || new Map<string, any>();
const value = updateFunc(context, cached?.value);
if (value !== cached?.value) {
_cached.set(key, { value, context });
}
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);
}
export function disposeAllMemo() {
_cached.clear();
}

View File

@@ -2,7 +2,7 @@ import { writable } from 'svelte/store';
import { Notice, type PluginManifest, parseYaml, normalizePath, type ListedFiles, diff_match_patch } from "../deps.ts";
import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, AnyEntry, SavingEntry, diff_result } from "../lib/src/common/types.ts";
import { CANCELLED, LEAVE_TO_SUBSEQUENT, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_SHINY } from "../lib/src/common/types.ts";
import { CANCELLED, LEAVE_TO_SUBSEQUENT, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_SHINY } from "../lib/src/common/types.ts";
import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "../common/types.ts";
import { createBlob, createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocData, getDocDataAsArray, isDocContentSame, isLoadedEntry, isObjectDifferent } from "../lib/src/common/utils.ts";
import { Logger } from "../lib/src/common/logger.ts";
@@ -11,7 +11,7 @@ import { arrayBufferToBase64, decodeBinary, readString } from 'src/lib/src/strin
import { serialized, shareRunningResult } from "../lib/src/concurrency/lock.ts";
import { LiveSyncCommands } from "./LiveSyncCommands.ts";
import { stripAllPrefixes } from "../lib/src/string_and_binary/path.ts";
import { PeriodicProcessor, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "../common/utils.ts";
import { EVEN, PeriodicProcessor, disposeMemoObject, isMarkedAsSameChanges, markChangesAreSame, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "../common/utils.ts";
import { PluginDialogModal } from "../common/dialogs.ts";
import { JsonResolveModal } from "../ui/JsonResolveModal.ts";
import { QueueProcessor } from '../lib/src/concurrency/processor.ts';
@@ -272,8 +272,10 @@ export class PluginDataExDisplayV2 {
this.confKey = `${categoryToFolder(this.category, this.term)}${this.name}`;
this.applyLoadedManifest();
}
setFile(file: LoadedEntryPluginDataExFile) {
if (this.files.find(e => e.filename == file.filename)) {
async setFile(file: LoadedEntryPluginDataExFile) {
const old = this.files.find(e => e.filename == file.filename);
if (old) {
if (old.mtime == file.mtime && await isDocContentSame(old.data, file.data)) return;
this.files = this.files.filter(e => e.filename != file.filename);
}
this.files.push(file);
@@ -319,6 +321,7 @@ export type PluginDataEx = {
version?: string,
mtime: number,
};
export class ConfigSync extends LiveSyncCommands {
constructor(plugin: ObsidianLiveSyncPlugin) {
super(plugin);
@@ -637,7 +640,7 @@ export class ConfigSync extends LiveSyncCommands {
if (!entry) return;
const file = await this.createPluginDataExFileV2(unifiedFilenameWithKey);
if (file) {
entry.setFile(file);
await entry.setFile(file);
} else {
entry.deleteFile(unifiedFilenameWithKey);
if (entry.files.length == 0) {
@@ -841,22 +844,46 @@ export class ConfigSync extends LiveSyncCommands {
Logger(`Applying ${filename} of ${data.displayName || data.name}..`);
const path = `${baseDir}/${filename}` as FilePath;
await this.vaultAccess.ensureDirectory(path);
// If the content has applied, modified time will be updated to the current time.
await this.vaultAccess.adapterWrite(path, content);
await this.storeCustomisationFileV2(path, this.plugin.deviceAndVaultName);
} else {
const files = data.files;
for (const f of files) {
// If files have applied, modified time will be updated to the current time.
const stat = { mtime: f.mtime, ctime: f.ctime };
const path = `${baseDir}/${f.filename}` as FilePath;
Logger(`Applying ${f.filename} of ${data.displayName || data.name}..`);
// const contentEach = createBlob(f.data);
this.vaultAccess.ensureDirectory(path);
if (f.datatype == "newnote") {
let oldData;
try {
oldData = await this.vaultAccess.adapterReadBinary(path);
} catch (ex) {
oldData = new ArrayBuffer(0);
}
const content = base64ToArrayBuffer(f.data);
await this.vaultAccess.adapterWrite(path, content);
if (await isDocContentSame(oldData, content)) {
Logger(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE);
continue;
}
await this.vaultAccess.adapterWrite(path, content, stat);
} else {
let oldData;
try {
oldData = await this.vaultAccess.adapterRead(path);
} catch (ex) {
oldData = "";
}
const content = getDocData(f.data);
await this.vaultAccess.adapterWrite(path, content);
if (await isDocContentSame(oldData, content)) {
Logger(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE);
continue;
}
await this.vaultAccess.adapterWrite(path, content, stat);
}
Logger(`Applied ${f.filename} of ${data.displayName || data.name}..`);
await this.storeCustomisationFileV2(path, this.plugin.deviceAndVaultName);
@@ -1063,7 +1090,7 @@ export class ConfigSync extends LiveSyncCommands {
}
async storeCustomisationFileV2(path: FilePath, term: string, saveRelatives = false) {
async storeCustomisationFileV2(path: FilePath, term: string, force = false) {
const vf = this.filenameWithUnifiedKey(path, term);
return await serialized(`plugin-${vf}`, async () => {
const prefixedFileName = vf;
@@ -1095,8 +1122,21 @@ export class ConfigSync extends LiveSyncCommands {
eden: {}
};
} else {
if (old.mtime == mtime) {
// Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Same time)`, LOG_LEVEL_VERBOSE);
if (isMarkedAsSameChanges(prefixedFileName, [old.mtime, mtime + 1]) == EVEN) {
Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Already checked the same)`, LOG_LEVEL_DEBUG);
return;
}
const docXDoc = await this.localDatabase.getDBEntryFromMeta(old, {}, false, false);
if (docXDoc == false) {
throw "Could not load the document";
}
const dataSrc = getDocData(docXDoc.data);
const dataStart = dataSrc.indexOf(DUMMY_END);
const oldContent = dataSrc.substring(dataStart + DUMMY_END.length);
const oldContentArray = base64ToArrayBuffer(oldContent);
if (await isDocContentSame(oldContentArray, content)) {
Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (the same content)`, LOG_LEVEL_VERBOSE);
markChangesAreSame(prefixedFileName, old.mtime, mtime + 1);
return true;
}
saveData =

View File

@@ -374,7 +374,7 @@ Of course, we are able to disable these features.`
await this.plugin.openDatabase();
this.plugin.isReady = true;
if (makeLocalChunkBeforeSync) {
await this.plugin.initializeDatabase(true);
await this.plugin.createAllChunks(true);
}
await this.plugin.markRemoteResolved();
await delay(500);
@@ -390,6 +390,7 @@ Of course, we are able to disable these features.`
async rebuildRemote() {
this.suspendExtraSync();
this.plugin.settings.isConfigured = true;
await this.plugin.realizeSettingSyncMode();
await this.plugin.markRemoteLocked();
await this.plugin.tryResetRemoteDatabase();

View File

@@ -0,0 +1,234 @@
import { computed, reactive, reactiveSource, type ReactiveValue } from "octagonal-wheels/dataobject/reactive";
import type { DatabaseConnectingStatus, EntryDoc } from "../lib/src/common/types";
import { LiveSyncCommands } from "./LiveSyncCommands";
import { scheduleTask } from "octagonal-wheels/concurrency/task";
import { isDirty, throttle } from "../lib/src/common/utils";
import { collectingChunks, pluginScanningCount, hiddenFilesEventCount, hiddenFilesProcessingCount } from "../lib/src/mock_and_interop/stores";
import { eventHub } from "../lib/src/hub/hub";
import { EVENT_FILE_RENAMED, EVENT_LAYOUT_READY, EVENT_LEAF_ACTIVE_CHANGED } from "../common/events";
export class LogAddOn extends LiveSyncCommands {
statusBar?: HTMLElement;
statusDiv?: HTMLElement;
statusLine?: HTMLDivElement;
logMessage?: HTMLDivElement;
logHistory?: HTMLDivElement;
messageArea?: HTMLDivElement;
statusBarLabels!: ReactiveValue<{ message: string, status: string }>;
observeForLogs() {
const padSpaces = `\u{2007}`.repeat(10);
// const emptyMark = `\u{2003}`;
function padLeftSpComputed(numI: ReactiveValue<number>, mark: string) {
const formatted = reactiveSource("");
let timer: ReturnType<typeof setTimeout> | undefined = undefined;
let maxLen = 1;
numI.onChanged(numX => {
const num = numX.value;
const numLen = `${Math.abs(num)}`.length + 1;
maxLen = maxLen < numLen ? numLen : maxLen;
if (timer) clearTimeout(timer);
if (num == 0) {
timer = setTimeout(() => {
formatted.value = "";
maxLen = 1;
}, 3000);
}
formatted.value = ` ${mark}${`${padSpaces}${num}`.slice(-(maxLen))}`;
})
return computed(() => formatted.value);
}
const labelReplication = padLeftSpComputed(this.plugin.replicationResultCount, `📥`);
const labelDBCount = padLeftSpComputed(this.plugin.databaseQueueCount, `📄`);
const labelStorageCount = padLeftSpComputed(this.plugin.storageApplyingCount, `💾`);
const labelChunkCount = padLeftSpComputed(collectingChunks, `🧩`);
const labelPluginScanCount = padLeftSpComputed(pluginScanningCount, `🔌`);
const labelConflictProcessCount = padLeftSpComputed(this.plugin.conflictProcessQueueCount, `🔩`);
const hiddenFilesCount = reactive(() => hiddenFilesEventCount.value + hiddenFilesProcessingCount.value);
const labelHiddenFilesCount = padLeftSpComputed(hiddenFilesCount, `⚙️`)
const queueCountLabelX = reactive(() => {
return `${labelReplication()}${labelDBCount()}${labelStorageCount()}${labelChunkCount()}${labelPluginScanCount()}${labelHiddenFilesCount()}${labelConflictProcessCount()}`;
})
const queueCountLabel = () => queueCountLabelX.value;
const requestingStatLabel = computed(() => {
const diff = this.plugin.requestCount.value - this.plugin.responseCount.value;
return diff != 0 ? "📲 " : "";
})
const replicationStatLabel = computed(() => {
const e = this.plugin.replicationStat.value;
const sent = e.sent;
const arrived = e.arrived;
const maxPullSeq = e.maxPullSeq;
const maxPushSeq = e.maxPushSeq;
const lastSyncPullSeq = e.lastSyncPullSeq;
const lastSyncPushSeq = e.lastSyncPushSeq;
let pushLast = "";
let pullLast = "";
let w = "";
const labels: Partial<Record<DatabaseConnectingStatus, string>> = {
"CONNECTED": "⚡",
"JOURNAL_SEND": "📦↑",
"JOURNAL_RECEIVE": "📦↓",
}
switch (e.syncStatus) {
case "CLOSED":
case "COMPLETED":
case "NOT_CONNECTED":
w = "⏹";
break;
case "STARTED":
w = "🌀";
break;
case "PAUSED":
w = "💤";
break;
case "CONNECTED":
case "JOURNAL_SEND":
case "JOURNAL_RECEIVE":
w = labels[e.syncStatus] || "⚡";
pushLast = ((lastSyncPushSeq == 0) ? "" : (lastSyncPushSeq >= maxPushSeq ? " (LIVE)" : ` (${maxPushSeq - lastSyncPushSeq})`));
pullLast = ((lastSyncPullSeq == 0) ? "" : (lastSyncPullSeq >= maxPullSeq ? " (LIVE)" : ` (${maxPullSeq - lastSyncPullSeq})`));
break;
case "ERRORED":
w = "⚠";
break;
default:
w = "?";
}
return { w, sent, pushLast, arrived, pullLast };
})
const labelProc = padLeftSpComputed(this.plugin.vaultManager.processing, ``);
const labelPend = padLeftSpComputed(this.plugin.vaultManager.totalQueued, `🛫`);
const labelInBatchDelay = padLeftSpComputed(this.plugin.vaultManager.batched, `📬`);
const waitingLabel = computed(() => {
return `${labelProc()}${labelPend()}${labelInBatchDelay()}`;
})
const statusLineLabel = computed(() => {
const { w, sent, pushLast, arrived, pullLast } = replicationStatLabel();
const queued = queueCountLabel();
const waiting = waitingLabel();
const networkActivity = requestingStatLabel();
return {
message: `${networkActivity}Sync: ${w}${sent}${pushLast}${arrived}${pullLast}${waiting}${queued}`,
};
})
const statusBarLabels = reactive(() => {
const scheduleMessage = this.plugin.isReloadingScheduled ? `WARNING! RESTARTING OBSIDIAN IS SCHEDULED!\n` : "";
const { message } = statusLineLabel();
const status = scheduleMessage + this.plugin.statusLog.value;
return {
message, status
}
})
this.statusBarLabels = statusBarLabels;
const applyToDisplay = throttle((label: typeof statusBarLabels.value) => {
// const v = label;
this.applyStatusBarText();
}, 20);
statusBarLabels.onChanged(label => applyToDisplay(label.value))
}
adjustStatusDivPosition() {
const mdv = this.app.workspace.getMostRecentLeaf();
if (mdv && this.statusDiv) {
this.statusDiv.remove();
// this.statusDiv.pa();
const container = mdv.view.containerEl;
container.insertBefore(this.statusDiv, container.lastChild);
}
}
onunload() {
if (this.statusDiv) {
this.statusDiv.remove();
}
document.querySelectorAll(`.livesync-status`)?.forEach(e => e.remove());
}
async setFileStatus() {
this.messageArea!.innerText = await this.plugin.getActiveFileStatus();
}
onActiveLeafChange() {
this.adjustStatusDivPosition();
this.setFileStatus();
}
onload(): void | Promise<void> {
eventHub.onEvent(EVENT_FILE_RENAMED, (evt: CustomEvent<{ oldPath: string, newPath: string }>) => {
this.setFileStatus();
});
eventHub.onEvent(EVENT_LEAF_ACTIVE_CHANGED, () => this.onActiveLeafChange());
const w = document.querySelectorAll(`.livesync-status`);
w.forEach(e => e.remove());
this.observeForLogs();
this.adjustStatusDivPosition();
this.statusDiv = this.app.workspace.containerEl.createDiv({ cls: "livesync-status" });
this.statusLine = this.statusDiv.createDiv({ cls: "livesync-status-statusline" });
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.onEvent(EVENT_LAYOUT_READY, () => this.adjustStatusDivPosition());
if (this.settings.showStatusOnStatusbar) {
this.statusBar = this.plugin.addStatusBarItem();
this.statusBar.addClass("syncstatusbar");
}
}
nextFrameQueue: ReturnType<typeof requestAnimationFrame> | undefined = undefined;
logLines: { ttl: number, message: string }[] = [];
applyStatusBarText() {
if (this.nextFrameQueue) {
return;
}
this.nextFrameQueue = requestAnimationFrame(() => {
this.nextFrameQueue = undefined;
const { message, status } = this.statusBarLabels.value;
// const recent = logMessages.value;
const newMsg = message;
const newLog = this.settings.showOnlyIconsOnEditor ? "" : status;
this.statusBar?.setText(newMsg.split("\n")[0]);
if (this.settings.showStatusOnEditor && this.statusDiv) {
// const root = activeDocument.documentElement;
// root.style.setProperty("--sls-log-text", "'" + (newMsg + "\\A " + newLog) + "'");
// this.statusDiv.innerText = newMsg + "\\A " + newLog;
if (this.settings.showLongerLogInsideEditor) {
const now = new Date().getTime();
this.logLines = this.logLines.filter(e => e.ttl > now);
const minimumNext = this.logLines.reduce((a, b) => a < b.ttl ? a : b.ttl, Number.MAX_SAFE_INTEGER);
if (this.logLines.length > 0) setTimeout(() => this.applyStatusBarText(), minimumNext - now);
const recent = this.logLines.map(e => e.message);
const recentLogs = recent.reverse().join("\n");
if (isDirty("recentLogs", recentLogs)) this.logHistory!.innerText = recentLogs;
}
if (isDirty("newMsg", newMsg)) this.statusLine!.innerText = newMsg;
if (isDirty("newLog", newLog)) this.logMessage!.innerText = newLog;
} else {
// const root = activeDocument.documentElement;
// root.style.setProperty("--log-text", "'" + (newMsg + "\\A " + newLog) + "'");
}
});
scheduleTask("log-hide", 3000, () => { this.plugin.statusLog.value = "" });
}
onInitializeDatabase(showNotice: boolean) { }
beforeReplicate(showNotice: boolean) { }
onResume() { }
parseReplicationResultItem(docs: PouchDB.Core.ExistingDocument<EntryDoc>): boolean | Promise<boolean> {
return false;
}
async realizeSettingSyncMode() { }
}

Submodule src/lib updated: f0253a8548...3108e3e3db

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ import { type App, TFile, type DataWriteOptions, TFolder, TAbstractFile } from "
import { serialized } from "../lib/src/concurrency/lock.ts";
import { Logger } from "../lib/src/common/logger.ts";
import { isPlainText } from "../lib/src/string_and_binary/path.ts";
import type { FilePath } from "../lib/src/common/types.ts";
import type { FilePath, HasSettings } from "../lib/src/common/types.ts";
import { createBinaryBlob, isDocContentSame } from "../lib/src/common/utils.ts";
import type { InternalFileInfo } from "../common/types.ts";
import { markChangesAreSame } from "../common/utils.ts";
@@ -31,8 +31,10 @@ async function processWriteFile<T>(file: TFile | TFolder | string, proc: () => P
}
export class SerializedFileAccess {
app: App
constructor(app: App) {
plugin: HasSettings<{ handleFilenameCaseSensitive: boolean }>
constructor(app: App, plugin: typeof this["plugin"]) {
this.app = app;
this.plugin = plugin;
}
async adapterStat(file: TFile | string) {
@@ -138,17 +140,23 @@ export class SerializedFileAccess {
return await processWriteFile(file, () => this.app.vault.trash(file, force));
}
isStorageInsensitive(): boolean {
//@ts-ignore
return this.app.vault.adapter.insensitive ?? true;
}
getAbstractFileByPathInsensitive(path: FilePath | string): TAbstractFile | null {
//@ts-ignore
return app.vault.getAbstractFileByPathInsensitive(path);
}
getAbstractFileByPath(path: FilePath | string): TAbstractFile | null {
// Disabled temporary.
if (!this.plugin.settings.handleFilenameCaseSensitive || this.isStorageInsensitive()) {
return this.getAbstractFileByPathInsensitive(path);
}
return this.app.vault.getAbstractFileByPath(path);
// // Hidden API but so useful.
// // @ts-ignore
// if ("getAbstractFileByPathInsensitive" in app.vault && (app.vault.adapter?.insensitive ?? false)) {
// // @ts-ignore
// return app.vault.getAbstractFileByPathInsensitive(path);
// } else {
// return app.vault.getAbstractFileByPath(path);
// }
}
getFiles() {

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

@@ -53,7 +53,7 @@
canApply = true;
} else {
const dtDiff = (local?.mtime ?? 0) - (remote?.mtime ?? 0);
const diff = timeDeltaToHumanReadable(Math.abs(dtDiff / 1000));
const diff = timeDeltaToHumanReadable(Math.abs(dtDiff));
if (dtDiff / 1000 < -10) {
// freshness = "✓ Newer";
freshness = `Newer (${diff})`;

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",
@@ -329,6 +330,30 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
name: "Enable per-file customization sync",
desc: "If enabled, efficient per-file customization sync will be used. A minor migration is required when enabling this feature, and all devices must be updated to v0.23.18. Enabling this feature will result in losing compatibility with older versions."
}
"handleFilenameCaseSensitive": {
name: "Handle files as Case-Sensitive",
desc: "If this enabled, All files are handled as case-Sensitive (Previous behaviour)."
},
"doNotUseFixedRevisionForChunks": {
name: "Compute revisions for chunks (Previous behaviour)",
desc: "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)"
},
"sendChunksBulkMaxSize": {
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

@@ -97,55 +97,6 @@
max-width: 100%;
}
.CodeMirror-wrap::before,
.markdown-preview-view.cm-s-obsidian::before,
.markdown-source-view.cm-s-obsidian::before,
.canvas-wrapper::before,
.empty-state::before {
content: var(--sls-log-text, "");
font-variant-numeric: tabular-nums;
font-variant-emoji: emoji;
tab-size: 4;
text-align: right;
white-space: pre-wrap;
position: absolute;
border-radius: 4px;
/* border:1px solid --background-modifier-border; */
display: inline-block;
top: 8px;
color: --text-normal;
opacity: 0.5;
font-size: 80%;
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.empty-state::before,
.markdown-preview-view.cm-s-obsidian::before,
.markdown-source-view.cm-s-obsidian::before {
top: var(--header-height);
right: 1em;
}
.is-mobile .empty-state::before,
.is-mobile .markdown-preview-view.cm-s-obsidian::before,
.is-mobile .markdown-source-view.cm-s-obsidian::before {
top: var(--view-header-height);
right: 1em;
}
.canvas-wrapper::before {
right: 48px;
}
.CodeMirror-wrap::before {
right: 0px;
}
.cm-s-obsidian > .cm-editor::before {
right: 16px;
}
.sls-setting-tab {
display: none;
}
@@ -171,19 +122,73 @@ div.sls-setting-menu-btn {
/* width: 100%; */
}
.sls-setting-tab:hover ~ div.sls-setting-menu-btn,
.sls-setting-label.selected .sls-setting-tab:checked ~ div.sls-setting-menu-btn {
.sls-setting-tab:hover~div.sls-setting-menu-btn,
.sls-setting-label.selected .sls-setting-tab:checked~div.sls-setting-menu-btn {
background-color: var(--interactive-accent);
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;
@@ -291,7 +296,18 @@ div.sls-setting-menu-btn {
display: none;
}
.password-input > .setting-item-control > input {
.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;
}
@@ -321,6 +337,7 @@ span.ls-mark-cr::after {
top: 0;
left: 0;
}
.ls-imgdiff-wrap .overlay .img-overlay {
-webkit-filter: invert(100%) opacity(50%);
filter: invert(100%) opacity(50%);
@@ -329,14 +346,84 @@ span.ls-mark-cr::after {
left: 0;
animation: ls-blink-diff 0.5s cubic-bezier(0.4, 0, 1, 1) infinite alternate;
}
@keyframes ls-blink-diff {
0% {
opacity: 0;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.livesync-status {
user-select: none;
pointer-events: none;
height: auto;
min-height: 1em;
position: absolute;
background-color: transparent;
width: 100%;
padding: 10px;
padding-right: 16px;
top: var(--header-height);
z-index: calc(var(--layer-cover) + 1);
font-variant-numeric: tabular-nums;
font-variant-emoji: emoji;
tab-size: 4;
text-align: right;
white-space: pre-wrap;
display: inline-block;
color: var(--text-normal);
font-size: 80%;
}
.livesync-status div {
opacity: 0.6;
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.livesync-status .livesync-status-loghistory {
text-align: left;
opacity: 0.4;
}
.livesync-status div.livesync-status-messagearea {
opacity: 0.6;
color: var(--text-on-accent);
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": [

View File

@@ -18,54 +18,80 @@ I have a lot of respect for that plugin, even though it is sometimes treated as
Hooray for open source, and generous licences, and the sharing of knowledge by experts.
#### Version history
- 0.23.18:
- New feature:
- Per-file-saved customization sync has been shipped.
- We can synchronise plug-igs etc., more smoothly.
- Default: disabled. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost compatibility with old versions.
- Customisation sync has got beta3.
- We can set `Flag` to each item to select the newest, automatically.
- This configuration is per device.
- Improved:
- Start-up speed has been improved.
- 0.23.23:
- Refined:
- Setting dialogue very slightly refined.
- The hodgepodge inside the `Hatch` pane has been sorted into more explicit categorised panes.
- Now we have new panes for:
- `Selector`
- `Advanced`
- `Power users`
- `Patches (Edge case)`
- Applying the settings will now be more informative.
- The header bar will be shown for applying the settings which needs a database rebuild.
- Applying methods are now more clearly navigated.
- Definitely, drastic change. I hope this will be more user-friendly. However, if you notice any issues, please let me know. I hope that nothing missed.
- New features:
- Word-segmented chunk building on users language
- Chunks can now be built with word-segmented data, enhancing efficiency for markdown files which contains the multiple sentences in a single line.
- This feature is enabled by default through `Use Segmented-splitter`.
- (Default: Disabled, Please be relived, I have learnt).
- Fixed:
- On the customisation sync dialogue, buttons are kept within the screen.
- No more unnecessary entries on `data.json` for customisation sync.
- Selections are no longer lost while updating customisation items.
- Tidied on source codes:
- Many typos have been fixed.
- Some unnecessary type casting removed.
- 0.23.17:
- Improved:
- Overall performance has been improved by using PouchDB 9.0.0.
- Configuration mismatch detection is refined. We can resolve mismatches more smoothly and naturally.
More detail is on `troubleshooting.md` on the repository.
- 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. (Sorry, not applied to the migrated users; I did not think we should deepen the wound any further "automatically").
- 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.
- 0.23.22:
- Fixed:
- Customisation Sync will be disabled when a corrupted configuration is detected.
Therefore, the Device Name can be changed even in the event of a configuration mismatch.
- New feature:
- We can get a notification about the storage usage of the remote database.
- Default: We will be asked.
- If the remote storage usage approaches the configured value, we will be asked whether we want to Rebuild or increase the limit.
- 0.23.16:
- Maintenance Update:
- Library refining (Phase 1 - step 2). There are no significant changes on the user side.
- Including the following fixes of potentially problems:
- the problem which the path had been obfuscating twice has been resolved.
- Note: Potential problems of the library; which has not happened in Self-hosted LiveSync for some reasons.
- 0.23.15:
- Maintenance Update:
- Library refining (Phase 1). There are no significant changes on the user side.
- 0.23.14:
- Case-insensitive file handling
- Full-lower-case files are no longer created during database checking.
- Bulk chunk transfer
- The default value will automatically adjust to an acceptable size when using IBM Cloudant.
- 0.23.21:
- New Features:
- Case-insensitive file handling
- Files can now be handled case-insensitively.
- This behaviour can be modified in the settings under `Handle files as Case-Sensitive` (Default: Prompt, Enabled for previous behaviour).
- Improved chunk revision fixing
- Revisions for chunks can now be fixed for faster chunk creation.
- This can be adjusted in the settings under `Compute revisions for chunks` (Default: Prompt, Enabled for previous behaviour).
- Bulk chunk transfer
- Chunks can now be transferred in bulk during uploads.
- This feature is enabled by default through `Send chunks in bulk`.
- Creation of missing chunks without
- Missing chunks can be created without storing notes, enhancing efficiency for first synchronisation or after prolonged periods without synchronisation.
- Improvements:
- File status scanning on the startup
- Quite significant performance improvements.
- No more missing scans of some files.
- Status in editor enhancements
- Significant performance improvements in the status display within the editor.
- Notifications for files that will not be synchronised will now be properly communicated.
- Encryption and Decryption
- These processes are now performed in background threads to ensure fast and stable transfers.
- Verify and repair all files
- Got faster through parallel checking.
- Migration on update
- Migration messages and wizards have become more helpful.
- Behavioural changes:
- Chunk size adjustments
- Large chunks will no longer be created for older, stable files, addressing storage consumption issues.
- Flag file automation
- Confirmation will be shown and we can cancel it.
- Fixed:
- No longer batch-saving ignores editor inputs.
- The file-watching and serialisation processes have been changed to the one which is similar to previous implementations.
- We can configure the settings (Especially about text-boxes) even if we have configured the device name.
- Database File Scanning
- All files in the database will now be enumerated correctly.
- Miscellaneous
- Dependency updated.
- Now, tree shaking is left to terser, from esbuild.
- 0.23.20:
- Fixed:
- Customisation Sync now checks the difference while storing or applying the configuration.
- No longer storing the same configuration multiple times.
- Time difference in the dialogue has been fixed.
- Remote Storage Limit Notification dialogue has been fixed, now the chosen value is saved.
- Improved:
- We can configure the delay of batch-saving.
- Default: 5 seconds, the same as the previous hard-coded value. (Note: also, the previous behaviour was not correct).
- Also, we can configure the limit of delaying batch-saving.
- The performance of showing status indicators has been improved.
- The Enlarging button on the enlarging threshold dialogue now displays the new value.
Older notes is in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md).

View File

@@ -18,6 +18,56 @@ I have a lot of respect for that plugin, even though it is sometimes treated as
Hooray for open source, and generous licences, and the sharing of knowledge by experts.
#### Version history
- 0.23.19:
- Not released.
- 0.23.18:
- New feature:
- Per-file-saved customization sync has been shipped.
- We can synchronise plug-igs etc., more smoothly.
- Default: disabled. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost compatibility with old versions.
- Customisation sync has got beta3.
- We can set `Flag` to each item to select the newest, automatically.
- This configuration is per device.
- Improved:
- Start-up speed has been improved.
- Fixed:
- On the customisation sync dialogue, buttons are kept within the screen.
- No more unnecessary entries on `data.json` for customisation sync.
- Selections are no longer lost while updating customisation items.
- Tidied on source codes:
- Many typos have been fixed.
- Some unnecessary type casting removed.
- 0.23.17:
- Improved:
- Overall performance has been improved by using PouchDB 9.0.0.
- Configuration mismatch detection is refined. We can resolve mismatches more smoothly and naturally.
More detail is on `troubleshooting.md` on the repository.
- Fixed:
- Customisation Sync will be disabled when a corrupted configuration is detected.
Therefore, the Device Name can be changed even in the event of a configuration mismatch.
- New feature:
- We can get a notification about the storage usage of the remote database.
- Default: We will be asked.
- If the remote storage usage approaches the configured value, we will be asked whether we want to Rebuild or increase the limit.
- 0.23.16:
- Maintenance Update:
- Library refining (Phase 1 - step 2). There are no significant changes on the user side.
- Including the following fixes of potentially problems:
- the problem which the path had been obfuscating twice has been resolved.
- Note: Potential problems of the library; which has not happened in Self-hosted LiveSync for some reasons.
- 0.23.15:
- Maintenance Update:
- Library refining (Phase 1). There are no significant changes on the user side.
- 0.23.14:
- Fixed:
- No longer batch-saving ignores editor inputs.
- The file-watching and serialisation processes have been changed to the one which is similar to previous implementations.
- We can configure the settings (Especially about text-boxes) even if we have configured the device name.
- Improved:
- We can configure the delay of batch-saving.
- Default: 5 seconds, the same as the previous hard-coded value. (Note: also, the previous behaviour was not correct).
- Also, we can configure the limit of delaying batch-saving.
- The performance of showing status indicators has been improved.
- 0.23.13:
- Fixed:
- No longer files have been trimmed even delimiters have been continuous.