mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-02-27 06:28:47 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2289bea8d9 | ||
|
|
cda90259c5 | ||
|
|
432a211f80 | ||
|
|
eaf8c4998e | ||
|
|
55601f7910 | ||
|
|
13e70475d9 | ||
|
|
2572177879 | ||
|
|
e82a2560e4 |
18
README.md
18
README.md
@@ -34,11 +34,10 @@ Useful for researchers, engineers and developers with a need to keep their notes
|
||||
|
||||
### Get your database ready.
|
||||
|
||||
First, get your database ready. IBM Cloudant is preferred for testing. Or you can use your own server with CouchDB. For more information, refer below:
|
||||
1. [Setup IBM Cloudant](docs/setup_cloudant.md)
|
||||
2. [Setup your CouchDB](docs/setup_own_server.md)
|
||||
|
||||
Note: More information about alternative hosting methods is needed! Currently, [using fly.io](https://github.com/vrtmrz/obsidian-livesync/discussions/85) is being discussed.
|
||||
First, get your database ready. fly.io is preferred for testing. Or you can use your own server with CouchDB. For more information, refer below:
|
||||
1. [Setup fly.io](docs/setup_flyio.md)
|
||||
2. [Setup IBM Cloudant](docs/setup_cloudant.md)
|
||||
3. [Setup your CouchDB](docs/setup_own_server.md)
|
||||
|
||||
### Configure the plugin
|
||||
|
||||
@@ -54,12 +53,6 @@ Please open the configuration link again and Answer below:
|
||||
|
||||
If you answered `No` to both, your databases will be rebuilt by the content on your device. And the remote database will lock out other devices. You have to synchronize all your devices again. (When this time, almost all your files should be synchronized with a timestamp. So you can use an existing vault).
|
||||
|
||||
## Test Server
|
||||
|
||||
~~Setting up an instance of Cloudant or local CouchDB is a little complicated, so I set up a [Tasting server for self-hosted-livesync](https://olstaste.vrtmrz.net/). Try it out for free!~~
|
||||
Now (30 May 2023) suspending while the server transfer.
|
||||
Note: Please read "Limitations" carefully. Do not send your private vault.
|
||||
|
||||
## Information in StatusBar
|
||||
|
||||
Synchronization status is shown in statusbar.
|
||||
@@ -94,6 +87,9 @@ If you have deleted or renamed files, please wait until ⏳ icon disappeared.
|
||||
|
||||
Repo is here: [obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip). (Docs are a work in progress.)
|
||||
|
||||
## Troubleshooting
|
||||
If you are having problems getting the plugin working see: [Troubleshooting](docs/troubleshooting.md)
|
||||
|
||||
## License
|
||||
|
||||
The source code is licensed under the MIT License.
|
||||
|
||||
287
docs/setup_flyio.md
Normal file
287
docs/setup_flyio.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# Setup CouchDB on fly.io
|
||||
|
||||
In some cases, the use of IBM Cloudant was found to be hard. We looked for alternatives, but there were no services available. Therefore, we have to build our own servers, which is quite a challenge. In this situation, with fly.io, most of the processes are simplified and only CouchDB needs to be configured.
|
||||
|
||||
This is how to configure fly.io and CouchDB on it for Self-hosted LiveSync.
|
||||
|
||||
It generally falls within the `Free Allowances` range in most cases.
|
||||
|
||||
**[Automatic setup using Colaboratory](#automatic-setup-using-colaboratory) is recommended, after reading this document through. It is reproducible and hard to fail.**
|
||||
|
||||
## Required materials
|
||||
|
||||
- A valid credit or debit card.
|
||||
|
||||
## Warning
|
||||
|
||||
- It will be `your` instance. Check the log regularly.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
For simplicity, the following description assumes that the settings are as shown in the table below. Please read it in accordance with the actual settings you wish to make.
|
||||
|
||||
| Used in the text | Meaning and where to use | Memo |
|
||||
| ---------------- | --------------------------- | ------------------------------------------------------------------------ |
|
||||
| campanella | Username | It is less likely to fail if it consists only of letters and numbers. |
|
||||
| dfusiuada9suy | Password | |
|
||||
| nrt | Region to make the instance | We can use any [region](https://fly.io/docs/reference/regions/) near us. |
|
||||
|
||||
## Steps with your computer
|
||||
|
||||
If you want to avoid installing anything, please skip to [Automatic setup using Colaboratory](#automatic-setup-using-colaboratory).
|
||||
|
||||
### 1. Install flyctl
|
||||
|
||||
- Mac or Linux
|
||||
|
||||
```sh
|
||||
$ curl -L https://fly.io/install.sh | sh
|
||||
```
|
||||
|
||||
- Windows
|
||||
|
||||
```powershell
|
||||
$ iwr https://fly.io/install.ps1 -useb | iex
|
||||
```
|
||||
|
||||
### 2. Sign up or Sign in to fly.io
|
||||
|
||||
- Sign up
|
||||
|
||||
```bash
|
||||
$ fly auth signup
|
||||
```
|
||||
|
||||
- Sign in
|
||||
|
||||
```bash
|
||||
$ fly auth login
|
||||
```
|
||||
|
||||
For more information, please refer [Sign up](https://fly.io/docs/hands-on/sign-up/) and [Sign in](https://fly.io/docs/hands-on/sign-in/).
|
||||
|
||||
### 3. Make configuration files
|
||||
|
||||
Please be careful, `nrt` is the region where near to Japan. Please use your preferred region.
|
||||
|
||||
1. Make fly.toml
|
||||
|
||||
```
|
||||
$ flyctl launch --generate-name --detach --no-deploy --region nrt
|
||||
Creating app in /home/vrtmrz/dev/fly/demo
|
||||
Scanning source code
|
||||
Could not find a Dockerfile, nor detect a runtime or framework from source code. Continuing with a blank app.
|
||||
automatically selected personal organization: vorotamoroz
|
||||
App will use 'nrt' region as primary
|
||||
|
||||
Created app 'billowing-dawn-6619' in organization 'personal'
|
||||
Admin URL: https://fly.io/apps/billowing-dawn-6619
|
||||
Hostname: billowing-dawn-6619.fly.dev
|
||||
Wrote config file fly.toml
|
||||
```
|
||||
|
||||
`billowing-dawn-6619` is an automatically generated name. It is used as the hostname. Please note it in something.
|
||||
Note: we can specify this without `--generate-name`, but does not recommend in the trial phases.
|
||||
|
||||
1. Make volume
|
||||
|
||||
```
|
||||
$ flyctl volumes create --region nrt couchdata --size 2 --yes
|
||||
ID: vol_g67340kxgmmvydxw
|
||||
Name: couchdata
|
||||
App: billowing-dawn-6619
|
||||
Region: nrt
|
||||
Zone: 35b7
|
||||
Size GB: 2
|
||||
Encrypted: true
|
||||
Created at: 02 Jun 23 01:19 UTC
|
||||
```
|
||||
|
||||
3. Edit fly.toml
|
||||
Changes:
|
||||
- Change exposing port from `8080` to `5984`
|
||||
- Mounting the volume `couchdata` created in step 2 under `/opt/couchdb/data`
|
||||
- Set `campanella` for the administrator of CouchDB
|
||||
- Customise CouchDB to use persistent ini-file; which is located under the data directory.
|
||||
- To use Dockerfile
|
||||
|
||||
```diff
|
||||
# fly.toml app configuration file generated for billowing-dawn-6619 on 2023-06-02T10:18:59+09:00
|
||||
#
|
||||
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
||||
#
|
||||
|
||||
app = "billowing-dawn-6619"
|
||||
primary_region = "nrt"
|
||||
|
||||
[http_service]
|
||||
- internal_port = 8080
|
||||
+ internal_port = 5984
|
||||
force_https = true
|
||||
auto_stop_machines = true
|
||||
auto_start_machines = true
|
||||
min_machines_running = 0
|
||||
|
||||
+[mounts]
|
||||
+ source="couchdata"
|
||||
+ destination="/opt/couchdb/data"
|
||||
+
|
||||
+[env]
|
||||
+ COUCHDB_USER = "campanella"
|
||||
+ ERL_FLAGS="-couch_ini /opt/couchdb/etc/default.ini /opt/couchdb/etc/default.d/ /opt/couchdb/etc/local.d /opt/couchdb/etc/local.ini /opt/couchdb/data/persistence.ini"
|
||||
+
|
||||
+[build]
|
||||
+ dockerfile = "./Dockerfile"
|
||||
```
|
||||
|
||||
4. Make `Dockerfile`
|
||||
Create a Dockerfile that patches the start-up script to fix ini file permissions.
|
||||
|
||||
```dockerfile
|
||||
FROM couchdb:latest
|
||||
RUN sed -i '2itouch /opt/couchdb/data/persistence.ini && chmod +w /opt/couchdb/data/persistence.ini' /docker-entrypoint.sh
|
||||
```
|
||||
|
||||
5. Set credential
|
||||
|
||||
```
|
||||
flyctl secrets set COUCHDB_PASSWORD=dfusiuada9suy
|
||||
```
|
||||
|
||||
### 4. Deploy
|
||||
|
||||
```
|
||||
$ flyctl deploy --detach --remote-only
|
||||
==> Verifying app config
|
||||
Validating /home/vrtmrz/dev/fly/demo/fly.toml
|
||||
Platform: machines
|
||||
✓ Configuration is valid
|
||||
--> Verified app config
|
||||
==> Building image
|
||||
Remote builder fly-builder-bold-sky-4515 ready
|
||||
==> Creating build context
|
||||
--> Creating build context done
|
||||
==> Building image with Docker
|
||||
--> docker host: 20.10.12 linux x86_64
|
||||
|
||||
-------------:SNIPPED:-------------
|
||||
|
||||
Watch your app at https://fly.io/apps/billowing-dawn-6619/monitoring
|
||||
|
||||
Provisioning ips for billowing-dawn-6619
|
||||
Dedicated ipv6: 2a09:8280:1::2d:240f
|
||||
Shared ipv4: 66.241.125.213
|
||||
Add a dedicated ipv4 with: fly ips allocate-v4
|
||||
This deployment will:
|
||||
* create 1 "app" machine
|
||||
|
||||
No machines in group app, launching a new machine
|
||||
Machine e7845d1f297183 [app] has state: started
|
||||
Finished launching new machines
|
||||
|
||||
NOTE: The machines for [app] have services with 'auto_stop_machines = true' that will be stopped when idling
|
||||
```
|
||||
|
||||
Now your CouchDB has been launched. (Do not forget to delete it if no longer need).
|
||||
If failed, please check by `flyctl doctor`. Failure of remote build may be resolved by `flyctl` wireguard reset` or something.
|
||||
|
||||
```
|
||||
$ flyctl status
|
||||
App
|
||||
Name = billowing-dawn-6619
|
||||
Owner = personal
|
||||
Hostname = billowing-dawn-6619.fly.dev
|
||||
Image = billowing-dawn-6619:deployment-01H1WWB3CK5Z9ZX71KHBSDGHF1
|
||||
Platform = machines
|
||||
|
||||
Machines
|
||||
PROCESS ID VERSION REGION STATE CHECKS LAST UPDATED
|
||||
app e7845d1f297183 1 nrt started 2023-06-02T01:43:34Z
|
||||
```
|
||||
|
||||
### 5. Apply CouchDB configuration
|
||||
|
||||
After the initial setup, CouchDB needs some more customisations to be used from Self-hosted LiveSync. It can be configured in browsers or by HTTP-REST APIs.
|
||||
|
||||
This section is set up using the REST API.
|
||||
|
||||
1. Prepare environment variables.
|
||||
|
||||
- Mac or Linux:
|
||||
|
||||
```bash
|
||||
export couchHost=https://billowing-dawn-6619.fly.dev
|
||||
export couchUser=campanella
|
||||
export couchPwd=dfusiuada9suy
|
||||
```
|
||||
|
||||
- Windows
|
||||
|
||||
```powershell
|
||||
set couchHost https://billowing-dawn-6619.fly.dev
|
||||
set couchUser campanella
|
||||
set couchPwd dfusiuada9suy
|
||||
$creds = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("${couchUser}:${couchPwd}"))
|
||||
```
|
||||
|
||||
2. Perform cluster setup
|
||||
|
||||
- Mac or Linux
|
||||
|
||||
```bash
|
||||
curl -X POST "${couchHost}/_cluster_setup" -H "Content-Type: application/json" -d "{\"action\":\"enable_single_node\",\"username\":\"${couchUser}\",\"password\":\"${couchPwd}\",\"bind_address\":\"0.0.0.0\",\"port\":5984,\"singlenode\":true}" --user "${couchUser}:${couchPwd}"
|
||||
```
|
||||
|
||||
- Windows
|
||||
|
||||
```powershell
|
||||
iwr -UseBasicParsing -Method 'POST' -ContentType 'application/json; charset=utf-8' -Headers @{ 'Authorization' = 'Basic ' + $creds } "${couchHost}/_cluster_setup" -Body "{""action"":""enable_single_node"",""username"":""${couchUser}"",""password"":""${couchPwd}"",""bind_address"":""0.0.0.0"",""port"":5984,""singlenode"":true}"
|
||||
```
|
||||
|
||||
Note: if the response code is not 200. We have to retry the request once again.
|
||||
If you run the request several times and it does not result in 200, something is wrong. Please report it.
|
||||
|
||||
3. Configure parameters
|
||||
|
||||
- Mac or Linux
|
||||
|
||||
```bash
|
||||
curl -X PUT "${couchHost}/_node/nonode@nohost/_config/chttpd/require_valid_user" -H "Content-Type: application/json" -d '"true"' --user "${couchUser}:${couchPwd}"
|
||||
curl -X PUT "${couchHost}/_node/nonode@nohost/_config/chttpd_auth/require_valid_user" -H "Content-Type: application/json" -d '"true"' --user "${couchUser}:${couchPwd}"
|
||||
curl -X PUT "${couchHost}/_node/nonode@nohost/_config/httpd/WWW-Authenticate" -H "Content-Type: application/json" -d '"Basic realm=\"couchdb\""' --user "${couchUser}:${couchPwd}"
|
||||
curl -X PUT "${couchHost}/_node/nonode@nohost/_config/httpd/enable_cors" -H "Content-Type: application/json" -d '"true"' --user "${couchUser}:${couchPwd}"
|
||||
curl -X PUT "${couchHost}/_node/nonode@nohost/_config/chttpd/enable_cors" -H "Content-Type: application/json" -d '"true"' --user "${couchUser}:${couchPwd}"
|
||||
curl -X PUT "${couchHost}/_node/nonode@nohost/_config/chttpd/max_http_request_size" -H "Content-Type: application/json" -d '"4294967296"' --user "${couchUser}:${couchPwd}"
|
||||
curl -X PUT "${couchHost}/_node/nonode@nohost/_config/couchdb/max_document_size" -H "Content-Type: application/json" -d '"50000000"' --user "${couchUser}:${couchPwd}"
|
||||
curl -X PUT "${couchHost}/_node/nonode@nohost/_config/cors/credentials" -H "Content-Type: application/json" -d '"true"' --user "${couchUser}:${couchPwd}"
|
||||
curl -X PUT "${couchHost}/_node/nonode@nohost/_config/cors/origins" -H "Content-Type: application/json" -d '"app://obsidian.md,capacitor://localhost,http://localhost"' --user "${couchUser}:${couchPwd}"
|
||||
```
|
||||
|
||||
- Windows
|
||||
|
||||
```powershell
|
||||
iwr -UseBasicParsing -Method 'PUT' -ContentType 'application/json; charset=utf-8' -Headers @{ 'Authorization' = 'Basic ' + $creds } "${couchHost}/_node/nonode@nohost/_config/chttpd/require_valid_user" -Body '"true"'
|
||||
iwr -UseBasicParsing -Method 'PUT' -ContentType 'application/json; charset=utf-8' -Headers @{ 'Authorization' = 'Basic ' + $creds } "${couchHost}/_node/nonode@nohost/_config/chttpd_auth/require_valid_user" -Body '"true"'
|
||||
iwr -UseBasicParsing -Method 'PUT' -ContentType 'application/json; charset=utf-8' -Headers @{ 'Authorization' = 'Basic ' + $creds } "${couchHost}/_node/nonode@nohost/_config/httpd/WWW-Authenticate" -Body '"Basic realm=\"couchdb\""'
|
||||
iwr -UseBasicParsing -Method 'PUT' -ContentType 'application/json; charset=utf-8' -Headers @{ 'Authorization' = 'Basic ' + $creds } "${couchHost}/_node/nonode@nohost/_config/httpd/enable_cors" -Body '"true"'
|
||||
iwr -UseBasicParsing -Method 'PUT' -ContentType 'application/json; charset=utf-8' -Headers @{ 'Authorization' = 'Basic ' + $creds } "${couchHost}/_node/nonode@nohost/_config/chttpd/enable_cors" -Body '"true"'
|
||||
iwr -UseBasicParsing -Method 'PUT' -ContentType 'application/json; charset=utf-8' -Headers @{ 'Authorization' = 'Basic ' + $creds } "${couchHost}/_node/nonode@nohost/_config/chttpd/max_http_request_size" -Body '"4294967296"'
|
||||
iwr -UseBasicParsing -Method 'PUT' -ContentType 'application/json; charset=utf-8' -Headers @{ 'Authorization' = 'Basic ' + $creds } "${couchHost}/_node/nonode@nohost/_config/couchdb/max_document_size" -Body '"50000000"'
|
||||
iwr -UseBasicParsing -Method 'PUT' -ContentType 'application/json; charset=utf-8' -Headers @{ 'Authorization' = 'Basic ' + $creds } "${couchHost}/_node/nonode@nohost/_config/cors/credentials" -Body '"true"'
|
||||
iwr -UseBasicParsing -Method 'PUT' -ContentType 'application/json; charset=utf-8' -Headers @{ 'Authorization' = 'Basic ' + $creds } "${couchHost}/_node/nonode@nohost/_config/cors/origins" -Body '"app://obsidian.md,capacitor://localhost,http://localhost"'
|
||||
```
|
||||
|
||||
Note: Each of these should also be repeated until finished in 200.
|
||||
|
||||
### 6. Use it from Self-hosted LiveSync
|
||||
|
||||
Now the CouchDB is ready to use from Self-hosted LiveSync. We can use `https://billowing-dawn-6619.fly.dev` in URI, `campanella` in `Username` and `dfusiuada9suy` in `Password` on Self-hosted LiveSync. `Database name` could be anything you want.
|
||||
`Enhance chunk size` could be up to around `100`.
|
||||
|
||||
## Automatic setup using Colaboratory
|
||||
|
||||
We can perform all these steps by using [this Colaboratory notebook](https://gist.github.com/vrtmrz/b437a539af25ef191bd452aae369242f) without installing anything.
|
||||
|
||||
## After testing / before creating a new instance
|
||||
|
||||
**Be sure to delete the instance. We can check instances on the [Dashboard](https://fly.io/dashboard/personal)**
|
||||
12
docs/troubleshooting.md
Normal file
12
docs/troubleshooting.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Info
|
||||
In this document some tips for solving issues will be given.
|
||||
|
||||
|
||||
# Issue with syncing with mobile device
|
||||
- If you are getting problem like could not sync files when doing initial sync. Try lower batch size
|
||||
- Open setting
|
||||
- Open Live Sync settings
|
||||
- Go to sync settings
|
||||
- Go down to Advanced settings.
|
||||
- Lower "Batch size" and "Batch limit" and try to sync again.
|
||||
- If you keep getting error keep lowering until you find the sweet spot.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.19.7",
|
||||
"version": "0.19.8",
|
||||
"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",
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.19.7",
|
||||
"version": "0.19.8",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.19.7",
|
||||
"version": "0.19.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"diff-match-patch": "^1.0.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.19.7",
|
||||
"version": "0.19.8",
|
||||
"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",
|
||||
|
||||
@@ -482,6 +482,10 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
async storeCustomizationFiles(path: FilePath, termOverRide?: string) {
|
||||
const term = termOverRide || this.plugin.deviceAndVaultName;
|
||||
if (term == "") {
|
||||
Logger("We have to configure the device name", LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
const vf = this.filenameToUnifiedKey(path, term);
|
||||
return await runWithLock(`plugin-${vf}`, false, async () => {
|
||||
const category = this.getFileCategory(path);
|
||||
|
||||
@@ -41,7 +41,7 @@ export class SetupLiveSync extends LiveSyncCommands {
|
||||
async realizeSettingSyncMode() { }
|
||||
|
||||
async command_copySetupURI() {
|
||||
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "");
|
||||
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "", true);
|
||||
if (encryptingPassphrase === false)
|
||||
return;
|
||||
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
|
||||
@@ -57,7 +57,7 @@ export class SetupLiveSync extends LiveSyncCommands {
|
||||
Logger("Setup URI copied to clipboard", LOG_LEVEL.NOTICE);
|
||||
}
|
||||
async command_copySetupURIFull() {
|
||||
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "");
|
||||
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "", true);
|
||||
if (encryptingPassphrase === false)
|
||||
return;
|
||||
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
|
||||
@@ -81,7 +81,7 @@ export class SetupLiveSync extends LiveSyncCommands {
|
||||
async setupWizard(confString: string) {
|
||||
try {
|
||||
const oldConf = JSON.parse(JSON.stringify(this.settings));
|
||||
const encryptingPassphrase = await askString(this.app, "Passphrase", "The passphrase to decrypt your setup URI", "");
|
||||
const encryptingPassphrase = await askString(this.app, "Passphrase", "The passphrase to decrypt your setup URI", "", true);
|
||||
if (encryptingPassphrase === false)
|
||||
return;
|
||||
const newConf = await JSON.parse(await decrypt(confString, encryptingPassphrase, false));
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getPathFromTFile, isValidPath } from "./utils";
|
||||
import { base64ToArrayBuffer, base64ToString, escapeStringToHTML } from "./lib/src/strbin";
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
||||
import { DocumentID, FilePathWithPrefix, LoadedEntry, LOG_LEVEL } from "./lib/src/types";
|
||||
import { type DocumentID, type FilePathWithPrefix, type LoadedEntry, LOG_LEVEL } from "./lib/src/types";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { isErrorOfMissingDoc } from "./lib/src/utils_couchdb";
|
||||
import { getDocData } from "./lib/src/utils";
|
||||
@@ -24,12 +24,14 @@ export class DocumentHistoryModal extends Modal {
|
||||
currentDoc: LoadedEntry;
|
||||
currentText = "";
|
||||
currentDeleted = false;
|
||||
initialRev: string;
|
||||
|
||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | FilePathWithPrefix, id: DocumentID) {
|
||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | FilePathWithPrefix, id: DocumentID, revision?: string) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.file = (file instanceof TFile) ? getPathFromTFile(file) : file;
|
||||
this.id = id;
|
||||
this.initialRev = revision;
|
||||
if (!file) {
|
||||
this.file = this.plugin.id2path(id, null);
|
||||
}
|
||||
@@ -38,7 +40,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
}
|
||||
}
|
||||
|
||||
async loadFile() {
|
||||
async loadFile(initialRev: string) {
|
||||
if (!this.id) {
|
||||
this.id = await this.plugin.path2id(this.file);
|
||||
}
|
||||
@@ -49,7 +51,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.range.max = `${this.revs_info.length - 1}`;
|
||||
this.range.value = this.range.max;
|
||||
this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`);
|
||||
await this.loadRevs();
|
||||
await this.loadRevs(initialRev);
|
||||
} catch (ex) {
|
||||
if (isErrorOfMissingDoc(ex)) {
|
||||
this.range.max = "0";
|
||||
@@ -63,18 +65,27 @@ export class DocumentHistoryModal extends Modal {
|
||||
}
|
||||
}
|
||||
}
|
||||
async loadRevs() {
|
||||
async loadRevs(initialRev?: string) {
|
||||
if (this.revs_info.length == 0) return;
|
||||
const db = this.plugin.localDatabase;
|
||||
if (initialRev) {
|
||||
const rIndex = this.revs_info.findIndex(e => e.rev == initialRev);
|
||||
if (rIndex >= 0) {
|
||||
this.range.value = `${this.revs_info.length - 1 - rIndex}`;
|
||||
}
|
||||
}
|
||||
const index = this.revs_info.length - 1 - (this.range.value as any) / 1;
|
||||
const rev = this.revs_info[index];
|
||||
const w = await db.getDBEntry(this.file, { rev: rev.rev }, false, false, true);
|
||||
await this.showExactRev(rev.rev);
|
||||
}
|
||||
async showExactRev(rev: string) {
|
||||
const db = this.plugin.localDatabase;
|
||||
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
||||
this.currentText = "";
|
||||
this.currentDeleted = false;
|
||||
if (w === false) {
|
||||
this.currentDeleted = true;
|
||||
this.info.innerHTML = "";
|
||||
this.contentView.innerHTML = `Could not read this revision<br>(${rev.rev})`;
|
||||
this.contentView.innerHTML = `Could not read this revision<br>(${rev})`;
|
||||
} else {
|
||||
this.currentDoc = w;
|
||||
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
||||
@@ -158,7 +169,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
.addClass("op-info");
|
||||
this.info = contentEl.createDiv("");
|
||||
this.info.addClass("op-info");
|
||||
this.loadFile();
|
||||
this.loadFile(this.initialRev);
|
||||
const div = contentEl.createDiv({ text: "Loading old revisions..." });
|
||||
this.contentView = div;
|
||||
div.addClass("op-scrollable");
|
||||
|
||||
328
src/GlobalHistory.svelte
Normal file
328
src/GlobalHistory.svelte
Normal file
@@ -0,0 +1,328 @@
|
||||
<script lang="ts">
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import type { AnyEntry, FilePathWithPrefix } from "./lib/src/types";
|
||||
import { getDocData, isDocContentSame } from "./lib/src/utils";
|
||||
import { diff_match_patch } from "diff-match-patch";
|
||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||
import { isPlainText, stripAllPrefixes } from "./lib/src/path";
|
||||
import { TFile } from "./deps";
|
||||
import { arrayBufferToBase64 } from "./lib/src/strbin";
|
||||
export let plugin: ObsidianLiveSyncPlugin;
|
||||
|
||||
let showDiffInfo = false;
|
||||
let showChunkCorrected = false;
|
||||
let checkStorageDiff = false;
|
||||
|
||||
let range_from_epoch = Date.now() - 3600000 * 24 * 7;
|
||||
let range_to_epoch = Date.now() + 3600000 * 24 * 2;
|
||||
const timezoneOffset = new Date().getTimezoneOffset();
|
||||
let dispDateFrom = new Date(range_from_epoch - timezoneOffset).toISOString().split("T")[0];
|
||||
let dispDateTo = new Date(range_to_epoch - timezoneOffset).toISOString().split("T")[0];
|
||||
$: {
|
||||
range_from_epoch = new Date(dispDateFrom).getTime() + timezoneOffset;
|
||||
range_to_epoch = new Date(dispDateTo).getTime() + timezoneOffset;
|
||||
|
||||
getHistory(showDiffInfo, showChunkCorrected, checkStorageDiff);
|
||||
}
|
||||
function mtimeToDate(mtime: number) {
|
||||
return new Date(mtime).toLocaleString();
|
||||
}
|
||||
|
||||
type HistoryData = {
|
||||
id: string;
|
||||
rev: string;
|
||||
path: string;
|
||||
dirname: string;
|
||||
filename: string;
|
||||
mtime: number;
|
||||
mtimeDisp: string;
|
||||
isDeleted: boolean;
|
||||
size: number;
|
||||
changes: string;
|
||||
chunks: string;
|
||||
isPlain: boolean;
|
||||
};
|
||||
let history = [] as HistoryData[];
|
||||
let loading = false;
|
||||
|
||||
async function fetchChanges(): Promise<HistoryData[]> {
|
||||
try {
|
||||
const db = plugin.localDatabase;
|
||||
let result = [] as typeof history;
|
||||
for await (const docA of db.findAllNormalDocs()) {
|
||||
if (docA.mtime < range_from_epoch) {
|
||||
continue;
|
||||
}
|
||||
if (docA.type != "newnote" && docA.type != "plain") continue;
|
||||
const path = plugin.getPath(docA as AnyEntry);
|
||||
const isPlain = isPlainText(docA.path);
|
||||
const revs = await db.getRaw(docA._id, { revs_info: true });
|
||||
let p: string = undefined;
|
||||
const reversedRevs = revs._revs_info.reverse();
|
||||
const DIFF_DELETE = -1;
|
||||
|
||||
const DIFF_EQUAL = 0;
|
||||
const DIFF_INSERT = 1;
|
||||
|
||||
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);
|
||||
if (doc === false) continue;
|
||||
const rev = revInfo.rev;
|
||||
|
||||
const mtime = "mtime" in doc ? doc.mtime : 0;
|
||||
if (range_from_epoch > mtime) {
|
||||
continue;
|
||||
}
|
||||
if (range_to_epoch < mtime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let diffDetail = "";
|
||||
if (showDiffInfo && !isPlain) {
|
||||
const data = getDocData(doc.data);
|
||||
if (p === undefined) {
|
||||
p = data;
|
||||
}
|
||||
if (p != data) {
|
||||
const dmp = new diff_match_patch();
|
||||
const diff = dmp.diff_main(p, data);
|
||||
dmp.diff_cleanupSemantic(diff);
|
||||
p = data;
|
||||
const pxinit = {
|
||||
[DIFF_DELETE]: 0,
|
||||
[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);
|
||||
diffDetail = `-${px[DIFF_DELETE]}, +${px[DIFF_INSERT]}`;
|
||||
}
|
||||
}
|
||||
const isDeleted = doc._deleted || (doc as any)?.deleted || false;
|
||||
if (isDeleted) {
|
||||
diffDetail += " 🗑️";
|
||||
}
|
||||
if (rev == docA._rev) {
|
||||
if (checkStorageDiff) {
|
||||
const abs = plugin.app.vault.getAbstractFileByPath(stripAllPrefixes(plugin.getPath(docA)));
|
||||
if (abs instanceof TFile) {
|
||||
let result = false;
|
||||
if (isPlainText(docA.path)) {
|
||||
const data = await plugin.app.vault.read(abs);
|
||||
result = isDocContentSame(data, doc.data);
|
||||
} else {
|
||||
const data = await plugin.app.vault.readBinary(abs);
|
||||
const dataEEncoded = await arrayBufferToBase64(data);
|
||||
result = isDocContentSame(dataEEncoded, doc.data);
|
||||
}
|
||||
if (result) {
|
||||
diffDetail += " ⚖️";
|
||||
} else {
|
||||
diffDetail += " ⚠️";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const docPath = plugin.getPath(doc as AnyEntry);
|
||||
const [filename, ...pathItems] = docPath.split("/").reverse();
|
||||
|
||||
let chunksStatus = "";
|
||||
if (showChunkCorrected) {
|
||||
const chunks = (doc as any)?.children ?? [];
|
||||
const loadedChunks = await db.allDocsRaw({ keys: [...chunks] });
|
||||
const totalCount = loadedChunks.rows.length;
|
||||
const errorCount = loadedChunks.rows.filter((e) => "error" in e).length;
|
||||
if (errorCount == 0) {
|
||||
chunksStatus = `✅ ${totalCount}`;
|
||||
} else {
|
||||
chunksStatus = `🔎 ${errorCount} ✅ ${totalCount}`;
|
||||
}
|
||||
}
|
||||
|
||||
result.push({
|
||||
id: doc._id,
|
||||
rev: doc._rev,
|
||||
path: docPath,
|
||||
dirname: pathItems.reverse().join("/"),
|
||||
filename: filename,
|
||||
mtime: mtime,
|
||||
mtimeDisp: mtimeToDate(mtime),
|
||||
size: (doc as any)?.size ?? 0,
|
||||
isDeleted: isDeleted,
|
||||
changes: diffDetail,
|
||||
chunks: chunksStatus,
|
||||
isPlain: isPlain,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...result].sort((a, b) => b.mtime - a.mtime);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
async function getHistory(showDiffInfo: boolean, showChunkCorrected: boolean, checkStorageDiff: boolean) {
|
||||
loading = true;
|
||||
const newDisplay = [];
|
||||
const page = await fetchChanges();
|
||||
newDisplay.push(...page);
|
||||
history = [...newDisplay];
|
||||
}
|
||||
|
||||
function nextWeek() {
|
||||
dispDateTo = new Date(range_to_epoch - timezoneOffset + 3600 * 1000 * 24 * 7).toISOString().split("T")[0];
|
||||
}
|
||||
function prevWeek() {
|
||||
dispDateFrom = new Date(range_from_epoch - timezoneOffset - 3600 * 1000 * 24 * 7).toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await getHistory(showDiffInfo, showChunkCorrected, checkStorageDiff);
|
||||
});
|
||||
onDestroy(() => {});
|
||||
|
||||
function showHistory(file: string, rev: string) {
|
||||
new DocumentHistoryModal(plugin.app, plugin, file as unknown as FilePathWithPrefix, null, rev).open();
|
||||
}
|
||||
function openFile(file: string) {
|
||||
plugin.app.workspace.openLinkText(file, file);
|
||||
}
|
||||
</script>
|
||||
|
||||
<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="">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>
|
||||
</div>
|
||||
</div>
|
||||
{#if loading}
|
||||
<div class="">Gathering information...</div>
|
||||
{/if}
|
||||
<table>
|
||||
<tr>
|
||||
<th> Date </th>
|
||||
<th> Path </th>
|
||||
<th> Rev </th>
|
||||
<th> Stat </th>
|
||||
{#if showChunkCorrected}
|
||||
<th> Chunks </th>
|
||||
{/if}
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="5" class="more">
|
||||
{#if loading}
|
||||
<div class="" />
|
||||
{:else}
|
||||
<div><button on:click={() => nextWeek()}>+1 week</button></div>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{#each history as entry}
|
||||
<tr>
|
||||
<td class="mtime">
|
||||
{entry.mtimeDisp}
|
||||
</td>
|
||||
<td class="path">
|
||||
<div class="filenames">
|
||||
<span class="path">/{entry.dirname.split("/").join(`/`)}</span>
|
||||
<span class="filename"><a on:click={() => openFile(entry.path)}>{entry.filename}</a></span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="rev">
|
||||
{#if entry.isPlain}
|
||||
<a on:click={() => showHistory(entry.path, entry.rev)}>{entry.rev}</a>
|
||||
{:else}
|
||||
{entry.rev}
|
||||
{/if}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{entry.changes}
|
||||
</td>
|
||||
{#if showChunkCorrected}
|
||||
<td>
|
||||
{entry.chunks}
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
<tr>
|
||||
<td colspan="5" class="more">
|
||||
{#if loading}
|
||||
<div class="" />
|
||||
{:else}
|
||||
<div><button on:click={() => prevWeek()}>+1 week</button></div>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.globalhistory {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
.more > div {
|
||||
display: flex;
|
||||
}
|
||||
.more > div > button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
td.mtime {
|
||||
white-space: break-spaces;
|
||||
}
|
||||
td.path {
|
||||
word-break: break-word;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.row > label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 5em;
|
||||
}
|
||||
.row > input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.filenames {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.filenames > .path {
|
||||
font-size: 70%;
|
||||
}
|
||||
.rev {
|
||||
text-overflow: ellipsis;
|
||||
max-width: 3em;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
47
src/GlobalHistoryView.ts
Normal file
47
src/GlobalHistoryView.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
ItemView,
|
||||
WorkspaceLeaf
|
||||
} from "./deps";
|
||||
import GlobalHistoryComponent from "./GlobalHistory.svelte";
|
||||
import type ObsidianLiveSyncPlugin from "./main";
|
||||
|
||||
export const VIEW_TYPE_GLOBAL_HISTORY = "global-history";
|
||||
export class GlobalHistoryView extends ItemView {
|
||||
|
||||
component: GlobalHistoryComponent;
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
icon: "clock";
|
||||
title: string;
|
||||
navigation: true;
|
||||
|
||||
getIcon(): string {
|
||||
return "clock";
|
||||
}
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
|
||||
getViewType() {
|
||||
return VIEW_TYPE_GLOBAL_HISTORY;
|
||||
}
|
||||
|
||||
getDisplayText() {
|
||||
return "Vault history";
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
this.component = new GlobalHistoryComponent({
|
||||
target: this.contentEl,
|
||||
props: {
|
||||
plugin: this.plugin,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
this.component.$destroy();
|
||||
}
|
||||
}
|
||||
81
src/LogPane.svelte
Normal file
81
src/LogPane.svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { logMessageStore } from "./lib/src/stores";
|
||||
|
||||
let unsubscribe: () => void;
|
||||
let messages = [] as string[];
|
||||
let wrapRight = false;
|
||||
let autoScroll = true;
|
||||
let suspended = false;
|
||||
|
||||
onMount(async () => {
|
||||
unsubscribe = logMessageStore.observe((e) => {
|
||||
if (!suspended) {
|
||||
messages = [...e];
|
||||
if (autoScroll) {
|
||||
if (scroll) scroll.scrollTop = scroll.scrollHeight;
|
||||
}
|
||||
}
|
||||
});
|
||||
logMessageStore.invalidate();
|
||||
setTimeout(() => {
|
||||
if (scroll) scroll.scrollTop = scroll.scrollHeight;
|
||||
}, 100);
|
||||
});
|
||||
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>
|
||||
</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;
|
||||
}
|
||||
</style>
|
||||
46
src/LogPaneView.ts
Normal file
46
src/LogPaneView.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
ItemView,
|
||||
WorkspaceLeaf
|
||||
} from "obsidian";
|
||||
import LogPaneComponent from "./LogPane.svelte";
|
||||
import type ObsidianLiveSyncPlugin from "./main";
|
||||
export const VIEW_TYPE_LOG = "log-log";
|
||||
// Show notes as like scroll.
|
||||
export class LogPaneView extends ItemView {
|
||||
|
||||
component: LogPaneComponent;
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
icon: "view-log";
|
||||
title: string;
|
||||
navigation: true;
|
||||
|
||||
getIcon(): string {
|
||||
return "view-log";
|
||||
}
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
|
||||
getViewType() {
|
||||
return VIEW_TYPE_LOG;
|
||||
}
|
||||
|
||||
getDisplayText() {
|
||||
return "Self-hosted LiveSync Log";
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
this.component = new LogPaneComponent({
|
||||
target: this.contentEl,
|
||||
props: {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
this.component.$destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FilePath } from "./lib/src/types";
|
||||
import { type FilePath } from "./lib/src/types";
|
||||
|
||||
export {
|
||||
addIcon, App, DataWriteOptions, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginManifest,
|
||||
PluginSettingTab, Plugin_2, requestUrl, RequestUrlParam, RequestUrlResponse, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder,
|
||||
parseYaml
|
||||
addIcon, App, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginSettingTab, Plugin_2, requestUrl, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder,
|
||||
parseYaml, ItemView, WorkspaceLeaf
|
||||
} from "obsidian";
|
||||
export type { DataWriteOptions, PluginManifest, RequestUrlParam, RequestUrlResponse } from "obsidian";
|
||||
import {
|
||||
normalizePath as normalizePath_
|
||||
} from "obsidian";
|
||||
|
||||
@@ -43,13 +43,15 @@ export class InputStringDialog extends Modal {
|
||||
key: string;
|
||||
placeholder: string;
|
||||
isManuallyClosed = false;
|
||||
isPassword = false;
|
||||
|
||||
constructor(app: App, title: string, key: string, placeholder: string, onSubmit: (result: string | false) => void) {
|
||||
constructor(app: App, title: string, key: string, placeholder: string, isPassword: boolean, onSubmit: (result: string | false) => void) {
|
||||
super(app);
|
||||
this.onSubmit = onSubmit;
|
||||
this.title = title;
|
||||
this.placeholder = placeholder;
|
||||
this.key = key;
|
||||
this.isPassword = isPassword;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
@@ -58,7 +60,7 @@ export class InputStringDialog extends Modal {
|
||||
contentEl.createEl("h1", { text: this.title });
|
||||
// For enter to submit
|
||||
const formEl = contentEl.createEl("form");
|
||||
new Setting(formEl).setName(this.key).addText((text) =>
|
||||
new Setting(formEl).setName(this.key).setClass(this.isPassword ? "password-input" : "normal-input").addText((text) =>
|
||||
text.onChange((value) => {
|
||||
this.result = value;
|
||||
})
|
||||
|
||||
49
src/main.ts
49
src/main.ts
@@ -7,7 +7,6 @@ import { type InternalFileInfo, type queueItem, type CacheData, type FileEventIt
|
||||
import { getDocData, isDocContentSame, Parallels } from "./lib/src/utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||
import { LogDisplayModal } from "./LogDisplayModal";
|
||||
import { ConflictResolveModal } from "./ConflictResolveModal";
|
||||
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||
@@ -30,6 +29,8 @@ import { HiddenFileSync } from "./CmdHiddenFileSync";
|
||||
import { SetupLiveSync } from "./CmdSetupLiveSync";
|
||||
import { ConfigSync } from "./CmdConfigSync";
|
||||
import { confirmWithMessage } from "./dialogs";
|
||||
import { GlobalHistoryView, VIEW_TYPE_GLOBAL_HISTORY } from "./GlobalHistoryView";
|
||||
import { LogPaneView, VIEW_TYPE_LOG } from "./LogPaneView";
|
||||
|
||||
setNoticeClass(Notice);
|
||||
|
||||
@@ -539,7 +540,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
});
|
||||
|
||||
this.addRibbonIcon("view-log", "Show log", () => {
|
||||
new LogDisplayModal(this.app, this).open();
|
||||
this.showView(VIEW_TYPE_LOG);
|
||||
});
|
||||
|
||||
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
|
||||
@@ -650,8 +651,44 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
},
|
||||
})
|
||||
|
||||
this.registerView(
|
||||
VIEW_TYPE_GLOBAL_HISTORY,
|
||||
(leaf) => new GlobalHistoryView(leaf, this)
|
||||
);
|
||||
this.registerView(
|
||||
VIEW_TYPE_LOG,
|
||||
(leaf) => new LogPaneView(leaf, this)
|
||||
);
|
||||
this.addCommand({
|
||||
id: "livesync-global-history",
|
||||
name: "Show vault history",
|
||||
callback: () => {
|
||||
this.showGlobalHistory()
|
||||
}
|
||||
})
|
||||
}
|
||||
async showView(viewType: string) {
|
||||
const leaves = this.app.workspace.getLeavesOfType(viewType);
|
||||
if (leaves.length == 0) {
|
||||
await this.app.workspace.getLeaf(true).setViewState({
|
||||
type: viewType,
|
||||
active: true,
|
||||
});
|
||||
} else {
|
||||
leaves[0].setViewState({
|
||||
type: viewType,
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
if (leaves.length > 0) {
|
||||
this.app.workspace.revealLeaf(
|
||||
leaves[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
showGlobalHistory() {
|
||||
this.showView(VIEW_TYPE_GLOBAL_HISTORY);
|
||||
}
|
||||
|
||||
|
||||
onunload() {
|
||||
for (const addOn of this.addOns) {
|
||||
@@ -1003,7 +1040,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
}
|
||||
|
||||
//--> Basic document Functions
|
||||
notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {};
|
||||
notifies: { [key: string]: { notice: Notice; timer: ReturnType<typeof setTimeout>; count: number } } = {};
|
||||
|
||||
lastLog = "";
|
||||
// eslint-disable-next-line require-await
|
||||
@@ -1382,7 +1419,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
|
||||
//---> Sync
|
||||
async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> {
|
||||
const docsSorted = docs.sort((a, b) => b.mtime - a.mtime);
|
||||
const docsSorted = docs.sort((a: any, b: any) => b?.mtime ?? 0 - a?.mtime ?? 0);
|
||||
L1:
|
||||
for (const change of docsSorted) {
|
||||
if (isChunk(change._id)) {
|
||||
@@ -1471,7 +1508,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
this.statusBar.title = e.syncStatus;
|
||||
let waiting = "";
|
||||
if (this.settings.batchSave && !this.settings.liveSync) {
|
||||
const len = this.vaultManager.getQueueLength();
|
||||
const len = this.vaultManager?.getQueueLength();
|
||||
if (len != 0) {
|
||||
waiting = ` 🛫${len}`;
|
||||
}
|
||||
|
||||
@@ -405,9 +405,9 @@ export const askSelectString = (app: App, message: string, items: string[]): Pro
|
||||
};
|
||||
|
||||
|
||||
export const askString = (app: App, title: string, key: string, placeholder: string): Promise<string | false> => {
|
||||
export const askString = (app: App, title: string, key: string, placeholder: string, isPassword?: boolean): Promise<string | false> => {
|
||||
return new Promise((res) => {
|
||||
const dialog = new InputStringDialog(app, title, key, placeholder, (result) => res(result));
|
||||
const dialog = new InputStringDialog(app, title, key, placeholder, isPassword, (result) => res(result));
|
||||
dialog.open();
|
||||
});
|
||||
};
|
||||
@@ -737,4 +737,4 @@ export const remoteDatabaseCleanup = async (plugin: ObsidianLiveSyncPlugin, dryR
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,4 +255,8 @@ div.sls-setting-menu-btn {
|
||||
|
||||
.sls-setting-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.password-input > .setting-item-control >input {
|
||||
-webkit-text-security: disc;
|
||||
}
|
||||
|
||||
@@ -62,5 +62,13 @@ I hope you will give it a try.
|
||||
- Fixed:
|
||||
- The initial pane of Setting dialogue is now changed to General Settings.
|
||||
- The Setup Wizard is now able to flush existing settings and get into the mode again.
|
||||
- 0.19.8
|
||||
- New feature:
|
||||
- Vault history: A tab has been implemented to give a birds-eye view of the changes that have occurred in the vault.
|
||||
- Improved:
|
||||
- Now the passphrases on the dialogue masked out. Thank you @antoKeinanen!
|
||||
- Log dialogue is now shown as one of tabs.
|
||||
- Fixed:
|
||||
- Some minor issues has been fixed.
|
||||
|
||||
... To continue on to `updates_old.md`.
|
||||
|
||||
Reference in New Issue
Block a user