mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-02-23 04:28:48 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6d5b78cc8 | ||
|
|
405624b51b | ||
|
|
90c0ff22b9 | ||
|
|
67568ea886 | ||
|
|
cc29b4058d | ||
|
|
4e8243b3d5 | ||
|
|
4eb1787784 | ||
|
|
1cd1465f2c | ||
|
|
45ceca8bb6 | ||
|
|
7b385aab9e | ||
|
|
98411e5f48 | ||
|
|
b6687e2fb0 | ||
|
|
658cbb7ded | ||
|
|
08a48154fa | ||
|
|
62501a5940 | ||
|
|
ccb3dd52de | ||
|
|
3e5f4c8946 | ||
|
|
54e64c59a9 | ||
|
|
588840ff8b | ||
|
|
e6b8dfb279 | ||
|
|
73782c5389 | ||
|
|
f2b667d75e | ||
|
|
9b1588a65b | ||
|
|
0629bc04bb | ||
|
|
b0e97e6c96 | ||
|
|
7f853b0222 | ||
|
|
3d4ad4a3b4 | ||
|
|
4b1fff852a | ||
|
|
08d7d24baf | ||
|
|
9db3c3df0a | ||
|
|
672940ad6f | ||
|
|
2338601fae | ||
|
|
3e657b38a9 | ||
|
|
21861d8c51 | ||
|
|
3bb4aba395 | ||
|
|
751de5a13e | ||
|
|
29229f809b | ||
|
|
2d0dc2a389 | ||
|
|
6cbe319b80 | ||
|
|
e9fe58f818 | ||
|
|
61524e1c44 | ||
|
|
c25eaa09c9 | ||
|
|
71987e6814 | ||
|
|
d062b13040 | ||
|
|
7eceab59af |
@@ -7,3 +7,5 @@ rollup.config.js
|
||||
src/lib/test
|
||||
src/lib/src/cli
|
||||
main.js
|
||||
src/lib/apps/webpeer/dist
|
||||
src/lib/apps/webpeer/svelte.config.js
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '18.x' # You might need to adjust this value to your own version
|
||||
node-version: '22.x' # You might need to adjust this value to your own version
|
||||
# Get the version number and put it in a variable
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,6 +10,9 @@ package-lock.json
|
||||
main.js
|
||||
main_org.js
|
||||
*.js.map
|
||||
meta.json
|
||||
meta-*.json
|
||||
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"tabWidth": 4,
|
||||
"printWidth": 120,
|
||||
"semi": true,
|
||||
"endOfLine": "crlf"
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
|
||||
13
README.md
13
README.md
@@ -18,6 +18,10 @@ Note: This plugin cannot synchronise with the official "Obsidian Sync".
|
||||
- Supporting End-to-end encryption.
|
||||
- Synchronisation of settings, snippets, themes, and plug-ins, via [Customization sync(Beta)](#customization-sync) or [Hidden File Sync](#hiddenfilesync)
|
||||
- WebClip from [obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
|
||||
- WebRTC peer-to-peer synchronisation without the need any `host` is now possible. (Experimental)
|
||||
- This feature is still in the experimental stage. Please be careful when using it.
|
||||
- Instead of using server, you can use [webpeer](https://github.com/vrtmrz/livesync-commonlib/tree/main/apps/webpeer) the pseudo client for receiving and sending between devices.
|
||||
|
||||
|
||||
This plug-in might be useful for researchers, engineers, and developers with a need to keep their notes fully self-hosted for security reasons. Or just anyone who would like the peace of mind of knowing that their notes are fully private.
|
||||
|
||||
@@ -80,6 +84,15 @@ To prevent file and database corruption, please wait to stop Obsidian until all
|
||||
## Tips and Troubleshooting
|
||||
If you are having problems getting the plugin working see: [Tips and Troubleshooting](docs/troubleshooting.md)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
The project has been in continual progress and harmony because of
|
||||
- Many [Contributors](https://github.com/vrtmrz/obsidian-livesync/graphs/contributors)
|
||||
- Many [GitHub Sponsors](https://github.com/sponsors/vrtmrz#sponsors)
|
||||
- JetBrains Community Programs / Support for Open-Source Projects <img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" alt="JetBrains logo." height="24">
|
||||
|
||||
May those who have contributed be honoured and remembered for their kindness and generosity.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the MIT License.
|
||||
|
||||
93
README_es.md
Normal file
93
README_es.md
Normal file
@@ -0,0 +1,93 @@
|
||||
<!-- For translation: 20240227r0 -->
|
||||
# Self-hosted LiveSync
|
||||
[Documentación en inglés](./README_ja.md) - [Documentación en japonés](./README_ja.md) - [Documentación en chino](./README_cn.md).
|
||||
|
||||
Self-hosted LiveSync es un plugin de sincronización implementado por la comunidad, disponible en todas las plataformas compatibles con Obsidian y utiliza CouchDB o Almacenamiento de Objetos (por ejemplo, MinIO, S3, R2, etc.) como servidor.
|
||||
|
||||

|
||||
|
||||
Nota: Este plugin no puede sincronizarse con el "Obsidian Sync" oficial.
|
||||
|
||||
## Características
|
||||
|
||||
- Sincroniza bóvedas de manera eficiente con menos tráfico.
|
||||
- Buen manejo de modificaciones en conflicto.
|
||||
- Fusión automática para conflictos simples.
|
||||
- Uso de soluciones de código abierto para el servidor.
|
||||
- Pueden usarse soluciones compatibles.
|
||||
- Soporte de cifrado de extremo a extremo.
|
||||
- Sincronización de configuraciones, fragmentos, temas y complementos a través de [Sincronización de personalización \(Beta\)](#customization-sync) o [Sincronización de archivos ocultos](#hiddenfilesync)
|
||||
- WebClip de [obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
|
||||
|
||||
Este plugin puede ser útil para investigadores, ingenieros y desarrolladores que necesitan mantener sus notas totalmente autoalojadas por razones de seguridad, o para aquellos que deseen tener la tranquilidad de saber que sus notas son totalmente privadas.
|
||||
|
||||
>[!IMPORTANTE]
|
||||
> - Antes de instalar o actualizar este plugin, realice un respaldo de su bóveda.
|
||||
> - No active este plugin junto con otra solución de sincronización al mismo tiempo (incluyendo iCloud y Obsidian Sync).
|
||||
> - Este es un plugin de sincronización, no una solución de respaldo. No confíe en él para realizar respaldos.
|
||||
|
||||
## Cómo usar
|
||||
|
||||
### Configuración en 3 minutos - CouchDB en fly.io
|
||||
|
||||
**Recomendado para principiantes**
|
||||
|
||||
[](https://www.youtube.com/watch?v=7sa_I1832Xc)
|
||||
|
||||
1. [Configurar CouchDB en fly.io](docs/setup_flyio_es.md)
|
||||
2. Configurar el plugin en [Configuración rápida](docs/quick_setup_es.md)
|
||||
|
||||
### Configuración manual
|
||||
|
||||
1. Configurar el servidor
|
||||
1. [Configurar CouchDB en fly.io](docs/setup_flyio_es.md)
|
||||
2. [Configurar su CouchDB](docs/setup_own_server_es.md)
|
||||
2. Configura el plugin en [Configuración rápida](docs/quick_setup_es.md)
|
||||
|
||||
> [!CONSEJO]
|
||||
> Actualmente, fly.io ya no es gratuito. Afortunadamente, aunque hay algunos problemas, aún podemos usar IBM Cloudant. Aquí está como [Configurar IBM Cloudant](docs/setup_cloudant.md). ¡Se actualizará pronto!
|
||||
|
||||
|
||||
## Información en la barra de estado
|
||||
|
||||
El estado de sincronización se muestra en la barra de estado con los siguientes iconos.
|
||||
|
||||
- Indicador de actividad
|
||||
- 📲 Solicitud de red
|
||||
- Estado
|
||||
- ⏹️ Detenido
|
||||
- 💤 LiveSync activado. Esperando cambios
|
||||
- ⚡️ Sincronización en progreso
|
||||
- ⚠ Ocurrió un error
|
||||
- Indicador estadístico
|
||||
- ↑ Chunks y metadatos subidos
|
||||
- ↓ Chunks y metadatos descargados
|
||||
- Indicador de progreso
|
||||
- 📥 Elementos transferidos sin procesar
|
||||
- 📄 Operación de base de datos en curso
|
||||
- 💾 Procesos de escritura en almacenamiento en curso
|
||||
- ⏳ Procesos de lectura en almacenamiento en curso
|
||||
- 🛫 Procesos de lectura en almacenamiento pendientes
|
||||
- 📬 Procesos de lectura en almacenamiento por lotes
|
||||
- ⚙️ Procesos de almacenamiento de archivos ocultos en curso o pendientes
|
||||
- 🧩 Chunks en espera
|
||||
- 🔌 Elementos de personalización en curso (Configuración, fragmentos y plugins)
|
||||
|
||||
Para prevenir la corrupción de archivos y bases de datos, antes de detener Obsidian espere hasta que todos los indicadores de progreso hayan desaparecido (el plugin también intentará reanudar, sin embargo). Especialmente en caso de que haya eliminado o renombrado archivos.
|
||||
|
||||
|
||||
## Consejos y Solución de Problemas
|
||||
Si tienes problemas para hacer funcionar el plugin, consulta: [Consejos y solución de problemas](docs/troubleshooting_es.md).
|
||||
|
||||
## Agradecimientos
|
||||
|
||||
El proyecto ha progresado y mantenido en armonía gracias a:
|
||||
- Muchos [Colaboradores](https://github.com/vrtmrz/obsidian-livesync/graphs/contributors)
|
||||
- Muchos [Patrocinadores de GitHub](https://github.com/sponsors/vrtmrz#sponsors)
|
||||
- Programas comunitarios de JetBrains / Soporte para Proyectos de Código Abierto <img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" alt="JetBrains logo." height="24">
|
||||
|
||||
Que aquellos que han contribuido sean honrados y recordados por su amabilidad y generosidad.
|
||||
|
||||
## Licencia
|
||||
|
||||
Licenciado bajo la Licencia MIT.
|
||||
@@ -25,10 +25,10 @@ npm run buildDev
|
||||
## Make messages to be translated
|
||||
|
||||
1. Find the message that you want to be translated.
|
||||
2. Change the literal to a tagged template literal using `$f`, like below.
|
||||
2. Change the literal to use `$tf`, like below.
|
||||
```diff
|
||||
- Logger("Could not determine passphrase to save data.json! You probably make the configuration sure again!", LOG_LEVEL_URGENT);
|
||||
+ Logger($f`Could not determine passphrase to save data.json! You probably make the configuration sure again!`, LOG_LEVEL_URGENT);
|
||||
+ Logger($tf('someKeyForPassphraseError'), LOG_LEVEL_URGENT);
|
||||
```
|
||||
3. Make the PR to `https://github.com/vrtmrz/obsidian-livesync`.
|
||||
4. Follow the steps of "Add translations for already defined terms" to add the translations.
|
||||
137
docs/settings.md
137
docs/settings.md
@@ -6,7 +6,7 @@ There are many settings in Self-hosted LiveSync. This document describes each se
|
||||
|
||||
| Icon | Description |
|
||||
| :--: | ------------------------------------------------------------------ |
|
||||
| 💬 | [0. Update Information](#0-update-information) |
|
||||
| 💬 | [0. Change Log](#0-change-log) |
|
||||
| 🧙♂️ | [1. Setup](#1-setup) |
|
||||
| ⚙️ | [2. General Settings](#2-general-settings) |
|
||||
| 🛰️ | [3. Remote Configuration](#3-remote-configuration) |
|
||||
@@ -19,7 +19,7 @@ There are many settings in Self-hosted LiveSync. This document describes each se
|
||||
| 🩹 | [10. Patches (Edge Case)](#10-patches-edge-case) |
|
||||
| 🎛️ | [11. Maintenance](#11-maintenance) |
|
||||
|
||||
## 0. Update Information
|
||||
## 0. Change Log
|
||||
|
||||
This pane shows version up information. You can check what has been changed in recent versions.
|
||||
|
||||
@@ -31,21 +31,21 @@ This pane is used for setting up Self-hosted LiveSync. There are several options
|
||||
|
||||
Most preferred method to setup Self-hosted LiveSync. You can setup Self-hosted LiveSync with a few clicks.
|
||||
|
||||
#### Use the copied setup URI
|
||||
#### Connect with Setup URI
|
||||
|
||||
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.
|
||||
|
||||
#### Minimal setup
|
||||
#### Manual setup
|
||||
|
||||
Step-by-step setup for Self-hosted LiveSync. You can setup Self-hosted LiveSync manually with Minimal setting items.
|
||||
|
||||
#### Enable LiveSync on this device as the setup was completed manually
|
||||
#### Enable LiveSync
|
||||
|
||||
This button only appears when the setup was not completed. If you have completed the setup manually, you can enable LiveSync on this device by this button.
|
||||
|
||||
### 2. To setup the other devices
|
||||
### 2. To setup other devices
|
||||
|
||||
#### Copy current settings as a new setup URI
|
||||
#### Copy the current settings to a Setup URI
|
||||
|
||||
You can copy the current settings as a new setup URI. And this URI can be used to setup the other devices as [Use the copied setup URI](#use-the-copied-setup-uri).
|
||||
|
||||
@@ -71,7 +71,7 @@ Following panes will be shown when you enable this setting.
|
||||
| 🔌 | [6. Customization sync (Advanced)](#6-customization-sync-advanced) |
|
||||
| 🔧 | [8. Advanced (Advanced)](#8-advanced-advanced) |
|
||||
|
||||
#### Enable power user features
|
||||
#### Enable poweruser features
|
||||
|
||||
Setting key: usePowerUserMode
|
||||
|
||||
@@ -152,7 +152,7 @@ Setting key: notifyThresholdOfRemoteStorageSize
|
||||
|
||||
MB (0 to disable). We can get a notification when the estimated remote storage size exceeds this value.
|
||||
|
||||
### 3. Confidentiality
|
||||
### 3. Privacy & Encryption
|
||||
|
||||
#### End-to-End Encryption
|
||||
|
||||
@@ -178,13 +178,19 @@ Setting key: useDynamicIterationCount
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
**now writing from here onwards, sorry**
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
### 4. Minio,S3,R2
|
||||
### 4. Fetch settings
|
||||
|
||||
#### Fetch config from remote server
|
||||
|
||||
Fetch necessary settings from already configured remote server.
|
||||
|
||||
### 5. Minio,S3,R2
|
||||
|
||||
#### Endpoint URL
|
||||
|
||||
@@ -209,15 +215,15 @@ Setting key: bucket
|
||||
#### Use Custom HTTP Handler
|
||||
|
||||
Setting key: useCustomRequestHandler
|
||||
If your Object Storage could not configured accepting CORS, enable this.
|
||||
Enable this if your Object Storage doesn't support CORS
|
||||
|
||||
#### Test Connection
|
||||
|
||||
#### Apply Settings
|
||||
|
||||
### 5. CouchDB
|
||||
### 6. CouchDB
|
||||
|
||||
#### URI
|
||||
#### Server URI
|
||||
|
||||
Setting key: couchDB_URI
|
||||
|
||||
@@ -231,17 +237,17 @@ username
|
||||
Setting key: couchDB_PASSWORD
|
||||
password
|
||||
|
||||
#### Database name
|
||||
#### 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.
|
||||
Open database connection. If the remote database is not found and you have permission to create a database, the database will be created.
|
||||
|
||||
#### Check and fix database configuration
|
||||
#### Validate Database Configuration
|
||||
|
||||
Check the database configuration, and fix if there are any problems.
|
||||
Checks and fixes any potential issues with the database config.
|
||||
|
||||
#### Apply Settings
|
||||
|
||||
@@ -254,7 +260,7 @@ Check the database configuration, and fix if there are any problems.
|
||||
Setting key: preset
|
||||
Apply preset configuration
|
||||
|
||||
### 2. Synchronization Methods
|
||||
### 2. Synchronization Method
|
||||
|
||||
#### Sync Mode
|
||||
|
||||
@@ -268,22 +274,22 @@ Interval (sec)
|
||||
#### Sync on Save
|
||||
|
||||
Setting key: syncOnSave
|
||||
When you save a file, sync automatically
|
||||
Starts synchronisation when a file is saved.
|
||||
|
||||
#### Sync on Editor Save
|
||||
|
||||
Setting key: syncOnEditorSave
|
||||
When you save a file in the editor, sync automatically
|
||||
When you save a file in the editor, start a sync automatically
|
||||
|
||||
#### Sync on File Open
|
||||
|
||||
Setting key: syncOnFileOpen
|
||||
When you open a file, sync automatically
|
||||
Forces the file to be synced when opened.
|
||||
|
||||
#### Sync on Start
|
||||
#### Sync on Startup
|
||||
|
||||
Setting key: syncOnStart
|
||||
Start synchronization after launching Obsidian.
|
||||
Automatically Sync all files when opening Obsidian.
|
||||
|
||||
#### Sync after merging file
|
||||
|
||||
@@ -312,34 +318,36 @@ Saving will be performed forcefully after this number of seconds.
|
||||
#### Use the trash bin
|
||||
|
||||
Setting key: trashInsteadDelete
|
||||
Do not delete files that are deleted in remote, just move to trash.
|
||||
Move remotely deleted files to the trash, instead of deleting.
|
||||
|
||||
#### 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
|
||||
Should we keep folders that don't have any files inside?
|
||||
|
||||
### 5. Conflict resolution (Advanced)
|
||||
|
||||
#### Always overwrite with a newer file (beta)
|
||||
#### (BETA) Always overwrite with a newer file
|
||||
|
||||
Setting key: resolveConflictsByNewerFile
|
||||
(Def off) Resolve conflicts by newer files automatically.
|
||||
Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.
|
||||
|
||||
#### Postpone resolution of inactive files
|
||||
#### Delay conflict resolution of inactive files
|
||||
|
||||
Setting key: checkConflictOnlyOnOpen
|
||||
Should we only check for conflicts when a file is opened?
|
||||
|
||||
#### Postpone manual resolution of inactive files
|
||||
#### Delay merge conflict prompt for inactive files.
|
||||
|
||||
Setting key: showMergeDialogOnlyOnActive
|
||||
Should we prompt you about conflicting files when a file is opened?
|
||||
|
||||
### 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.
|
||||
Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.
|
||||
|
||||
#### Write credentials in the file
|
||||
|
||||
@@ -350,7 +358,7 @@ Setting key: writeCredentialsForSettingSync
|
||||
|
||||
Setting key: notifyAllSettingSyncFile
|
||||
|
||||
### 7. Hidden files (Advanced)
|
||||
### 7. Hidden Files (Advanced)
|
||||
|
||||
#### Hidden file synchronization
|
||||
|
||||
@@ -390,7 +398,7 @@ If this is set, changes to local files which are matched by the ignore files wil
|
||||
#### Ignore files
|
||||
|
||||
Setting key: ignoreFiles
|
||||
We can use multiple ignore files, e.g.) `.gitignore, .dockerignore`
|
||||
Comma separated `.gitignore, .dockerignore`
|
||||
|
||||
### 2. Hidden Files (Advanced)
|
||||
|
||||
@@ -451,7 +459,7 @@ Warning! This will have a serious impact on performance. And the logs will not b
|
||||
#### Suspend file watching
|
||||
|
||||
Setting key: suspendFileWatching
|
||||
Stop watching for file change.
|
||||
Stop watching for file changes.
|
||||
|
||||
#### Suspend database reflecting
|
||||
|
||||
@@ -464,6 +472,10 @@ Stop reflecting database changes to storage files.
|
||||
|
||||
This will recreate chunks for all files. If there were missing chunks, this may fix the errors.
|
||||
|
||||
#### Resolve All conflicted files by the newer one
|
||||
|
||||
Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.
|
||||
|
||||
#### 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.
|
||||
@@ -520,16 +532,6 @@ Setting key: concurrencyOfReadChunksOnline
|
||||
|
||||
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
|
||||
@@ -563,7 +565,7 @@ Setting key: enableCompression
|
||||
#### Batch size
|
||||
|
||||
Setting key: batch_size
|
||||
Number of change feed items to process at a time. Defaults to 50. Minimum is 2.
|
||||
Number of changes to sync at a time. Defaults to 50. Minimum is 2.
|
||||
|
||||
#### Batch limit
|
||||
|
||||
@@ -586,6 +588,13 @@ Setting key: configPassphraseStore
|
||||
Setting key: configPassphrase
|
||||
This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.
|
||||
|
||||
### 4. Developer
|
||||
|
||||
#### Enable Developers' Debug Tools.
|
||||
|
||||
Setting key: enableDebugTools
|
||||
Requires restart of Obsidian
|
||||
|
||||
## 10. Patches (Edge Case)
|
||||
|
||||
### 1. Compatibility (Metadata)
|
||||
@@ -601,15 +610,15 @@ Setting key: automaticallyDeleteMetadataOfDeletedFiles
|
||||
|
||||
### 2. Compatibility (Conflict Behaviour)
|
||||
|
||||
#### Always resolve conflicts manually
|
||||
#### Always prompt merge conflicts
|
||||
|
||||
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)
|
||||
Should we prompt you for every single merge, even if we can safely merge automatcially?
|
||||
|
||||
#### Always reflect synchronized changes even if the note has a conflict
|
||||
#### Apply Latest Change if Conflicting
|
||||
|
||||
Setting key: writeDocumentsIfConflicted
|
||||
Turn on to previous behavior
|
||||
Enable this option to automatically apply the most recent change to documents even when it conflicts
|
||||
|
||||
### 3. Compatibility (Database structure)
|
||||
|
||||
@@ -655,7 +664,7 @@ 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
|
||||
Should we keep folders that don't have any files inside?
|
||||
|
||||
### 7. Edge case addressing (Processing)
|
||||
|
||||
@@ -679,15 +688,15 @@ Setting key: disableCheckingConfigMismatch
|
||||
|
||||
### 1. Scram!
|
||||
|
||||
#### Lock remote
|
||||
#### Lock Server
|
||||
|
||||
Lock remote to prevent synchronization with other devices.
|
||||
Lock the remote server to prevent synchronization with other devices.
|
||||
|
||||
#### Emergency restart
|
||||
|
||||
place the flag file to prevent all operation and restart.
|
||||
Disables all synchronization and restart.
|
||||
|
||||
### 2. Data-complementary Operations
|
||||
### 2. Syncing
|
||||
|
||||
#### Resend
|
||||
|
||||
@@ -719,9 +728,9 @@ Rebuild local and remote database with local files.
|
||||
|
||||
### 5. Rebuilding Operations (Remote Only)
|
||||
|
||||
#### Perform compaction
|
||||
#### Perform cleanup
|
||||
|
||||
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.
|
||||
Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.
|
||||
|
||||
#### Overwrite remote
|
||||
|
||||
@@ -733,18 +742,18 @@ Initialise all journal history, On the next sync, every item will be received an
|
||||
|
||||
#### Purge all journal counter
|
||||
|
||||
Purge all sending and downloading cache.
|
||||
Purge all download/upload cache.
|
||||
|
||||
#### Make empty the bucket
|
||||
#### Fresh Start Wipe
|
||||
|
||||
Delete all data on the remote.
|
||||
Delete all data on the remote server.
|
||||
|
||||
### 6. Niches
|
||||
### 6. Deprecated
|
||||
|
||||
#### (Obsolete) Clean up databases
|
||||
#### Run database cleanup
|
||||
|
||||
Delete unused chunks to shrink the database. However, this feature could be not effective in some cases. Please use rebuild everything instead.
|
||||
Attempt to shrink the database by deleting unused chunks. This may not work consistently. Use the 'Rebuild everything' under Total Overhaul.
|
||||
|
||||
### 7. Reset
|
||||
|
||||
#### Discard local database to reset or uninstall Self-hosted LiveSync
|
||||
#### Delete local database to reset or uninstall Self-hosted LiveSync
|
||||
|
||||
@@ -54,7 +54,7 @@ Please refer the [official document](https://docs.couchdb.org/en/stable/install/
|
||||
|
||||
## 2. Run couchdb-init.sh for initialise
|
||||
```
|
||||
$ curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | bash
|
||||
curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | bash
|
||||
```
|
||||
|
||||
If it results like following:
|
||||
@@ -83,7 +83,12 @@ Your CouchDB has been initialised successfully. If you want this manually, pleas
|
||||
Whatever solutions we can use. For the simplicity, following sample uses Cloudflare Zero Trust for testing.
|
||||
|
||||
```
|
||||
$ cloudflared tunnel --url http://localhost:5984
|
||||
cloudflared tunnel --url http://localhost:5984
|
||||
```
|
||||
|
||||
You will then get the following output:
|
||||
|
||||
```
|
||||
2024-02-14T10:35:25Z INF Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee. If you intend to use Tunnels in production you should use a pre-created named tunnel by following: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
|
||||
2024-02-14T10:35:25Z INF Requesting new quick Tunnel on trycloudflare.com...
|
||||
2024-02-14T10:35:26Z INF +--------------------------------------------------------------------------------------------+
|
||||
@@ -103,12 +108,17 @@ Now `https://tiles-photograph-routine-groundwater.trycloudflare.com` is our serv
|
||||
|
||||
### 1. Generate the setup URI on a desktop device or server
|
||||
```bash
|
||||
$ 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
|
||||
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
|
||||
```
|
||||
|
||||
You will then get the following output:
|
||||
|
||||
```bash
|
||||
obsidian://setuplivesync?settings=%5B%22tm2DpsOE74nJAryprZO2M93wF%2Fvg.......4b26ed33230729%22%5D
|
||||
|
||||
Your passphrase of Setup-URI is: patient-haze
|
||||
|
||||
@@ -10,6 +10,7 @@ import fs from "node:fs";
|
||||
import { minify } from "terser";
|
||||
import inlineWorkerPlugin from "esbuild-plugin-inline-worker";
|
||||
import { terserOption } from "./terser.config.mjs";
|
||||
import path from "node:path";
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
const keepTest = true; //!prod;
|
||||
@@ -18,6 +19,46 @@ const manifestJson = JSON.parse(fs.readFileSync("./manifest.json") + "");
|
||||
const packageJson = JSON.parse(fs.readFileSync("./package.json") + "");
|
||||
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
|
||||
|
||||
const moduleAliasPlugin = {
|
||||
name: "module-alias",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /.(dev)(.ts|)$/ }, (args) => {
|
||||
// console.log(args.path);
|
||||
if (prod) {
|
||||
let prodTs = args.path.replace(".dev", ".prod");
|
||||
const statFile = prodTs.endsWith(".ts") ? prodTs : prodTs + ".ts";
|
||||
const realPath = path.join(args.resolveDir, statFile);
|
||||
console.log(`Checking ${statFile}`);
|
||||
if (fs.existsSync(realPath)) {
|
||||
console.log(`Replaced ${args.path} with ${prodTs}`);
|
||||
return {
|
||||
path: realPath,
|
||||
namespace: "file",
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
build.onResolve({ filter: /.(platform)(.ts|)$/ }, (args) => {
|
||||
// console.log(args.path);
|
||||
if (prod) {
|
||||
let prodTs = args.path.replace(".platform", ".obsidian");
|
||||
const statFile = prodTs.endsWith(".ts") ? prodTs : prodTs + ".ts";
|
||||
const realPath = path.join(args.resolveDir, statFile);
|
||||
console.log(`Checking ${statFile}`);
|
||||
if (fs.existsSync(realPath)) {
|
||||
console.log(`Replaced ${args.path} with ${prodTs}`);
|
||||
return {
|
||||
path: realPath,
|
||||
namespace: "file",
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/** @type esbuild.Plugin[] */
|
||||
const plugins = [
|
||||
{
|
||||
@@ -26,10 +67,22 @@ const plugins = [
|
||||
let count = 0;
|
||||
build.onEnd(async (result) => {
|
||||
if (count++ === 0) {
|
||||
console.log("first build:", result);
|
||||
console.log("first build:");
|
||||
if (prod) {
|
||||
console.log("MetaFile:");
|
||||
if (result.metafile) {
|
||||
fs.writeFileSync("meta.json", JSON.stringify(result.metafile));
|
||||
let text = await esbuild.analyzeMetafile(result.metafile, {
|
||||
verbose: true,
|
||||
});
|
||||
// console.log(text);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log("subsequent build:");
|
||||
}
|
||||
const filename = `meta-${prod ? "prod" : "dev"}.json`;
|
||||
await fs.promises.writeFile(filename, JSON.stringify(result.metafile, null, 2));
|
||||
if (prod) {
|
||||
console.log("Performing terser");
|
||||
const src = fs.readFileSync("./main_org.js").toString();
|
||||
@@ -47,7 +100,22 @@ 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 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: "// Leave it all to terser",
|
||||
@@ -66,6 +134,7 @@ const context = await esbuild.context({
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
platform: "browser",
|
||||
metafile: true,
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: false,
|
||||
outfile: "main_org.js",
|
||||
@@ -77,6 +146,7 @@ const context = await esbuild.context({
|
||||
dropLabels: prod && !keepTest ? ["TEST", "DEV"] : [],
|
||||
// keepNames: true,
|
||||
plugins: [
|
||||
moduleAliasPlugin,
|
||||
inlineWorkerPlugin({
|
||||
external: externals,
|
||||
treeShaking: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.24.2",
|
||||
"version": "0.24.13",
|
||||
"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",
|
||||
|
||||
8547
package-lock.json
generated
8547
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.24.2",
|
||||
"version": "0.24.13",
|
||||
"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",
|
||||
"scripts": {
|
||||
"bakei18n": "npx tsx ./src/lib/_tools/bakei18n.ts",
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "node esbuild.config.mjs production",
|
||||
"build": "npm run bakei18n && node esbuild.config.mjs production",
|
||||
"buildDev": "node esbuild.config.mjs dev",
|
||||
"lint": "eslint src",
|
||||
"svelte-check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
@@ -32,17 +33,17 @@
|
||||
"@types/pouchdb-mapreduce": "^6.1.10",
|
||||
"@types/pouchdb-replication": "^6.4.7",
|
||||
"@types/transform-pouch": "^1.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.4.0",
|
||||
"@typescript-eslint/parser": "^8.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.23.0",
|
||||
"@typescript-eslint/parser": "^8.23.0",
|
||||
"builtin-modules": "^4.0.0",
|
||||
"esbuild": "0.23.1",
|
||||
"esbuild-svelte": "^0.8.1",
|
||||
"esbuild": "0.24.2",
|
||||
"esbuild-svelte": "^0.9.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"events": "^3.3.0",
|
||||
"obsidian": "^1.6.6",
|
||||
"postcss": "^8.4.45",
|
||||
"obsidian": "^1.7.2",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"pouchdb-adapter-http": "^9.0.0",
|
||||
"pouchdb-adapter-idb": "^9.0.0",
|
||||
@@ -54,13 +55,14 @@
|
||||
"pouchdb-merge": "^9.0.0",
|
||||
"pouchdb-replication": "^9.0.0",
|
||||
"pouchdb-utils": "^9.0.0",
|
||||
"prettier": "^3.3.3",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-preprocess": "^6.0.2",
|
||||
"terser": "^5.31.6",
|
||||
"prettier": "^3.4.2",
|
||||
"svelte": "^5.19.7",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"terser": "^5.37.0",
|
||||
"transform-pouch": "^2.0.0",
|
||||
"tslib": "^2.7.0",
|
||||
"typescript": "^5.5.4"
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.645.0",
|
||||
@@ -70,11 +72,11 @@
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||
"fflate": "^0.8.2",
|
||||
"idb": "^8.0.0",
|
||||
"idb": "^8.0.2",
|
||||
"minimatch": "^10.0.1",
|
||||
"octagonal-wheels": "^0.1.15",
|
||||
"svelte-check": "^4.0.4",
|
||||
"xxhash-wasm": "0.4.2",
|
||||
"octagonal-wheels": "^0.1.23",
|
||||
"svelte-check": "^4.1.4",
|
||||
"trystero": "^0.20.0",
|
||||
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
28
src/common/SvelteItemView.ts
Normal file
28
src/common/SvelteItemView.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ItemView } from "obsidian";
|
||||
import { type mount, unmount } from "svelte";
|
||||
|
||||
export abstract class SvelteItemView extends ItemView {
|
||||
abstract instantiateComponent(target: HTMLElement): ReturnType<typeof mount> | Promise<ReturnType<typeof mount>>;
|
||||
component?: ReturnType<typeof mount>;
|
||||
async onOpen() {
|
||||
await super.onOpen();
|
||||
this.contentEl.empty();
|
||||
await this._dismountComponent();
|
||||
this.component = await this.instantiateComponent(this.contentEl);
|
||||
return;
|
||||
}
|
||||
async _dismountComponent() {
|
||||
if (this.component) {
|
||||
await unmount(this.component);
|
||||
this.component = undefined;
|
||||
}
|
||||
}
|
||||
async onClose() {
|
||||
await super.onClose();
|
||||
if (this.component) {
|
||||
await unmount(this.component);
|
||||
this.component = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,11 @@
|
||||
import type { FilePathWithPrefix, ObsidianLiveSyncSettings } from "../lib/src/common/types";
|
||||
import { eventHub } from "../lib/src/hub/hub";
|
||||
import type ObsidianLiveSyncPlugin from "../main";
|
||||
|
||||
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_FILE_SAVED = "file-saved";
|
||||
export const EVENT_LEAF_ACTIVE_CHANGED = "leaf-active-changed";
|
||||
|
||||
export const EVENT_LOG_ADDED = "log-added";
|
||||
|
||||
export const EVENT_REQUEST_OPEN_SETTINGS = "request-open-settings";
|
||||
export const EVENT_REQUEST_OPEN_SETTING_WIZARD = "request-open-setting-wizard";
|
||||
export const EVENT_REQUEST_OPEN_SETUP_URI = "request-open-setup-uri";
|
||||
@@ -21,29 +15,26 @@ export const EVENT_REQUEST_RELOAD_SETTING_TAB = "reload-setting-tab";
|
||||
|
||||
export const EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG = "request-open-plugin-sync-dialog";
|
||||
|
||||
export const EVENT_REQUEST_OPEN_P2P = "request-open-p2p";
|
||||
export const EVENT_REQUEST_CLOSE_P2P = "request-close-p2p";
|
||||
|
||||
// export const EVENT_FILE_CHANGED = "file-changed";
|
||||
|
||||
declare global {
|
||||
interface LSEvents {
|
||||
[EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG]: undefined;
|
||||
[EVENT_FILE_SAVED]: undefined;
|
||||
[EVENT_REQUEST_OPEN_SETUP_URI]: undefined;
|
||||
[EVENT_REQUEST_COPY_SETUP_URI]: undefined;
|
||||
[EVENT_REQUEST_RELOAD_SETTING_TAB]: undefined;
|
||||
[EVENT_PLUGIN_UNLOADED]: undefined;
|
||||
[EVENT_SETTING_SAVED]: ObsidianLiveSyncSettings;
|
||||
[EVENT_PLUGIN_LOADED]: ObsidianLiveSyncPlugin;
|
||||
[EVENT_LAYOUT_READY]: undefined;
|
||||
"event-file-changed": { file: FilePathWithPrefix; automated: boolean };
|
||||
"document-stub-created": {
|
||||
toc: Set<string>;
|
||||
stub: { [key: string]: { [key: string]: Map<string, Record<string, string>> } };
|
||||
};
|
||||
[EVENT_PLUGIN_UNLOADED]: undefined;
|
||||
[EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG]: undefined;
|
||||
[EVENT_REQUEST_OPEN_SETTINGS]: undefined;
|
||||
[EVENT_REQUEST_OPEN_SETTING_WIZARD]: undefined;
|
||||
[EVENT_FILE_RENAMED]: { newPath: FilePathWithPrefix; old: FilePathWithPrefix };
|
||||
[EVENT_REQUEST_RELOAD_SETTING_TAB]: undefined;
|
||||
[EVENT_LEAF_ACTIVE_CHANGED]: undefined;
|
||||
[EVENT_REQUEST_CLOSE_P2P]: undefined;
|
||||
[EVENT_REQUEST_OPEN_P2P]: undefined;
|
||||
[EVENT_REQUEST_OPEN_SETUP_URI]: undefined;
|
||||
[EVENT_REQUEST_COPY_SETUP_URI]: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export * from "../lib/src/events/coreEvents.ts";
|
||||
export { eventHub };
|
||||
|
||||
@@ -85,6 +85,7 @@ export function getStoragePathFromUXFileInfo(file: UXFileInfoStub | string | Fil
|
||||
return stripAllPrefixes(file.path);
|
||||
}
|
||||
export function getDatabasePathFromUXFileInfo(file: UXFileInfoStub | string | FilePathWithPrefix) {
|
||||
if (typeof file == "string" && file.startsWith(ICXHeader)) return file as FilePathWithPrefix;
|
||||
const prefix = isInternalFile(file) ? ICHeader : "";
|
||||
if (typeof file == "string") return (prefix + stripAllPrefixes(file as FilePathWithPrefix)) as FilePathWithPrefix;
|
||||
return (prefix + stripAllPrefixes(file.path)) as FilePathWithPrefix;
|
||||
|
||||
@@ -71,6 +71,7 @@ import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||
import type { IObsidianModule } from "../../modules/AbstractObsidianModule.ts";
|
||||
import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "../../common/events.ts";
|
||||
import { PluginDialogModal } from "./PluginDialogModal.ts";
|
||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||
|
||||
const d = "\u200b";
|
||||
const d2 = "\n";
|
||||
@@ -446,7 +447,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
|
||||
this.showPluginSyncModal();
|
||||
},
|
||||
});
|
||||
this.addRibbonIcon("custom-sync", "Show Customization sync", () => {
|
||||
this.addRibbonIcon("custom-sync", $msg("cmdConfigSync.showCustomizationSync"), () => {
|
||||
this.showPluginSyncModal();
|
||||
}).addClass("livesync-ribbon-showcustom");
|
||||
eventHub.onEvent(EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, () => this.showPluginSyncModal());
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { ConfigSync, PluginDataExDisplayV2, type IPluginDataExDisplay, type PluginDataExFile } from "./CmdConfigSync.ts";
|
||||
import {
|
||||
ConfigSync,
|
||||
PluginDataExDisplayV2,
|
||||
type IPluginDataExDisplay,
|
||||
type PluginDataExFile,
|
||||
} from "./CmdConfigSync.ts";
|
||||
import { Logger } from "../../lib/src/common/logger";
|
||||
import { type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types";
|
||||
import { getDocData, timeDeltaToHumanReadable, unique } from "../../lib/src/common/utils";
|
||||
@@ -15,7 +20,11 @@
|
||||
export let applyAllPluse = 0;
|
||||
|
||||
export let applyData: (data: IPluginDataExDisplay) => Promise<boolean>;
|
||||
export let compareData: (dataA: IPluginDataExDisplay, dataB: IPluginDataExDisplay, compareEach?: boolean) => Promise<boolean>;
|
||||
export let compareData: (
|
||||
dataA: IPluginDataExDisplay,
|
||||
dataB: IPluginDataExDisplay,
|
||||
compareEach?: boolean
|
||||
) => Promise<boolean>;
|
||||
export let deleteData: (data: IPluginDataExDisplay) => Promise<boolean>;
|
||||
export let hidden: boolean;
|
||||
export let plugin: ObsidianLiveSyncPlugin;
|
||||
@@ -151,7 +160,11 @@
|
||||
canCompare = result.canCompare;
|
||||
pickToCompare = false;
|
||||
if (canCompare) {
|
||||
if (local?.files.length == remote?.files.length && local?.files.length == 1 && local?.files[0].filename == remote?.files[0].filename) {
|
||||
if (
|
||||
local?.files.length == remote?.files.length &&
|
||||
local?.files.length == 1 &&
|
||||
local?.files[0].filename == remote?.files[0].filename
|
||||
) {
|
||||
pickToCompare = false;
|
||||
} else {
|
||||
pickToCompare = true;
|
||||
@@ -250,7 +263,11 @@
|
||||
const selectedItem = list.find((e) => e.term == selected);
|
||||
await compareItems(local, selectedItem);
|
||||
}
|
||||
async function compareItems(local: IPluginDataExDisplay | undefined, remote: IPluginDataExDisplay | undefined, filename?: string) {
|
||||
async function compareItems(
|
||||
local: IPluginDataExDisplay | undefined,
|
||||
remote: IPluginDataExDisplay | undefined,
|
||||
filename?: string
|
||||
) {
|
||||
if (local && remote) {
|
||||
if (!filename) {
|
||||
if (await compareData(local, remote)) {
|
||||
@@ -258,8 +275,10 @@
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
const localCopy = local instanceof PluginDataExDisplayV2 ? new PluginDataExDisplayV2(local) : { ...local };
|
||||
const remoteCopy = remote instanceof PluginDataExDisplayV2 ? new PluginDataExDisplayV2(remote) : { ...remote };
|
||||
const localCopy =
|
||||
local instanceof PluginDataExDisplayV2 ? new PluginDataExDisplayV2(local) : { ...local };
|
||||
const remoteCopy =
|
||||
remote instanceof PluginDataExDisplayV2 ? new PluginDataExDisplayV2(remote) : { ...remote };
|
||||
localCopy.files = localCopy.files.filter((e) => e.filename == filename);
|
||||
remoteCopy.files = remoteCopy.files.filter((e) => e.filename == filename);
|
||||
if (await compareData(localCopy, remoteCopy, true)) {
|
||||
@@ -329,7 +348,7 @@
|
||||
</script>
|
||||
|
||||
{#if terms.length > 0}
|
||||
<span class="spacer" />
|
||||
<span class="spacer"></span>
|
||||
{#if !hidden}
|
||||
<span class="chip-wrap">
|
||||
<span class="chip modified">{freshness}</span>
|
||||
@@ -351,12 +370,15 @@
|
||||
<button on:click={compareSelected}>⮂</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<button disabled />
|
||||
<!-- svelte-ignore a11y_consider_explicit_label -->
|
||||
<button disabled></button>
|
||||
{/if}
|
||||
<button on:click={applySelected}>✓</button>
|
||||
{:else}
|
||||
<button disabled />
|
||||
<button disabled />
|
||||
<!-- svelte-ignore a11y_consider_explicit_label -->
|
||||
<button disabled></button>
|
||||
<!-- svelte-ignore a11y_consider_explicit_label -->
|
||||
<button disabled></button>
|
||||
{/if}
|
||||
{#if isMaintenanceMode}
|
||||
{#if selected != ""}
|
||||
@@ -367,10 +389,12 @@
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<span class="spacer" />
|
||||
<span class="spacer"></span>
|
||||
<span class="message even">All the same or non-existent</span>
|
||||
<button disabled />
|
||||
<button disabled />
|
||||
<!-- svelte-ignore a11y_consider_explicit_label -->
|
||||
<button disabled></button>
|
||||
<!-- svelte-ignore a11y_consider_explicit_label -->
|
||||
<button disabled></button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { mount, unmount } from "svelte";
|
||||
import { App, Modal } from "../../deps.ts";
|
||||
import ObsidianLiveSyncPlugin from "../../main.ts";
|
||||
import PluginPane from "./PluginPane.svelte";
|
||||
export class PluginDialogModal extends Modal {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
component: PluginPane | undefined;
|
||||
component: ReturnType<typeof mount> | undefined;
|
||||
isOpened() {
|
||||
return this.component != undefined;
|
||||
}
|
||||
@@ -20,7 +21,7 @@ export class PluginDialogModal extends Modal {
|
||||
this.contentEl.style.flexDirection = "column";
|
||||
this.titleEl.setText("Customization Sync (Beta3)");
|
||||
if (!this.component) {
|
||||
this.component = new PluginPane({
|
||||
this.component = mount(PluginPane, {
|
||||
target: contentEl,
|
||||
props: { plugin: this.plugin },
|
||||
});
|
||||
@@ -29,7 +30,7 @@ export class PluginDialogModal extends Modal {
|
||||
|
||||
onClose() {
|
||||
if (this.component) {
|
||||
this.component.$destroy();
|
||||
void unmount(this.component);
|
||||
this.component = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,10 +542,12 @@
|
||||
padding-right: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.filerow.hideeven:has(.even),
|
||||
.labelrow.hideeven:has(.even) {
|
||||
|
||||
.filerow.hideeven:has(:global(.even)),
|
||||
.labelrow.hideeven:has(:global(.even)) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.noterow {
|
||||
min-height: 2em;
|
||||
display: flex;
|
||||
|
||||
@@ -136,7 +136,9 @@
|
||||
{#if selectedObj != false}
|
||||
<div class="op-scrollable json-source">
|
||||
{#each diffs as diff}
|
||||
<span class={diff[0] == DIFF_DELETE ? "deleted" : diff[0] == DIFF_INSERT ? "added" : "normal"}>{diff[1]}</span>
|
||||
<span class={diff[0] == DIFF_DELETE ? "deleted" : diff[0] == DIFF_INSERT ? "added" : "normal"}
|
||||
>{diff[1]}</span
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
@@ -145,6 +147,7 @@
|
||||
|
||||
<div class="infos">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{nameA}</th>
|
||||
<td
|
||||
@@ -169,6 +172,7 @@
|
||||
{docBContent.length} letters
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -203,6 +207,7 @@
|
||||
overflow-y: scroll;
|
||||
max-height: 60vh;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
.json-source {
|
||||
white-space: pre;
|
||||
|
||||
@@ -154,7 +154,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule
|
||||
this.settings.syncInternalFilesBeforeReplication &&
|
||||
!this.settings.watchInternalFileChanges
|
||||
) {
|
||||
await this.scanAllStorageChanges();
|
||||
await this.scanAllStorageChanges(showNotice);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1108,6 +1108,9 @@ Offline Changed files: ${files.length}`;
|
||||
}
|
||||
|
||||
queueNotification(key: FilePath) {
|
||||
if (this.settings.suppressNotifyHiddenFilesChange) {
|
||||
return;
|
||||
}
|
||||
const configDir = this.plugin.app.vault.configDir;
|
||||
if (!key.startsWith(configDir)) return;
|
||||
const dirName = key.split("/").slice(0, -1).join("/");
|
||||
|
||||
265
src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts
Normal file
265
src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import { sizeToHumanReadable } from "octagonal-wheels/number";
|
||||
import { LOG_LEVEL_NOTICE, type MetaEntry } from "../../lib/src/common/types";
|
||||
import { getNoFromRev } from "../../lib/src/pouchdb/LiveSyncLocalDB";
|
||||
import type { IObsidianModule } from "../../modules/AbstractObsidianModule";
|
||||
import { LiveSyncCommands } from "../LiveSyncCommands";
|
||||
|
||||
export class LocalDatabaseMaintenance extends LiveSyncCommands implements IObsidianModule {
|
||||
$everyOnload(): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
onunload(): void {
|
||||
// NO OP.
|
||||
}
|
||||
onload(): void | Promise<void> {
|
||||
// NO OP.
|
||||
}
|
||||
async allChunks(includeDeleted: boolean = false) {
|
||||
const p = this._progress("", LOG_LEVEL_NOTICE);
|
||||
p.log("Retrieving chunks informations..");
|
||||
try {
|
||||
const ret = await this.localDatabase.allChunks(includeDeleted);
|
||||
return ret;
|
||||
} finally {
|
||||
p.done();
|
||||
}
|
||||
}
|
||||
get database() {
|
||||
return this.localDatabase.localDatabase;
|
||||
}
|
||||
clearHash() {
|
||||
this.localDatabase.hashCaches.clear();
|
||||
}
|
||||
|
||||
async confirm(title: string, message: string, affirmative = "Yes", negative = "No") {
|
||||
return (
|
||||
(await this.plugin.confirm.askSelectStringDialogue(message, [affirmative, negative], {
|
||||
title,
|
||||
defaultAction: affirmative,
|
||||
})) === affirmative
|
||||
);
|
||||
}
|
||||
isAvailable() {
|
||||
if (!this.settings.doNotUseFixedRevisionForChunks) {
|
||||
this._notice("Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.");
|
||||
return false;
|
||||
}
|
||||
if (this.settings.readChunksOnline) {
|
||||
this._notice("Please disable 'Read chunks online' in settings to use Garbage Collection.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Resurrect deleted chunks that are still used in the database.
|
||||
*/
|
||||
async resurrectChunks() {
|
||||
if (!this.isAvailable()) return;
|
||||
const { used, existing } = await this.allChunks(true);
|
||||
const excessiveDeletions = [...existing]
|
||||
.filter(([key, e]) => e._deleted)
|
||||
.filter(([key, e]) => used.has(e._id))
|
||||
.map(([key, e]) => e);
|
||||
const completelyLostChunks = [] as string[];
|
||||
// Data lost chunks : chunks that are deleted and data is purged.
|
||||
const dataLostChunks = [...existing]
|
||||
.filter(([key, e]) => e._deleted && e.data === "")
|
||||
.map(([key, e]) => e)
|
||||
.filter((e) => used.has(e._id));
|
||||
for (const e of dataLostChunks) {
|
||||
// Retrieve the data from the previous revision.
|
||||
const doc = await this.database.get(e._id, { rev: e._rev, revs: true, revs_info: true, conflicts: true });
|
||||
const history = doc._revs_info || [];
|
||||
// Chunks are immutable. So, we can resurrect the chunk by copying the data from any of previous revisions.
|
||||
let resurrected = null as null | string;
|
||||
const availableRevs = history
|
||||
.filter((e) => e.status == "available")
|
||||
.map((e) => e.rev)
|
||||
.sort((a, b) => getNoFromRev(a) - getNoFromRev(b));
|
||||
for (const rev of availableRevs) {
|
||||
const revDoc = await this.database.get(e._id, { rev: rev });
|
||||
if (revDoc.type == "leaf" && revDoc.data !== "") {
|
||||
// Found the data.
|
||||
resurrected = revDoc.data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If the data is not found, we cannot resurrect the chunk, add it to the excessiveDeletions.
|
||||
if (resurrected !== null) {
|
||||
excessiveDeletions.push({ ...e, data: resurrected, _deleted: false });
|
||||
} else {
|
||||
completelyLostChunks.push(e._id);
|
||||
}
|
||||
}
|
||||
// Chunks to be resurrected.
|
||||
const resurrectChunks = excessiveDeletions.filter((e) => e.data !== "").map((e) => ({ ...e, _deleted: false }));
|
||||
|
||||
if (resurrectChunks.length == 0) {
|
||||
this._notice("No chunks are found to be resurrected.");
|
||||
return;
|
||||
}
|
||||
const message = `We have following chunks that are deleted but still used in the database.
|
||||
|
||||
- Completely lost chunks: ${completelyLostChunks.length}
|
||||
- Resurrectable chunks: ${resurrectChunks.length}
|
||||
|
||||
Do you want to resurrect these chunks?`;
|
||||
if (await this.confirm("Resurrect Chunks", message, "Resurrect", "Cancel")) {
|
||||
const result = await this.database.bulkDocs(resurrectChunks);
|
||||
this.clearHash();
|
||||
const resurrectedChunks = result.filter((e) => "ok" in e).map((e) => e.id);
|
||||
this._notice(`Resurrected chunks: ${resurrectedChunks.length} / ${resurrectChunks.length}`);
|
||||
} else {
|
||||
this._notice("Resurrect operation is cancelled.");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Commit deletion of files that are marked as deleted.
|
||||
* This method makes the deletion permanent, and the files will not be recovered.
|
||||
* After this, chunks that are used in the deleted files become ready for compaction.
|
||||
*/
|
||||
async commitFileDeletion() {
|
||||
if (!this.isAvailable()) return;
|
||||
const p = this._progress("", LOG_LEVEL_NOTICE);
|
||||
p.log("Searching for deleted files..");
|
||||
const docs = await this.database.allDocs<MetaEntry>({ include_docs: true });
|
||||
const deletedDocs = docs.rows.filter(
|
||||
(e) => (e.doc?.type == "newnote" || e.doc?.type == "plain") && e.doc?.deleted
|
||||
);
|
||||
if (deletedDocs.length == 0) {
|
||||
p.done("No deleted files found.");
|
||||
return;
|
||||
}
|
||||
p.log(`Found ${deletedDocs.length} deleted files.`);
|
||||
|
||||
const message = `We have following files that are marked as deleted.
|
||||
|
||||
- Deleted files: ${deletedDocs.length}
|
||||
|
||||
Are you sure to delete these files permanently?
|
||||
|
||||
Note: **Make sure to synchronise all devices before deletion.**
|
||||
|
||||
> [!Note]
|
||||
> This operation affects the database permanently. Deleted files will not be recovered after this operation.
|
||||
> And, the chunks that are used in the deleted files will be ready for compaction.`;
|
||||
|
||||
const deletingDocs = deletedDocs.map((e) => ({ ...e.doc, _deleted: true }) as MetaEntry);
|
||||
|
||||
if (await this.confirm("Delete Files", message, "Delete", "Cancel")) {
|
||||
const result = await this.database.bulkDocs(deletingDocs);
|
||||
this.clearHash();
|
||||
p.done(`Deleted ${result.filter((e) => "ok" in e).length} / ${deletedDocs.length} files.`);
|
||||
} else {
|
||||
p.done("Deletion operation is cancelled.");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Commit deletion of chunks that are not used in the database.
|
||||
* This method makes the deletion permanent, and the chunks will not be recovered if the database run compaction.
|
||||
* After this, the database can shrink the database size by compaction.
|
||||
* It is recommended to compact the database after this operation (History should be kept once before compaction).
|
||||
*/
|
||||
async commitChunkDeletion() {
|
||||
if (!this.isAvailable()) return;
|
||||
const { existing } = await this.allChunks(true);
|
||||
const deletedChunks = [...existing].filter(([key, e]) => e._deleted && e.data !== "").map(([key, e]) => e);
|
||||
const deletedNotVacantChunks = deletedChunks.map((e) => ({ ...e, data: "", _deleted: true }));
|
||||
const size = deletedChunks.reduce((acc, e) => acc + e.data.length, 0);
|
||||
const humanSize = sizeToHumanReadable(size);
|
||||
const message = `We have following chunks that are marked as deleted.
|
||||
|
||||
- Deleted chunks: ${deletedNotVacantChunks.length} (${humanSize})
|
||||
|
||||
Are you sure to delete these chunks permanently?
|
||||
|
||||
Note: **Make sure to synchronise all devices before deletion.**
|
||||
|
||||
> [!Note]
|
||||
> This operation finally reduces the capacity of the remote.`;
|
||||
|
||||
if (deletedNotVacantChunks.length == 0) {
|
||||
this._notice("No deleted chunks found.");
|
||||
return;
|
||||
}
|
||||
if (await this.confirm("Delete Chunks", message, "Delete", "Cancel")) {
|
||||
const result = await this.database.bulkDocs(deletedNotVacantChunks);
|
||||
this.clearHash();
|
||||
this._notice(
|
||||
`Deleted chunks: ${result.filter((e) => "ok" in e).length} / ${deletedNotVacantChunks.length}`
|
||||
);
|
||||
} else {
|
||||
this._notice("Deletion operation is cancelled.");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Compact the database.
|
||||
* This method removes all deleted chunks that are not used in the database.
|
||||
* Make sure all devices are synchronized before running this method.
|
||||
*/
|
||||
async markUnusedChunks() {
|
||||
if (!this.isAvailable()) return;
|
||||
const { used, existing } = await this.allChunks();
|
||||
const existChunks = [...existing];
|
||||
const unusedChunks = existChunks.filter(([key, e]) => !used.has(e._id)).map(([key, e]) => e);
|
||||
const deleteChunks = unusedChunks.map((e) => ({
|
||||
...e,
|
||||
_deleted: true,
|
||||
}));
|
||||
const size = deleteChunks.reduce((acc, e) => acc + e.data.length, 0);
|
||||
const humanSize = sizeToHumanReadable(size);
|
||||
if (deleteChunks.length == 0) {
|
||||
this._notice("No unused chunks found.");
|
||||
return;
|
||||
}
|
||||
const message = `We have following chunks that are not used from any files.
|
||||
|
||||
- Chunks: ${deleteChunks.length} (${humanSize})
|
||||
|
||||
Are you sure to mark these chunks to be deleted?
|
||||
|
||||
Note: **Make sure to synchronise all devices before deletion.**
|
||||
|
||||
> [!Note]
|
||||
> This operation will not reduces the capacity of the remote until permanent deletion.`;
|
||||
|
||||
if (await this.confirm("Mark unused chunks", message, "Mark", "Cancel")) {
|
||||
const result = await this.database.bulkDocs(deleteChunks);
|
||||
this.clearHash();
|
||||
this._notice(`Marked chunks: ${result.filter((e) => "ok" in e).length} / ${deleteChunks.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
async removeUnusedChunks() {
|
||||
const { used, existing } = await this.allChunks();
|
||||
const existChunks = [...existing];
|
||||
const unusedChunks = existChunks.filter(([key, e]) => !used.has(e._id)).map(([key, e]) => e);
|
||||
const deleteChunks = unusedChunks.map((e) => ({
|
||||
...e,
|
||||
data: "",
|
||||
_deleted: true,
|
||||
}));
|
||||
const size = unusedChunks.reduce((acc, e) => acc + e.data.length, 0);
|
||||
const humanSize = sizeToHumanReadable(size);
|
||||
if (deleteChunks.length == 0) {
|
||||
this._notice("No unused chunks found.");
|
||||
return;
|
||||
}
|
||||
const message = `We have following chunks that are not used from any files.
|
||||
|
||||
- Chunks: ${deleteChunks.length} (${humanSize})
|
||||
|
||||
Are you sure to delete these chunks?
|
||||
|
||||
Note: **Make sure to synchronise all devices before deletion.**
|
||||
|
||||
> [!Note]
|
||||
> Chunks referenced from deleted files are not deleted. Please run "Commit File Deletion" before this operation.`;
|
||||
|
||||
if (await this.confirm("Mark unused chunks", message, "Mark", "Cancel")) {
|
||||
const result = await this.database.bulkDocs(deleteChunks);
|
||||
this._notice(`Deleted chunks: ${result.filter((e) => "ok" in e).length} / ${deleteChunks.length}`);
|
||||
this.clearHash();
|
||||
}
|
||||
}
|
||||
}
|
||||
175
src/features/P2PSync/CmdP2PReplicator.ts
Normal file
175
src/features/P2PSync/CmdP2PReplicator.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import type { IObsidianModule } from "../../modules/AbstractObsidianModule";
|
||||
import { P2PReplicatorPaneView, VIEW_TYPE_P2P } from "./P2PReplicator/P2PReplicatorPaneView.ts";
|
||||
import {
|
||||
AutoAccepting,
|
||||
LOG_LEVEL_NOTICE,
|
||||
REMOTE_P2P,
|
||||
type EntryDoc,
|
||||
type P2PSyncSetting,
|
||||
type RemoteDBSettings,
|
||||
} from "../../lib/src/common/types.ts";
|
||||
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
|
||||
import { LiveSyncTrysteroReplicator } from "../../lib/src/replication/trystero/LiveSyncTrysteroReplicator.ts";
|
||||
import { EVENT_REQUEST_OPEN_P2P, eventHub } from "../../common/events.ts";
|
||||
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator.ts";
|
||||
import { Logger } from "octagonal-wheels/common/logger";
|
||||
import type { CommandShim } from "../../lib/src/replication/trystero/P2PReplicatorPaneCommon.ts";
|
||||
import {
|
||||
P2PReplicatorMixIn,
|
||||
removeP2PReplicatorInstance,
|
||||
type P2PReplicatorBase,
|
||||
} from "../../lib/src/replication/trystero/P2PReplicatorCore.ts";
|
||||
import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
|
||||
import type { Confirm } from "../../lib/src/interfaces/Confirm.ts";
|
||||
import type ObsidianLiveSyncPlugin from "../../main.ts";
|
||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||
|
||||
class P2PReplicatorCommandBase extends LiveSyncCommands implements P2PReplicatorBase {
|
||||
storeP2PStatusLine = reactiveSource("");
|
||||
|
||||
getSettings(): P2PSyncSetting {
|
||||
return this.plugin.settings;
|
||||
}
|
||||
get settings() {
|
||||
return this.plugin.settings;
|
||||
}
|
||||
getDB() {
|
||||
return this.plugin.localDatabase.localDatabase;
|
||||
}
|
||||
|
||||
get confirm(): Confirm {
|
||||
return this.plugin.confirm;
|
||||
}
|
||||
_simpleStore!: SimpleStore<any>;
|
||||
|
||||
simpleStore(): SimpleStore<any> {
|
||||
return this._simpleStore;
|
||||
}
|
||||
|
||||
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
async handleReplicatedDocuments(docs: EntryDoc[]): Promise<void> {
|
||||
// console.log("Processing Replicated Docs", docs);
|
||||
return await this.plugin.$$parseReplicationResult(docs as PouchDB.Core.ExistingDocument<EntryDoc>[]);
|
||||
}
|
||||
onunload(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
onload(): void | Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
init() {
|
||||
this._simpleStore = this.plugin.$$getSimpleStore("p2p-sync");
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class P2PReplicator
|
||||
extends P2PReplicatorMixIn(P2PReplicatorCommandBase)
|
||||
implements IObsidianModule, CommandShim
|
||||
{
|
||||
storeP2PStatusLine = reactiveSource("");
|
||||
$anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
|
||||
const settings = { ...this.settings, ...settingOverride };
|
||||
if (settings.remoteType == REMOTE_P2P) {
|
||||
return Promise.resolve(new LiveSyncTrysteroReplicator(this.plugin));
|
||||
}
|
||||
return undefined!;
|
||||
}
|
||||
|
||||
override onunload(): void {
|
||||
removeP2PReplicatorInstance();
|
||||
void this.close();
|
||||
}
|
||||
|
||||
override onload(): void | Promise<void> {
|
||||
eventHub.onEvent(EVENT_REQUEST_OPEN_P2P, () => {
|
||||
void this.openPane();
|
||||
});
|
||||
this.p2pLogCollector.p2pReplicationLine.onChanged((line) => {
|
||||
this.storeP2PStatusLine.value = line.value;
|
||||
});
|
||||
}
|
||||
async $everyOnInitializeDatabase(): Promise<boolean> {
|
||||
await this.initialiseP2PReplicator();
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
async $allSuspendExtraSync() {
|
||||
this.plugin.settings.P2P_Enabled = false;
|
||||
this.plugin.settings.P2P_AutoAccepting = AutoAccepting.NONE;
|
||||
this.plugin.settings.P2P_AutoBroadcast = false;
|
||||
this.plugin.settings.P2P_AutoStart = false;
|
||||
this.plugin.settings.P2P_AutoSyncPeers = "";
|
||||
this.plugin.settings.P2P_AutoWatchPeers = "";
|
||||
return await Promise.resolve(true);
|
||||
}
|
||||
|
||||
async $everyOnLoadStart() {
|
||||
return await Promise.resolve();
|
||||
}
|
||||
|
||||
async openPane() {
|
||||
await this.plugin.$$showView(VIEW_TYPE_P2P);
|
||||
}
|
||||
|
||||
async $everyOnloadStart(): Promise<boolean> {
|
||||
this.plugin.registerView(VIEW_TYPE_P2P, (leaf) => new P2PReplicatorPaneView(leaf, this.plugin));
|
||||
this.plugin.addCommand({
|
||||
id: "open-p2p-replicator",
|
||||
name: "P2P Sync : Open P2P Replicator",
|
||||
callback: async () => {
|
||||
await this.openPane();
|
||||
},
|
||||
});
|
||||
this.plugin.addCommand({
|
||||
id: "p2p-establish-connection",
|
||||
name: "P2P Sync : Connect to the Signalling Server",
|
||||
checkCallback: (isChecking) => {
|
||||
if (isChecking) {
|
||||
return !(this._replicatorInstance?.server?.isServing ?? false);
|
||||
}
|
||||
void this.open();
|
||||
},
|
||||
});
|
||||
this.plugin.addCommand({
|
||||
id: "p2p-close-connection",
|
||||
name: "P2P Sync : Disconnect from the Signalling Server",
|
||||
checkCallback: (isChecking) => {
|
||||
if (isChecking) {
|
||||
return this._replicatorInstance?.server?.isServing ?? false;
|
||||
}
|
||||
Logger(`Closing P2P Connection`, LOG_LEVEL_NOTICE);
|
||||
void this.close();
|
||||
},
|
||||
});
|
||||
this.plugin.addCommand({
|
||||
id: "replicate-now-by-p2p",
|
||||
name: "Replicate now by P2P",
|
||||
checkCallback: (isChecking) => {
|
||||
if (isChecking) {
|
||||
if (this.settings.remoteType == REMOTE_P2P) return false;
|
||||
if (!this._replicatorInstance?.server?.isServing) return false;
|
||||
return true;
|
||||
}
|
||||
void this._replicatorInstance?.replicateFromCommand(false);
|
||||
},
|
||||
});
|
||||
this.plugin
|
||||
.addRibbonIcon("waypoints", "P2P Replicator", async () => {
|
||||
await this.openPane();
|
||||
})
|
||||
.addClass("livesync-ribbon-replicate-p2p");
|
||||
|
||||
return await Promise.resolve(true);
|
||||
}
|
||||
$everyAfterResumeProcess(): Promise<boolean> {
|
||||
if (this.settings.P2P_Enabled && this.settings.P2P_AutoStart) {
|
||||
setTimeout(() => void this.open(), 100);
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
473
src/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte
Normal file
473
src/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte
Normal file
@@ -0,0 +1,473 @@
|
||||
<script lang="ts">
|
||||
import { onMount, setContext } from "svelte";
|
||||
import { AutoAccepting, DEFAULT_SETTINGS, type P2PSyncSetting } from "../../../lib/src/common/types";
|
||||
import {
|
||||
AcceptedStatus,
|
||||
ConnectionStatus,
|
||||
type CommandShim,
|
||||
type PeerStatus,
|
||||
type PluginShim,
|
||||
} from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon";
|
||||
import PeerStatusRow from "../P2PReplicator/PeerStatusRow.svelte";
|
||||
import { EVENT_LAYOUT_READY, eventHub } from "../../../common/events";
|
||||
import {
|
||||
type PeerInfo,
|
||||
type P2PServerInfo,
|
||||
EVENT_SERVER_STATUS,
|
||||
EVENT_REQUEST_STATUS,
|
||||
EVENT_P2P_REPLICATOR_STATUS,
|
||||
} from "../../../lib/src/replication/trystero/TrysteroReplicatorP2PServer";
|
||||
import { type P2PReplicatorStatus } from "../../../lib/src/replication/trystero/TrysteroReplicator";
|
||||
import { $msg as _msg } from "../../../lib/src/common/i18n";
|
||||
|
||||
interface Props {
|
||||
plugin: PluginShim;
|
||||
cmdSync: CommandShim;
|
||||
}
|
||||
|
||||
let { plugin, cmdSync }: Props = $props();
|
||||
// const cmdSync = plugin.getAddOn<P2PReplicator>("P2PReplicator")!;
|
||||
setContext("getReplicator", () => cmdSync);
|
||||
|
||||
const initialSettings = { ...plugin.settings };
|
||||
|
||||
let settings = $state<P2PSyncSetting>(initialSettings);
|
||||
// const vaultName = plugin.$$getVaultName();
|
||||
// const dbKey = `${vaultName}-p2p-device-name`;
|
||||
|
||||
const initialDeviceName = cmdSync.getConfig("p2p_device_name") ?? plugin.$$getVaultName();
|
||||
let deviceName = $state<string>(initialDeviceName);
|
||||
|
||||
let eP2PEnabled = $state<boolean>(initialSettings.P2P_Enabled);
|
||||
let eRelay = $state<string>(initialSettings.P2P_relays);
|
||||
let eRoomId = $state<string>(initialSettings.P2P_roomID);
|
||||
let ePassword = $state<string>(initialSettings.P2P_passphrase);
|
||||
let eAppId = $state<string>(initialSettings.P2P_AppID);
|
||||
let eDeviceName = $state<string>(initialDeviceName);
|
||||
let eAutoAccept = $state<boolean>(initialSettings.P2P_AutoAccepting == AutoAccepting.ALL);
|
||||
let eAutoStart = $state<boolean>(initialSettings.P2P_AutoStart);
|
||||
let eAutoBroadcast = $state<boolean>(initialSettings.P2P_AutoBroadcast);
|
||||
|
||||
const isP2PEnabledModified = $derived.by(() => eP2PEnabled !== settings.P2P_Enabled);
|
||||
const isRelayModified = $derived.by(() => eRelay !== settings.P2P_relays);
|
||||
const isRoomIdModified = $derived.by(() => eRoomId !== settings.P2P_roomID);
|
||||
const isPasswordModified = $derived.by(() => ePassword !== settings.P2P_passphrase);
|
||||
const isAppIdModified = $derived.by(() => eAppId !== settings.P2P_AppID);
|
||||
const isDeviceNameModified = $derived.by(() => eDeviceName !== deviceName);
|
||||
const isAutoAcceptModified = $derived.by(() => eAutoAccept !== (settings.P2P_AutoAccepting == AutoAccepting.ALL));
|
||||
const isAutoStartModified = $derived.by(() => eAutoStart !== settings.P2P_AutoStart);
|
||||
const isAutoBroadcastModified = $derived.by(() => eAutoBroadcast !== settings.P2P_AutoBroadcast);
|
||||
|
||||
const isAnyModified = $derived.by(
|
||||
() =>
|
||||
isP2PEnabledModified ||
|
||||
isRelayModified ||
|
||||
isRoomIdModified ||
|
||||
isPasswordModified ||
|
||||
isAppIdModified ||
|
||||
isDeviceNameModified ||
|
||||
isAutoAcceptModified ||
|
||||
isAutoStartModified ||
|
||||
isAutoBroadcastModified
|
||||
);
|
||||
|
||||
async function saveAndApply() {
|
||||
const newSettings = {
|
||||
...plugin.settings,
|
||||
P2P_Enabled: eP2PEnabled,
|
||||
P2P_relays: eRelay,
|
||||
P2P_roomID: eRoomId,
|
||||
P2P_passphrase: ePassword,
|
||||
P2P_AppID: eAppId,
|
||||
P2P_AutoAccepting: eAutoAccept ? AutoAccepting.ALL : AutoAccepting.NONE,
|
||||
P2P_AutoStart: eAutoStart,
|
||||
P2P_AutoBroadcast: eAutoBroadcast,
|
||||
};
|
||||
plugin.settings = newSettings;
|
||||
cmdSync.setConfig("p2p_device_name", eDeviceName);
|
||||
deviceName = eDeviceName;
|
||||
await plugin.saveSettings();
|
||||
}
|
||||
async function revert() {
|
||||
eP2PEnabled = settings.P2P_Enabled;
|
||||
eRelay = settings.P2P_relays;
|
||||
eRoomId = settings.P2P_roomID;
|
||||
ePassword = settings.P2P_passphrase;
|
||||
eAppId = settings.P2P_AppID;
|
||||
eAutoAccept = settings.P2P_AutoAccepting == AutoAccepting.ALL;
|
||||
eAutoStart = settings.P2P_AutoStart;
|
||||
eAutoBroadcast = settings.P2P_AutoBroadcast;
|
||||
}
|
||||
|
||||
let serverInfo = $state<P2PServerInfo | undefined>(undefined);
|
||||
let replicatorInfo = $state<P2PReplicatorStatus | undefined>(undefined);
|
||||
const applyLoadSettings = (d: P2PSyncSetting, force: boolean) => {
|
||||
const { P2P_relays, P2P_roomID, P2P_passphrase, P2P_AppID, P2P_AutoAccepting } = d;
|
||||
if (force || !isP2PEnabledModified) eP2PEnabled = d.P2P_Enabled;
|
||||
if (force || !isRelayModified) eRelay = P2P_relays;
|
||||
if (force || !isRoomIdModified) eRoomId = P2P_roomID;
|
||||
if (force || !isPasswordModified) ePassword = P2P_passphrase;
|
||||
if (force || !isAppIdModified) eAppId = P2P_AppID;
|
||||
const newAutoAccept = P2P_AutoAccepting === AutoAccepting.ALL;
|
||||
if (force || !isAutoAcceptModified) eAutoAccept = newAutoAccept;
|
||||
if (force || !isAutoStartModified) eAutoStart = d.P2P_AutoStart;
|
||||
if (force || !isAutoBroadcastModified) eAutoBroadcast = d.P2P_AutoBroadcast;
|
||||
|
||||
settings = d;
|
||||
};
|
||||
onMount(() => {
|
||||
const r = eventHub.onEvent("setting-saved", async (d) => {
|
||||
applyLoadSettings(d, false);
|
||||
closeServer();
|
||||
});
|
||||
const rx = eventHub.onEvent(EVENT_LAYOUT_READY, () => {
|
||||
applyLoadSettings(plugin.settings, true);
|
||||
});
|
||||
const r2 = eventHub.onEvent(EVENT_SERVER_STATUS, (status) => {
|
||||
serverInfo = status;
|
||||
advertisements = status?.knownAdvertisements ?? [];
|
||||
});
|
||||
const r3 = eventHub.onEvent(EVENT_P2P_REPLICATOR_STATUS, (status) => {
|
||||
replicatorInfo = status;
|
||||
});
|
||||
eventHub.emitEvent(EVENT_REQUEST_STATUS);
|
||||
return () => {
|
||||
r();
|
||||
r2();
|
||||
r3();
|
||||
};
|
||||
});
|
||||
let isConnected = $derived.by(() => {
|
||||
return serverInfo?.isConnected ?? false;
|
||||
});
|
||||
let serverPeerId = $derived.by(() => {
|
||||
return serverInfo?.serverPeerId ?? "";
|
||||
});
|
||||
let advertisements = $state<PeerInfo[]>([]);
|
||||
|
||||
let autoSyncPeers = $derived.by(() =>
|
||||
settings.P2P_AutoSyncPeers.split(",")
|
||||
.map((e) => e.trim())
|
||||
.filter((e) => e)
|
||||
);
|
||||
let autoWatchPeers = $derived.by(() =>
|
||||
settings.P2P_AutoWatchPeers.split(",")
|
||||
.map((e) => e.trim())
|
||||
.filter((e) => e)
|
||||
);
|
||||
let syncOnCommand = $derived.by(() =>
|
||||
settings.P2P_SyncOnReplication.split(",")
|
||||
.map((e) => e.trim())
|
||||
.filter((e) => e)
|
||||
);
|
||||
|
||||
const peers = $derived.by(() =>
|
||||
advertisements.map((ad) => {
|
||||
let accepted: AcceptedStatus;
|
||||
const isTemporaryAccepted = ad.isTemporaryAccepted;
|
||||
if (isTemporaryAccepted === undefined) {
|
||||
if (ad.isAccepted === undefined) {
|
||||
accepted = AcceptedStatus.UNKNOWN;
|
||||
} else {
|
||||
accepted = ad.isAccepted ? AcceptedStatus.ACCEPTED : AcceptedStatus.DENIED;
|
||||
}
|
||||
} else if (isTemporaryAccepted === true) {
|
||||
accepted = AcceptedStatus.ACCEPTED_IN_SESSION;
|
||||
} else {
|
||||
accepted = AcceptedStatus.DENIED_IN_SESSION;
|
||||
}
|
||||
const isFetching = replicatorInfo?.replicatingFrom.indexOf(ad.peerId) !== -1;
|
||||
const isSending = replicatorInfo?.replicatingTo.indexOf(ad.peerId) !== -1;
|
||||
const isWatching = replicatorInfo?.watchingPeers.indexOf(ad.peerId) !== -1;
|
||||
const syncOnStart = autoSyncPeers.indexOf(ad.name) !== -1;
|
||||
const watchOnStart = autoWatchPeers.indexOf(ad.name) !== -1;
|
||||
const syncOnReplicationCommand = syncOnCommand.indexOf(ad.name) !== -1;
|
||||
const st: PeerStatus = {
|
||||
name: ad.name,
|
||||
peerId: ad.peerId,
|
||||
accepted: accepted,
|
||||
status: ad.isAccepted ? ConnectionStatus.CONNECTED : ConnectionStatus.DISCONNECTED,
|
||||
isSending: isSending,
|
||||
isFetching: isFetching,
|
||||
isWatching: isWatching,
|
||||
syncOnConnect: syncOnStart,
|
||||
watchOnConnect: watchOnStart,
|
||||
syncOnReplicationCommand: syncOnReplicationCommand,
|
||||
};
|
||||
return st;
|
||||
})
|
||||
);
|
||||
|
||||
function useDefaultRelay() {
|
||||
eRelay = DEFAULT_SETTINGS.P2P_relays;
|
||||
}
|
||||
function _generateRandom() {
|
||||
return (Math.floor(Math.random() * 1000) + 1000).toString().substring(1);
|
||||
}
|
||||
function generateRandom(length: number) {
|
||||
let buf = "";
|
||||
while (buf.length < length) {
|
||||
buf += "-" + _generateRandom();
|
||||
}
|
||||
return buf.substring(1, length);
|
||||
}
|
||||
function chooseRandom() {
|
||||
eRoomId = generateRandom(12) + "-" + Math.random().toString(36).substring(2, 5);
|
||||
}
|
||||
|
||||
async function openServer() {
|
||||
await cmdSync.open();
|
||||
}
|
||||
async function closeServer() {
|
||||
await cmdSync.close();
|
||||
}
|
||||
function startBroadcasting() {
|
||||
void cmdSync.enableBroadcastCastings();
|
||||
}
|
||||
function stopBroadcasting() {
|
||||
void cmdSync.disableBroadcastCastings();
|
||||
}
|
||||
|
||||
const initialDialogStatusKey = `p2p-dialog-status`;
|
||||
const getDialogStatus = () => {
|
||||
try {
|
||||
const initialDialogStatus = JSON.parse(cmdSync.getConfig(initialDialogStatusKey) ?? "{}") as {
|
||||
notice?: boolean;
|
||||
setting?: boolean;
|
||||
};
|
||||
return initialDialogStatus;
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
const initialDialogStatus = getDialogStatus();
|
||||
let isNoticeOpened = $state<boolean>(initialDialogStatus.notice ?? true);
|
||||
let isSettingOpened = $state<boolean>(initialDialogStatus.setting ?? true);
|
||||
$effect(() => {
|
||||
const dialogStatus = {
|
||||
notice: isNoticeOpened,
|
||||
setting: isSettingOpened,
|
||||
};
|
||||
cmdSync.setConfig(initialDialogStatusKey, JSON.stringify(dialogStatus));
|
||||
});
|
||||
</script>
|
||||
|
||||
<article>
|
||||
<h1>Peer to Peer Replicator</h1>
|
||||
<details bind:open={isNoticeOpened}>
|
||||
<summary>{_msg("P2P.Note.Summary")}</summary>
|
||||
<p class="important">{_msg("P2P.Note.important_note")}</p>
|
||||
<p class="important-sub">
|
||||
{_msg("P2P.Note.important_note_sub")}
|
||||
</p>
|
||||
{#each _msg("P2P.Note.description").split("\n\n") as paragraph}
|
||||
<p>{paragraph}</p>
|
||||
{/each}
|
||||
</details>
|
||||
<h2>Connection Settings</h2>
|
||||
<details bind:open={isSettingOpened}>
|
||||
<summary>{eRelay}</summary>
|
||||
<table class="settings">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th> Enable P2P Replicator </th>
|
||||
<td>
|
||||
<label class={{ "is-dirty": isP2PEnabledModified }}>
|
||||
<input type="checkbox" bind:checked={eP2PEnabled} />
|
||||
</label>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<th> Relay settings </th>
|
||||
<td>
|
||||
<label class={{ "is-dirty": isRelayModified }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="wss://exp-relay.vrtmrz.net, wss://xxxxx"
|
||||
bind:value={eRelay}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button onclick={() => useDefaultRelay()}> Use vrtmrz's relay </button>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th> Room ID </th>
|
||||
<td>
|
||||
<label class={{ "is-dirty": isRoomIdModified }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="anything-you-like"
|
||||
bind:value={eRoomId}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button onclick={() => chooseRandom()}> Use Random Number </button>
|
||||
</label>
|
||||
<span>
|
||||
<small>
|
||||
This can isolate your connections between devices. Use the same Room ID for the same
|
||||
devices.</small
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th> Password </th>
|
||||
<td>
|
||||
<label class={{ "is-dirty": isPasswordModified }}>
|
||||
<input type="password" placeholder="password" bind:value={ePassword} />
|
||||
</label>
|
||||
<span>
|
||||
<small> This password is used to encrypt the connection. Use something long enough. </small>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th> This device name </th>
|
||||
<td>
|
||||
<label class={{ "is-dirty": isDeviceNameModified }}>
|
||||
<input type="text" placeholder="iphone-16" bind:value={eDeviceName} autocomplete="off" />
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th> Auto Connect </th>
|
||||
<td>
|
||||
<label class={{ "is-dirty": isAutoStartModified }}>
|
||||
<input type="checkbox" bind:checked={eAutoStart} />
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th> Start change-broadcasting on Connect </th>
|
||||
<td>
|
||||
<label class={{ "is-dirty": isAutoBroadcastModified }}>
|
||||
<input type="checkbox" bind:checked={eAutoBroadcast} />
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- <tr>
|
||||
<th> Auto Accepting </th>
|
||||
<td>
|
||||
<label class={{ "is-dirty": isAutoAcceptModified }}>
|
||||
<input type="checkbox" bind:checked={eAutoAccept} />
|
||||
</label>
|
||||
</td>
|
||||
</tr> -->
|
||||
</tbody>
|
||||
</table>
|
||||
<button disabled={!isAnyModified} class="button mod-cta" onclick={saveAndApply}>Save and Apply</button>
|
||||
<button disabled={!isAnyModified} class="button" onclick={revert}>Revert changes</button>
|
||||
</details>
|
||||
|
||||
<div>
|
||||
<h2>Signaling Server Connection</h2>
|
||||
<div>
|
||||
{#if !isConnected}
|
||||
<p>No Connection</p>
|
||||
{:else}
|
||||
<p>Connected to Signaling Server (as Peer ID: {serverPeerId})</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
{#if !isConnected}
|
||||
<button onclick={openServer}>Connect</button>
|
||||
{:else}
|
||||
<button onclick={closeServer}>Disconnect</button>
|
||||
{#if replicatorInfo?.isBroadcasting !== undefined}
|
||||
{#if replicatorInfo?.isBroadcasting}
|
||||
<button onclick={stopBroadcasting}>Stop Broadcasting</button>
|
||||
{:else}
|
||||
<button onclick={startBroadcasting}>Start Broadcasting</button>
|
||||
{/if}
|
||||
{/if}
|
||||
<details>
|
||||
<summary>Broadcasting?</summary>
|
||||
<p>
|
||||
<small>
|
||||
If you want to use `LiveSync`, you should broadcast changes. All `watching` peers which
|
||||
detects this will start the replication for fetching. <br />
|
||||
However, This should not be enabled if you want to increase your secrecy more.
|
||||
</small>
|
||||
</p>
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Peers</h2>
|
||||
<table class="peers">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Action</th>
|
||||
<th>Command</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each peers as peer}
|
||||
<PeerStatusRow peerStatus={peer}></PeerStatusRow>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
article {
|
||||
max-width: 100%;
|
||||
}
|
||||
article p {
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
h2 {
|
||||
margin-top: var(--size-4-1);
|
||||
margin-bottom: var(--size-4-1);
|
||||
padding-bottom: var(--size-4-1);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
label.is-dirty {
|
||||
background-color: var(--background-modifier-error);
|
||||
}
|
||||
input {
|
||||
background-color: transparent;
|
||||
}
|
||||
th {
|
||||
/* display: flex;
|
||||
justify-content: center;
|
||||
align-items: center; */
|
||||
min-height: var(--input-height);
|
||||
}
|
||||
td {
|
||||
min-height: var(--input-height);
|
||||
}
|
||||
td > label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
min-height: var(--input-height);
|
||||
}
|
||||
td > label > * {
|
||||
margin: auto var(--size-4-1);
|
||||
}
|
||||
table.peers {
|
||||
width: 100%;
|
||||
}
|
||||
.important {
|
||||
color: var(--text-error);
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.important-sub {
|
||||
color: var(--text-warning);
|
||||
}
|
||||
.settings label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
198
src/features/P2PSync/P2PReplicator/P2PReplicatorPaneView.ts
Normal file
198
src/features/P2PSync/P2PReplicator/P2PReplicatorPaneView.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { Menu, WorkspaceLeaf } from "obsidian";
|
||||
import ReplicatorPaneComponent from "./P2PReplicatorPane.svelte";
|
||||
import type ObsidianLiveSyncPlugin from "../../../main.ts";
|
||||
import { mount } from "svelte";
|
||||
import { SvelteItemView } from "../../../common/SvelteItemView.ts";
|
||||
import { eventHub } from "../../../common/events.ts";
|
||||
|
||||
import { unique } from "octagonal-wheels/collection";
|
||||
import { LOG_LEVEL_NOTICE, REMOTE_P2P } from "../../../lib/src/common/types.ts";
|
||||
import { Logger } from "../../../lib/src/common/logger.ts";
|
||||
import { P2PReplicator } from "../CmdP2PReplicator.ts";
|
||||
import {
|
||||
EVENT_P2P_PEER_SHOW_EXTRA_MENU,
|
||||
type PeerStatus,
|
||||
} from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon.ts";
|
||||
export const VIEW_TYPE_P2P = "p2p-replicator";
|
||||
|
||||
function addToList(item: string, list: string) {
|
||||
return unique(
|
||||
list
|
||||
.split(",")
|
||||
.map((e) => e.trim())
|
||||
.concat(item)
|
||||
.filter((p) => p)
|
||||
).join(",");
|
||||
}
|
||||
function removeFromList(item: string, list: string) {
|
||||
return list
|
||||
.split(",")
|
||||
.map((e) => e.trim())
|
||||
.filter((p) => p !== item)
|
||||
.filter((p) => p)
|
||||
.join(",");
|
||||
}
|
||||
|
||||
export class P2PReplicatorPaneView extends SvelteItemView {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
icon = "waypoints";
|
||||
title: string = "";
|
||||
navigation = false;
|
||||
|
||||
getIcon(): string {
|
||||
return "waypoints";
|
||||
}
|
||||
get replicator() {
|
||||
const r = this.plugin.getAddOn<P2PReplicator>(P2PReplicator.name);
|
||||
if (!r || !r._replicatorInstance) {
|
||||
throw new Error("Replicator not found");
|
||||
}
|
||||
return r._replicatorInstance;
|
||||
}
|
||||
async replicateFrom(peer: PeerStatus) {
|
||||
await this.replicator.replicateFrom(peer.peerId);
|
||||
}
|
||||
async replicateTo(peer: PeerStatus) {
|
||||
await this.replicator.requestSynchroniseToPeer(peer.peerId);
|
||||
}
|
||||
async getRemoteConfig(peer: PeerStatus) {
|
||||
Logger(
|
||||
`Requesting remote config for ${peer.name}. Please input the passphrase on the remote device`,
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
const remoteConfig = await this.replicator.getRemoteConfig(peer.peerId);
|
||||
if (remoteConfig) {
|
||||
Logger(`Remote config for ${peer.name} is retrieved successfully`);
|
||||
const DROP = "Yes, and drop local database";
|
||||
const KEEP = "Yes, but keep local database";
|
||||
const CANCEL = "No, cancel";
|
||||
const yn = await this.plugin.confirm.askSelectStringDialogue(
|
||||
`Do you really want to apply the remote config? This will overwrite your current config immediately and restart.
|
||||
And you can also drop the local database to rebuild from the remote device.`,
|
||||
[DROP, KEEP, CANCEL] as const,
|
||||
{
|
||||
defaultAction: CANCEL,
|
||||
title: "Apply Remote Config ",
|
||||
}
|
||||
);
|
||||
if (yn === DROP || yn === KEEP) {
|
||||
if (yn === DROP) {
|
||||
if (remoteConfig.remoteType !== REMOTE_P2P) {
|
||||
const yn2 = await this.plugin.confirm.askYesNoDialog(
|
||||
`Do you want to set the remote type to "P2P Sync" to rebuild by "P2P replication"?`,
|
||||
{
|
||||
title: "Rebuild from remote device",
|
||||
}
|
||||
);
|
||||
if (yn2 === "yes") {
|
||||
remoteConfig.remoteType = REMOTE_P2P;
|
||||
remoteConfig.P2P_RebuildFrom = peer.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.plugin.settings = remoteConfig;
|
||||
await this.plugin.saveSettings();
|
||||
if (yn === DROP) {
|
||||
await this.plugin.rebuilder.scheduleFetch();
|
||||
} else {
|
||||
await this.plugin.$$scheduleAppReload();
|
||||
}
|
||||
} else {
|
||||
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
} else {
|
||||
Logger(`Cannot retrieve remote config for ${peer.peerId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleProp(peer: PeerStatus, prop: "syncOnConnect" | "watchOnConnect" | "syncOnReplicationCommand") {
|
||||
const settingMap = {
|
||||
syncOnConnect: "P2P_AutoSyncPeers",
|
||||
watchOnConnect: "P2P_AutoWatchPeers",
|
||||
syncOnReplicationCommand: "P2P_SyncOnReplication",
|
||||
} as const;
|
||||
|
||||
const targetSetting = settingMap[prop];
|
||||
if (peer[prop]) {
|
||||
this.plugin.settings[targetSetting] = removeFromList(peer.name, this.plugin.settings[targetSetting]);
|
||||
await this.plugin.saveSettings();
|
||||
} else {
|
||||
this.plugin.settings[targetSetting] = addToList(peer.name, this.plugin.settings[targetSetting]);
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
m?: Menu;
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
eventHub.onEvent(EVENT_P2P_PEER_SHOW_EXTRA_MENU, ({ peer, event }) => {
|
||||
if (this.m) {
|
||||
this.m.hide();
|
||||
}
|
||||
this.m = new Menu()
|
||||
.addItem((item) => item.setTitle("📥 Only Fetch").onClick(() => this.replicateFrom(peer)))
|
||||
.addItem((item) => item.setTitle("📤 Only Send").onClick(() => this.replicateTo(peer)))
|
||||
.addSeparator()
|
||||
.addItem((item) => {
|
||||
item.setTitle("🔧 Get Configuration").onClick(async () => {
|
||||
await this.getRemoteConfig(peer);
|
||||
});
|
||||
})
|
||||
.addSeparator()
|
||||
.addItem((item) => {
|
||||
const mark = peer.syncOnConnect ? "checkmark" : null;
|
||||
item.setTitle("Toggle Sync on connect")
|
||||
.onClick(async () => {
|
||||
await this.toggleProp(peer, "syncOnConnect");
|
||||
})
|
||||
.setIcon(mark);
|
||||
})
|
||||
.addItem((item) => {
|
||||
const mark = peer.watchOnConnect ? "checkmark" : null;
|
||||
item.setTitle("Toggle Watch on connect")
|
||||
.onClick(async () => {
|
||||
await this.toggleProp(peer, "watchOnConnect");
|
||||
})
|
||||
.setIcon(mark);
|
||||
})
|
||||
.addItem((item) => {
|
||||
const mark = peer.syncOnReplicationCommand ? "checkmark" : null;
|
||||
item.setTitle("Toggle Sync on `Replicate now` command")
|
||||
.onClick(async () => {
|
||||
await this.toggleProp(peer, "syncOnReplicationCommand");
|
||||
})
|
||||
.setIcon(mark);
|
||||
});
|
||||
this.m.showAtPosition({ x: event.x, y: event.y });
|
||||
});
|
||||
}
|
||||
|
||||
getViewType() {
|
||||
return VIEW_TYPE_P2P;
|
||||
}
|
||||
|
||||
getDisplayText() {
|
||||
return "Peer-to-Peer Replicator";
|
||||
}
|
||||
|
||||
override async onClose(): Promise<void> {
|
||||
await super.onClose();
|
||||
if (this.m) {
|
||||
this.m.hide();
|
||||
}
|
||||
}
|
||||
instantiateComponent(target: HTMLElement) {
|
||||
const cmdSync = this.plugin.getAddOn<P2PReplicator>(P2PReplicator.name);
|
||||
if (!cmdSync) {
|
||||
throw new Error("Replicator not found");
|
||||
}
|
||||
return mount(ReplicatorPaneComponent, {
|
||||
target: target,
|
||||
props: {
|
||||
plugin: cmdSync.plugin,
|
||||
cmdSync: cmdSync,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
259
src/features/P2PSync/P2PReplicator/PeerStatusRow.svelte
Normal file
259
src/features/P2PSync/P2PReplicator/PeerStatusRow.svelte
Normal file
@@ -0,0 +1,259 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import { AcceptedStatus, type PeerStatus } from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon";
|
||||
import type { P2PReplicator } from "../CmdP2PReplicator";
|
||||
import { eventHub } from "../../../common/events";
|
||||
import { EVENT_P2P_PEER_SHOW_EXTRA_MENU } from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon";
|
||||
|
||||
interface Props {
|
||||
peerStatus: PeerStatus;
|
||||
}
|
||||
|
||||
let { peerStatus }: Props = $props();
|
||||
let peer = $derived(peerStatus);
|
||||
|
||||
function select<T extends string | number | symbol, U>(d: T, cond: Record<T, U>): U;
|
||||
function select<T extends string | number | symbol, U, V>(d: T, cond: Record<T, U>, def: V): U | V;
|
||||
function select<T extends string | number | symbol, U>(d: T, cond: Record<T, U>, def?: U): U | undefined {
|
||||
return d in cond ? cond[d] : def;
|
||||
}
|
||||
|
||||
let statusChips = $derived.by(() =>
|
||||
[
|
||||
peer.isWatching ? ["WATCHING"] : [],
|
||||
peer.isFetching ? ["FETCHING"] : [],
|
||||
peer.isSending ? ["SENDING"] : [],
|
||||
].flat()
|
||||
);
|
||||
let acceptedStatusChip = $derived.by(() =>
|
||||
select(
|
||||
peer.accepted.toString(),
|
||||
{
|
||||
[AcceptedStatus.ACCEPTED]: "ACCEPTED",
|
||||
[AcceptedStatus.ACCEPTED_IN_SESSION]: "ACCEPTED (in session)",
|
||||
[AcceptedStatus.DENIED_IN_SESSION]: "DENIED (in session)",
|
||||
[AcceptedStatus.DENIED]: "DENIED",
|
||||
[AcceptedStatus.UNKNOWN]: "NEW",
|
||||
},
|
||||
""
|
||||
)
|
||||
);
|
||||
const classList = {
|
||||
["SENDING"]: "connected",
|
||||
["FETCHING"]: "connected",
|
||||
["WATCHING"]: "connected-live",
|
||||
["WAITING"]: "waiting",
|
||||
["ACCEPTED"]: "accepted",
|
||||
["DENIED"]: "denied",
|
||||
["NEW"]: "unknown",
|
||||
};
|
||||
let isAccepted = $derived.by(
|
||||
() => peer.accepted === AcceptedStatus.ACCEPTED || peer.accepted === AcceptedStatus.ACCEPTED_IN_SESSION
|
||||
);
|
||||
let isDenied = $derived.by(
|
||||
() => peer.accepted === AcceptedStatus.DENIED || peer.accepted === AcceptedStatus.DENIED_IN_SESSION
|
||||
);
|
||||
|
||||
let isNew = $derived.by(() => peer.accepted === AcceptedStatus.UNKNOWN);
|
||||
|
||||
function makeDecision(isAccepted: boolean, isTemporary: boolean) {
|
||||
cmdReplicator._replicatorInstance?.server?.makeDecision({
|
||||
peerId: peer.peerId,
|
||||
name: peer.name,
|
||||
decision: isAccepted,
|
||||
isTemporary: isTemporary,
|
||||
});
|
||||
}
|
||||
function revokeDecision() {
|
||||
cmdReplicator._replicatorInstance?.server?.revokeDecision({
|
||||
peerId: peer.peerId,
|
||||
name: peer.name,
|
||||
});
|
||||
}
|
||||
const cmdReplicator = getContext<() => P2PReplicator>("getReplicator")();
|
||||
const replicator = cmdReplicator._replicatorInstance!;
|
||||
|
||||
const peerAttrLabels = $derived.by(() => {
|
||||
const attrs = [];
|
||||
if (peer.syncOnConnect) {
|
||||
attrs.push("✔ SYNC");
|
||||
}
|
||||
if (peer.watchOnConnect) {
|
||||
attrs.push("✔ WATCH");
|
||||
}
|
||||
if (peer.syncOnReplicationCommand) {
|
||||
attrs.push("✔ SELECT");
|
||||
}
|
||||
return attrs;
|
||||
});
|
||||
function startWatching() {
|
||||
replicator.watchPeer(peer.peerId);
|
||||
}
|
||||
function stopWatching() {
|
||||
replicator.unwatchPeer(peer.peerId);
|
||||
}
|
||||
|
||||
function sync() {
|
||||
replicator.sync(peer.peerId, false);
|
||||
}
|
||||
|
||||
function moreMenu(evt: MouseEvent) {
|
||||
eventHub.emitEvent(EVENT_P2P_PEER_SHOW_EXTRA_MENU, { peer, event: evt });
|
||||
}
|
||||
</script>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div class="info">
|
||||
<div class="row name">
|
||||
<span class="peername">{peer.name}</span>
|
||||
</div>
|
||||
<div class="row peer-id">
|
||||
<span class="peerid">({peer.peerId})</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-chips">
|
||||
<div class="row">
|
||||
<span class="chip {select(acceptedStatusChip, classList)}">{acceptedStatusChip}</span>
|
||||
</div>
|
||||
{#if isAccepted}
|
||||
<div class="row">
|
||||
{#each statusChips as chip}
|
||||
<span class="chip {select(chip, classList)}">{chip}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="row">
|
||||
{#each peerAttrLabels as attr}
|
||||
<span class="chip attr">{attr}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<div class="row">
|
||||
{#if isNew}
|
||||
{#if !isAccepted}
|
||||
<button class="button" onclick={() => makeDecision(true, true)}>Accept in session</button>
|
||||
<button class="button mod-cta" onclick={() => makeDecision(true, false)}>Accept</button>
|
||||
{/if}
|
||||
{#if !isDenied}
|
||||
<button class="button" onclick={() => makeDecision(false, true)}>Deny in session</button>
|
||||
<button class="button mod-warning" onclick={() => makeDecision(false, false)}>Deny</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<button class="button mod-warning" onclick={() => revokeDecision()}>Revoke</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{#if isAccepted}
|
||||
<div class="buttons">
|
||||
<div class="row">
|
||||
<button class="button" onclick={sync} disabled={peer.isSending || peer.isFetching}>🔄</button>
|
||||
<!-- <button class="button" onclick={replicateFrom} disabled={peer.isFetching}>📥</button>
|
||||
<button class="button" onclick={replicateTo} disabled={peer.isSending}>📤</button> -->
|
||||
{#if peer.isWatching}
|
||||
<button class="button" onclick={stopWatching}>Stop ⚡</button>
|
||||
{:else}
|
||||
<button class="button" onclick={startWatching} title="live">⚡</button>
|
||||
{/if}
|
||||
<button class="button" onclick={moreMenu}>...</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<style>
|
||||
tr:nth-child(odd) {
|
||||
background-color: var(--background-primary-alt);
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: var(--size-4-1) var(--size-4-1);
|
||||
}
|
||||
|
||||
.peer-id {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.status-chips {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
/* min-width: 10em; */
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.buttons .row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
/* padding: var(--size-4-1) var(--size-4-1); */
|
||||
}
|
||||
.chip {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
background-color: var(--tag-background);
|
||||
border: var(--tag-border-width) solid var(--tag-border-color);
|
||||
}
|
||||
.chip.connected {
|
||||
background-color: var(--background-modifier-success);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
.chip.connected-live {
|
||||
background-color: var(--background-modifier-success);
|
||||
border-color: var(--background-modifier-success);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
.chip.accepted {
|
||||
background-color: var(--background-modifier-success);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
.chip.waiting {
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
.chip.unknown {
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-warning);
|
||||
}
|
||||
.chip.denied {
|
||||
background-color: var(--background-modifier-error);
|
||||
color: var(--text-error);
|
||||
}
|
||||
.chip.attr {
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
.button {
|
||||
margin: var(--size-4-1);
|
||||
}
|
||||
.button.affirmative {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
.button.affirmative:hover {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
}
|
||||
.button.negative {
|
||||
background-color: var(--background-modifier-error);
|
||||
color: var(--text-error);
|
||||
}
|
||||
.button.negative:hover {
|
||||
background-color: var(--background-modifier-error-hover);
|
||||
}
|
||||
</style>
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 2e156a988b...7c3d7547e2
24
src/main.ts
24
src/main.ts
@@ -51,7 +51,7 @@ import { ModuleRedFlag } from "./modules/coreFeatures/ModuleRedFlag.ts";
|
||||
import { ModuleObsidianMenu } from "./modules/essentialObsidian/ModuleObsidianMenu.ts";
|
||||
import { ModuleSetupObsidian } from "./modules/features/ModuleSetupObsidian.ts";
|
||||
import type { StorageAccess } from "./modules/interfaces/StorageAccess.ts";
|
||||
import type { Confirm } from "./modules/interfaces/Confirm.ts";
|
||||
import type { Confirm } from "./lib/src/interfaces/Confirm.ts";
|
||||
import type { Rebuilder } from "./modules/interfaces/DatabaseRebuilder.ts";
|
||||
import type { DatabaseFileAccess } from "./modules/interfaces/DatabaseFileAccess.ts";
|
||||
import { ModuleDatabaseFileAccess } from "./modules/core/ModuleDatabaseFileAccess.ts";
|
||||
@@ -81,6 +81,8 @@ import { ModuleRebuilder } from "./modules/core/ModuleRebuilder.ts";
|
||||
import { ModuleReplicateTest } from "./modules/extras/ModuleReplicateTest.ts";
|
||||
import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain.ts";
|
||||
import { ModuleExtraSyncObsidian } from "./modules/extraFeaturesObsidian/ModuleExtraSyncObsidian.ts";
|
||||
import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
|
||||
import { P2PReplicator } from "./features/P2PSync/CmdP2PReplicator.ts";
|
||||
|
||||
function throwShouldBeOverridden(): never {
|
||||
throw new Error("This function should be overridden by the module.");
|
||||
@@ -117,7 +119,12 @@ export default class ObsidianLiveSyncPlugin
|
||||
}
|
||||
|
||||
// Keep order to display the dialogue in order.
|
||||
addOns = [new ConfigSync(this), new HiddenFileSync(this)] as LiveSyncCommands[];
|
||||
addOns = [
|
||||
new ConfigSync(this),
|
||||
new HiddenFileSync(this),
|
||||
new LocalDatabaseMaintenance(this),
|
||||
new P2PReplicator(this),
|
||||
] as LiveSyncCommands[];
|
||||
|
||||
modules = [
|
||||
new ModuleLiveSyncMain(this),
|
||||
@@ -398,6 +405,7 @@ export default class ObsidianLiveSyncPlugin
|
||||
6. localDatabase.onunload
|
||||
7. replicator.closeReplication
|
||||
8. localDatabase.close
|
||||
9. (event) EVENT_PLATFORM_UNLOADED
|
||||
|
||||
*/
|
||||
|
||||
@@ -537,8 +545,10 @@ export default class ObsidianLiveSyncPlugin
|
||||
$everyAfterResumeProcess(): Promise<boolean> {
|
||||
return InterceptiveEvery;
|
||||
}
|
||||
|
||||
$$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
|
||||
$$checkAndAskResolvingMismatchedTweaks(preferred: Partial<TweakValues>): Promise<[TweakValues | boolean, boolean]> {
|
||||
throwShouldBeOverridden();
|
||||
}
|
||||
$$askResolvingMismatchedTweaks(preferredSource: TweakValues): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
|
||||
throwShouldBeOverridden();
|
||||
}
|
||||
|
||||
@@ -548,6 +558,12 @@ export default class ObsidianLiveSyncPlugin
|
||||
throwShouldBeOverridden();
|
||||
}
|
||||
|
||||
$$askUseRemoteConfiguration(
|
||||
trialSetting: RemoteDBSettings,
|
||||
preferred: TweakValues
|
||||
): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
|
||||
throwShouldBeOverridden();
|
||||
}
|
||||
$everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
|
||||
return InterceptiveEvery;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { getDocDataAsArray, isDocContentSame, readContent } from "../../lib/src/
|
||||
import { shouldBeIgnored } from "../../lib/src/string_and_binary/path";
|
||||
import type { ICoreModule } from "../ModuleTypes";
|
||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||
import { eventHub } from "../../common/events.ts";
|
||||
|
||||
export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
||||
get db() {
|
||||
@@ -204,13 +205,10 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
||||
): Promise<boolean> {
|
||||
const file = typeof info === "string" ? this.storage.getFileStub(info) : info;
|
||||
const mode = file == null ? "create" : "modify";
|
||||
|
||||
const docEntry =
|
||||
typeof entryInfo === "string"
|
||||
? await this.db.fetchEntryMeta(entryInfo, undefined, true)
|
||||
: await this.db.fetchEntryMeta(entryInfo.path, undefined, true);
|
||||
const pathFromEntryInfo = typeof entryInfo === "string" ? entryInfo : getPath(entryInfo);
|
||||
const docEntry = await this.db.fetchEntryMeta(pathFromEntryInfo, undefined, true);
|
||||
if (!docEntry) {
|
||||
this._log(`File ${entryInfo} is not exist on the database`, LOG_LEVEL_VERBOSE);
|
||||
this._log(`File ${pathFromEntryInfo} is not exist on the database`, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
const path = getPath(docEntry);
|
||||
@@ -274,7 +272,7 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
||||
}
|
||||
// 2. if not, the content should be checked.
|
||||
|
||||
if (shouldApplied) {
|
||||
if (!shouldApplied) {
|
||||
const readFile = await this.readFileFromStub(existDoc);
|
||||
if (await isDocContentSame(docData, readFile.body)) {
|
||||
// The content is same. So, we do not need to update the file.
|
||||
@@ -356,6 +354,8 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
|
||||
`Processing ${path} (${entry._id.substring(0, 8)}: ${entry._rev?.substring(0, 5)}) :Started...`,
|
||||
LOG_LEVEL_VERBOSE
|
||||
);
|
||||
// Before writing (or skipped ), merging dialogue should be cancelled.
|
||||
eventHub.emitEvent("conflict-cancelled", path);
|
||||
const ret = await this.dbToStorage(entry, targetFile);
|
||||
this._log(`Processing ${path} (${entry._id.substring(0, 8)} :${entry._rev?.substring(0, 5)}) : Done`);
|
||||
return ret;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { $f } from "../../lib/src/common/i18n";
|
||||
import { $msg } from "../../lib/src/common/i18n";
|
||||
import { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB.ts";
|
||||
import { initializeStores } from "../../common/stores.ts";
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
@@ -13,7 +13,7 @@ export class ModuleLocalDatabaseObsidian extends AbstractModule implements ICore
|
||||
await this.localDatabase.close();
|
||||
}
|
||||
const vaultName = this.core.$$getVaultName();
|
||||
this._log($f`Waiting for ready...`);
|
||||
this._log($msg("moduleLocalDatabase.logWaitingForReady"));
|
||||
this.core.localDatabase = new LiveSyncLocalDB(vaultName, this.core);
|
||||
initializeStores(vaultName);
|
||||
return await this.localDatabase.initializeDatabase();
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { Rebuilder } from "../interfaces/DatabaseRebuilder.ts";
|
||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
||||
import type { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator.ts";
|
||||
import { fetchAllUsedChunks } from "../../lib/src/pouchdb/utils_couchdb.ts";
|
||||
import { EVENT_DATABASE_REBUILT, eventHub } from "src/common/events.ts";
|
||||
|
||||
export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebuilder {
|
||||
$everyOnload(): Promise<boolean> {
|
||||
@@ -214,6 +215,7 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
|
||||
const suffix = (await this.core.$anyGetAppId()) || "";
|
||||
this.core.settings.additionalSuffixOfDatabaseName = suffix;
|
||||
await this.core.$$resetLocalDatabase();
|
||||
eventHub.emitEvent(EVENT_DATABASE_REBUILT);
|
||||
}
|
||||
async fetchRemoteChunks() {
|
||||
if (
|
||||
|
||||
@@ -13,15 +13,16 @@ import {
|
||||
VER,
|
||||
type EntryBody,
|
||||
type EntryDoc,
|
||||
type EntryLeaf,
|
||||
type LoadedEntry,
|
||||
type MetaEntry,
|
||||
} from "../../lib/src/common/types";
|
||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||
import { getPath, isChunk, isValidPath, scheduleTask } from "../../common/utils";
|
||||
import { sendValue } from "octagonal-wheels/messagepassing/signal";
|
||||
import { isAnyNote } from "../../lib/src/common/utils";
|
||||
import { EVENT_FILE_SAVED, eventHub } from "../../common/events";
|
||||
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
|
||||
import { globalSlipBoard } from "../../lib/src/bureau/bureau";
|
||||
|
||||
export class ModuleReplicator extends AbstractModule implements ICoreModule {
|
||||
$everyOnloadAfterLoadSettings(): Promise<boolean> {
|
||||
@@ -83,8 +84,8 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule {
|
||||
//<-- Here could be an module.
|
||||
const ret = await this.core.replicator.openReplication(this.settings, false, showMessage, false);
|
||||
if (!ret) {
|
||||
if (this.core.replicator.tweakSettingsMismatched) {
|
||||
await this.core.$$askResolvingMismatchedTweaks();
|
||||
if (this.core.replicator.tweakSettingsMismatched && this.core.replicator.preferredTweakValue) {
|
||||
await this.core.$$askResolvingMismatchedTweaks(this.core.replicator.preferredTweakValue);
|
||||
} else {
|
||||
if (this.core.replicator?.remoteLockedAndDeviceNotAccepted) {
|
||||
if (this.core.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
|
||||
@@ -219,6 +220,11 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
const ids = [...new Set((await this.core.kvDB.get<string[]>(kvDBKey)) ?? [])];
|
||||
const batchSize = 100;
|
||||
const chunkedIds = arrayToChunkedArray(ids, batchSize);
|
||||
|
||||
// suspendParseReplicationResult is true, so we have to resume it if it is suspended.
|
||||
if (this.replicationResultProcessor.isSuspended) {
|
||||
this.replicationResultProcessor.resume();
|
||||
}
|
||||
for await (const idsBatch of chunkedIds) {
|
||||
const ret = await this.localDatabase.allDocsRaw<EntryDoc>({
|
||||
keys: idsBatch,
|
||||
@@ -232,8 +238,11 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
Logger(JSON.stringify(errors), LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
this.replicationResultProcessor.enqueueAll(docs);
|
||||
await this.replicationResultProcessor.waitForAllProcessed();
|
||||
}
|
||||
if (this.replicationResultProcessor.isSuspended) {
|
||||
this.replicationResultProcessor.resume();
|
||||
}
|
||||
await this.replicationResultProcessor.waitForAllProcessed();
|
||||
}
|
||||
|
||||
replicationResultProcessor = new QueueProcessor(
|
||||
@@ -242,9 +251,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
const change = docs[0];
|
||||
if (!change) return;
|
||||
if (isChunk(change._id)) {
|
||||
// SendSignal?
|
||||
// this.parseIncomingChunk(change);
|
||||
sendValue(`leaf-${change._id}`, change);
|
||||
globalSlipBoard.submit("read-chunk", change._id, change as EntryLeaf);
|
||||
return;
|
||||
}
|
||||
if (await this.core.$anyModuleParsedReplicationResultItem(change)) return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fireAndForget } from "octagonal-wheels/promises";
|
||||
import { REMOTE_MINIO, type RemoteDBSettings } from "../../lib/src/common/types";
|
||||
import { REMOTE_MINIO, REMOTE_P2P, type RemoteDBSettings } from "../../lib/src/common/types";
|
||||
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
|
||||
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
|
||||
import { AbstractModule } from "../AbstractModule";
|
||||
@@ -9,13 +9,13 @@ export class ModuleReplicatorCouchDB extends AbstractModule implements ICoreModu
|
||||
$anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
|
||||
const settings = { ...this.settings, ...settingOverride };
|
||||
// If new remote types were added, add them here. Do not use `REMOTE_COUCHDB` directly for the safety valve.
|
||||
if (settings.remoteType == REMOTE_MINIO) {
|
||||
if (settings.remoteType == REMOTE_MINIO || settings.remoteType == REMOTE_P2P) {
|
||||
return undefined!;
|
||||
}
|
||||
return Promise.resolve(new LiveSyncCouchDBReplicator(this.core));
|
||||
}
|
||||
$everyAfterResumeProcess(): Promise<boolean> {
|
||||
if (this.settings.remoteType != REMOTE_MINIO) {
|
||||
if (this.settings.remoteType != REMOTE_MINIO && this.settings.remoteType != REMOTE_P2P) {
|
||||
// If LiveSync enabled, open replication
|
||||
if (this.settings.liveSync) {
|
||||
fireAndForget(() => this.core.replicator.openReplication(this.settings, true, false, false));
|
||||
|
||||
30
src/modules/core/ModuleReplicatorP2P.ts
Normal file
30
src/modules/core/ModuleReplicatorP2P.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { REMOTE_P2P, type RemoteDBSettings } from "../../lib/src/common/types";
|
||||
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
|
||||
import { AbstractModule } from "../AbstractModule";
|
||||
import type { ICoreModule } from "../ModuleTypes";
|
||||
import { LiveSyncTrysteroReplicator } from "../../lib/src/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
|
||||
export class ModuleReplicatorP2P extends AbstractModule implements ICoreModule {
|
||||
$anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
|
||||
const settings = { ...this.settings, ...settingOverride };
|
||||
if (settings.remoteType == REMOTE_P2P) {
|
||||
return Promise.resolve(new LiveSyncTrysteroReplicator(this.core));
|
||||
}
|
||||
return undefined!;
|
||||
}
|
||||
$everyAfterResumeProcess(): Promise<boolean> {
|
||||
if (this.settings.remoteType == REMOTE_P2P) {
|
||||
// // If LiveSync enabled, open replication
|
||||
// if (this.settings.liveSync) {
|
||||
// fireAndForget(() => this.core.replicator.openReplication(this.settings, true, false, false));
|
||||
// }
|
||||
// // If sync on start enabled, open replication
|
||||
// if (!this.settings.liveSync && this.settings.syncOnStart) {
|
||||
// // Possibly ok as if only share the result
|
||||
// fireAndForget(() => this.core.replicator.openReplication(this.settings, false, false, false));
|
||||
// }
|
||||
}
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
@@ -2,34 +2,24 @@ import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-w
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
import { sizeToHumanReadable } from "octagonal-wheels/number";
|
||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||
|
||||
export class ModuleCheckRemoteSize extends AbstractModule implements ICoreModule {
|
||||
async $allScanStat(): Promise<boolean> {
|
||||
this._log(`Checking storage sizes`, LOG_LEVEL_VERBOSE);
|
||||
this._log($msg("moduleCheckRemoteSize.logCheckingStorageSizes"), LOG_LEVEL_VERBOSE);
|
||||
if (this.settings.notifyThresholdOfRemoteStorageSize < 0) {
|
||||
const message = `We can set a maximum database capacity warning, **to take action before running out of space on the remote storage**.
|
||||
Do you want to enable this?
|
||||
|
||||
> [!MORE]-
|
||||
> - 0: Do not warn about storage size.
|
||||
> This is recommended if you have enough space on the remote storage especially you have self-hosted. And you can check the storage size and rebuild manually.
|
||||
> - 800: Warn if the remote storage size exceeds 800MB.
|
||||
> This is recommended if you are using fly.io with 1GB limit or IBM Cloudant.
|
||||
> - 2000: Warn if the remote storage size exceeds 2GB.
|
||||
|
||||
If we have reached the limit, we will be asked to enlarge the limit step by step.
|
||||
`;
|
||||
const ANSWER_0 = "No, never warn please";
|
||||
const ANSWER_800 = "800MB (Cloudant, fly.io)";
|
||||
const ANSWER_2000 = "2GB (Standard)";
|
||||
const ASK_ME_NEXT_TIME = "Ask me later";
|
||||
const message = $msg("moduleCheckRemoteSize.msgSetDBCapacity");
|
||||
const ANSWER_0 = $msg("moduleCheckRemoteSize.optionNoWarn");
|
||||
const ANSWER_800 = $msg("moduleCheckRemoteSize.option800MB");
|
||||
const ANSWER_2000 = $msg("moduleCheckRemoteSize.option2GB");
|
||||
const ASK_ME_NEXT_TIME = $msg("moduleCheckRemoteSize.optionAskMeLater");
|
||||
|
||||
const ret = await this.core.confirm.askSelectStringDialogue(
|
||||
message,
|
||||
[ANSWER_0, ANSWER_800, ANSWER_2000, ASK_ME_NEXT_TIME],
|
||||
{
|
||||
defaultAction: ASK_ME_NEXT_TIME,
|
||||
title: "Setting up database size notification",
|
||||
title: $msg("moduleCheckRemoteSize.titleDatabaseSizeNotify"),
|
||||
timeout: 40,
|
||||
}
|
||||
);
|
||||
@@ -51,40 +41,28 @@ If we have reached the limit, we will be asked to enlarge the limit step by step
|
||||
if (estimatedSize) {
|
||||
const maxSize = this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024;
|
||||
if (estimatedSize > maxSize) {
|
||||
const message = `**Your database is getting larger!** But do not worry, we can address it now. The time before running out of space on the remote storage.
|
||||
|
||||
| Measured size | Configured size |
|
||||
| --- | --- |
|
||||
| ${sizeToHumanReadable(estimatedSize)} | ${sizeToHumanReadable(maxSize)} |
|
||||
|
||||
> [!MORE]-
|
||||
> If you have been using it for many years, there may be unreferenced chunks - that is, garbage - accumulating in the database. Therefore, we recommend rebuilding everything. It will probably become much smaller.
|
||||
>
|
||||
> If the volume of your vault is simply increasing, it is better to rebuild everything after organizing the files. Self-hosted LiveSync does not delete the actual data even if you delete it to speed up the process. It is roughly [documented](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md).
|
||||
>
|
||||
> If you don't mind the increase, you can increase the notification limit by 100MB. This is the case if you are running it on your own server. However, it is better to rebuild everything from time to time.
|
||||
>
|
||||
|
||||
> [!WARNING]
|
||||
> If you perform rebuild everything, make sure all devices are synchronised. The plug-in will merge as much as possible, though.
|
||||
|
||||
\n`;
|
||||
const message = $msg("moduleCheckRemoteSize.msgDatabaseGrowing", {
|
||||
estimatedSize: sizeToHumanReadable(estimatedSize),
|
||||
maxSize: sizeToHumanReadable(maxSize),
|
||||
});
|
||||
const newMax = ~~(estimatedSize / 1024 / 1024) + 100;
|
||||
const ANSWER_ENLARGE_LIMIT = `increase to ${newMax}MB`;
|
||||
const ANSWER_REBUILD = "Rebuild Everything Now";
|
||||
const ANSWER_IGNORE = "Dismiss";
|
||||
const ANSWER_ENLARGE_LIMIT = $msg("moduleCheckRemoteSize.optionIncreaseLimit", {
|
||||
newMax: newMax.toString(),
|
||||
});
|
||||
const ANSWER_REBUILD = $msg("moduleCheckRemoteSize.optionRebuildAll");
|
||||
const ANSWER_IGNORE = $msg("moduleCheckRemoteSize.optionDismiss");
|
||||
const ret = await this.core.confirm.askSelectStringDialogue(
|
||||
message,
|
||||
[ANSWER_ENLARGE_LIMIT, ANSWER_REBUILD, ANSWER_IGNORE],
|
||||
{
|
||||
defaultAction: ANSWER_IGNORE,
|
||||
title: "Remote storage size exceeded the limit",
|
||||
title: $msg("moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded"),
|
||||
timeout: 60,
|
||||
}
|
||||
);
|
||||
if (ret == ANSWER_REBUILD) {
|
||||
const ret = await this.core.confirm.askYesNoDialog(
|
||||
"This may take a bit of a long time. Do you really want to rebuild everything now?",
|
||||
$msg("moduleCheckRemoteSize.msgConfirmRebuild"),
|
||||
{ defaultOption: "No" }
|
||||
);
|
||||
if (ret == "yes") {
|
||||
@@ -95,7 +73,9 @@ If we have reached the limit, we will be asked to enlarge the limit step by step
|
||||
} else if (ret == ANSWER_ENLARGE_LIMIT) {
|
||||
this.settings.notifyThresholdOfRemoteStorageSize = ~~(estimatedSize / 1024 / 1024) + 100;
|
||||
this._log(
|
||||
`Threshold has been enlarged to ${this.settings.notifyThresholdOfRemoteStorageSize}MB`,
|
||||
$msg("moduleCheckRemoteSize.logThresholdEnlarged", {
|
||||
size: this.settings.notifyThresholdOfRemoteStorageSize.toString(),
|
||||
}),
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
await this.core.saveSettings();
|
||||
@@ -104,11 +84,21 @@ If we have reached the limit, we will be asked to enlarge the limit step by step
|
||||
}
|
||||
|
||||
this._log(
|
||||
`Remote storage size: ${sizeToHumanReadable(estimatedSize)} exceeded ${sizeToHumanReadable(this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024)} `,
|
||||
$msg("moduleCheckRemoteSize.logExceededWarning", {
|
||||
measuredSize: sizeToHumanReadable(estimatedSize),
|
||||
notifySize: sizeToHumanReadable(
|
||||
this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024
|
||||
),
|
||||
}),
|
||||
LOG_LEVEL_INFO
|
||||
);
|
||||
} else {
|
||||
this._log(`Remote storage size: ${sizeToHumanReadable(estimatedSize)}`, LOG_LEVEL_INFO);
|
||||
this._log(
|
||||
$msg("moduleCheckRemoteSize.logCurrentStorageSize", {
|
||||
measuredSize: sizeToHumanReadable(estimatedSize),
|
||||
}),
|
||||
LOG_LEVEL_INFO
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,23 @@ import {
|
||||
type diff_check_result,
|
||||
type FilePathWithPrefix,
|
||||
} from "../../lib/src/common/types";
|
||||
import { compareMTime, displayRev, TARGET_IS_NEW } from "../../common/utils";
|
||||
import {
|
||||
compareMTime,
|
||||
displayRev,
|
||||
isCustomisationSyncMetadata,
|
||||
isPluginMetadata,
|
||||
TARGET_IS_NEW,
|
||||
} from "../../common/utils";
|
||||
import diff_match_patch from "diff-match-patch";
|
||||
import { stripAllPrefixes, isPlainText } from "../../lib/src/string_and_binary/path";
|
||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
||||
import { eventHub } from "../../common/events.ts";
|
||||
|
||||
declare global {
|
||||
interface LSEvents {
|
||||
"conflict-cancelled": FilePathWithPrefix;
|
||||
}
|
||||
}
|
||||
|
||||
export class ModuleConflictResolver extends AbstractModule implements ICoreModule {
|
||||
async $$resolveConflictByDeletingRev(
|
||||
@@ -30,11 +43,16 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
|
||||
);
|
||||
return MISSING_OR_ERROR;
|
||||
}
|
||||
eventHub.emitEvent("conflict-cancelled", path);
|
||||
this._log(`${title} Conflicted revision deleted ${displayRev(deleteRevision)} ${path}`, LOG_LEVEL_INFO);
|
||||
if ((await this.core.databaseFileAccess.getConflictedRevs(path)).length != 0) {
|
||||
this._log(`${title} some conflicts are left in ${path}`, LOG_LEVEL_INFO);
|
||||
return AUTO_MERGED;
|
||||
}
|
||||
this._log(`${title} ${path} is a plugin metadata file, no need to write to storage`, LOG_LEVEL_INFO);
|
||||
if (isPluginMetadata(path) || isCustomisationSyncMetadata(path)) {
|
||||
return AUTO_MERGED;
|
||||
}
|
||||
// If no conflicts were found, write the resolved content to the storage.
|
||||
if (!(await this.core.fileHandler.dbToStorage(path, stripAllPrefixes(path), true))) {
|
||||
this._log(`Could not write the resolved content to the storage: ${path}`, LOG_LEVEL_NOTICE);
|
||||
@@ -139,26 +157,42 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
|
||||
}
|
||||
}
|
||||
this._log("[conflict] Manual merge required!");
|
||||
eventHub.emitEvent("conflict-cancelled", filename);
|
||||
await this.core.$anyResolveConflictByUI(filename, conflictCheckResult);
|
||||
});
|
||||
}
|
||||
|
||||
async $anyResolveConflictByNewest(filename: FilePathWithPrefix): Promise<boolean> {
|
||||
const currentRev = await this.core.databaseFileAccess.fetchEntryMeta(filename, undefined, true);
|
||||
if (currentRev == false) {
|
||||
this._log(`Could not get current revision of ${filename}`);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const revs = await this.core.databaseFileAccess.getConflictedRevs(filename);
|
||||
if (revs.length == 0) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
const mTimeAndRev = (
|
||||
await Promise.all(
|
||||
revs.map(async (rev) => {
|
||||
const leaf = await this.core.databaseFileAccess.fetchEntryMeta(filename, rev);
|
||||
if (leaf == false) {
|
||||
return [0, rev] as [number, string];
|
||||
}
|
||||
return [leaf.mtime, rev] as [number, string];
|
||||
})
|
||||
)
|
||||
).sort((a, b) => b[0] - a[0]);
|
||||
[
|
||||
[currentRev.mtime, currentRev._rev],
|
||||
...(await Promise.all(
|
||||
revs.map(async (rev) => {
|
||||
const leaf = await this.core.databaseFileAccess.fetchEntryMeta(filename, rev);
|
||||
if (leaf == false) {
|
||||
return [0, rev] as [number, string];
|
||||
}
|
||||
return [leaf.mtime, rev] as [number, string];
|
||||
})
|
||||
)),
|
||||
] as [number, string][]
|
||||
).sort((a, b) => {
|
||||
const diff = b[0] - a[0];
|
||||
if (diff == 0) {
|
||||
return a[1].localeCompare(b[1], "en", { numeric: true });
|
||||
}
|
||||
return diff;
|
||||
});
|
||||
// console.warn(mTimeAndRev);
|
||||
this._log(
|
||||
`Resolving conflict by newest: ${filename} (Newest: ${new Date(mTimeAndRev[0][0]).toLocaleString()}) (${mTimeAndRev.length} revisions exists)`
|
||||
);
|
||||
|
||||
@@ -13,18 +13,18 @@ import type { ICoreModule } from "../ModuleTypes.ts";
|
||||
|
||||
export class ModuleResolvingMismatchedTweaks extends AbstractModule implements ICoreModule {
|
||||
async $anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> {
|
||||
if (!this.core.replicator.tweakSettingsMismatched) return false;
|
||||
const ret = await this.core.$$askResolvingMismatchedTweaks();
|
||||
if (!this.core.replicator.tweakSettingsMismatched && !this.core.replicator.preferredTweakValue) return false;
|
||||
const preferred = this.core.replicator.preferredTweakValue;
|
||||
if (!preferred) return false;
|
||||
const ret = await this.core.$$askResolvingMismatchedTweaks(preferred);
|
||||
if (ret == "OK") return false;
|
||||
if (ret == "CHECKAGAIN") return "CHECKAGAIN";
|
||||
if (ret == "IGNORE") return true;
|
||||
}
|
||||
|
||||
async $$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
|
||||
if (!this.core.replicator.tweakSettingsMismatched) {
|
||||
return "OK";
|
||||
}
|
||||
const preferred = extractObject(TweakValuesShouldMatchedTemplate, this.core.replicator.preferredTweakValue!);
|
||||
async $$checkAndAskResolvingMismatchedTweaks(
|
||||
preferred: Partial<TweakValues>
|
||||
): Promise<[TweakValues | boolean, boolean]> {
|
||||
const mine = extractObject(TweakValuesShouldMatchedTemplate, this.settings);
|
||||
const items = Object.entries(TweakValuesShouldMatchedTemplate);
|
||||
let rebuildRequired = false;
|
||||
@@ -85,8 +85,22 @@ Please select which one you want to use.
|
||||
CHOICE_DISMISS,
|
||||
60
|
||||
);
|
||||
if (!retKey) return "IGNORE";
|
||||
const conf = CHOICES[retKey];
|
||||
if (!retKey) return [false, false];
|
||||
return [CHOICES[retKey], rebuildRequired];
|
||||
}
|
||||
|
||||
async $$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
|
||||
if (!this.core.replicator.tweakSettingsMismatched) {
|
||||
return "OK";
|
||||
}
|
||||
const tweaks = this.core.replicator.preferredTweakValue;
|
||||
if (!tweaks) {
|
||||
return "IGNORE";
|
||||
}
|
||||
const preferred = extractObject(TweakValuesShouldMatchedTemplate, tweaks);
|
||||
|
||||
const [conf, rebuildRequired] = await this.core.$$checkAndAskResolvingMismatchedTweaks(preferred);
|
||||
if (!conf) return "IGNORE";
|
||||
|
||||
if (conf === true) {
|
||||
await this.core.replicator.setPreferredRemoteTweakSettings(this.settings);
|
||||
@@ -119,44 +133,55 @@ Please select which one you want to use.
|
||||
if (await replicator.tryConnectRemote(trialSetting)) {
|
||||
const preferred = await replicator.getRemotePreferredTweakValues(trialSetting);
|
||||
if (preferred) {
|
||||
const items = Object.entries(TweakValuesShouldMatchedTemplate);
|
||||
let rebuildRequired = false;
|
||||
// Making tables:
|
||||
let table = `| Value name | This device | Stored | \n` + `|: --- |: ---- :|: ---- :| \n`;
|
||||
let differenceCount = 0;
|
||||
// const items = [mine,preferred]
|
||||
for (const v of items) {
|
||||
const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate;
|
||||
const valuePreferred = escapeMarkdownValue(preferred[key]);
|
||||
const currentDisp = `${escapeMarkdownValue((trialSetting as TweakValues)?.[key])} |`;
|
||||
if ((trialSetting as TweakValues)?.[key] !== preferred[key]) {
|
||||
if (CompatibilityBreakingTweakValues.indexOf(key) !== -1) {
|
||||
rebuildRequired = true;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
table += `| ${confName(key)} | ${currentDisp} ${valuePreferred} | \n`;
|
||||
differenceCount++;
|
||||
return await this.$$askUseRemoteConfiguration(trialSetting, preferred);
|
||||
} else {
|
||||
this._log("Failed to get the preferred tweak values from the remote server.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
return { result: false, requireFetch: false };
|
||||
} else {
|
||||
this._log("Failed to connect to the remote server.", LOG_LEVEL_NOTICE);
|
||||
return { result: false, requireFetch: false };
|
||||
}
|
||||
}
|
||||
async $$askUseRemoteConfiguration(
|
||||
trialSetting: RemoteDBSettings,
|
||||
preferred: TweakValues
|
||||
): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
|
||||
const items = Object.entries(TweakValuesShouldMatchedTemplate);
|
||||
let rebuildRequired = false;
|
||||
// Making tables:
|
||||
let table = `| Value name | This device | Stored | \n` + `|: --- |: ---- :|: ---- :| \n`;
|
||||
let differenceCount = 0;
|
||||
// const items = [mine,preferred]
|
||||
for (const v of items) {
|
||||
const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate;
|
||||
const valuePreferred = escapeMarkdownValue(preferred[key]);
|
||||
const currentDisp = `${escapeMarkdownValue((trialSetting as TweakValues)?.[key])} |`;
|
||||
if ((trialSetting as TweakValues)?.[key] !== preferred[key]) {
|
||||
if (CompatibilityBreakingTweakValues.indexOf(key) !== -1) {
|
||||
rebuildRequired = true;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
table += `| ${confName(key)} | ${currentDisp} ${valuePreferred} | \n`;
|
||||
differenceCount++;
|
||||
}
|
||||
|
||||
if (differenceCount === 0) {
|
||||
this._log(
|
||||
"The settings in the remote database are the same as the local database.",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
return { result: false, requireFetch: false };
|
||||
}
|
||||
const additionalMessage =
|
||||
rebuildRequired && this.core.settings.isConfigured
|
||||
? `
|
||||
if (differenceCount === 0) {
|
||||
this._log("The settings in the remote database are the same as the local database.", LOG_LEVEL_NOTICE);
|
||||
return { result: false, requireFetch: false };
|
||||
}
|
||||
const additionalMessage =
|
||||
rebuildRequired && this.core.settings.isConfigured
|
||||
? `
|
||||
|
||||
>[!WARNING]
|
||||
> Some remote configurations are not compatible with the local database of this device. Rebuilding the local database will be required.
|
||||
***Please ensure that you have time and are connected to a stable network to apply!***`
|
||||
: "";
|
||||
: "";
|
||||
|
||||
const message = `
|
||||
const message = `
|
||||
The settings in the remote database are as follows.
|
||||
If you want to use these settings, please select "Use configured".
|
||||
If you want to keep the settings of this device, please select "Dismiss".
|
||||
@@ -168,29 +193,22 @@ ${table}
|
||||
|
||||
${additionalMessage}`;
|
||||
|
||||
const CHOICE_USE_REMOTE = "Use configured";
|
||||
const CHOICE_DISMISS = "Dismiss";
|
||||
// const CHOICE_AND_VALUES = [
|
||||
// [CHOICE_USE_REMOTE, preferred],
|
||||
// [CHOICE_DISMISS, false]]
|
||||
const CHOICES = [CHOICE_USE_REMOTE, CHOICE_DISMISS];
|
||||
const retKey = await this.core.confirm.askSelectStringDialogue(message, CHOICES, {
|
||||
title: "Use Remote Configuration",
|
||||
timeout: 0,
|
||||
defaultAction: CHOICE_DISMISS,
|
||||
});
|
||||
if (!retKey) return { result: false, requireFetch: false };
|
||||
if (retKey === CHOICE_DISMISS) return { result: false, requireFetch: false };
|
||||
if (retKey === CHOICE_USE_REMOTE) {
|
||||
return { result: { ...trialSetting, ...preferred }, requireFetch: rebuildRequired };
|
||||
}
|
||||
} else {
|
||||
this._log("Failed to get the preferred tweak values from the remote server.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
return { result: false, requireFetch: false };
|
||||
} else {
|
||||
this._log("Failed to connect to the remote server.", LOG_LEVEL_NOTICE);
|
||||
return { result: false, requireFetch: false };
|
||||
const CHOICE_USE_REMOTE = "Use configured";
|
||||
const CHOICE_DISMISS = "Dismiss";
|
||||
// const CHOICE_AND_VALUES = [
|
||||
// [CHOICE_USE_REMOTE, preferred],
|
||||
// [CHOICE_DISMISS, false]]
|
||||
const CHOICES = [CHOICE_USE_REMOTE, CHOICE_DISMISS];
|
||||
const retKey = await this.core.confirm.askSelectStringDialogue(message, CHOICES, {
|
||||
title: "Use Remote Configuration",
|
||||
timeout: 0,
|
||||
defaultAction: CHOICE_DISMISS,
|
||||
});
|
||||
if (!retKey) return { result: false, requireFetch: false };
|
||||
if (retKey === CHOICE_DISMISS) return { result: false, requireFetch: false };
|
||||
if (retKey === CHOICE_USE_REMOTE) {
|
||||
return { result: { ...trialSetting, ...preferred }, requireFetch: rebuildRequired };
|
||||
}
|
||||
return { result: false, requireFetch: false };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ModuleInputUIObsidian.ts
|
||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
||||
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
||||
import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject } from "../../common/utils.ts";
|
||||
@@ -9,7 +10,9 @@ import {
|
||||
confirmWithMessageWithWideButton,
|
||||
} from "./UILib/dialogs.ts";
|
||||
import { Notice } from "../../deps.ts";
|
||||
import type { Confirm } from "../interfaces/Confirm.ts";
|
||||
import type { Confirm } from "../../lib/src/interfaces/Confirm.ts";
|
||||
import { setConfirmInstance } from "../../lib/src/PlatformAPIs/obsidian/Confirm.ts";
|
||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||
|
||||
// This module cannot be a common module because it depends on Obsidian's API.
|
||||
// However, we have to make compatible one for other platform.
|
||||
@@ -17,6 +20,7 @@ import type { Confirm } from "../interfaces/Confirm.ts";
|
||||
export class ModuleInputUIObsidian extends AbstractObsidianModule implements IObsidianModule, Confirm {
|
||||
$everyOnload(): Promise<boolean> {
|
||||
this.core.confirm = this;
|
||||
setConfirmInstance(this);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
@@ -31,29 +35,34 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb
|
||||
message: string,
|
||||
opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = { title: "Confirmation" }
|
||||
): Promise<"yes" | "no"> {
|
||||
const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleConfirmation");
|
||||
const yesLabel = $msg("moduleInputUIObsidian.optionYes");
|
||||
const noLabel = $msg("moduleInputUIObsidian.optionNo");
|
||||
const defaultOption = opt.defaultOption === "Yes" ? yesLabel : noLabel;
|
||||
const ret = await confirmWithMessageWithWideButton(
|
||||
this.plugin,
|
||||
opt.title || "Confirmation",
|
||||
opt.title || defaultTitle,
|
||||
message,
|
||||
["Yes", "No"],
|
||||
opt.defaultOption ?? "No",
|
||||
[yesLabel, noLabel],
|
||||
defaultOption,
|
||||
opt.timeout
|
||||
);
|
||||
return ret == "Yes" ? "yes" : "no";
|
||||
return ret === yesLabel ? "yes" : "no";
|
||||
}
|
||||
|
||||
askSelectString(message: string, items: string[]): Promise<string> {
|
||||
return askSelectString(this.app, message, items);
|
||||
}
|
||||
|
||||
askSelectStringDialogue(
|
||||
askSelectStringDialogue<T extends readonly string[]>(
|
||||
message: string,
|
||||
buttons: string[],
|
||||
opt: { title?: string; defaultAction: (typeof buttons)[number]; timeout?: number }
|
||||
): Promise<(typeof buttons)[number] | false> {
|
||||
buttons: T,
|
||||
opt: { title?: string; defaultAction: T[number]; timeout?: number }
|
||||
): Promise<T[number] | false> {
|
||||
const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleSelect");
|
||||
return confirmWithMessageWithWideButton(
|
||||
this.plugin,
|
||||
opt.title || "Select",
|
||||
opt.title || defaultTitle,
|
||||
message,
|
||||
buttons,
|
||||
opt.defaultAction,
|
||||
@@ -91,6 +100,7 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
confirmWithMessage(
|
||||
title: string,
|
||||
contentMd: string,
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import { ButtonComponent } from "obsidian";
|
||||
import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting } from "../../../deps.ts";
|
||||
import { EVENT_PLUGIN_UNLOADED, eventHub } from "../../../common/events.ts";
|
||||
import { delay } from "octagonal-wheels/promises";
|
||||
|
||||
class AutoClosableModal extends Modal {
|
||||
removeEvent: (() => void) | undefined;
|
||||
_closeByUnload() {
|
||||
eventHub.off(EVENT_PLUGIN_UNLOADED, this._closeByUnload);
|
||||
this.close();
|
||||
}
|
||||
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
this.removeEvent = eventHub.onEvent(EVENT_PLUGIN_UNLOADED, async () => {
|
||||
await delay(100);
|
||||
if (!this.removeEvent) return;
|
||||
this.close();
|
||||
this.removeEvent = undefined;
|
||||
});
|
||||
this._closeByUnload = this._closeByUnload.bind(this);
|
||||
eventHub.once(EVENT_PLUGIN_UNLOADED, this._closeByUnload);
|
||||
}
|
||||
onClose() {
|
||||
if (this.removeEvent) {
|
||||
this.removeEvent();
|
||||
this.removeEvent = undefined;
|
||||
}
|
||||
eventHub.off(EVENT_PLUGIN_UNLOADED, this._closeByUnload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,11 +130,11 @@ export class PopoverSelectString extends FuzzySuggestModal<string> {
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageBox extends AutoClosableModal {
|
||||
export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
plugin: Plugin;
|
||||
title: string;
|
||||
contentMd: string;
|
||||
buttons: string[];
|
||||
buttons: T;
|
||||
result: string | false = false;
|
||||
isManuallyClosed = false;
|
||||
defaultAction: string | undefined;
|
||||
@@ -154,11 +149,11 @@ export class MessageBox extends AutoClosableModal {
|
||||
plugin: Plugin,
|
||||
title: string,
|
||||
contentMd: string,
|
||||
buttons: string[],
|
||||
defaultAction: (typeof buttons)[number],
|
||||
buttons: T,
|
||||
defaultAction: T[number],
|
||||
timeout: number | undefined,
|
||||
wideButton: boolean,
|
||||
onSubmit: (result: (typeof buttons)[number] | false) => void
|
||||
onSubmit: (result: T[number] | false) => void
|
||||
) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
@@ -194,6 +189,7 @@ export class MessageBox extends AutoClosableModal {
|
||||
this.titleEl.setText(this.title);
|
||||
const div = contentEl.createDiv();
|
||||
div.style.userSelect = "text";
|
||||
div.style["webkitUserSelect"] = "text";
|
||||
void MarkdownRenderer.render(this.plugin.app, this.contentMd, div, "/", this.plugin);
|
||||
const buttonSetting = new Setting(contentEl);
|
||||
const labelWrapper = contentEl.createDiv();
|
||||
@@ -262,14 +258,14 @@ export class MessageBox extends AutoClosableModal {
|
||||
}
|
||||
}
|
||||
|
||||
export function confirmWithMessage(
|
||||
export function confirmWithMessage<T extends readonly string[]>(
|
||||
plugin: Plugin,
|
||||
title: string,
|
||||
contentMd: string,
|
||||
buttons: string[],
|
||||
defaultAction: (typeof buttons)[number],
|
||||
buttons: T,
|
||||
defaultAction: T[number],
|
||||
timeout?: number
|
||||
): Promise<(typeof buttons)[number] | false> {
|
||||
): Promise<T[number] | false> {
|
||||
return new Promise((res) => {
|
||||
const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, false, (result) =>
|
||||
res(result)
|
||||
@@ -277,14 +273,14 @@ export function confirmWithMessage(
|
||||
dialog.open();
|
||||
});
|
||||
}
|
||||
export function confirmWithMessageWithWideButton(
|
||||
export function confirmWithMessageWithWideButton<T extends readonly string[]>(
|
||||
plugin: Plugin,
|
||||
title: string,
|
||||
contentMd: string,
|
||||
buttons: string[],
|
||||
defaultAction: (typeof buttons)[number],
|
||||
buttons: T,
|
||||
defaultAction: T[number],
|
||||
timeout?: number
|
||||
): Promise<(typeof buttons)[number] | false> {
|
||||
): Promise<T[number] | false> {
|
||||
return new Promise((res) => {
|
||||
const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, true, (result) =>
|
||||
res(result)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { unique } from "octagonal-wheels/collection";
|
||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||
import { throttle } from "octagonal-wheels/function";
|
||||
import { eventHub } from "../../common/events.ts";
|
||||
import { BASE_IS_NEW, compareFileFreshness, EVEN, getPath, isValidPath, TARGET_IS_NEW } from "../../common/utils.ts";
|
||||
@@ -19,7 +18,7 @@ import { isAnyNote } from "../../lib/src/common/utils.ts";
|
||||
import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
||||
|
||||
import { withConcurrency } from "octagonal-wheels/iterable/map";
|
||||
export class ModuleInitializerFile extends AbstractModule implements ICoreModule {
|
||||
async $$performFullScan(showingNotice?: boolean): Promise<void> {
|
||||
this._log("Opening the key-value database", LOG_LEVEL_VERBOSE);
|
||||
@@ -152,35 +151,30 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
|
||||
if (!this.localDatabase.isReady) throw Error("Database is not ready!");
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
const step = 10;
|
||||
const processor = new QueueProcessor(
|
||||
let total = 0;
|
||||
for await (const result of withConcurrency(
|
||||
objects,
|
||||
async (e) => {
|
||||
try {
|
||||
await callback(e[0]);
|
||||
success++;
|
||||
// return
|
||||
await callback(e);
|
||||
return true;
|
||||
} catch (ex) {
|
||||
this._log(`Error while ${procedureName}`, LOG_LEVEL_NOTICE);
|
||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
||||
failed++;
|
||||
return false;
|
||||
}
|
||||
if ((success + failed) % step == 0) {
|
||||
const msg = `${procedureName}: DONE:${success}, FAILED:${failed}, LAST:${processor._queue.length}`;
|
||||
updateLog(procedureName, msg);
|
||||
}
|
||||
return;
|
||||
},
|
||||
{
|
||||
batchSize: 1,
|
||||
concurrentLimit: 10,
|
||||
delay: 0,
|
||||
suspended: true,
|
||||
maintainDelay: false,
|
||||
interval: 0,
|
||||
},
|
||||
objects
|
||||
);
|
||||
await processor.waitForAllDoneAndTerminate();
|
||||
10
|
||||
)) {
|
||||
if (result) {
|
||||
success++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
total++;
|
||||
const msg = `${procedureName}: DONE:${success}, FAILED:${failed}, LAST:${objects.length - total}`;
|
||||
updateLog(procedureName, msg);
|
||||
}
|
||||
const msg = `${procedureName} All done: DONE:${success}, FAILED:${failed}`;
|
||||
updateLog(procedureName, msg);
|
||||
};
|
||||
@@ -237,11 +231,6 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
|
||||
const { file, doc } = e;
|
||||
if (!this.core.$$isFileSizeExceeded(file.stat.size) && !this.core.$$isFileSizeExceeded(doc.size)) {
|
||||
await this.syncFileBetweenDBandStorage(file, doc);
|
||||
// fireAndForget(() => this.checkAndApplySettingFromMarkdown(getPath(doc), true));
|
||||
// eventHub.emitEvent("event-file-changed", {
|
||||
// file: getPath(doc),
|
||||
// automated: true,
|
||||
// });
|
||||
} else {
|
||||
this._log(
|
||||
`SYNC DATABASE AND STORAGE: ${getPath(doc)} has been skipped due to file size exceeding the limit`,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger.js";
|
||||
import { SETTING_VERSION_SUPPORT_CASE_INSENSITIVE } from "../../lib/src/common/types.js";
|
||||
import {
|
||||
EVENT_REQUEST_OPEN_P2P,
|
||||
EVENT_REQUEST_OPEN_SETTING_WIZARD,
|
||||
EVENT_REQUEST_OPEN_SETTINGS,
|
||||
EVENT_REQUEST_OPEN_SETUP_URI,
|
||||
@@ -8,16 +9,12 @@ import {
|
||||
} from "../../common/events.ts";
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
||||
|
||||
const URI_DOC = "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use";
|
||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||
|
||||
export class ModuleMigration extends AbstractModule implements ICoreModule {
|
||||
async migrateDisableBulkSend() {
|
||||
if (this.settings.sendChunksBulk) {
|
||||
this._log(
|
||||
"Send chunks in bulk has been enabled, however, this feature had been corrupted. Sorry for your inconvenience. Automatically disabled.",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
this._log($msg("moduleMigration.logBulkSendCorrupted"), LOG_LEVEL_NOTICE);
|
||||
this.settings.sendChunksBulk = false;
|
||||
this.settings.sendChunksBulkMaxSize = 1;
|
||||
await this.saveSettings();
|
||||
@@ -28,7 +25,13 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
|
||||
const current = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE;
|
||||
// Check each migrations(old -> current)
|
||||
if (!(await this.migrateToCaseInsensitive(old, current))) {
|
||||
this._log(`Migration failed or cancelled from ${old} to ${current}`, LOG_LEVEL_NOTICE);
|
||||
this._log(
|
||||
$msg("moduleMigration.logMigrationFailed", {
|
||||
old: old.toString(),
|
||||
current: current.toString(),
|
||||
}),
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -68,10 +71,10 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
|
||||
remoteChecked = true;
|
||||
}
|
||||
} else {
|
||||
this._log("Failed to fetch remote tweak values", LOG_LEVEL_INFO);
|
||||
this._log($msg("moduleMigration.logFetchRemoteTweakFailed"), LOG_LEVEL_INFO);
|
||||
}
|
||||
} catch (ex) {
|
||||
this._log("Could not get remote tweak values", LOG_LEVEL_INFO);
|
||||
this._log($msg("moduleMigration.logRemoteTweakUnavailable"), LOG_LEVEL_INFO);
|
||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
|
||||
@@ -82,27 +85,24 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
|
||||
this.settings.handleFilenameCaseSensitive = true;
|
||||
this.settings.doNotUseFixedRevisionForChunks = true;
|
||||
this.settings.settingVersion = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE;
|
||||
this._log(`Migrated to db:${current} with the same behaviour as before`, LOG_LEVEL_INFO);
|
||||
this._log(
|
||||
$msg("moduleMigration.logMigratedSameBehaviour", {
|
||||
current: current.toString(),
|
||||
}),
|
||||
LOG_LEVEL_INFO
|
||||
);
|
||||
await this.saveSettings();
|
||||
return true;
|
||||
}
|
||||
const message = `As you may already know, the self-hosted LiveSync has changed its default behaviour and database structure.
|
||||
|
||||
And thankfully, with your time and efforts, the remote database appears to have already been migrated. Congratulations!
|
||||
|
||||
However, we need a bit more. The configuration of this device is not compatible with the remote database. We will need to fetch the remote database again. Should we fetch from the remote again now?
|
||||
|
||||
___Note: We cannot synchronise until the configuration has been changed and the database has been fetched again.___
|
||||
___Note2: The chunks are completely immutable, we can fetch only the metadata and difference.___
|
||||
`;
|
||||
const OPTION_FETCH = "Yes, fetch again";
|
||||
const DISMISS = "No, please ask again";
|
||||
const message = $msg("moduleMigration.msgFetchRemoteAgain");
|
||||
const OPTION_FETCH = $msg("moduleMigration.optionYesFetchAgain");
|
||||
const DISMISS = $msg("moduleMigration.optionNoAskAgain");
|
||||
const options = [OPTION_FETCH, DISMISS];
|
||||
const ret = await this.core.confirm.confirmWithMessage(
|
||||
"Case Sensitivity",
|
||||
$msg("moduleMigration.titleCaseSensitivity"),
|
||||
message,
|
||||
options,
|
||||
"No, please ask again",
|
||||
DISMISS,
|
||||
40
|
||||
);
|
||||
if (ret == OPTION_FETCH) {
|
||||
@@ -114,7 +114,7 @@ ___Note2: The chunks are completely immutable, we can fetch only the metadata an
|
||||
await this.core.rebuilder.scheduleFetch();
|
||||
return;
|
||||
} catch (ex) {
|
||||
this._log("Failed to create redflag2", LOG_LEVEL_VERBOSE);
|
||||
this._log($msg("moduleMigration.logRedflag2CreationFail"), LOG_LEVEL_VERBOSE);
|
||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
return false;
|
||||
@@ -123,33 +123,25 @@ ___Note2: The chunks are completely immutable, we can fetch only the metadata an
|
||||
}
|
||||
}
|
||||
|
||||
const ENABLE_BOTH = "Enable both";
|
||||
const ENABLE_FILENAME_CASE_INSENSITIVE = "Enable only #1";
|
||||
const ENABLE_FIXED_REVISION_FOR_CHUNKS = "Enable only #2";
|
||||
const ADJUST_TO_REMOTE = "Adjust to remote";
|
||||
const DISMISS = "Decide it later";
|
||||
const KEEP = "Keep previous behaviour";
|
||||
const message = `Since v0.23.21, the self-hosted LiveSync has changed the default behaviour and database structure. The following changes have been made:
|
||||
|
||||
1. **Case sensitivity of filenames**
|
||||
The handling of filenames is now case-insensitive. This is a beneficial change for most platforms, other than Linux and iOS, which do not manage filename case sensitivity effectively.
|
||||
(On These, a warning will be displayed for files with the same name but different cases).
|
||||
|
||||
2. **Revision handling of the chunks**
|
||||
Chunks are immutable, which allows their revisions to be fixed. This change will enhance the performance of file saving.
|
||||
|
||||
___However, to enable either of these changes, both remote and local databases need to be rebuilt. This process takes a few minutes, and we recommend doing it when you have ample time.___
|
||||
|
||||
- If you wish to maintain the previous behaviour, you can skip this process by using \`${KEEP}\`.
|
||||
- If you do not have enough time, please choose \`${DISMISS}\`. You will be prompted again later.
|
||||
- If you have rebuilt the database on another device, please select \`${DISMISS}\` and try synchronizing again. Since a difference has been detected, you will be prompted again.
|
||||
`;
|
||||
const ENABLE_BOTH = $msg("moduleMigration.optionEnableBoth");
|
||||
const ENABLE_FILENAME_CASE_INSENSITIVE = $msg("moduleMigration.optionEnableFilenameCaseInsensitive");
|
||||
const ENABLE_FIXED_REVISION_FOR_CHUNKS = $msg("moduleMigration.optionEnableFixedRevisionForChunks");
|
||||
const ADJUST_TO_REMOTE = $msg("moduleMigration.optionAdjustRemote");
|
||||
const KEEP = $msg("moduleMigration.optionKeepPreviousBehaviour");
|
||||
const DISMISS = $msg("moduleMigration.optionDecideLater");
|
||||
const message = $msg("moduleMigration.msgSinceV02321");
|
||||
const options = [ENABLE_BOTH, ENABLE_FILENAME_CASE_INSENSITIVE, ENABLE_FIXED_REVISION_FOR_CHUNKS];
|
||||
if (remoteChecked) {
|
||||
options.push(ADJUST_TO_REMOTE);
|
||||
}
|
||||
options.push(KEEP, DISMISS);
|
||||
const ret = await this.core.confirm.confirmWithMessage("Case Sensitivity", message, options, DISMISS, 40);
|
||||
const ret = await this.core.confirm.confirmWithMessage(
|
||||
$msg("moduleMigration.titleCaseSensitivity"),
|
||||
message,
|
||||
options,
|
||||
DISMISS,
|
||||
40
|
||||
);
|
||||
console.dir(ret);
|
||||
switch (ret) {
|
||||
case ENABLE_BOTH:
|
||||
@@ -181,20 +173,14 @@ ___However, to enable either of these changes, both remote and local databases n
|
||||
}
|
||||
|
||||
async initialMessage() {
|
||||
const message = `Your device has **not been set up yet**. Let me guide you through the setup process.
|
||||
|
||||
Please keep in mind that every dialogue content can be copied to the clipboard. If you need to refer to it later, you can paste it into a note in Obsidian. You can also translate it into your language using a translation tool.
|
||||
|
||||
First, do you have **Setup URI**?
|
||||
|
||||
Note: If you do not know what it is, please refer to the [documentation](${URI_DOC}).
|
||||
`;
|
||||
|
||||
const USE_SETUP = "Yes, I have";
|
||||
const NEXT = "No, I do not have";
|
||||
const message = $msg("moduleMigration.msgInitialSetup", {
|
||||
URI_DOC: $msg("moduleMigration.docUri"),
|
||||
});
|
||||
const USE_SETUP = $msg("moduleMigration.optionHaveSetupUri");
|
||||
const NEXT = $msg("moduleMigration.optionNoSetupUri");
|
||||
|
||||
const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_SETUP, NEXT], {
|
||||
title: "Welcome to Self-hosted LiveSync",
|
||||
title: $msg("moduleMigration.titleWelcome"),
|
||||
defaultAction: USE_SETUP,
|
||||
});
|
||||
if (ret === USE_SETUP) {
|
||||
@@ -207,23 +193,24 @@ Note: If you do not know what it is, please refer to the [documentation](${URI_D
|
||||
}
|
||||
|
||||
async askAgainForSetupURI() {
|
||||
const message = `We strongly recommend that you generate a set-up URI and use it.
|
||||
If you do not have knowledge about it, please refer to the [documentation](${URI_DOC}) (Sorry again, but it is important).
|
||||
const message = $msg("moduleMigration.msgRecommendSetupUri", { URI_DOC: $msg("moduleMigration.docUri") });
|
||||
const USE_MINIMAL = $msg("moduleMigration.optionSetupWizard");
|
||||
const USE_P2P = $msg("moduleMigration.optionSetupViaP2P");
|
||||
const USE_SETUP = $msg("moduleMigration.optionManualSetup");
|
||||
const NEXT = $msg("moduleMigration.optionRemindNextLaunch");
|
||||
|
||||
How do you want to set it up manually?`;
|
||||
|
||||
const USE_MINIMAL = "Take me into the setup wizard";
|
||||
const USE_SETUP = "Set it up all manually";
|
||||
const NEXT = "Remind me at the next launch";
|
||||
|
||||
const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_MINIMAL, USE_SETUP, NEXT], {
|
||||
title: "Recommendation to use Setup URI",
|
||||
const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_MINIMAL, USE_SETUP, USE_P2P, NEXT], {
|
||||
title: $msg("moduleMigration.titleRecommendSetupUri"),
|
||||
defaultAction: USE_MINIMAL,
|
||||
});
|
||||
if (ret === USE_MINIMAL) {
|
||||
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTING_WIZARD);
|
||||
return false;
|
||||
}
|
||||
if (ret === USE_P2P) {
|
||||
eventHub.emitEvent(EVENT_REQUEST_OPEN_P2P);
|
||||
return false;
|
||||
}
|
||||
if (ret === USE_SETUP) {
|
||||
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTINGS);
|
||||
return false;
|
||||
@@ -235,7 +222,7 @@ How do you want to set it up manually?`;
|
||||
|
||||
async $everyOnFirstInitialize(): Promise<boolean> {
|
||||
if (!this.localDatabase.isReady) {
|
||||
this._log(`Something went wrong! The local database is not ready`, LOG_LEVEL_NOTICE);
|
||||
this._log($msg("moduleMigration.logLocalDatabaseNotReady"), LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
}
|
||||
if (this.settings.isConfigured) {
|
||||
@@ -245,10 +232,7 @@ How do you want to set it up manually?`;
|
||||
if (!this.settings.isConfigured) {
|
||||
// Case sensitivity
|
||||
if (!(await this.initialMessage()) || !(await this.askAgainForSetupURI())) {
|
||||
this._log(
|
||||
"The setup has been cancelled, Self-hosted LiveSync waiting for your setup!",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
this._log($msg("moduleMigration.logSetupCancelled"), LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { fireAndForget } from "octagonal-wheels/promises";
|
||||
import { addIcon, type Editor, type MarkdownFileInfo, type MarkdownView } from "../../deps.ts";
|
||||
import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "../../lib/src/common/types.ts";
|
||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||
|
||||
export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsidianModule {
|
||||
$everyOnloadStart(): Promise<boolean> {
|
||||
@@ -16,7 +17,7 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
|
||||
</g>`
|
||||
);
|
||||
|
||||
this.addRibbonIcon("replicate", "Replicate", async () => {
|
||||
this.addRibbonIcon("replicate", $msg("moduleObsidianMenu.replicate"), async () => {
|
||||
await this.core.$$replicate(true);
|
||||
}).addClass("livesync-ribbon-replicate");
|
||||
|
||||
@@ -123,7 +124,7 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
|
||||
});
|
||||
}
|
||||
if (leaves.length > 0) {
|
||||
this.app.workspace.revealLeaf(leaves[0]);
|
||||
await this.app.workspace.revealLeaf(leaves[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { delay } from "octagonal-wheels/promises";
|
||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||
import { eventHub } from "../../common/events";
|
||||
import { webcrypto } from "crypto";
|
||||
import { getWebCrypto } from "../../lib/src/mods.ts";
|
||||
import { uint8ArrayToHexString } from "octagonal-wheels/binary/hex";
|
||||
import { parseYaml, requestUrl, stringifyYaml } from "obsidian";
|
||||
import type { FilePath } from "../../lib/src/common/types.ts";
|
||||
@@ -162,6 +162,7 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
|
||||
async _dumpFileList(outFile?: string) {
|
||||
const files = this.core.storageAccess.getFiles();
|
||||
const out = [] as any[];
|
||||
const webcrypto = await getWebCrypto();
|
||||
for (const file of files) {
|
||||
if (!(await this.core.$$isTargetFile(file.path))) {
|
||||
continue;
|
||||
@@ -202,7 +203,8 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
|
||||
.map((e) => new RegExp(e, "i"));
|
||||
const out = [] as any[];
|
||||
const files = await this.core.storageAccess.getFilesIncludeHidden("", undefined, ignorePatterns);
|
||||
console.dir(files);
|
||||
// console.dir(files);
|
||||
const webcrypto = await getWebCrypto();
|
||||
for (const file of files) {
|
||||
// if (!await this.core.$$isTargetFile(file)) {
|
||||
// continue;
|
||||
|
||||
@@ -98,9 +98,9 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.range.max = "0";
|
||||
this.range.value = "";
|
||||
this.range.disabled = true;
|
||||
this.contentView.setText(`History of this file was not recorded.`);
|
||||
this.contentView.setText(`We don't have any history for this note.`);
|
||||
} else {
|
||||
this.contentView.setText(`Error occurred.`);
|
||||
this.contentView.setText(`Error while loading file.`);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
}
|
||||
@@ -268,7 +268,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
const leaf = this.plugin.app.workspace.getLeaf(false);
|
||||
await leaf.openFile(targetFile);
|
||||
} else {
|
||||
Logger("The file could not view on the editor", LOG_LEVEL_NOTICE);
|
||||
Logger("Unable to display the file in the editor", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
};
|
||||
buttons.createEl("button", { text: "Back to this revision" }, (e) => {
|
||||
|
||||
@@ -66,7 +66,10 @@
|
||||
|
||||
for (const revInfo of reversedRevs) {
|
||||
if (revInfo.status == "available") {
|
||||
const doc = (!isPlain && showDiffInfo) || (checkStorageDiff && revInfo.rev == docA._rev) ? await db.getDBEntry(path, { rev: revInfo.rev }, false, false, true) : await db.getDBEntryMeta(path, { rev: revInfo.rev }, true);
|
||||
const doc =
|
||||
(!isPlain && showDiffInfo) || (checkStorageDiff && revInfo.rev == docA._rev)
|
||||
? await db.getDBEntry(path, { rev: revInfo.rev }, false, false, true)
|
||||
: await db.getDBEntryMeta(path, { rev: revInfo.rev }, true);
|
||||
if (doc === false) continue;
|
||||
const rev = revInfo.rev;
|
||||
|
||||
@@ -94,7 +97,10 @@
|
||||
[DIFF_EQUAL]: 0,
|
||||
[DIFF_INSERT]: 0,
|
||||
} as { [key: number]: number };
|
||||
const px = diff.reduce((p, c) => ({ ...p, [c[0]]: (p[c[0]] ?? 0) + c[1].length }), pxInit);
|
||||
const px = diff.reduce(
|
||||
(p, c) => ({ ...p, [c[0]]: (p[c[0]] ?? 0) + c[1].length }),
|
||||
pxInit
|
||||
);
|
||||
diffDetail = `-${px[DIFF_DELETE]}, +${px[DIFF_INSERT]}`;
|
||||
}
|
||||
}
|
||||
@@ -104,9 +110,13 @@
|
||||
}
|
||||
if (rev == docA._rev) {
|
||||
if (checkStorageDiff) {
|
||||
const isExist = await plugin.storageAccess.isExistsIncludeHidden(stripAllPrefixes(getPath(docA)));
|
||||
const isExist = await plugin.storageAccess.isExistsIncludeHidden(
|
||||
stripAllPrefixes(getPath(docA))
|
||||
);
|
||||
if (isExist) {
|
||||
const data = await plugin.storageAccess.readHiddenFileBinary(stripAllPrefixes(getPath(docA)));
|
||||
const data = await plugin.storageAccess.readHiddenFileBinary(
|
||||
stripAllPrefixes(getPath(docA))
|
||||
);
|
||||
const d = readAsBlob(doc);
|
||||
const result = await isDocContentSame(data, d);
|
||||
if (result) {
|
||||
@@ -187,19 +197,28 @@
|
||||
<div class="globalhistory">
|
||||
<h1>Vault history</h1>
|
||||
<div class="control">
|
||||
<div class="row"><label for="">From:</label><input type="date" bind:value={dispDateFrom} disabled={loading} /></div>
|
||||
<div class="row">
|
||||
<label for="">From:</label><input type="date" bind:value={dispDateFrom} disabled={loading} />
|
||||
</div>
|
||||
<div class="row"><label for="">To:</label><input type="date" bind:value={dispDateTo} disabled={loading} /></div>
|
||||
<div class="row">
|
||||
<label for="">Info:</label>
|
||||
<label><input type="checkbox" bind:checked={showDiffInfo} disabled={loading} /><span>Diff</span></label>
|
||||
<label><input type="checkbox" bind:checked={showChunkCorrected} disabled={loading} /><span>Chunks</span></label>
|
||||
<label><input type="checkbox" bind:checked={checkStorageDiff} disabled={loading} /><span>File integrity</span></label>
|
||||
<label
|
||||
><input type="checkbox" bind:checked={showChunkCorrected} disabled={loading} /><span>Chunks</span
|
||||
></label
|
||||
>
|
||||
<label
|
||||
><input type="checkbox" bind:checked={checkStorageDiff} disabled={loading} /><span>File integrity</span
|
||||
></label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{#if loading}
|
||||
<div class="">Gathering information...</div>
|
||||
{/if}
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th> Date </th>
|
||||
<th> Path </th>
|
||||
@@ -212,7 +231,7 @@
|
||||
<tr>
|
||||
<td colspan="5" class="more">
|
||||
{#if loading}
|
||||
<div class="" />
|
||||
<div class=""></div>
|
||||
{:else}
|
||||
<div><button on:click={() => nextWeek()}>+1 week</button></div>
|
||||
{/if}
|
||||
@@ -257,12 +276,13 @@
|
||||
<tr>
|
||||
<td colspan="5" class="more">
|
||||
{#if loading}
|
||||
<div class="" />
|
||||
<div class=""></div>
|
||||
{:else}
|
||||
<div><button on:click={() => prevWeek()}>+1 week</button></div>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { ItemView, WorkspaceLeaf } from "../../../deps.ts";
|
||||
import { WorkspaceLeaf } from "../../../deps.ts";
|
||||
import GlobalHistoryComponent from "./GlobalHistory.svelte";
|
||||
import type ObsidianLiveSyncPlugin from "../../../main.ts";
|
||||
import { SvelteItemView } from "../../../common/SvelteItemView.ts";
|
||||
import { mount } from "svelte";
|
||||
|
||||
export const VIEW_TYPE_GLOBAL_HISTORY = "global-history";
|
||||
export class GlobalHistoryView extends ItemView {
|
||||
component?: GlobalHistoryComponent;
|
||||
export class GlobalHistoryView extends SvelteItemView {
|
||||
instantiateComponent(target: HTMLElement) {
|
||||
return mount(GlobalHistoryComponent, {
|
||||
target: target,
|
||||
props: {
|
||||
plugin: this.plugin,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
icon = "clock";
|
||||
title: string = "";
|
||||
@@ -26,19 +36,4 @@ export class GlobalHistoryView extends ItemView {
|
||||
getDisplayText() {
|
||||
return "Vault history";
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
this.component = new GlobalHistoryComponent({
|
||||
target: this.contentEl,
|
||||
props: {
|
||||
plugin: this.plugin,
|
||||
},
|
||||
});
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
this.component?.$destroy();
|
||||
await Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { App, Modal } from "../../../deps.ts";
|
||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
||||
import { CANCELLED, LEAVE_TO_SUBSEQUENT, RESULT_TIMED_OUT, type diff_result } from "../../../lib/src/common/types.ts";
|
||||
import { CANCELLED, LEAVE_TO_SUBSEQUENT, type diff_result } from "../../../lib/src/common/types.ts";
|
||||
import { escapeStringToHTML } from "../../../lib/src/string_and_binary/convert.ts";
|
||||
import { delay, fireAndForget, sendValue, waitForValue } from "../../../lib/src/common/utils.ts";
|
||||
import { delay } from "../../../lib/src/common/utils.ts";
|
||||
import { eventHub } from "../../../common/events.ts";
|
||||
import { globalSlipBoard } from "../../../lib/src/bureau/bureau.ts";
|
||||
|
||||
export type MergeDialogResult = typeof CANCELLED | typeof LEAVE_TO_SUBSEQUENT | string;
|
||||
|
||||
declare global {
|
||||
interface Slips extends LSSlips {
|
||||
"conflict-resolved": typeof CANCELLED | MergeDialogResult;
|
||||
}
|
||||
}
|
||||
|
||||
export type MergeDialogResult = typeof LEAVE_TO_SUBSEQUENT | typeof CANCELLED | string;
|
||||
export class ConflictResolveModal extends Modal {
|
||||
result: diff_result;
|
||||
filename: string;
|
||||
@@ -18,6 +27,7 @@ export class ConflictResolveModal extends Modal {
|
||||
pluginPickMode: boolean = false;
|
||||
localName: string = "Keep A";
|
||||
remoteName: string = "Keep B";
|
||||
offEvent?: ReturnType<typeof eventHub.onEvent>;
|
||||
|
||||
constructor(app: App, filename: string, diff: diff_result, pluginPickMode?: boolean, remoteName?: string) {
|
||||
super(app);
|
||||
@@ -32,23 +42,21 @@ export class ConflictResolveModal extends Modal {
|
||||
// Send cancel signal for the previous merge dialogue
|
||||
// if not there, simply be ignored.
|
||||
// sendValue("close-resolve-conflict:" + this.filename, false);
|
||||
sendValue("cancel-resolve-conflict:" + this.filename, true);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
// Send cancel signal for the previous merge dialogue
|
||||
// if not there, simply be ignored.
|
||||
sendValue("cancel-resolve-conflict:" + this.filename, true);
|
||||
setTimeout(() => {
|
||||
fireAndForget(async () => {
|
||||
const forceClose = await waitForValue("cancel-resolve-conflict:" + this.filename);
|
||||
// debugger;
|
||||
if (forceClose) {
|
||||
this.sendResponse(CANCELLED);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
globalSlipBoard.submit("conflict-resolved", this.filename, CANCELLED);
|
||||
if (this.offEvent) {
|
||||
this.offEvent();
|
||||
}
|
||||
this.offEvent = eventHub.onEvent("conflict-cancelled", (path) => {
|
||||
if (path === this.filename) {
|
||||
this.sendResponse(CANCELLED);
|
||||
}
|
||||
});
|
||||
// sendValue("close-resolve-conflict:" + this.filename, false);
|
||||
this.titleEl.setText(this.title);
|
||||
contentEl.empty();
|
||||
@@ -111,18 +119,19 @@ export class ConflictResolveModal extends Modal {
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
if (this.offEvent) {
|
||||
this.offEvent();
|
||||
}
|
||||
if (this.consumed) {
|
||||
return;
|
||||
}
|
||||
this.consumed = true;
|
||||
sendValue("close-resolve-conflict:" + this.filename, this.response);
|
||||
sendValue("cancel-resolve-conflict:" + this.filename, false);
|
||||
globalSlipBoard.submit("conflict-resolved", this.filename, this.response);
|
||||
}
|
||||
|
||||
async waitForResult(): Promise<MergeDialogResult> {
|
||||
await delay(100);
|
||||
const r = await waitForValue<MergeDialogResult>("close-resolve-conflict:" + this.filename);
|
||||
if (r === RESULT_TIMED_OUT) return CANCELLED;
|
||||
const r = await globalSlipBoard.awaitNext("conflict-resolved", this.filename);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +1,105 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { logMessages } from "../../../lib/src/mock_and_interop/stores";
|
||||
import type { ReactiveInstance } from "../../../lib/src/dataobject/reactive";
|
||||
import { Logger } from "../../../lib/src/common/logger";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { logMessages } from "../../../lib/src/mock_and_interop/stores";
|
||||
import { reactive, type ReactiveInstance } from "../../../lib/src/dataobject/reactive";
|
||||
import { Logger } from "../../../lib/src/common/logger";
|
||||
import { $msg as msg, currentLang as lang } from "../../../lib/src/common/i18n.ts";
|
||||
|
||||
let unsubscribe: () => void;
|
||||
let messages = [] as string[];
|
||||
let wrapRight = false;
|
||||
let autoScroll = true;
|
||||
let suspended = false;
|
||||
function updateLog(logs: ReactiveInstance<string[]>) {
|
||||
const e = logs.value;
|
||||
if (!suspended) {
|
||||
messages = [...e];
|
||||
setTimeout(() => {
|
||||
if (scroll) scroll.scrollTop = scroll.scrollHeight;
|
||||
}, 10);
|
||||
}
|
||||
let unsubscribe: () => void;
|
||||
let messages = $state([] as string[]);
|
||||
let wrapRight = $state(false);
|
||||
let autoScroll = $state(true);
|
||||
let suspended = $state(false);
|
||||
|
||||
type Props = {
|
||||
close: () => void;
|
||||
};
|
||||
let { close }: Props = $props();
|
||||
// export let close: () => void;
|
||||
function updateLog(logs: ReactiveInstance<string[]>) {
|
||||
const e = logs.value;
|
||||
if (!suspended) {
|
||||
messages = [...e];
|
||||
setTimeout(() => {
|
||||
if (scroll) scroll.scrollTop = scroll.scrollHeight;
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
onMount(async () => {
|
||||
const _logMessages = reactive(() => logMessages.value);
|
||||
_logMessages.onChanged(updateLog);
|
||||
Logger(msg("logPane.logWindowOpened", {}, lang));
|
||||
unsubscribe = () => _logMessages.offChanged(updateLog);
|
||||
});
|
||||
onDestroy(() => {
|
||||
if (unsubscribe) unsubscribe();
|
||||
});
|
||||
let scroll: HTMLDivElement;
|
||||
function closeDialogue() {
|
||||
close();
|
||||
}
|
||||
onMount(async () => {
|
||||
logMessages.onChanged(updateLog);
|
||||
Logger("Log window opened");
|
||||
unsubscribe = () => logMessages.offChanged(updateLog);
|
||||
});
|
||||
onDestroy(() => {
|
||||
if (unsubscribe) unsubscribe();
|
||||
});
|
||||
let scroll: HTMLDivElement;
|
||||
</script>
|
||||
|
||||
<div class="logpane">
|
||||
<!-- <h1>Self-hosted LiveSync Log</h1> -->
|
||||
<div class="control">
|
||||
<div class="row">
|
||||
<label><input type="checkbox" bind:checked={wrapRight} /><span>Wrap</span></label>
|
||||
<label><input type="checkbox" bind:checked={autoScroll} /><span>Auto scroll</span></label>
|
||||
<label><input type="checkbox" bind:checked={suspended} /><span>Pause</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log" bind:this={scroll}>
|
||||
{#each messages as line}
|
||||
<pre class:wrap-right={wrapRight}>{line}</pre>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- <h1>{msg("logPane.title", {}, lang)}</h1> -->
|
||||
<div class="control">
|
||||
<div class="row">
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={wrapRight} />
|
||||
<span>{msg("logPane.wrap", {}, lang)}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={autoScroll} />
|
||||
<span>{msg("logPane.autoScroll", {}, lang)}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={suspended} />
|
||||
<span>{msg("logPane.pause", {}, lang)}</span>
|
||||
</label>
|
||||
<span class="spacer"></span>
|
||||
<button onclick={() => closeDialogue()}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log" bind:this={scroll}>
|
||||
{#each messages as line}
|
||||
<pre class:wrap-right={wrapRight}>{line}</pre>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.logpane {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
.log {
|
||||
overflow-y: scroll;
|
||||
user-select: text;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
.log > pre {
|
||||
margin: 0;
|
||||
}
|
||||
.log > pre.wrap-right {
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.row > label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 5em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.logpane {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
.log {
|
||||
overflow-y: scroll;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
.log > pre {
|
||||
margin: 0;
|
||||
}
|
||||
.log > pre.wrap-right {
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.row > label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 5em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
import { ItemView, WorkspaceLeaf } from "obsidian";
|
||||
import { WorkspaceLeaf } from "obsidian";
|
||||
import LogPaneComponent from "./LogPane.svelte";
|
||||
import type ObsidianLiveSyncPlugin from "../../../main.ts";
|
||||
import { SvelteItemView } from "../../../common/SvelteItemView.ts";
|
||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||
import { mount } from "svelte";
|
||||
export const VIEW_TYPE_LOG = "log-log";
|
||||
//Log view
|
||||
export class LogPaneView extends ItemView {
|
||||
component?: LogPaneComponent;
|
||||
export class LogPaneView extends SvelteItemView {
|
||||
instantiateComponent(target: HTMLElement) {
|
||||
return mount(LogPaneComponent, {
|
||||
target: target,
|
||||
props: {
|
||||
close: () => {
|
||||
this.leaf.detach();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
icon = "view-log";
|
||||
title: string = "";
|
||||
navigation = true;
|
||||
navigation = false;
|
||||
|
||||
getIcon(): string {
|
||||
return "view-log";
|
||||
@@ -24,19 +37,7 @@ export class LogPaneView extends ItemView {
|
||||
}
|
||||
|
||||
getDisplayText() {
|
||||
return "Self-hosted LiveSync Log";
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
this.component = new LogPaneComponent({
|
||||
target: this.contentEl,
|
||||
props: {},
|
||||
});
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
this.component?.$destroy();
|
||||
await Promise.resolve();
|
||||
// TODO: This function is not reactive and does not update the title based on the current language
|
||||
return $msg("logPane.title");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im
|
||||
this._log(`Conflicted: ${note.path}`);
|
||||
}
|
||||
} else {
|
||||
this._log(`There are no conflicted files`, LOG_LEVEL_VERBOSE);
|
||||
this._log(`There are no conflicting files`, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import { LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/
|
||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||
import { LogPaneView, VIEW_TYPE_LOG } from "./Log/LogPaneView.ts";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||
import { P2PLogCollector } from "../../lib/src/replication/trystero/P2PReplicatorCore.ts";
|
||||
|
||||
// This module cannot be a core module because it depends on the Obsidian UI.
|
||||
|
||||
@@ -62,6 +64,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
|
||||
statusBarLabels!: ReactiveValue<{ message: string; status: string }>;
|
||||
statusLog = reactiveSource("");
|
||||
notifies: { [key: string]: { notice: Notice; count: number } } = {};
|
||||
p2pLogCollector = new P2PLogCollector();
|
||||
|
||||
observeForLogs() {
|
||||
const padSpaces = `\u{2007}`.repeat(10);
|
||||
@@ -167,10 +170,12 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
|
||||
const queued = queueCountLabel();
|
||||
const waiting = waitingLabel();
|
||||
const networkActivity = requestingStatLabel();
|
||||
const p2p = this.p2pLogCollector.p2pReplicationLine.value;
|
||||
return {
|
||||
message: `${networkActivity}Sync: ${w} ↑ ${sent}${pushLast} ↓ ${arrived}${pullLast}${waiting}${queued}`,
|
||||
message: `${networkActivity}Sync: ${w} ↑ ${sent}${pushLast} ↓ ${arrived}${pullLast}${waiting}${queued}${p2p == "" ? "" : "\n" + p2p}`,
|
||||
};
|
||||
});
|
||||
|
||||
const statusBarLabels = reactive(() => {
|
||||
const scheduleMessage = this.core.$$isReloadingScheduled()
|
||||
? `WARNING! RESTARTING OBSIDIAN IS SCHEDULED!\n`
|
||||
@@ -195,6 +200,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
|
||||
$everyOnload(): Promise<boolean> {
|
||||
eventHub.onEvent(EVENT_LEAF_ACTIVE_CHANGED, () => this.onActiveLeafChange());
|
||||
eventHub.onceEvent(EVENT_LAYOUT_READY, () => this.onActiveLeafChange());
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
adjustStatusDivPosition() {
|
||||
@@ -292,7 +298,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
|
||||
<path d="m106 346v44h70v-44zm45 16h-20v-8h20z"/>
|
||||
</g>`
|
||||
);
|
||||
this.addRibbonIcon("view-log", "Show log", () => {
|
||||
this.addRibbonIcon("view-log", $msg("moduleLog.showLog"), () => {
|
||||
void this.core.$$showView(VIEW_TYPE_LOG);
|
||||
}).addClass("livesync-ribbon-showlog");
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
||||
const passphrase = await this.getPassphrase(settings);
|
||||
if (passphrase === false) {
|
||||
this._log(
|
||||
"Could not determine passphrase to save data.json! You probably make the configuration sure again!",
|
||||
"Failed to obtain passphrase when saving data.json! Please verify the configuration.",
|
||||
LOG_LEVEL_URGENT
|
||||
);
|
||||
return "";
|
||||
@@ -75,10 +75,7 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
||||
const settings = { ...this.settings };
|
||||
settings.deviceAndVaultName = "";
|
||||
if (this.usedPassphrase == "" && !(await this.getPassphrase(settings))) {
|
||||
this._log(
|
||||
"Could not determine passphrase for saving data.json! Our data.json have insecure items!",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
this._log("Failed to retrieve passphrase. data.json contains unencrypted items!", LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
if (
|
||||
settings.couchDB_PASSWORD != "" ||
|
||||
@@ -144,10 +141,7 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
||||
}
|
||||
const passphrase = await this.getPassphrase(settings);
|
||||
if (passphrase === false) {
|
||||
this._log(
|
||||
"Could not determine passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!",
|
||||
LOG_LEVEL_URGENT
|
||||
);
|
||||
this._log("No passphrase found for data.json! Verify configuration before syncing.", LOG_LEVEL_URGENT);
|
||||
} else {
|
||||
if (settings.encryptedCouchDBConnection) {
|
||||
const keys = [
|
||||
@@ -173,7 +167,7 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
||||
}
|
||||
} else {
|
||||
this._log(
|
||||
"Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!",
|
||||
"Failed to decrypt passphrase from data.json! Ensure configuration is correct before syncing with remote.",
|
||||
LOG_LEVEL_URGENT
|
||||
);
|
||||
for (const key of keys) {
|
||||
@@ -189,7 +183,7 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
||||
settings.passphrase = decrypted;
|
||||
} else {
|
||||
this._log(
|
||||
"Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!",
|
||||
"Failed to decrypt passphrase from data.json! Ensure configuration is correct before syncing with remote.",
|
||||
LOG_LEVEL_URGENT
|
||||
);
|
||||
settings.passphrase = "";
|
||||
@@ -220,7 +214,7 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
||||
}
|
||||
if (isCloudantURI(this.settings.couchDB_URI) && this.settings.customChunkSize != 0) {
|
||||
this._log(
|
||||
"Configuration verification founds problems with your configuration. This has been fixed automatically. But you may already have data that cannot be synchronised. If this is the case, please rebuild everything.",
|
||||
"Configuration issues detected and automatically resolved. However, unsynchronized data may exist. Consider rebuilding if necessary.",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
this.settings.customChunkSize = 0;
|
||||
@@ -228,7 +222,7 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
|
||||
this.core.$$setDeviceAndVaultName(localStorage.getItem(lsKey) || "");
|
||||
if (this.core.$$getDeviceAndVaultName() == "") {
|
||||
if (this.settings.usePluginSync) {
|
||||
this._log("Device name is not set. Plug-in sync has been disabled.", LOG_LEVEL_NOTICE);
|
||||
this._log("Device name missing. Disabling plug-in sync.", LOG_LEVEL_NOTICE);
|
||||
this.settings.usePluginSync = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp
|
||||
if (automated && !this.settings.notifyAllSettingSyncFile) {
|
||||
if (!this.settings.settingSyncFile || this.settings.settingSyncFile != filename) {
|
||||
this._log(
|
||||
`Setting file (${filename}) is not matched to the current configuration. skipped.`,
|
||||
`Setting file (${filename}) does not match the current configuration. skipped.`,
|
||||
LOG_LEVEL_DEBUG
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -139,11 +139,11 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
|
||||
newSettingW.encryptedPassphrase = "";
|
||||
newSettingW.encryptedCouchDBConnection = "";
|
||||
newSettingW.additionalSuffixOfDatabaseName = `${"appId" in this.app ? this.app.appId : ""}`;
|
||||
const setupJustImport = "Just import setting";
|
||||
const setupAsNew = "Set it up as secondary or subsequent device";
|
||||
const setupAsMerge = "Secondary device but try keeping local changes";
|
||||
const setupAgain = "Reconfigure and reconstitute the data";
|
||||
const setupManually = "Leave everything to me";
|
||||
const setupJustImport = "Don't sync anything, just apply the settings.";
|
||||
const setupAsNew = "This is a new client - sync everything from the remote server.";
|
||||
const setupAsMerge = "This is an existing client - merge existing files with the server.";
|
||||
const setupAgain = "Initialise new server data - ideal for new or broken servers.";
|
||||
const setupManually = "Continue and configure manually.";
|
||||
newSettingW.syncInternalFiles = false;
|
||||
newSettingW.usePluginSync = false;
|
||||
newSettingW.isConfigured = true;
|
||||
@@ -171,10 +171,10 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
|
||||
await this.core.rebuilder.$fetchLocal(true);
|
||||
} else if (setupType == setupAgain) {
|
||||
const confirm =
|
||||
"I know this operation will rebuild all my databases with files on this device, and files that are on the remote database and I didn't synchronize to any other devices will be lost and want to proceed indeed.";
|
||||
"This operation will rebuild all databases with files on this device. Any files on the remote database not synced here will be lost.";
|
||||
if (
|
||||
(await this.core.confirm.askSelectStringDialogue(
|
||||
"Do you really want to do this?",
|
||||
"Are you sure you want to do this?",
|
||||
["Cancel", confirm],
|
||||
{ defaultAction: "Cancel" }
|
||||
)) != confirm
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
type AllNumericItemKey,
|
||||
type AllBooleanItemKey,
|
||||
} from "./settingConstants.ts";
|
||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||
|
||||
export class LiveSyncSetting extends Setting {
|
||||
autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent;
|
||||
@@ -86,7 +87,7 @@ export class LiveSyncSetting extends Setting {
|
||||
autoWireSetting(key: AllSettingItemKey, opt?: AutoWireOption) {
|
||||
const conf = getConfig(key);
|
||||
if (!conf) {
|
||||
// throw new Error(`No such setting item :${key}`)
|
||||
// throw new Error($msg("liveSyncSetting.errorNoSuchSettingItem", { key }));
|
||||
return;
|
||||
}
|
||||
const name = `${conf.name}${statusDisplay(conf.status)}`;
|
||||
@@ -216,7 +217,12 @@ export class LiveSyncSetting extends Setting {
|
||||
text.inputEl.toggleClass("sls-item-invalid-value", false);
|
||||
await this.commitValue(value);
|
||||
} else {
|
||||
this.setTooltip(`The value should ${opt.clampMin || "~"} < value < ${opt.clampMax || "~"}`);
|
||||
this.setTooltip(
|
||||
$msg("liveSyncSetting.valueShouldBeInRange", {
|
||||
min: opt.clampMin?.toString() || "~",
|
||||
max: opt.clampMax?.toString() || "~",
|
||||
})
|
||||
);
|
||||
text.inputEl.toggleClass("sls-item-invalid-value", true);
|
||||
lastError = true;
|
||||
return false;
|
||||
@@ -268,7 +274,7 @@ export class LiveSyncSetting extends Setting {
|
||||
this.addButton((button) => {
|
||||
this.applyButtonComponent = button;
|
||||
this.watchDirtyKeys = unique([...keys, ...this.watchDirtyKeys]);
|
||||
button.setButtonText(text ?? "Apply");
|
||||
button.setButtonText(text ?? $msg("liveSyncSettings.btnApply"));
|
||||
button.onClick(async () => {
|
||||
await LiveSyncSetting.env.saveSettings(keys);
|
||||
LiveSyncSetting.env.reloadAllSettings();
|
||||
@@ -363,7 +369,11 @@ export class LiveSyncSetting extends Setting {
|
||||
}
|
||||
if (this.holdValue && this.selfKey) {
|
||||
const isDirty = LiveSyncSetting.env.isDirty(this.selfKey);
|
||||
const alt = isDirty ? `Original: ${LiveSyncSetting.env.initialSettings![this.selfKey]}` : "";
|
||||
const alt = isDirty
|
||||
? $msg("liveSyncSetting.originalValue", {
|
||||
value: String(LiveSyncSetting.env.initialSettings?.[this.selfKey] ?? ""),
|
||||
})
|
||||
: "";
|
||||
this.controlEl.toggleClass("sls-item-dirty", isDirty);
|
||||
if (!this.hasPassword) {
|
||||
this.nameEl.toggleClass("sls-item-dirty-help", isDirty);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,7 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
|
||||
name: "Sync Mode",
|
||||
},
|
||||
couchDB_URI: {
|
||||
name: "URI",
|
||||
name: "Server URI",
|
||||
placeHolder: "https://........",
|
||||
},
|
||||
couchDB_USER: {
|
||||
@@ -57,30 +57,30 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
|
||||
desc: "password",
|
||||
},
|
||||
couchDB_DBNAME: {
|
||||
name: "Database name",
|
||||
name: "Database Name",
|
||||
},
|
||||
passphrase: {
|
||||
name: "Passphrase",
|
||||
desc: "Encrypting passphrase. If you change the passphrase of an existing database, overwriting the remote database is strongly recommended.",
|
||||
desc: "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.",
|
||||
},
|
||||
showStatusOnEditor: {
|
||||
name: "Show status inside the editor",
|
||||
desc: "Reflected after reboot",
|
||||
desc: "Requires restart of Obsidian.",
|
||||
},
|
||||
showOnlyIconsOnEditor: {
|
||||
name: "Show status as icons only",
|
||||
},
|
||||
showStatusOnStatusbar: {
|
||||
name: "Show status on the status bar",
|
||||
desc: "Reflected after reboot.",
|
||||
desc: "Requires restart of Obsidian.",
|
||||
},
|
||||
lessInformationInLog: {
|
||||
name: "Show only notifications",
|
||||
desc: "Prevent logging and show only notification. Please disable when you report the logs",
|
||||
desc: "Disables logging, only shows notifications. Please disable if you report an issue.",
|
||||
},
|
||||
showVerboseLog: {
|
||||
name: "Verbose Log",
|
||||
desc: "Show verbose log. Please enable when you report the logs",
|
||||
desc: "Show verbose log. Please enable if you report an issue.",
|
||||
},
|
||||
hashCacheMaxCount: {
|
||||
name: "Memory cache size (by total items)",
|
||||
@@ -105,19 +105,19 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
|
||||
},
|
||||
syncOnSave: {
|
||||
name: "Sync on Save",
|
||||
desc: "When you save a file, sync automatically",
|
||||
desc: "Starts synchronisation when a file is saved.",
|
||||
},
|
||||
syncOnEditorSave: {
|
||||
name: "Sync on Editor Save",
|
||||
desc: "When you save a file in the editor, sync automatically",
|
||||
desc: "When you save a file in the editor, start a sync automatically",
|
||||
},
|
||||
syncOnFileOpen: {
|
||||
name: "Sync on File Open",
|
||||
desc: "When you open a file, sync automatically",
|
||||
desc: "Forces the file to be synced when opened.",
|
||||
},
|
||||
syncOnStart: {
|
||||
name: "Sync on Start",
|
||||
desc: "Start synchronization after launching Obsidian.",
|
||||
name: "Sync on Startup",
|
||||
desc: "Automatically Sync all files when opening Obsidian.",
|
||||
},
|
||||
syncAfterMerge: {
|
||||
name: "Sync after merging file",
|
||||
@@ -125,29 +125,31 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
|
||||
},
|
||||
trashInsteadDelete: {
|
||||
name: "Use the trash bin",
|
||||
desc: "Do not delete files that are deleted in remote, just move to trash.",
|
||||
desc: "Move remotely deleted files to the trash, instead of deleting.",
|
||||
},
|
||||
doNotDeleteFolder: {
|
||||
name: "Keep empty folder",
|
||||
desc: "Normally, a folder is deleted when it becomes empty after a synchronization. Enabling this will prevent it from getting deleted",
|
||||
desc: "Should we keep folders that don't have any files inside?",
|
||||
},
|
||||
resolveConflictsByNewerFile: {
|
||||
name: "Always overwrite with a newer file (beta)",
|
||||
desc: "(Def off) Resolve conflicts by newer files automatically.",
|
||||
name: "(BETA) Always overwrite with a newer file",
|
||||
desc: "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.",
|
||||
},
|
||||
checkConflictOnlyOnOpen: {
|
||||
name: "Postpone resolution of inactive files",
|
||||
name: "Delay conflict resolution of inactive files",
|
||||
desc: "Should we only check for conflicts when a file is opened?",
|
||||
},
|
||||
showMergeDialogOnlyOnActive: {
|
||||
name: "Postpone manual resolution of inactive files",
|
||||
name: "Delay merge conflict prompt for inactive files.",
|
||||
desc: "Should we prompt you about conflicting files when a file is opened?",
|
||||
},
|
||||
disableMarkdownAutoMerge: {
|
||||
name: "Always resolve conflicts manually",
|
||||
desc: "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)",
|
||||
name: "Always prompt merge conflicts",
|
||||
desc: "Should we prompt you for every single merge, even if we can safely merge automatcially?",
|
||||
},
|
||||
writeDocumentsIfConflicted: {
|
||||
name: "Always reflect synchronized changes even if the note has a conflict",
|
||||
desc: "Turn on to previous behavior",
|
||||
name: "Apply Latest Change if Conflicting",
|
||||
desc: "Enable this option to automatically apply the most recent change to documents even when it conflicts",
|
||||
},
|
||||
syncInternalFilesInterval: {
|
||||
name: "Scan hidden files periodically",
|
||||
@@ -171,11 +173,11 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
|
||||
},
|
||||
ignoreFiles: {
|
||||
name: "Ignore files",
|
||||
desc: "We can use multiple ignore files, e.g.) `.gitignore, .dockerignore`",
|
||||
desc: "Comma separated `.gitignore, .dockerignore`",
|
||||
},
|
||||
batch_size: {
|
||||
name: "Batch size",
|
||||
desc: "Number of change feed items to process at a time. Defaults to 50. Minimum is 2.",
|
||||
desc: "Number of changes to sync at a time. Defaults to 50. Minimum is 2.",
|
||||
},
|
||||
batches_limit: {
|
||||
name: "Batch limit",
|
||||
@@ -193,7 +195,7 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
|
||||
},
|
||||
suspendFileWatching: {
|
||||
name: "Suspend file watching",
|
||||
desc: "Stop watching for file change.",
|
||||
desc: "Stop watching for file changes.",
|
||||
},
|
||||
suspendParseReplicationResult: {
|
||||
name: "Suspend database reflecting",
|
||||
@@ -259,7 +261,7 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
|
||||
},
|
||||
useCustomRequestHandler: {
|
||||
name: "Use Custom HTTP Handler",
|
||||
desc: "If your Object Storage could not configured accepting CORS, enable this.",
|
||||
desc: "Enable this if your Object Storage doesn't support CORS",
|
||||
},
|
||||
maxChunksInEden: {
|
||||
name: "Maximum Incubating Chunks",
|
||||
@@ -275,7 +277,7 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
|
||||
},
|
||||
settingSyncFile: {
|
||||
name: "Filename",
|
||||
desc: "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.",
|
||||
desc: "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.",
|
||||
},
|
||||
preset: {
|
||||
name: "Presets",
|
||||
@@ -356,7 +358,7 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
|
||||
// desc: "Enable advanced mode"
|
||||
},
|
||||
usePowerUserMode: {
|
||||
name: "Enable power user features",
|
||||
name: "Enable poweruser features",
|
||||
// desc: "Enable power user mode",
|
||||
// level: LEVEL_ADVANCED
|
||||
},
|
||||
@@ -365,7 +367,11 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
|
||||
},
|
||||
enableDebugTools: {
|
||||
name: "Enable Developers' Debug Tools.",
|
||||
desc: "You need a restart to apply this setting.",
|
||||
desc: "Requires restart of Obsidian",
|
||||
},
|
||||
suppressNotifyHiddenFilesChange: {
|
||||
name: "Suppress notification of hidden files change",
|
||||
desc: "If enabled, the notification of hidden files change will be suppressed.",
|
||||
},
|
||||
};
|
||||
function translateInfo(infoSrc: ConfigurationItem | undefined | false) {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
export interface Confirm {
|
||||
askYesNo(message: string): Promise<"yes" | "no">;
|
||||
askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise<string | false>;
|
||||
|
||||
askYesNoDialog(
|
||||
message: string,
|
||||
opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number }
|
||||
): Promise<"yes" | "no">;
|
||||
|
||||
askSelectString(message: string, items: string[]): Promise<string>;
|
||||
|
||||
askSelectStringDialogue(
|
||||
message: string,
|
||||
buttons: string[],
|
||||
opt: { title?: string; defaultAction: (typeof buttons)[number]; timeout?: number }
|
||||
): Promise<(typeof buttons)[number] | false>;
|
||||
|
||||
askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void;
|
||||
|
||||
confirmWithMessage(
|
||||
title: string,
|
||||
contentMd: string,
|
||||
buttons: string[],
|
||||
defaultAction: (typeof buttons)[number],
|
||||
timeout?: number
|
||||
): Promise<(typeof buttons)[number] | false>;
|
||||
}
|
||||
@@ -8,37 +8,29 @@ import {
|
||||
EVENT_SETTING_SAVED,
|
||||
eventHub,
|
||||
} from "../../common/events.ts";
|
||||
import { $f, setLang } from "../../lib/src/common/i18n.ts";
|
||||
import { $msg, setLang } from "../../lib/src/common/i18n.ts";
|
||||
import { versionNumberString2Number } from "../../lib/src/string_and_binary/convert.ts";
|
||||
import { cancelAllPeriodicTask, cancelAllTasks } from "octagonal-wheels/concurrency/task";
|
||||
import { stopAllRunningProcessors } from "octagonal-wheels/concurrency/processor";
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
import type { ICoreModule } from "../ModuleTypes.ts";
|
||||
import { EVENT_PLATFORM_UNLOADED } from "../../lib/src/PlatformAPIs/base/APIBase.ts";
|
||||
|
||||
export class ModuleLiveSyncMain extends AbstractModule implements ICoreModule {
|
||||
async $$onLiveSyncReady() {
|
||||
if (!(await this.core.$everyOnLayoutReady())) return;
|
||||
eventHub.emitEvent(EVENT_LAYOUT_READY);
|
||||
if (this.settings.suspendFileWatching || this.settings.suspendParseReplicationResult) {
|
||||
const ANSWER_KEEP = "Keep this plug-in suspended";
|
||||
const ANSWER_RESUME = "Resume and restart Obsidian";
|
||||
const message = `Self-hosted LiveSync has been configured to ignore some events. Is this intentional for you?
|
||||
|
||||
| Type | Status | Note |
|
||||
|:---:|:---:|---|
|
||||
| Storage Events | ${this.settings.suspendFileWatching ? "suspended" : "active"} | Every modification will be ignored |
|
||||
| Database Events | ${this.settings.suspendParseReplicationResult ? "suspended" : "active"} | Every synchronised change will be postponed |
|
||||
|
||||
Do you want to resume them and restart Obsidian?
|
||||
|
||||
> [!DETAILS]-
|
||||
> These flags are set by the plug-in while rebuilding, or fetching. If the process ends abnormally, it may be kept unintended.
|
||||
> If you are not sure, you can try to rerun these processes. Make sure to back your vault up.
|
||||
`;
|
||||
const ANSWER_KEEP = $msg("moduleLiveSyncMain.optionKeepLiveSyncDisabled");
|
||||
const ANSWER_RESUME = $msg("moduleLiveSyncMain.optionResumeAndRestart");
|
||||
const message = $msg("moduleLiveSyncMain.msgScramEnabled", {
|
||||
fileWatchingStatus: this.settings.suspendFileWatching ? "suspended" : "active",
|
||||
parseReplicationStatus: this.settings.suspendParseReplicationResult ? "suspended" : "active",
|
||||
});
|
||||
if (
|
||||
(await this.core.confirm.askSelectStringDialogue(message, [ANSWER_KEEP, ANSWER_RESUME], {
|
||||
defaultAction: ANSWER_KEEP,
|
||||
title: "Scram Enabled",
|
||||
title: $msg("moduleLiveSyncMain.titleScramEnabled"),
|
||||
})) == ANSWER_RESUME
|
||||
) {
|
||||
this.settings.suspendFileWatching = false;
|
||||
@@ -56,11 +48,11 @@ Do you want to resume them and restart Obsidian?
|
||||
if (!(await this.core.$everyOnFirstInitialize())) return;
|
||||
await this.core.$$realizeSettingSyncMode();
|
||||
fireAndForget(async () => {
|
||||
this._log(`Additional safety scan..`, LOG_LEVEL_VERBOSE);
|
||||
this._log($msg("moduleLiveSyncMain.logAdditionalSafetyScan"), LOG_LEVEL_VERBOSE);
|
||||
if (!(await this.core.$allScanStat())) {
|
||||
this._log(`Additional safety scan has been failed on some module`, LOG_LEVEL_NOTICE);
|
||||
this._log($msg("moduleLiveSyncMain.logSafetyScanFailed"), LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
this._log(`Additional safety scan done`, LOG_LEVEL_VERBOSE);
|
||||
this._log($msg("moduleLiveSyncMain.logSafetyScanCompleted"), LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -80,9 +72,9 @@ Do you want to resume them and restart Obsidian?
|
||||
this.$$wireUpEvents();
|
||||
// debugger;
|
||||
eventHub.emitEvent(EVENT_PLUGIN_LOADED, this.core);
|
||||
this._log("loading plugin");
|
||||
this._log($msg("moduleLiveSyncMain.logLoadingPlugin"));
|
||||
if (!(await this.core.$everyOnloadStart())) {
|
||||
this._log("Plugin initialising has been cancelled by some module", LOG_LEVEL_NOTICE);
|
||||
this._log($msg("moduleLiveSyncMain.logPluginInitCancelled"), LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
// this.addUIs();
|
||||
@@ -91,10 +83,10 @@ Do you want to resume them and restart Obsidian?
|
||||
//@ts-ignore
|
||||
const packageVersion: string = PACKAGE_VERSION || "0.0.0";
|
||||
|
||||
this._log($f`Self-hosted LiveSync${" v"}${manifestVersion} ${packageVersion}`);
|
||||
this._log($msg("moduleLiveSyncMain.logPluginVersion", { manifestVersion, packageVersion }));
|
||||
await this.core.$$loadSettings();
|
||||
if (!(await this.core.$everyOnloadAfterLoadSettings())) {
|
||||
this._log("Plugin initialising has been cancelled by some module", LOG_LEVEL_NOTICE);
|
||||
this._log($msg("moduleLiveSyncMain.logPluginInitCancelled"), LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
const lsKey = "obsidian-live-sync-ver" + this.core.$$getVaultName();
|
||||
@@ -102,7 +94,7 @@ Do you want to resume them and restart Obsidian?
|
||||
|
||||
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
|
||||
if (lastVersion > this.settings.lastReadUpdates && this.settings.isConfigured) {
|
||||
this._log($f`You have some unread release notes! Please read them once!`, LOG_LEVEL_NOTICE);
|
||||
this._log($msg("moduleLiveSyncMain.logReadChangelog"), LOG_LEVEL_NOTICE);
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
@@ -117,7 +109,7 @@ Do you want to resume them and restart Obsidian?
|
||||
this.settings.syncOnFileOpen = false;
|
||||
this.settings.syncAfterMerge = false;
|
||||
this.settings.periodicReplication = false;
|
||||
this.settings.versionUpFlash = $f`Self-hosted LiveSync has been upgraded and some behaviors have changed incompatibly. All automatic synchronization is now disabled temporary. Ensure that other devices are also upgraded, and enable synchronization again.`;
|
||||
this.settings.versionUpFlash = $msg("moduleLiveSyncMain.logVersionUpdate");
|
||||
await this.saveSettings();
|
||||
}
|
||||
localStorage.setItem(lsKey, `${VER}`);
|
||||
@@ -148,7 +140,9 @@ Do you want to resume them and restart Obsidian?
|
||||
}
|
||||
await this.localDatabase.close();
|
||||
}
|
||||
this._log($f`unloading plugin`);
|
||||
eventHub.emitEvent(EVENT_PLATFORM_UNLOADED);
|
||||
eventHub.offAll();
|
||||
this._log($msg("moduleLiveSyncMain.logUnloadingPlugin"));
|
||||
}
|
||||
|
||||
async $$realizeSettingSyncMode(): Promise<void> {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
/* min-height: 280px; */
|
||||
max-height: 280px;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
.op-pre {
|
||||
@@ -376,7 +377,6 @@ span.ls-mark-cr::after {
|
||||
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;
|
||||
|
||||
@@ -5,6 +5,9 @@ if you want to view the source, please visit the github repository of this plugi
|
||||
`;
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
/***
|
||||
* @type import("terser").MinifyOptions
|
||||
*/
|
||||
const terserOption = {
|
||||
sourceMap: !prod
|
||||
? {
|
||||
@@ -28,7 +31,6 @@ const terserOption = {
|
||||
evaluate: true,
|
||||
dead_code: true,
|
||||
// directives: true,
|
||||
// conditionals: true,
|
||||
inline: 3,
|
||||
join_vars: true,
|
||||
loops: true,
|
||||
@@ -38,12 +40,25 @@ const terserOption = {
|
||||
arrows: true,
|
||||
collapse_vars: true,
|
||||
comparisons: true,
|
||||
//@ts-ignore
|
||||
lhs_constants: true,
|
||||
hoist_props: true,
|
||||
side_effects: true,
|
||||
ecma: 2018,
|
||||
// hoist_vars: true,
|
||||
// hoist_funs: true,
|
||||
if_return: true,
|
||||
// unsafe_math: true,
|
||||
unused: true,
|
||||
// --
|
||||
typeofs: true,
|
||||
properties: true,
|
||||
module: true,
|
||||
booleans: true,
|
||||
conditionals: true,
|
||||
hoist_funs: true,
|
||||
hoist_vars: true,
|
||||
// toplevel: "vars",
|
||||
},
|
||||
mangle: false,
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"target": "ES2018",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["svelte", "node"],
|
||||
// "importsNotUsedAsValues": "error",
|
||||
"importHelpers": false,
|
||||
@@ -16,7 +16,11 @@
|
||||
"noEmit": true,
|
||||
"lib": ["es2018", "DOM", "ES5", "ES6", "ES7", "es2019.array", "ES2021.WeakRef", "ES2020.BigInt", "ESNext.Intl"],
|
||||
"strictBindCallApply": true,
|
||||
"strictFunctionTypes": true
|
||||
"strictFunctionTypes": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@lib/*": ["src/lib/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["pouchdb-browser-webpack", "utils"]
|
||||
|
||||
216
updates.md
216
updates.md
@@ -1,110 +1,136 @@
|
||||
## 0.24.0
|
||||
## 0.24.11
|
||||
|
||||
I know that we have been waiting for a long time. It is finally released!
|
||||
Peer-to-peer synchronisation has been implemented!
|
||||
|
||||
Over the past three years since the inception of the plugin, various features have been implemented to address diverse user needs. This is truly honourable, and I am grateful for your years of support. However, this process has led to an increasingly disorganised codebase, with features becoming entangled. Consequently, this has led to a situation where bugs can go unnoticed and resolving one issue may inadvertently introduce another.
|
||||
Until now, I have not provided a synchronisation server. More people may not even know that I have shut down the test server. I confess that this is a bit repetitive, but I confess it is a cautionary tale. This is out of a sense of self-discipline that someone has occurred who could see your data. Even if the 'someone' is me. I should not be unaware of its superiority, even though well-meaning and am a servant of all. (Half joking, but also serious).
|
||||
However, now I can provide you with a signalling server. Because, to the best of my knowledge, it is only the network that is connected to your device.
|
||||
Also, this signalling server is just a Nostr relay, not my implementation. You can run your implementation, which you consider trustworthy, on a trustworthy server. You do not even have to trust me. Mate, it is great, isn't it? For your information, strfry is running on my signalling server.
|
||||
|
||||
In 0.24.0, I reorganised the previously jumbled main codebase into clearly defined modules. Although I had assumed that the total size of the code would not increase, I discovered that it has in fact increased. While the complexity is still considerable, the refactoring has improved the clarity of the code's structure. Additionally, while testing the release candidates, we still found many bugs to fix, which helped to make this plug-in robust and stable. Therefore, we are now ready to use the updated plug-in, and in addition to that, proceed to the next step.
|
||||
Nevertheless, that being said, to be more honest, I still have not decided what to do with this signalling server if too much traffic comes in.
|
||||
|
||||
This is also the first step towards a fully-fledged-fancy LiveSync, not just a plug-in from Obsidian. Of course, it will still be a plug-in primarily and foremost, but this development marks a significant step towards the self-hosting concept.
|
||||
Note: Already you have noticed this, but let me mention it again, this is a significantly large update. If you have noticed anything, please let me know. I will try to fix it as soon as possible (Some address is on my [profile](https://github.com/vrtmrz)).
|
||||
|
||||
Finally, I would like to once again express my respect and gratitude to all of you. My gratitude extends to all of the dev testers! Your contributions have certainly made the plug-in robust and stable!
|
||||
## 0.24.13
|
||||
|
||||
Thank you, and I hope your troubles will be resolved!
|
||||
|
||||
---
|
||||
|
||||
## 0.24.2
|
||||
|
||||
#### Rewritten
|
||||
|
||||
- Hidden File Sync is now respects the file changes on the storage. Not simply comparing modified times.
|
||||
- This makes hidden file sync more robust and reliable.
|
||||
|
||||
#### Fixed
|
||||
|
||||
- `Scan hidden files before replication` is now configurable again.
|
||||
- Some unexpected errors are now handled more gracefully.
|
||||
- Meaningless event passing during boot sequence is now prevented.
|
||||
- Error handling for non-existing files has been fixed.
|
||||
- Hidden files will not be batched to avoid the potential error.
|
||||
- This behaviour had been causing the error in the previous versions in specific situations.
|
||||
- The log which checking automatic conflict resolution is now in verbose level.
|
||||
- Replication log (skipping non-targetting files) shows the correct information.
|
||||
- The dialogue that asking enabling optional feature during `Rebuild Everything` now prevents to show the `overwrite` option.
|
||||
- The rebuilding device is the first, meaningless.
|
||||
- Files with different modified time but identical content are no longer processed repeatedly.
|
||||
- Some unexpected errors which caused after terminating plug-in are now avoided.
|
||||
-
|
||||
|
||||
#### Improved
|
||||
|
||||
- JSON files are now more transferred efficiently.
|
||||
- Now the JSON files are transferred in more fine chunks, which makes the transfer more efficient.
|
||||
|
||||
## 0.24.1
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Vault History can show the correct information of match-or-not for each file and database even if it is a binary file.
|
||||
- `Sync settings via markdown` is now hidden during the setup wizard.
|
||||
- Verify and Fix will ignore the hidden files if the hidden file sync is disabled.
|
||||
|
||||
#### New feature
|
||||
|
||||
- Now we can fetch the tweaks from the remote database while the setting dialogue and wizard are processing.
|
||||
|
||||
#### Improved
|
||||
|
||||
- More things are moved to the modules.
|
||||
- Includes the Main codebase. Now `main.ts` is almost stub.
|
||||
- EventHub is now more robust and typesafe.
|
||||
|
||||
## 0.24.0
|
||||
|
||||
### Improved
|
||||
|
||||
- The welcome message is now more simple to encourage the use of the Setup-URI.
|
||||
- The secondary message is also simpler to guide users to Minimal Setup.
|
||||
- But Setup-URI will be recommended again, due to its importance.
|
||||
- These dialogues contain a link to the documentation which can be clicked.
|
||||
- The minimal setup is more minimal now. And, the setup is more user-friendly.
|
||||
- Now the Configuration of the remote database is checked more robustly, but we can ignore the warning and proceed with the setup.
|
||||
- Before we are asked about each feature, we are asked if we want to use optional features in the first place.
|
||||
- This is to prevent the user from being overwhelmed by the features.
|
||||
- And made it clear that it is not recommended for new users.
|
||||
- Many messages have been improved for better understanding.
|
||||
- Ridiculous messages have been (carefully) refined.
|
||||
- Dialogues are more informative and friendly.
|
||||
- A lot of messages have been mostly rewritten, leveraging Markdown.
|
||||
- Especially auto-closing dialogues are now explicitly labelled: `To stop the countdown, tap anywhere on the dialogue`.
|
||||
- Now if the is plugin configured to ignore some events, we will get a chance to fix it, in addition to the warning.
|
||||
- And why that has happened is also explained in the dialogue.
|
||||
- A note relating to device names has been added to Customisation Sync on the setting dialogue.
|
||||
- We can verify and resolve also the hidden files now.
|
||||
Sorry for the lack of replies. The ones that were not good are popping up, so I am just going to go ahead and get this one... However, they realised that refactoring and restructuring is about clarifying the problem. Your patience and understanding is much appreciated.
|
||||
|
||||
### Fixed
|
||||
|
||||
- We can resolve the conflict of the JSON file correctly now.
|
||||
- Verifying files between the local database and storage is now working correctly.
|
||||
- While restarting the plug-in, the shown dialogues will be automatically closed to avoid unexpected behaviour.
|
||||
- Replicated documents that the local device has configured to ignore are now correctly ignored.
|
||||
- The chunks of the document on the local device during the first transfer will be created correctly.
|
||||
- And why we should create them is now explained in the dialogue.
|
||||
- If optional features have been enabled in the wizard, `Enable advanced features` will be toggled correctly.
|
||||
The hidden file sync is now working correctly. - Now the deletion of hidden files is correctly synchronised.
|
||||
- Customisation Sync is now working correctly together with hidden file sync.
|
||||
- No longer database suffix is stored in the setting sharing markdown.
|
||||
- A fair number of bugs have been fixed.
|
||||
#### General Replication
|
||||
|
||||
### Changed
|
||||
- No longer unexpected errors occur when the replication is stopped during for some reason (e.g., network disconnection).
|
||||
|
||||
- Some default settings have been changed for an easier new user experience.
|
||||
- Preventing the meaningless migration of the settings.
|
||||
#### Peer-to-Peer Synchronisation
|
||||
|
||||
### Tiding
|
||||
- Set-up process will not receive data from unexpected sources.
|
||||
- No longer resource leaks while enabling the `broadcasting changes`
|
||||
- Logs are less verbose.
|
||||
- Received data is now correctly dispatched to other devices.
|
||||
- `Timeout` error now more informative.
|
||||
- No longer timeout error occurs for reporting the progress to other devices.
|
||||
- Decision dialogues for the same thing are not shown multiply at the same time anymore.
|
||||
- Disconnection of the peer-to-peer synchronisation is now more robust and less error-prone.
|
||||
|
||||
- The codebase has been reorganised into clearly defined modules.
|
||||
- Commented-out codes have been gradually removed.
|
||||
#### Webpeer
|
||||
|
||||
- Now we can toggle Peers' configuration.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Cross-platform compatibility layer has been improved.
|
||||
- Common events are moved to the common library.
|
||||
- Displaying replication status of the peer-to-peer synchronisation is separated from the main-log-logic.
|
||||
- Some file names have been changed to be more consistent.
|
||||
|
||||
## 0.24.12
|
||||
|
||||
I created a SPA called [webpeer](https://github.com/vrtmrz/livesync-commonlib/tree/main/apps/webpeer) (well, right... I will think of a name again), which replaces the server when using Peer-to-Peer synchronisation. This is a pseudo-client that appears to other devices as if it were one of the clients. . As with the client, it receives and sends data without storing it as a file.
|
||||
And, this is just a single web page, without any server-side code. It is a static web page that can be hosted on any static web server, such as GitHub Pages, Netlify, or Vercel. All you have to do is to open the page and enter several items, and leave it open.
|
||||
|
||||
### Fixed
|
||||
|
||||
- No longer unnecessary acknowledgements are sent when starting peer-to-peer synchronisation.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Platform impedance-matching-layer has been improved.
|
||||
- And you can see the actual usage of this on [webpeer](https://github.com/vrtmrz/livesync-commonlib/tree/main/apps/webpeer) that a pseudo client for peer-to-peer synchronisation.
|
||||
- Some UIs have been got isomorphic among Obsidian and web applications (for `webpeer`).
|
||||
|
||||
## 0.24.11
|
||||
|
||||
### Improved
|
||||
|
||||
- New Translation: `es` (Spanish) by @zeedif (Thank you so much)!
|
||||
- Now all of messages can be selectable and copyable, also on the iPhone, iPad, and Android devices. Now we can copy or share the messages easily.
|
||||
|
||||
### New Feature
|
||||
|
||||
- Peer-to-Peer Synchronisation has been implemented!
|
||||
- This feature is still in early beta, and it is recommended to use it with caution.
|
||||
- However, it is a significant step towards the self-hosting concept. It is now possible to synchronise your data without using any remote database or storage. It is a direct connection between your devices.
|
||||
- Note: We should keep the device online to synchronise the data. It is not a background synchronisation. Also it needs a signalling server to establish the connection. But, the signalling server is used only for establishing the connection, and it does not store any data.
|
||||
|
||||
### Fixed
|
||||
|
||||
- No longer memory or resource leaks when the plug-in is disabled.
|
||||
- Now deleted chunks are correctly detected on conflict resolution, and we are guided to resurrect them.
|
||||
- Hanging issue during the initial synchronisation has been fixed.
|
||||
- Some unnecessary logs have been removed.
|
||||
- Now all modal dialogues are correctly closed when the plug-in is disabled.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Several interfaces have been moved to the separated library.
|
||||
- Translations have been moved to each language file, and during the build, they are merged into one file.
|
||||
- Non-mobile friendly code has been removed and replaced with the safer code.
|
||||
- (Now a days, mostly server-side engine can use webcrypto, so it will be rewritten in the future more).
|
||||
- Started writing Platform impedance-matching-layer.
|
||||
- Svelte has been updated to v5.
|
||||
- Some function have got more robust type definitions.
|
||||
- Terser optimisation has slightly improved.
|
||||
- During the build, analysis meta-file of the bundled codes will be generated.
|
||||
|
||||
## 0.24.10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the issue which the filename is shown as `undefined`.
|
||||
- Fixed the issue where files transferred at short intervals were not reflected.
|
||||
|
||||
### Improved
|
||||
|
||||
- Add more translations: `ja-JP` (Japanese) by @kohki-shikata (Thank you so much)!
|
||||
|
||||
### Internal
|
||||
|
||||
- Some files have been prettified.
|
||||
|
||||
## 0.24.9
|
||||
|
||||
Skipped.
|
||||
|
||||
## 0.24.8
|
||||
|
||||
### Fixed
|
||||
|
||||
- Some parallel-processing tasks are now performed more safely.
|
||||
- Some error messages has been fixed.
|
||||
|
||||
### Improved
|
||||
|
||||
- Synchronisation is now more efficient and faster.
|
||||
- Saving chunks is a bit more robust.
|
||||
|
||||
### New Feature
|
||||
|
||||
- We can remove orphaned chunks again, now!
|
||||
- Without rebuilding the database!
|
||||
- Note: Please synchronise devices completely before removing orphaned chunks.
|
||||
- Note2: Deleted files are using chunks, if you want to remove them, please commit the deletion first. (`Commit File Deletion`)
|
||||
- Note3: If you lost some chunks, do not worry. They will be resurrected if not so much time has passed. Try `Resurrect deleted chunks`.
|
||||
- Note4: This feature is still beta. Please report any issues you encounter.
|
||||
- Note5: Please disable `On demand chunk fetching`, and enable `Compute revisions for each chunk` before using this feature.
|
||||
- These settings is going to be default in the future.
|
||||
|
||||
Older notes are in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md).
|
||||
|
||||
1808
updates_old.md
1808
updates_old.md
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user