mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-21 06:41:33 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4835fce62a | ||
|
|
ff814be4a0 | ||
|
|
b271b63efa | ||
|
|
23419e476a | ||
|
|
b9bd1f17b8 | ||
|
|
bcce277c36 | ||
|
|
5acbbe479e | ||
|
|
c9f9d511e0 | ||
|
|
b8cb94c498 | ||
|
|
52c736f6b9 | ||
|
|
ebd1cb7777 | ||
|
|
10decb7909 | ||
|
|
e0aab8d69d | ||
|
|
618600c753 | ||
|
|
d1aba87e37 | ||
|
|
db889f635e | ||
|
|
dd80e634f5 | ||
|
|
bec6fc1a74 |
322
deploy_couchdb_to_flyio_v2_with_swap.ipynb
Normal file
322
deploy_couchdb_to_flyio_v2_with_swap.ipynb
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {
|
||||||
|
"id": "view-in-github",
|
||||||
|
"colab_type": "text"
|
||||||
|
},
|
||||||
|
"source": [
|
||||||
|
"<a href=\"https://colab.research.google.com/gist/vrtmrz/37c3efd7842e49947aaaa7f665e5020a/deploy_couchdb_to_flyio_v2_with_swap.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"History:\n",
|
||||||
|
"- 18, May, 2023: Initial.\n",
|
||||||
|
"- 19, Jun., 2023: Patched for enabling swap.\n",
|
||||||
|
"- 22, Aug., 2023: Generating Setup-URI implemented."
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"id": "HiRV7G8Gk1Rs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"id": "2Vh0mEQEZuAK"
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Configurations\n",
|
||||||
|
"import os\n",
|
||||||
|
"os.environ['region']=\"nrt\"\n",
|
||||||
|
"os.environ['couchUser']=\"alkcsa93\"\n",
|
||||||
|
"os.environ['couchPwd']=\"c349usdfnv48fsasd\""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"id": "SPmbB0jZauQ1"
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Delete once\n",
|
||||||
|
"!rm ./fly.toml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"id": "Nze7QoxLZ7Yx"
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Installation\n",
|
||||||
|
"# You have to set up your account in here.\n",
|
||||||
|
"!curl -L https://fly.io/install.sh | sh\n",
|
||||||
|
"!/root/.fly/bin/flyctl auth signup"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"id": "MVJwsIYrbgtx"
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Generate server\n",
|
||||||
|
"!/root/.fly/bin/flyctl launch --auto-confirm --generate-name --detach --no-deploy --region ${region}\n",
|
||||||
|
"!/root/.fly/bin/fly volumes create --region ${region} couchdata --size 2 --yes"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"# Check the toml once.\n",
|
||||||
|
"!cat fly.toml"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"id": "2RSoO9o-i2TT"
|
||||||
|
},
|
||||||
|
"execution_count": null,
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"id": "zUtPZLVnbvdQ"
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Modify fly.toml\n",
|
||||||
|
"## Port modification\n",
|
||||||
|
"!sed -i 's/8080/5984/g' fly.toml\n",
|
||||||
|
"## Add user into.\n",
|
||||||
|
"!echo -e \"\\n[env]\\n COUCHDB_USER = \\\"${couchUser}\\\"\" >> ./fly.toml\n",
|
||||||
|
"## Set the location of an ini file which to save configurations persistently via erlang flags.\n",
|
||||||
|
"!echo -e \"\\nERL_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\\\"\" >> ./fly.toml\n",
|
||||||
|
"## Mounting volumes to store data and ini file.\n",
|
||||||
|
"!echo -e \"\\n[mounts]\\n source=\\\"couchdata\\\"\\n destination=\\\"/opt/couchdb/data\\\"\" >> ./fly.toml\n",
|
||||||
|
"!cat fly.toml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"# Make the Dockerfile to modify the permission of the ini file. If you want to use a specific version, you should change `latest` here.\n",
|
||||||
|
"!echo -e \"\\n[build]\\n dockerfile = \\\"./Dockerfile\\\"\" >> ./fly.toml"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"id": "LQPsZ_dYxkTu"
|
||||||
|
},
|
||||||
|
"execution_count": null,
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"!echo -e \"FROM couchdb:latest\\nRUN sed -i '2itouch /opt/couchdb/data/persistence.ini && chmod +w /opt/couchdb/data/persistence.ini && fallocate -l 512M /swapfile && chmod 0600 /swapfile && mkswap /swapfile && echo 10 > /proc/sys/vm/swappiness && swapon /swapfile && echo 1 > /proc/sys/vm/overcommit_memory' /docker-entrypoint.sh\" > ./Dockerfile"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"id": "44cBeGJ9on5i"
|
||||||
|
},
|
||||||
|
"execution_count": null,
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"# Check dockerfile\n",
|
||||||
|
"!cat ./Dockerfile"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"id": "ai2R3BbpxRSe"
|
||||||
|
},
|
||||||
|
"execution_count": null,
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"id": "xWdsTCI6bzk2"
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Configure password\n",
|
||||||
|
"!/root/.fly/bin/flyctl secrets set COUCHDB_PASSWORD=${couchPwd}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"id": "k0WIQlShcXGa"
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Deploy server\n",
|
||||||
|
"# Be sure to shutdown after the test.\n",
|
||||||
|
"!/root/.fly/bin/flyctl deploy --detach --remote-only\n",
|
||||||
|
"!/root/.fly/bin/flyctl status"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"id": "0ySggkdlfq7M"
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import subprocess, json\n",
|
||||||
|
"result = subprocess.run([\"/root/.fly/bin/flyctl\",\"status\",\"-j\"], capture_output=True, text=True)\n",
|
||||||
|
"if result.returncode==0:\n",
|
||||||
|
" hostname = json.loads(result.stdout)[\"Hostname\"]\n",
|
||||||
|
" os.environ['couchHost']=\"https://%s\" % (hostname)\n",
|
||||||
|
" print(\"Your couchDB server is https://%s/\" % (hostname))\n",
|
||||||
|
"else:\n",
|
||||||
|
" print(\"Something occured.\")\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"# Finish setting up the CouchDB\n",
|
||||||
|
"# Please repeat until the request is completed without error messages\n",
|
||||||
|
"# i.e., You have to redo this block while \"curl: (35) OpenSSL SSL_connect: Connection reset by peer in connection to xxxx\" is showing.\n",
|
||||||
|
"!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}\""
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"id": "cGlSzVqlQG_z"
|
||||||
|
},
|
||||||
|
"execution_count": null,
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"# Please repeat until all lines are completed without error messages\n",
|
||||||
|
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/chttpd/require_valid_user\" -H \"Content-Type: application/json\" -d '\"true\"' --user \"${couchUser}:${couchPwd}\"\n",
|
||||||
|
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/chttpd_auth/require_valid_user\" -H \"Content-Type: application/json\" -d '\"true\"' --user \"${couchUser}:${couchPwd}\"\n",
|
||||||
|
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/httpd/WWW-Authenticate\" -H \"Content-Type: application/json\" -d '\"Basic realm=\\\"couchdb\\\"\"' --user \"${couchUser}:${couchPwd}\"\n",
|
||||||
|
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/httpd/enable_cors\" -H \"Content-Type: application/json\" -d '\"true\"' --user \"${couchUser}:${couchPwd}\"\n",
|
||||||
|
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/chttpd/enable_cors\" -H \"Content-Type: application/json\" -d '\"true\"' --user \"${couchUser}:${couchPwd}\"\n",
|
||||||
|
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/chttpd/max_http_request_size\" -H \"Content-Type: application/json\" -d '\"4294967296\"' --user \"${couchUser}:${couchPwd}\"\n",
|
||||||
|
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/couchdb/max_document_size\" -H \"Content-Type: application/json\" -d '\"50000000\"' --user \"${couchUser}:${couchPwd}\"\n",
|
||||||
|
"!curl -X PUT \"${couchHost}/_node/nonode@nohost/_config/cors/credentials\" -H \"Content-Type: application/json\" -d '\"true\"' --user \"${couchUser}:${couchPwd}\"\n",
|
||||||
|
"!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}\""
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"id": "JePzrsHypY18"
|
||||||
|
},
|
||||||
|
"execution_count": null,
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"Now, our CouchDB has been surely installed and configured. Cheers!\n",
|
||||||
|
"\n",
|
||||||
|
"In the steps that follow, create a setup-URI.\n",
|
||||||
|
"\n",
|
||||||
|
"This URI could be imported directly into Self-hosted LiveSync, to configure the use of the CouchDB which we configured now."
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"id": "YfSOomsoXbGS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"# Database config\n",
|
||||||
|
"import random, string\n",
|
||||||
|
"\n",
|
||||||
|
"def randomname(n):\n",
|
||||||
|
" return ''.join(random.choices(string.ascii_letters + string.digits, k=n))\n",
|
||||||
|
"\n",
|
||||||
|
"# The database name\n",
|
||||||
|
"os.environ['database']=\"obsidiannote\"\n",
|
||||||
|
"# The passphrase to E2EE\n",
|
||||||
|
"os.environ['passphrase']=randomname(20)\n",
|
||||||
|
"\n",
|
||||||
|
"print(\"Your database:\"+os.environ['database'])\n",
|
||||||
|
"print(\"Your passphrase:\"+os.environ['passphrase'])"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"id": "416YncOqXdNn"
|
||||||
|
},
|
||||||
|
"execution_count": null,
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"# Install deno for make setup uri\n",
|
||||||
|
"!curl -fsSL https://deno.land/x/install/install.sh | sh"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"id": "C4d7C0HAXgsr"
|
||||||
|
},
|
||||||
|
"execution_count": null,
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"# Fetch module for encrypting a Setup URI\n",
|
||||||
|
"!curl -o encrypt.ts https://gist.githubusercontent.com/vrtmrz/f9d1d95ee2ca3afa1a924a2c6759b854/raw/d7a070d864a6f61403d8dc74208238d5741aeb5a/encrypt.ts"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"id": "hQL_Dx-PXise"
|
||||||
|
},
|
||||||
|
"execution_count": null,
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"# Make buttons!\n",
|
||||||
|
"from IPython.display import HTML\n",
|
||||||
|
"result = subprocess.run([\"/root/.deno/bin/deno\",\"run\",\"-A\",\"encrypt.ts\"], capture_output=True, text=True)\n",
|
||||||
|
"text=\"\"\n",
|
||||||
|
"if result.returncode==0:\n",
|
||||||
|
" text = result.stdout.strip()\n",
|
||||||
|
" result = HTML(f\"<button onclick=navigator.clipboard.writeText('{text}')>Copy setup uri</button><br>Importing passphrase is `welcome`. <br>If you want to synchronise in live mode, please apply a preset after setup.)\")\n",
|
||||||
|
"else:\n",
|
||||||
|
" result = \"Failed to encrypt the setup URI\"\n",
|
||||||
|
"result"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"id": "o0gX_thFXlIZ"
|
||||||
|
},
|
||||||
|
"execution_count": null,
|
||||||
|
"outputs": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"colab": {
|
||||||
|
"provenance": [],
|
||||||
|
"private_outputs": true,
|
||||||
|
"include_colab_link": true
|
||||||
|
},
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python"
|
||||||
|
},
|
||||||
|
"gpuClass": "standard"
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 0
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
# Quick setup
|
# Quick setup
|
||||||
|
|
||||||
|
[Japanese docs](./quick_setup_ja.md) - [Chinese docs](./quick_setup_cn.md).
|
||||||
|
|
||||||
The plugin has so many configuration options to deal with different circumstances. However, there are not so many settings that are actually used. Therefore, `The Setup wizard` has been implemented to simplify the initial setup.
|
The plugin has so many configuration options to deal with different circumstances. However, there are not so many settings that are actually used. Therefore, `The Setup wizard` has been implemented to simplify the initial setup.
|
||||||
|
|
||||||
Note: Subsequent devices are recommended to be set up using the `Copy setup URI` and `Open setup URI`.
|
Note: Subsequent devices are recommended to be set up using the `Copy setup URI` and `Open setup URI`.
|
||||||
|
|||||||
93
docs/quick_setup_cn.md
Normal file
93
docs/quick_setup_cn.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# 快速配置 (Quick setup)
|
||||||
|
|
||||||
|
该插件有较多配置项, 可以应对不同的情况. 不过, 实际使用的设置并不多. 因此, 我们采用了 "设置向导 (The Setup wizard)" 来简化初始设置.
|
||||||
|
|
||||||
|
Note: 建议使用 `Copy setup URI` and `Open setup URI` 来设置后续设备.
|
||||||
|
|
||||||
|
## 设置向导 (The Setup wizard)
|
||||||
|
|
||||||
|
在设置对话框中打开 `🧙♂️ Setup wizard`. 如果之前未配置插件, 则会自动打开该页面.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 放弃现有配置并进行设置
|
||||||
|
如果您先前有过任何设置, 此按钮允许您在设置前放弃所有更改.
|
||||||
|
|
||||||
|
- 保留现有配置和设置
|
||||||
|
快速重新配置. 请注意, 在向导模式下, 您无法看到所有已经配置过的配置项.
|
||||||
|
|
||||||
|
在上述选项中按下 `Next`, 配置对话框将进入向导模式 (wizard mode).
|
||||||
|
|
||||||
|
### 向导模式 (Wizard mode)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
接下来将介绍如何逐步使用向导模式.
|
||||||
|
|
||||||
|
## 配置远程数据库
|
||||||
|
|
||||||
|
### 开始配置远程数据库
|
||||||
|
|
||||||
|
输入已部署好的数据库的信息.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 测试数据库连接并检查数据库配置
|
||||||
|
|
||||||
|
我们可以检查数据库的连接性和数据库设置.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 测试数据库连接
|
||||||
|
|
||||||
|
检查是否能成功连接数据库. 如果连接失败, 可能是多种原因导致的, 但请先点击 `Check database configuration` 来检查数据库配置是否有问题.
|
||||||
|
|
||||||
|
#### 检查数据库配置
|
||||||
|
|
||||||
|
检查数据库设置并修复问题.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Config check 的显示内容可能因不同连接而异. 在上图情况下, 按下所有三个修复按钮.
|
||||||
|
如果修复按钮消失, 全部变为复选标记, 则表示修复完成.
|
||||||
|
|
||||||
|
### 加密配置
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
为您的数据库加密, 以防数据库意外曝光; 启用端到端加密后, 笔记内容在离开设备时就会被加密. 我们强烈建议启用该功能. `路径混淆 (Path Obfuscation)` 还能混淆文件名. 现已稳定并推荐使用.
|
||||||
|
加密基于 256 位 AES-GCM.
|
||||||
|
如果你在一个封闭的网络中, 而且很明显第三方不会访问你的文件, 则可以禁用这些设置.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Next
|
||||||
|
|
||||||
|
转到同步设置.
|
||||||
|
|
||||||
|
#### 放弃现有数据库并继续
|
||||||
|
|
||||||
|
清除远程数据库的内容, 然后转到同步设置.
|
||||||
|
|
||||||
|
### 同步设置
|
||||||
|
|
||||||
|
最后, 选择一个同步预设完成向导.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
选择我们要使用的任何同步方法, 然后 `Apply` 初始化并按要求建立本地和远程数据库. 如果显示 `All done!`, 我们就完成了. `Copy setup URI` 将自动打开,并要求我们输入密码以加密 `Setup URI`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
根据需要设置密码。.
|
||||||
|
设置 URI (Setup URI) 将被复制到剪贴板, 然后您可以通过某种方式将其传输到第二个及后续设备.
|
||||||
|
|
||||||
|
## 如何设置第二单元和后续单元 (the second and subsequent units)
|
||||||
|
|
||||||
|
在第一台设备上安装 Self-hosted LiveSync 后, 从命令面板上选择 `Open setup URI`, 然后输入您传输的设置 URI (Setup URI). 然后输入密码,安装向导就会打开.
|
||||||
|
在弹窗中选择以下内容.
|
||||||
|
|
||||||
|
- `Importing LiveSync's conf, OK?` 选择 `Yes`
|
||||||
|
- `How would you like to set it up?`. 选择 `Set it up as secondary or subsequent device`
|
||||||
|
|
||||||
|
然后, 配置将生效并开始复制. 您的文件很快就会同步! 您可能需要关闭设置对话框并重新打开, 才能看到设置字段正确填充, 但它们都将设置好.
|
||||||
@@ -280,7 +280,7 @@ Now the CouchDB is ready to use from Self-hosted LiveSync. We can use `https://b
|
|||||||
|
|
||||||
## Automatic setup using Colaboratory
|
## Automatic setup using Colaboratory
|
||||||
|
|
||||||
We can perform all these steps by using [this Colaboratory notebook](https://gist.github.com/vrtmrz/b437a539af25ef191bd452aae369242f) without installing anything.
|
We can perform all these steps by using [this Colaboratory notebook](/deploy_couchdb_to_flyio_v2_with_swap.ipynb) without installing anything.
|
||||||
|
|
||||||
## After testing / before creating a new instance
|
## After testing / before creating a new instance
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.19.17",
|
"version": "0.19.22",
|
||||||
"minAppVersion": "0.9.12",
|
"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.",
|
"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",
|
"author": "vorotamoroz",
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.19.17",
|
"version": "0.19.22",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.19.17",
|
"version": "0.19.22",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.19.17",
|
"version": "0.19.22",
|
||||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"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",
|
"main": "main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import { Notice, type PluginManifest, parseYaml } from "./deps";
|
import { Notice, type PluginManifest, parseYaml, normalizePath } from "./deps";
|
||||||
|
|
||||||
import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID, AnyEntry } from "./lib/src/types";
|
import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID, AnyEntry } from "./lib/src/types";
|
||||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types";
|
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE } from "./lib/src/types";
|
||||||
import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "./types";
|
import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "./types";
|
||||||
import { delay, getDocData } from "./lib/src/utils";
|
import { delay, getDocData } from "./lib/src/utils";
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { WrappedNotice } from "./lib/src/wrapper";
|
import { WrappedNotice } from "./lib/src/wrapper";
|
||||||
import { base64ToArrayBuffer, arrayBufferToBase64, readString, crc32CKHash } from "./lib/src/strbin";
|
import { base64ToArrayBuffer, arrayBufferToBase64, readString, crc32CKHash } from "./lib/src/strbin";
|
||||||
import { runWithLock } from "./lib/src/lock";
|
import { serialized } from "./lib/src/lock";
|
||||||
import { LiveSyncCommands } from "./LiveSyncCommands";
|
import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||||
import { stripAllPrefixes } from "./lib/src/path";
|
import { stripAllPrefixes } from "./lib/src/path";
|
||||||
import { PeriodicProcessor, askYesNo, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "./utils";
|
import { PeriodicProcessor, askYesNo, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "./utils";
|
||||||
@@ -16,10 +16,20 @@ import { PluginDialogModal } from "./dialogs";
|
|||||||
import { JsonResolveModal } from "./JsonResolveModal";
|
import { JsonResolveModal } from "./JsonResolveModal";
|
||||||
import { pipeGeneratorToGenerator, processAllGeneratorTasksWithConcurrencyLimit } from './lib/src/task';
|
import { pipeGeneratorToGenerator, processAllGeneratorTasksWithConcurrencyLimit } from './lib/src/task';
|
||||||
|
|
||||||
|
function serialize(data: PluginDataEx): string {
|
||||||
function serialize<T>(obj: T): string {
|
// To improve performance, make JSON manually.
|
||||||
return JSON.stringify(obj, null, 1);
|
// Self-hosted LiveSync uses `\n` to split chunks. Therefore, grouping together those with similar entropy would work nicely.
|
||||||
|
return `{"category":"${data.category}","name":"${data.name}","term":${JSON.stringify(data.term)}
|
||||||
|
${data.version ? `,"version":"${data.version}"` : ""},
|
||||||
|
"mtime":${data.mtime},
|
||||||
|
"files":[
|
||||||
|
${data.files.map(file => `{"filename":"${file.filename}"${file.displayName ? `,"displayName":"${file.displayName}"` : ""}${file.version ? `,"version":"${file.version}"` : ""},
|
||||||
|
"mtime":${file.mtime},"size":${file.size}
|
||||||
|
,"data":[${file.data.map(e => `"${e}"`).join(",")
|
||||||
|
}]}`).join(",")
|
||||||
|
}]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function deserialize<T>(str: string, def: T) {
|
function deserialize<T>(str: string, def: T) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(str) as T;
|
return JSON.parse(str) as T;
|
||||||
@@ -107,6 +117,7 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileCategory(filePath: string): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | "" {
|
getFileCategory(filePath: string): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | "" {
|
||||||
if (filePath.split("/").length == 2 && filePath.endsWith(".json")) return "CONFIG";
|
if (filePath.split("/").length == 2 && filePath.endsWith(".json")) return "CONFIG";
|
||||||
if (filePath.split("/").length == 4 && filePath.startsWith(`${this.app.vault.configDir}/themes/`)) return "THEME";
|
if (filePath.split("/").length == 4 && filePath.startsWith(`${this.app.vault.configDir}/themes/`)) return "THEME";
|
||||||
@@ -164,6 +175,46 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
pluginList.set(this.pluginList)
|
pluginList.set(this.pluginList)
|
||||||
await this.updatePluginList(showMessage);
|
await this.updatePluginList(showMessage);
|
||||||
}
|
}
|
||||||
|
async loadPluginData(path: FilePathWithPrefix): Promise<PluginDataExDisplay | false> {
|
||||||
|
const wx = await this.localDatabase.getDBEntry(path, null, false, false);
|
||||||
|
if (wx) {
|
||||||
|
const data = deserialize(getDocData(wx.data), {}) as PluginDataEx;
|
||||||
|
const xFiles = [] as PluginDataExFile[];
|
||||||
|
for (const file of data.files) {
|
||||||
|
const work = { ...file };
|
||||||
|
const tempStr = getDocData(work.data);
|
||||||
|
work.data = [crc32CKHash(tempStr)];
|
||||||
|
xFiles.push(work);
|
||||||
|
}
|
||||||
|
return ({
|
||||||
|
...data,
|
||||||
|
documentPath: this.getPath(wx),
|
||||||
|
files: xFiles
|
||||||
|
}) as PluginDataExDisplay;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
createMissingConfigurationEntry() {
|
||||||
|
let saveRequired = false;
|
||||||
|
for (const v of this.pluginList) {
|
||||||
|
const key = `${v.category}/${v.name}`;
|
||||||
|
if (!(key in this.plugin.settings.pluginSyncExtendedSetting)) {
|
||||||
|
this.plugin.settings.pluginSyncExtendedSetting[key] = {
|
||||||
|
key,
|
||||||
|
mode: MODE_SELECTIVE,
|
||||||
|
files: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.plugin.settings.pluginSyncExtendedSetting[key].files.sort().join(",").toLowerCase() !=
|
||||||
|
v.files.map(e => e.filename).sort().join(",").toLowerCase()) {
|
||||||
|
this.plugin.settings.pluginSyncExtendedSetting[key].files = v.files.map(e => e.filename).sort();
|
||||||
|
saveRequired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (saveRequired) {
|
||||||
|
this.plugin.saveSettingData();
|
||||||
|
}
|
||||||
|
}
|
||||||
async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise<void> {
|
async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise<void> {
|
||||||
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||||
// pluginList.set([]);
|
// pluginList.set([]);
|
||||||
@@ -174,7 +225,7 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
}
|
}
|
||||||
await Promise.resolve(); // Just to prevent warning.
|
await Promise.resolve(); // Just to prevent warning.
|
||||||
scheduleTask("update-plugin-list-task", 200, async () => {
|
scheduleTask("update-plugin-list-task", 200, async () => {
|
||||||
await runWithLock("update-plugin-list", false, async () => {
|
await serialized("update-plugin-list", async () => {
|
||||||
try {
|
try {
|
||||||
const updatedDocumentId = updatedDocumentPath ? await this.path2id(updatedDocumentPath) : "";
|
const updatedDocumentId = updatedDocumentPath ? await this.path2id(updatedDocumentPath) : "";
|
||||||
const plugins = updatedDocumentPath ?
|
const plugins = updatedDocumentPath ?
|
||||||
@@ -193,22 +244,7 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
count++;
|
count++;
|
||||||
if (count % 10 == 0) Logger(`Enumerating files... ${count}`, logLevel, "get-plugins");
|
if (count % 10 == 0) Logger(`Enumerating files... ${count}`, logLevel, "get-plugins");
|
||||||
Logger(`plugin-${path}`, LOG_LEVEL_VERBOSE);
|
Logger(`plugin-${path}`, LOG_LEVEL_VERBOSE);
|
||||||
const wx = await this.localDatabase.getDBEntry(path, null, false, false);
|
return this.loadPluginData(path);
|
||||||
if (wx) {
|
|
||||||
const data = deserialize(getDocData(wx.data), {}) as PluginDataEx;
|
|
||||||
const xFiles = [] as PluginDataExFile[];
|
|
||||||
for (const file of data.files) {
|
|
||||||
const work = { ...file };
|
|
||||||
const tempStr = getDocData(work.data);
|
|
||||||
work.data = [crc32CKHash(tempStr)];
|
|
||||||
xFiles.push(work);
|
|
||||||
}
|
|
||||||
return ({
|
|
||||||
...data,
|
|
||||||
documentPath: this.getPath(wx),
|
|
||||||
files: xFiles
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// return entries;
|
// return entries;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
//TODO
|
//TODO
|
||||||
@@ -218,7 +254,7 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
return false;
|
return false;
|
||||||
}))) {
|
}))) {
|
||||||
if ("ok" in v) {
|
if ("ok" in v) {
|
||||||
if (v.ok != false) {
|
if (v.ok !== false) {
|
||||||
let newList = [...this.pluginList];
|
let newList = [...this.pluginList];
|
||||||
const item = v.ok;
|
const item = v.ok;
|
||||||
newList = newList.filter(x => x.documentPath != item.documentPath);
|
newList = newList.filter(x => x.documentPath != item.documentPath);
|
||||||
@@ -230,6 +266,7 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Logger(`All files enumerated`, logLevel, "get-plugins");
|
Logger(`All files enumerated`, logLevel, "get-plugins");
|
||||||
|
this.createMissingConfigurationEntry();
|
||||||
} finally {
|
} finally {
|
||||||
pluginIsEnumerating.set(false);
|
pluginIsEnumerating.set(false);
|
||||||
}
|
}
|
||||||
@@ -257,7 +294,7 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
const fileA = { ...pluginDataA.files[0], ctime: pluginDataA.files[0].mtime, _id: `${pluginDataA.documentPath}` as DocumentID };
|
const fileA = { ...pluginDataA.files[0], ctime: pluginDataA.files[0].mtime, _id: `${pluginDataA.documentPath}` as DocumentID };
|
||||||
const fileB = pluginDataB.files[0];
|
const fileB = pluginDataB.files[0];
|
||||||
const docAx = { ...docA, ...fileA } as LoadedEntry, docBx = { ...docB, ...fileB } as LoadedEntry
|
const docAx = { ...docA, ...fileA } as LoadedEntry, docBx = { ...docB, ...fileB } as LoadedEntry
|
||||||
return runWithLock("config:merge-data", false, () => new Promise((res) => {
|
return serialized("config:merge-data", () => new Promise((res) => {
|
||||||
Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
|
Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
|
||||||
// const docs = [docA, docB];
|
// const docs = [docA, docB];
|
||||||
const path = stripAllPrefixes(docAx.path.split("/").slice(-1).join("/") as FilePath);
|
const path = stripAllPrefixes(docAx.path.split("/").slice(-1).join("/") as FilePath);
|
||||||
@@ -411,6 +448,7 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0);
|
this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
recentProcessedInternalFiles = [] as string[];
|
recentProcessedInternalFiles = [] as string[];
|
||||||
async makeEntryFromFile(path: FilePath): Promise<false | PluginDataExFile> {
|
async makeEntryFromFile(path: FilePath): Promise<false | PluginDataExFile> {
|
||||||
const stat = await this.app.vault.adapter.stat(path);
|
const stat = await this.app.vault.adapter.stat(path);
|
||||||
@@ -470,7 +508,7 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const vf = this.filenameToUnifiedKey(path, term);
|
const vf = this.filenameToUnifiedKey(path, term);
|
||||||
return await runWithLock(`plugin-${vf}`, false, async () => {
|
return await serialized(`plugin-${vf}`, async () => {
|
||||||
const category = this.getFileCategory(path);
|
const category = this.getFileCategory(path);
|
||||||
let mtime = 0;
|
let mtime = 0;
|
||||||
let fileTargets = [] as FilePath[];
|
let fileTargets = [] as FilePath[];
|
||||||
@@ -578,6 +616,13 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
// Make sure that target is a file.
|
// Make sure that target is a file.
|
||||||
if (stat && stat.type != "file")
|
if (stat && stat.type != "file")
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
const configDir = normalizePath(this.app.vault.configDir);
|
||||||
|
const synchronisedInConfigSync = Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode != MODE_SELECTIVE).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase());
|
||||||
|
if (synchronisedInConfigSync.some(e => e.startsWith(path.toLowerCase()))) {
|
||||||
|
Logger(`Customization file skipped: ${path}`, LOG_LEVEL_VERBOSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const storageMTime = ~~((stat && stat.mtime || 0) / 1000);
|
const storageMTime = ~~((stat && stat.mtime || 0) / 1000);
|
||||||
const key = `${path}-${storageMTime}`;
|
const key = `${path}-${storageMTime}`;
|
||||||
if (this.recentProcessedInternalFiles.contains(key)) {
|
if (this.recentProcessedInternalFiles.contains(key)) {
|
||||||
@@ -618,7 +663,7 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
|
|
||||||
// const id = await this.path2id(prefixedFileName);
|
// const id = await this.path2id(prefixedFileName);
|
||||||
const mtime = new Date().getTime();
|
const mtime = new Date().getTime();
|
||||||
await runWithLock("file-x-" + prefixedFileName, false, async () => {
|
await serialized("file-x-" + prefixedFileName, async () => {
|
||||||
try {
|
try {
|
||||||
const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, false) as InternalFileEntry | false;
|
const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, false) as InternalFileEntry | false;
|
||||||
let saveData: InternalFileEntry;
|
let saveData: InternalFileEntry;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Notice, normalizePath, type PluginManifest } from "./deps";
|
import { normalizePath, type PluginManifest } from "./deps";
|
||||||
import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types";
|
import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED } from "./lib/src/types";
|
||||||
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "./types";
|
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "./types";
|
||||||
import { Parallels, delay, isDocContentSame } from "./lib/src/utils";
|
import { Parallels, delay, isDocContentSame } from "./lib/src/utils";
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||||
import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask, isInternalMetadata, PeriodicProcessor } from "./utils";
|
import { scheduleTask, isInternalMetadata, PeriodicProcessor } from "./utils";
|
||||||
import { WrappedNotice } from "./lib/src/wrapper";
|
import { WrappedNotice } from "./lib/src/wrapper";
|
||||||
import { base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin";
|
import { base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin";
|
||||||
import { runWithLock } from "./lib/src/lock";
|
import { serialized } from "./lib/src/lock";
|
||||||
import { JsonResolveModal } from "./JsonResolveModal";
|
import { JsonResolveModal } from "./JsonResolveModal";
|
||||||
import { LiveSyncCommands } from "./LiveSyncCommands";
|
import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||||
import { addPrefix, stripAllPrefixes } from "./lib/src/path";
|
import { addPrefix, stripAllPrefixes } from "./lib/src/path";
|
||||||
@@ -77,7 +77,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
|
|
||||||
procInternalFiles: string[] = [];
|
procInternalFiles: string[] = [];
|
||||||
async execInternalFile() {
|
async execInternalFile() {
|
||||||
await runWithLock("execInternal", false, async () => {
|
await serialized("execInternal", async () => {
|
||||||
const w = [...this.procInternalFiles];
|
const w = [...this.procInternalFiles];
|
||||||
this.procInternalFiles = [];
|
this.procInternalFiles = [];
|
||||||
Logger(`Applying hidden ${w.length} files change...`);
|
Logger(`Applying hidden ${w.length} files change...`);
|
||||||
@@ -95,6 +95,14 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
recentProcessedInternalFiles = [] as string[];
|
recentProcessedInternalFiles = [] as string[];
|
||||||
async watchVaultRawEventsAsync(path: FilePath) {
|
async watchVaultRawEventsAsync(path: FilePath) {
|
||||||
if (!this.settings.syncInternalFiles) return;
|
if (!this.settings.syncInternalFiles) return;
|
||||||
|
|
||||||
|
// Exclude files handled by customization sync
|
||||||
|
const configDir = normalizePath(this.app.vault.configDir);
|
||||||
|
const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase());
|
||||||
|
if (synchronisedInConfigSync.some(e => e.startsWith(path.toLowerCase()))) {
|
||||||
|
Logger(`Hidden file skipped: ${path} is synchronized in customization sync.`, LOG_LEVEL_VERBOSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const stat = await this.app.vault.adapter.stat(path);
|
const stat = await this.app.vault.adapter.stat(path);
|
||||||
// sometimes folder is coming.
|
// sometimes folder is coming.
|
||||||
if (stat && stat.type != "file")
|
if (stat && stat.type != "file")
|
||||||
@@ -209,18 +217,24 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Tidy up. Even though it is experimental feature, So dirty...
|
//TODO: Tidy up. Even though it is experimental feature, So dirty...
|
||||||
async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe" | "pullForce" | "pushForce", showMessage: boolean, files: InternalFileInfo[] | false = false, targetFiles: string[] | false = false) {
|
async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe" | "pullForce" | "pushForce", showMessage: boolean, filesAll: InternalFileInfo[] | false = false, targetFiles: string[] | false = false) {
|
||||||
await this.resolveConflictOnInternalFiles();
|
await this.resolveConflictOnInternalFiles();
|
||||||
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||||
Logger("Scanning hidden files.", logLevel, "sync_internal");
|
Logger("Scanning hidden files.", logLevel, "sync_internal");
|
||||||
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns
|
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns
|
||||||
.replace(/\n| /g, "")
|
.replace(/\n| /g, "")
|
||||||
.split(",").filter(e => e).map(e => new RegExp(e, "i"));
|
.split(",").filter(e => e).map(e => new RegExp(e, "i"));
|
||||||
if (!files)
|
|
||||||
files = await this.scanInternalFiles();
|
const configDir = normalizePath(this.app.vault.configDir);
|
||||||
|
let files: InternalFileInfo[] =
|
||||||
|
filesAll ? filesAll : (await this.scanInternalFiles())
|
||||||
|
|
||||||
|
const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase());
|
||||||
|
files = files.filter(file => synchronisedInConfigSync.every(filterFile => !file.path.toLowerCase().startsWith(filterFile)))
|
||||||
|
|
||||||
const filesOnDB = ((await this.localDatabase.allDocsRaw({ startkey: ICHeader, endkey: ICHeaderEnd, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted);
|
const filesOnDB = ((await this.localDatabase.allDocsRaw({ startkey: ICHeader, endkey: ICHeaderEnd, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted);
|
||||||
const allFileNamesSrc = [...new Set([...files.map(e => normalizePath(e.path)), ...filesOnDB.map(e => stripAllPrefixes(this.getPath(e)))])];
|
const allFileNamesSrc = [...new Set([...files.map(e => normalizePath(e.path)), ...filesOnDB.map(e => stripAllPrefixes(this.getPath(e)))])];
|
||||||
const allFileNames = allFileNamesSrc.filter(filename => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1));
|
const allFileNames = allFileNamesSrc.filter(filename => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1)).filter(path => synchronisedInConfigSync.every(filterFile => !path.toLowerCase().startsWith(filterFile)))
|
||||||
function compareMTime(a: number, b: number) {
|
function compareMTime(a: number, b: number) {
|
||||||
const wa = ~~(a / 1000);
|
const wa = ~~(a / 1000);
|
||||||
const wb = ~~(b / 1000);
|
const wb = ~~(b / 1000);
|
||||||
@@ -274,7 +288,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
if (ignorePatterns.some(e => filename.match(e)))
|
if (ignorePatterns.some(e => filename.match(e)))
|
||||||
continue;
|
continue;
|
||||||
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
|
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileOnStorage = filename in filesMap ? filesMap[filename] : undefined;
|
const fileOnStorage = filename in filesMap ? filesMap[filename] : undefined;
|
||||||
@@ -335,7 +349,6 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
|
|
||||||
// When files has been retrieved from the database. they must be reloaded.
|
// When files has been retrieved from the database. they must be reloaded.
|
||||||
if ((direction == "pull" || direction == "pullForce") && filesChanged != 0) {
|
if ((direction == "pull" || direction == "pullForce") && filesChanged != 0) {
|
||||||
const configDir = normalizePath(this.app.vault.configDir);
|
|
||||||
// Show notification to restart obsidian when something has been changed in configDir.
|
// Show notification to restart obsidian when something has been changed in configDir.
|
||||||
if (configDir in updatedFolders) {
|
if (configDir in updatedFolders) {
|
||||||
// Numbers of updated files that is below of configDir.
|
// Numbers of updated files that is below of configDir.
|
||||||
@@ -352,44 +365,18 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
updatedCount -= updatedFolders[manifest.dir];
|
updatedCount -= updatedFolders[manifest.dir];
|
||||||
const updatePluginId = manifest.id;
|
const updatePluginId = manifest.id;
|
||||||
const updatePluginName = manifest.name;
|
const updatePluginName = manifest.name;
|
||||||
const fragment = createFragment((doc) => {
|
this.plugin.askInPopup(`updated-${updatePluginId}`, `Files in ${updatePluginName} has been updated, Press {HERE} to reload ${updatePluginName}, or press elsewhere to dismiss this message.`, (anchor) => {
|
||||||
doc.createEl("span", null, (a) => {
|
anchor.text = "HERE";
|
||||||
a.appendText(`Files in ${updatePluginName} has been updated, Press `);
|
anchor.addEventListener("click", async () => {
|
||||||
a.appendChild(a.createEl("a", null, (anchor) => {
|
Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId);
|
||||||
anchor.text = "HERE";
|
// @ts-ignore
|
||||||
anchor.addEventListener("click", async () => {
|
await this.app.plugins.unloadPlugin(updatePluginId);
|
||||||
Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId);
|
// @ts-ignore
|
||||||
// @ts-ignore
|
await this.app.plugins.loadPlugin(updatePluginId);
|
||||||
await this.app.plugins.unloadPlugin(updatePluginId);
|
Logger(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId);
|
||||||
// @ts-ignore
|
|
||||||
await this.app.plugins.loadPlugin(updatePluginId);
|
|
||||||
Logger(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId);
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
a.appendText(` to reload ${updatePluginName}, or press elsewhere to dismiss this message.`);
|
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
const updatedPluginKey = "popupUpdated-" + updatePluginId;
|
|
||||||
scheduleTask(updatedPluginKey, 1000, async () => {
|
|
||||||
const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0));
|
|
||||||
//@ts-ignore
|
|
||||||
const isShown = popup?.noticeEl?.isShown();
|
|
||||||
if (!isShown) {
|
|
||||||
memoObject(updatedPluginKey, new Notice(fragment, 0));
|
|
||||||
}
|
|
||||||
scheduleTask(updatedPluginKey + "-close", 20000, () => {
|
|
||||||
const popup = retrieveMemoObject<Notice>(updatedPluginKey);
|
|
||||||
if (!popup)
|
|
||||||
return;
|
|
||||||
//@ts-ignore
|
|
||||||
if (popup?.noticeEl?.isShown()) {
|
|
||||||
popup.hide();
|
|
||||||
}
|
|
||||||
disposeMemoObject(updatedPluginKey);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@@ -400,30 +387,11 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
|
|
||||||
// If something changes left, notify for reloading Obsidian.
|
// If something changes left, notify for reloading Obsidian.
|
||||||
if (updatedCount != 0) {
|
if (updatedCount != 0) {
|
||||||
const fragment = createFragment((doc) => {
|
this.plugin.askInPopup(`updated-any-hidden`, `Hidden files have been synchronized, Press {HERE} to reload Obsidian, or press elsewhere to dismiss this message.`, (anchor) => {
|
||||||
doc.createEl("span", null, (a) => {
|
anchor.text = "HERE";
|
||||||
a.appendText(`Hidden files have been synchronized, Press `);
|
anchor.addEventListener("click", () => {
|
||||||
a.appendChild(a.createEl("a", null, (anchor) => {
|
// @ts-ignore
|
||||||
anchor.text = "HERE";
|
this.app.commands.executeCommandById("app:reload");
|
||||||
anchor.addEventListener("click", () => {
|
|
||||||
// @ts-ignore
|
|
||||||
this.app.commands.executeCommandById("app:reload");
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
a.appendText(` to reload obsidian, or press elsewhere to dismiss this message.`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
scheduleTask("popupUpdated-" + configDir, 1000, () => {
|
|
||||||
//@ts-ignore
|
|
||||||
const isShown = this.confirmPopup?.noticeEl?.isShown();
|
|
||||||
if (!isShown) {
|
|
||||||
this.confirmPopup = new Notice(fragment, 0);
|
|
||||||
}
|
|
||||||
scheduleTask("popupClose" + configDir, 20000, () => {
|
|
||||||
this.confirmPopup?.hide();
|
|
||||||
this.confirmPopup = null;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -437,6 +405,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
if (await this.plugin.isIgnoredByIgnoreFiles(file.path)) {
|
if (await this.plugin.isIgnoredByIgnoreFiles(file.path)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = await this.path2id(file.path, ICHeader);
|
const id = await this.path2id(file.path, ICHeader);
|
||||||
const prefixedFileName = addPrefix(file.path, ICHeader);
|
const prefixedFileName = addPrefix(file.path, ICHeader);
|
||||||
const contentBin = await this.app.vault.adapter.readBinary(file.path);
|
const contentBin = await this.app.vault.adapter.readBinary(file.path);
|
||||||
@@ -449,7 +418,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const mtime = file.mtime;
|
const mtime = file.mtime;
|
||||||
return await runWithLock("file-" + prefixedFileName, false, async () => {
|
return await serialized("file-" + prefixedFileName, async () => {
|
||||||
try {
|
try {
|
||||||
const old = await this.localDatabase.getDBEntry(prefixedFileName, null, false, false);
|
const old = await this.localDatabase.getDBEntry(prefixedFileName, null, false, false);
|
||||||
let saveData: LoadedEntry;
|
let saveData: LoadedEntry;
|
||||||
@@ -501,7 +470,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
|
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await runWithLock("file-" + prefixedFileName, false, async () => {
|
await serialized("file-" + prefixedFileName, async () => {
|
||||||
try {
|
try {
|
||||||
const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, true) as InternalFileEntry | false;
|
const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, true) as InternalFileEntry | false;
|
||||||
let saveData: InternalFileEntry;
|
let saveData: InternalFileEntry;
|
||||||
@@ -547,7 +516,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
|
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return await runWithLock("file-" + prefixedFileName, false, async () => {
|
return await serialized("file-" + prefixedFileName, async () => {
|
||||||
try {
|
try {
|
||||||
// Check conflicted status
|
// Check conflicted status
|
||||||
//TODO option
|
//TODO option
|
||||||
@@ -618,7 +587,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
|
|
||||||
|
|
||||||
showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise<boolean> {
|
showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise<boolean> {
|
||||||
return runWithLock("conflict:merge-data", false, () => new Promise((res) => {
|
return serialized("conflict:merge-data", () => new Promise((res) => {
|
||||||
Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
|
Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
|
||||||
const docs = [docA, docB];
|
const docs = [docA, docB];
|
||||||
const path = stripAllPrefixes(docA.path);
|
const path = stripAllPrefixes(docA.path);
|
||||||
@@ -676,13 +645,16 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async scanInternalFiles(): Promise<InternalFileInfo[]> {
|
async scanInternalFiles(): Promise<InternalFileInfo[]> {
|
||||||
|
const configDir = normalizePath(this.app.vault.configDir);
|
||||||
const ignoreFilter = this.settings.syncInternalFilesIgnorePatterns
|
const ignoreFilter = this.settings.syncInternalFilesIgnorePatterns
|
||||||
.replace(/\n| /g, "")
|
.replace(/\n| /g, "")
|
||||||
.split(",").filter(e => e).map(e => new RegExp(e, "i"));
|
.split(",").filter(e => e).map(e => new RegExp(e, "i"));
|
||||||
|
const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase());
|
||||||
const root = this.app.vault.getRoot();
|
const root = this.app.vault.getRoot();
|
||||||
const findRoot = root.path;
|
const findRoot = root.path;
|
||||||
|
|
||||||
const filenames = (await this.getFiles(findRoot, [], null, ignoreFilter)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash"));
|
const filenames = (await this.getFiles(findRoot, [], null, ignoreFilter)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash"));
|
||||||
const files = filenames.map(async (e) => {
|
const files = filenames.filter(path => synchronisedInConfigSync.every(filterFile => !path.toLowerCase().startsWith(filterFile))).map(async (e) => {
|
||||||
return {
|
return {
|
||||||
path: e as FilePath,
|
path: e as FilePath,
|
||||||
stat: await this.app.vault.adapter.stat(e)
|
stat: await this.app.vault.adapter.stat(e)
|
||||||
@@ -716,7 +688,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
...w.files
|
...w.files
|
||||||
.filter((e) => !ignoreList.some((ee) => e.endsWith(ee)))
|
.filter((e) => !ignoreList.some((ee) => e.endsWith(ee)))
|
||||||
.filter((e) => !filter || filter.some((ee) => e.match(ee)))
|
.filter((e) => !filter || filter.some((ee) => e.match(ee)))
|
||||||
.filter((e) => !ignoreFilter || ignoreFilter.every((ee) => !e.match(ee))),
|
.filter((e) => !ignoreFilter || ignoreFilter.every((ee) => !e.match(ee)))
|
||||||
];
|
];
|
||||||
let files = [] as string[];
|
let files = [] as string[];
|
||||||
for (const file of filesSrc) {
|
for (const file of filesSrc) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { isPluginMetadata, PeriodicProcessor } from "./utils";
|
|||||||
import { PluginDialogModal } from "./dialogs";
|
import { PluginDialogModal } from "./dialogs";
|
||||||
import { NewNotice } from "./lib/src/wrapper";
|
import { NewNotice } from "./lib/src/wrapper";
|
||||||
import { versionNumberString2Number } from "./lib/src/strbin";
|
import { versionNumberString2Number } from "./lib/src/strbin";
|
||||||
import { runWithLock } from "./lib/src/lock";
|
import { serialized, skipIfDuplicated } from "./lib/src/lock";
|
||||||
import { LiveSyncCommands } from "./LiveSyncCommands";
|
import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||||
|
|
||||||
export class PluginAndTheirSettings extends LiveSyncCommands {
|
export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||||
@@ -164,7 +164,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
|||||||
if (specificPluginPath != "") {
|
if (specificPluginPath != "") {
|
||||||
specificPlugin = manifests.find(e => e.dir.endsWith("/" + specificPluginPath))?.id ?? "";
|
specificPlugin = manifests.find(e => e.dir.endsWith("/" + specificPluginPath))?.id ?? "";
|
||||||
}
|
}
|
||||||
await runWithLock("sweepplugin", true, async () => {
|
await skipIfDuplicated("sweepplugin", async () => {
|
||||||
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||||
if (!this.deviceAndVaultName) {
|
if (!this.deviceAndVaultName) {
|
||||||
Logger("You have to set your device name.", LOG_LEVEL_NOTICE);
|
Logger("You have to set your device name.", LOG_LEVEL_NOTICE);
|
||||||
@@ -223,7 +223,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
|||||||
type: "plain"
|
type: "plain"
|
||||||
};
|
};
|
||||||
Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL_VERBOSE);
|
Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL_VERBOSE);
|
||||||
await runWithLock("plugin-" + m.id, false, async () => {
|
await serialized("plugin-" + m.id, async () => {
|
||||||
const old = await this.localDatabase.getDBEntry(p._id as string as FilePathWithPrefix /* This also should be explained */, null, false, false);
|
const old = await this.localDatabase.getDBEntry(p._id as string as FilePathWithPrefix /* This also should be explained */, null, false, false);
|
||||||
if (old !== false) {
|
if (old !== false) {
|
||||||
const oldData = { data: old.data, deleted: old._deleted };
|
const oldData = { data: old.data, deleted: old._deleted };
|
||||||
@@ -266,7 +266,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async applyPluginData(plugin: PluginDataEntry) {
|
async applyPluginData(plugin: PluginDataEntry) {
|
||||||
await runWithLock("plugin-" + plugin.manifest.id, false, async () => {
|
await serialized("plugin-" + plugin.manifest.id, async () => {
|
||||||
const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/";
|
const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/";
|
||||||
const adapter = this.app.vault.adapter;
|
const adapter = this.app.vault.adapter;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -288,7 +288,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async applyPlugin(plugin: PluginDataEntry) {
|
async applyPlugin(plugin: PluginDataEntry) {
|
||||||
await runWithLock("plugin-" + plugin.manifest.id, false, async () => {
|
await serialized("plugin-" + plugin.manifest.id, async () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const stat = this.app.plugins.enabledPlugins.has(plugin.manifest.id) == true;
|
const stat = this.app.plugins.enabledPlugins.has(plugin.manifest.id) == true;
|
||||||
if (stat) {
|
if (stat) {
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ export class SetupLiveSync extends LiveSyncCommands {
|
|||||||
name: "Copy the setup URI",
|
name: "Copy the setup URI",
|
||||||
callback: this.command_copySetupURI.bind(this),
|
callback: this.command_copySetupURI.bind(this),
|
||||||
});
|
});
|
||||||
|
this.plugin.addCommand({
|
||||||
|
id: "livesync-copysetupuri-short",
|
||||||
|
name: "Copy the setup URI (With customization sync)",
|
||||||
|
callback: this.command_copySetupURIWithSync.bind(this),
|
||||||
|
});
|
||||||
|
|
||||||
this.plugin.addCommand({
|
this.plugin.addCommand({
|
||||||
id: "livesync-copysetupurifull",
|
id: "livesync-copysetupurifull",
|
||||||
@@ -41,11 +46,14 @@ export class SetupLiveSync extends LiveSyncCommands {
|
|||||||
}
|
}
|
||||||
async realizeSettingSyncMode() { }
|
async realizeSettingSyncMode() { }
|
||||||
|
|
||||||
async command_copySetupURI() {
|
async command_copySetupURI(stripExtra = true) {
|
||||||
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "", true);
|
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "", true);
|
||||||
if (encryptingPassphrase === false)
|
if (encryptingPassphrase === false)
|
||||||
return;
|
return;
|
||||||
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
|
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
|
||||||
|
if (stripExtra) {
|
||||||
|
delete setting.pluginSyncExtendedSetting;
|
||||||
|
}
|
||||||
const keys = Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[];
|
const keys = Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[];
|
||||||
for (const k of keys) {
|
for (const k of keys) {
|
||||||
if (JSON.stringify(k in setting ? setting[k] : "") == JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*")) {
|
if (JSON.stringify(k in setting ? setting[k] : "") == JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*")) {
|
||||||
@@ -67,6 +75,9 @@ export class SetupLiveSync extends LiveSyncCommands {
|
|||||||
await navigator.clipboard.writeText(uri);
|
await navigator.clipboard.writeText(uri);
|
||||||
Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
|
Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
|
||||||
}
|
}
|
||||||
|
async command_copySetupURIWithSync() {
|
||||||
|
this.command_copySetupURI(false);
|
||||||
|
}
|
||||||
async command_openSetupURI() {
|
async command_openSetupURI() {
|
||||||
const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`);
|
const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`);
|
||||||
if (setupURI === false)
|
if (setupURI === false)
|
||||||
@@ -99,6 +110,7 @@ export class SetupLiveSync extends LiveSyncCommands {
|
|||||||
newSettingW.encryptedCouchDBConnection = "";
|
newSettingW.encryptedCouchDBConnection = "";
|
||||||
const setupJustImport = "Just import setting";
|
const setupJustImport = "Just import setting";
|
||||||
const setupAsNew = "Set it up as secondary or subsequent device";
|
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 setupAgain = "Reconfigure and reconstitute the data";
|
||||||
const setupManually = "Leave everything to me";
|
const setupManually = "Leave everything to me";
|
||||||
newSettingW.syncInternalFiles = false;
|
newSettingW.syncInternalFiles = false;
|
||||||
@@ -108,7 +120,7 @@ export class SetupLiveSync extends LiveSyncCommands {
|
|||||||
newSettingW.useIndexedDBAdapter = true;
|
newSettingW.useIndexedDBAdapter = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupType = await askSelectString(this.app, "How would you like to set it up?", [setupAsNew, setupAgain, setupJustImport, setupManually]);
|
const setupType = await askSelectString(this.app, "How would you like to set it up?", [setupAsNew, setupAgain, setupAsMerge, setupJustImport, setupManually]);
|
||||||
if (setupType == setupJustImport) {
|
if (setupType == setupJustImport) {
|
||||||
this.plugin.settings = newSettingW;
|
this.plugin.settings = newSettingW;
|
||||||
this.plugin.usedPassphrase = "";
|
this.plugin.usedPassphrase = "";
|
||||||
@@ -117,6 +129,10 @@ export class SetupLiveSync extends LiveSyncCommands {
|
|||||||
this.plugin.settings = newSettingW;
|
this.plugin.settings = newSettingW;
|
||||||
this.plugin.usedPassphrase = "";
|
this.plugin.usedPassphrase = "";
|
||||||
await this.fetchLocal();
|
await this.fetchLocal();
|
||||||
|
} else if (setupType == setupAsMerge) {
|
||||||
|
this.plugin.settings = newSettingW;
|
||||||
|
this.plugin.usedPassphrase = "";
|
||||||
|
await this.fetchLocalWithKeepLocal();
|
||||||
} else if (setupType == setupAgain) {
|
} 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.";
|
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.";
|
||||||
if (await askSelectString(this.app, "Do you really want to do this?", ["Cancel", confirm]) != confirm) {
|
if (await askSelectString(this.app, "Do you really want to do this?", ["Cancel", confirm]) != confirm) {
|
||||||
@@ -285,6 +301,7 @@ Of course, we are able to disable these features.`
|
|||||||
this.plugin.settings.liveSync = false;
|
this.plugin.settings.liveSync = false;
|
||||||
this.plugin.settings.periodicReplication = false;
|
this.plugin.settings.periodicReplication = false;
|
||||||
this.plugin.settings.syncOnSave = false;
|
this.plugin.settings.syncOnSave = false;
|
||||||
|
this.plugin.settings.syncOnEditorSave = false;
|
||||||
this.plugin.settings.syncOnStart = false;
|
this.plugin.settings.syncOnStart = false;
|
||||||
this.plugin.settings.syncOnFileOpen = false;
|
this.plugin.settings.syncOnFileOpen = false;
|
||||||
this.plugin.settings.syncAfterMerge = false;
|
this.plugin.settings.syncAfterMerge = false;
|
||||||
@@ -302,13 +319,11 @@ Of course, we are able to disable these features.`
|
|||||||
Logger(`Database and storage reflection has been resumed!`, LOG_LEVEL_NOTICE);
|
Logger(`Database and storage reflection has been resumed!`, LOG_LEVEL_NOTICE);
|
||||||
this.plugin.settings.suspendParseReplicationResult = false;
|
this.plugin.settings.suspendParseReplicationResult = false;
|
||||||
this.plugin.settings.suspendFileWatching = false;
|
this.plugin.settings.suspendFileWatching = false;
|
||||||
|
await this.plugin.syncAllFiles(true);
|
||||||
|
await this.plugin.loadQueuedFiles();
|
||||||
|
this.plugin.procQueuedFiles();
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
if (this.plugin.settings.readChunksOnline) {
|
|
||||||
await this.plugin.syncAllFiles(true);
|
|
||||||
await this.plugin.loadQueuedFiles();
|
|
||||||
// Start processing
|
|
||||||
this.plugin.procQueuedFiles();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
async askUseNewAdapter() {
|
async askUseNewAdapter() {
|
||||||
if (!this.plugin.settings.useIndexedDBAdapter) {
|
if (!this.plugin.settings.useIndexedDBAdapter) {
|
||||||
@@ -353,6 +368,25 @@ Of course, we are able to disable these features.`
|
|||||||
await this.resumeReflectingDatabase();
|
await this.resumeReflectingDatabase();
|
||||||
await this.askHiddenFileConfiguration({ enableFetch: true });
|
await this.askHiddenFileConfiguration({ enableFetch: true });
|
||||||
}
|
}
|
||||||
|
async fetchLocalWithKeepLocal() {
|
||||||
|
this.suspendExtraSync();
|
||||||
|
this.askUseNewAdapter();
|
||||||
|
await this.suspendReflectingDatabase();
|
||||||
|
await this.plugin.realizeSettingSyncMode();
|
||||||
|
await this.plugin.resetLocalDatabase();
|
||||||
|
await delay(1000);
|
||||||
|
await this.plugin.initializeDatabase(true);
|
||||||
|
await this.plugin.markRemoteResolved();
|
||||||
|
await this.plugin.openDatabase();
|
||||||
|
this.plugin.isReady = true;
|
||||||
|
await delay(500);
|
||||||
|
await this.plugin.replicateAllFromServer(true);
|
||||||
|
await delay(1000);
|
||||||
|
await this.plugin.replicateAllFromServer(true);
|
||||||
|
await this.fetchRemoteChunks();
|
||||||
|
await this.resumeReflectingDatabase();
|
||||||
|
await this.askHiddenFileConfiguration({ enableFetch: true });
|
||||||
|
}
|
||||||
async rebuildRemote() {
|
async rebuildRemote() {
|
||||||
this.suspendExtraSync();
|
this.suspendExtraSync();
|
||||||
await this.plugin.realizeSettingSyncMode();
|
await this.plugin.realizeSettingSyncMode();
|
||||||
|
|||||||
@@ -18,10 +18,8 @@ export class ConflictResolveModal extends Modal {
|
|||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
|
this.titleEl.setText("Conflicting changes");
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
|
|
||||||
contentEl.createEl("h2", { text: "This document has conflicted changes." });
|
|
||||||
contentEl.createEl("span", { text: this.filename });
|
contentEl.createEl("span", { text: this.filename });
|
||||||
const div = contentEl.createDiv("");
|
const div = contentEl.createDiv("");
|
||||||
div.addClass("op-scrollable");
|
div.addClass("op-scrollable");
|
||||||
|
|||||||
@@ -10,29 +10,29 @@ import { stripPrefix } from "./lib/src/path";
|
|||||||
|
|
||||||
export class DocumentHistoryModal extends Modal {
|
export class DocumentHistoryModal extends Modal {
|
||||||
plugin: ObsidianLiveSyncPlugin;
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
range: HTMLInputElement;
|
range!: HTMLInputElement;
|
||||||
contentView: HTMLDivElement;
|
contentView!: HTMLDivElement;
|
||||||
info: HTMLDivElement;
|
info!: HTMLDivElement;
|
||||||
fileInfo: HTMLDivElement;
|
fileInfo!: HTMLDivElement;
|
||||||
showDiff = false;
|
showDiff = false;
|
||||||
id: DocumentID;
|
id?: DocumentID;
|
||||||
|
|
||||||
file: FilePathWithPrefix;
|
file: FilePathWithPrefix;
|
||||||
|
|
||||||
revs_info: PouchDB.Core.RevisionInfo[] = [];
|
revs_info: PouchDB.Core.RevisionInfo[] = [];
|
||||||
currentDoc: LoadedEntry;
|
currentDoc?: LoadedEntry;
|
||||||
currentText = "";
|
currentText = "";
|
||||||
currentDeleted = false;
|
currentDeleted = false;
|
||||||
initialRev: string;
|
initialRev?: string;
|
||||||
|
|
||||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | FilePathWithPrefix, id: DocumentID, revision?: string) {
|
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | FilePathWithPrefix, id?: DocumentID, revision?: string) {
|
||||||
super(app);
|
super(app);
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.file = (file instanceof TFile) ? getPathFromTFile(file) : file;
|
this.file = (file instanceof TFile) ? getPathFromTFile(file) : file;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.initialRev = revision;
|
this.initialRev = revision;
|
||||||
if (!file) {
|
if (!file && id) {
|
||||||
this.file = this.plugin.id2path(id, null);
|
this.file = this.plugin.id2path(id);
|
||||||
}
|
}
|
||||||
if (localStorage.getItem("ols-history-highlightdiff") == "1") {
|
if (localStorage.getItem("ols-history-highlightdiff") == "1") {
|
||||||
this.showDiff = true;
|
this.showDiff = true;
|
||||||
@@ -46,8 +46,8 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
const db = this.plugin.localDatabase;
|
const db = this.plugin.localDatabase;
|
||||||
try {
|
try {
|
||||||
const w = await db.localDatabase.get(this.id, { revs_info: true });
|
const w = await db.localDatabase.get(this.id, { revs_info: true });
|
||||||
this.revs_info = w._revs_info.filter((e) => e?.status == "available");
|
this.revs_info = w._revs_info?.filter((e) => e?.status == "available") ?? [];
|
||||||
this.range.max = `${this.revs_info.length - 1}`;
|
this.range.max = `${Math.max(this.revs_info.length - 1, 0)}`;
|
||||||
this.range.value = this.range.max;
|
this.range.value = this.range.max;
|
||||||
this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`);
|
this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`);
|
||||||
await this.loadRevs(initialRev);
|
await this.loadRevs(initialRev);
|
||||||
@@ -90,7 +90,7 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
||||||
let result = "";
|
let result = "";
|
||||||
const w1data = w.datatype == "plain" ? getDocData(w.data) : base64ToString(w.data);
|
const w1data = w.datatype == "plain" ? getDocData(w.data) : base64ToString(w.data);
|
||||||
this.currentDeleted = w.deleted;
|
this.currentDeleted = !!w.deleted;
|
||||||
this.currentText = w1data;
|
this.currentText = w1data;
|
||||||
if (this.showDiff) {
|
if (this.showDiff) {
|
||||||
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
||||||
@@ -130,9 +130,8 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
|
this.titleEl.setText("Document History");
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
contentEl.createEl("h2", { text: "Document History" });
|
|
||||||
this.fileInfo = contentEl.createDiv("");
|
this.fileInfo = contentEl.createDiv("");
|
||||||
this.fileInfo.addClass("op-info");
|
this.fileInfo.addClass("op-info");
|
||||||
const divView = contentEl.createDiv("");
|
const divView = contentEl.createDiv("");
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export class JsonResolveModal extends Modal {
|
|||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
|
this.titleEl.setText("Conflicted Setting");
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
|
|
||||||
if (this.component == null) {
|
if (this.component == null) {
|
||||||
|
|||||||
@@ -104,7 +104,6 @@
|
|||||||
] as ["" | "A" | "B" | "AB" | "BA", string][];
|
] as ["" | "A" | "B" | "AB" | "BA", string][];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>Conflicted settings</h1>
|
|
||||||
<h2>{filename}</h2>
|
<h2>{filename}</h2>
|
||||||
{#if !docA || !docB}
|
{#if !docA || !docB}
|
||||||
<div class="message">Just for a minute, please!</div>
|
<div class="message">Just for a minute, please!</div>
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ export class LogDisplayModal extends Modal {
|
|||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
|
this.titleEl.setText("Sync status");
|
||||||
|
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
contentEl.createEl("h2", { text: "Sync Status" });
|
|
||||||
const div = contentEl.createDiv("");
|
const div = contentEl.createDiv("");
|
||||||
div.addClass("op-scrollable");
|
div.addClass("op-scrollable");
|
||||||
div.addClass("op-pre");
|
div.addClass("op-pre");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "./deps";
|
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "./deps";
|
||||||
import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings, type ConfigPassphraseStore, type RemoteDBSettings, type FilePathWithPrefix, type HashAlgorithm, type DocumentID, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types";
|
import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings, type ConfigPassphraseStore, type RemoteDBSettings, type FilePathWithPrefix, type HashAlgorithm, type DocumentID, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, LOG_LEVEL_INFO } from "./lib/src/types";
|
||||||
import { delay } from "./lib/src/utils";
|
import { delay } from "./lib/src/utils";
|
||||||
import { Semaphore } from "./lib/src/semaphore";
|
import { Semaphore } from "./lib/src/semaphore";
|
||||||
import { versionNumberString2Number } from "./lib/src/strbin";
|
import { versionNumberString2Number } from "./lib/src/strbin";
|
||||||
@@ -120,6 +120,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
if (this.plugin.settings.periodicReplication) return true;
|
if (this.plugin.settings.periodicReplication) return true;
|
||||||
if (this.plugin.settings.syncOnFileOpen) return true;
|
if (this.plugin.settings.syncOnFileOpen) return true;
|
||||||
if (this.plugin.settings.syncOnSave) return true;
|
if (this.plugin.settings.syncOnSave) return true;
|
||||||
|
if (this.plugin.settings.syncOnEditorSave) return true;
|
||||||
if (this.plugin.settings.syncOnStart) return true;
|
if (this.plugin.settings.syncOnStart) return true;
|
||||||
if (this.plugin.settings.syncAfterMerge) return true;
|
if (this.plugin.settings.syncAfterMerge) return true;
|
||||||
if (this.plugin.replicator.syncStatus == "CONNECTED") return true;
|
if (this.plugin.replicator.syncStatus == "CONNECTED") return true;
|
||||||
@@ -157,6 +158,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
this.plugin.settings.liveSync = false;
|
this.plugin.settings.liveSync = false;
|
||||||
this.plugin.settings.periodicReplication = false;
|
this.plugin.settings.periodicReplication = false;
|
||||||
this.plugin.settings.syncOnSave = false;
|
this.plugin.settings.syncOnSave = false;
|
||||||
|
this.plugin.settings.syncOnEditorSave = false;
|
||||||
this.plugin.settings.syncOnStart = false;
|
this.plugin.settings.syncOnStart = false;
|
||||||
this.plugin.settings.syncOnFileOpen = false;
|
this.plugin.settings.syncOnFileOpen = false;
|
||||||
this.plugin.settings.syncAfterMerge = false;
|
this.plugin.settings.syncAfterMerge = false;
|
||||||
@@ -216,7 +218,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
syncLive.forEach((e) => {
|
syncLive.forEach((e) => {
|
||||||
e.setDisabled(false).setTooltip("");
|
e.setDisabled(false).setTooltip("");
|
||||||
});
|
});
|
||||||
} else if (this.plugin.settings.syncOnFileOpen || this.plugin.settings.syncOnSave || this.plugin.settings.syncOnStart || this.plugin.settings.periodicReplication || this.plugin.settings.syncAfterMerge) {
|
} else if (this.plugin.settings.syncOnFileOpen || this.plugin.settings.syncOnSave || this.plugin.settings.syncOnEditorSave || this.plugin.settings.syncOnStart || this.plugin.settings.periodicReplication || this.plugin.settings.syncAfterMerge) {
|
||||||
syncNonLive.forEach((e) => {
|
syncNonLive.forEach((e) => {
|
||||||
e.setDisabled(false).setTooltip("");
|
e.setDisabled(false).setTooltip("");
|
||||||
});
|
});
|
||||||
@@ -301,6 +303,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
.setDisabled(false)
|
.setDisabled(false)
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
const checkConfig = async () => {
|
const checkConfig = async () => {
|
||||||
|
Logger(`Checking database configuration`, LOG_LEVEL_INFO);
|
||||||
try {
|
try {
|
||||||
if (isCloudantURI(this.plugin.settings.couchDB_URI)) {
|
if (isCloudantURI(this.plugin.settings.couchDB_URI)) {
|
||||||
Logger("This feature cannot be used with IBM Cloudant.", LOG_LEVEL_NOTICE);
|
Logger("This feature cannot be used with IBM Cloudant.", LOG_LEVEL_NOTICE);
|
||||||
@@ -309,8 +312,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
const r = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, window.origin);
|
const r = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, window.origin);
|
||||||
|
|
||||||
Logger(JSON.stringify(r.json, null, 2));
|
|
||||||
|
|
||||||
const responseConfig = r.json;
|
const responseConfig = r.json;
|
||||||
|
|
||||||
const emptyDiv = createDiv();
|
const emptyDiv = createDiv();
|
||||||
@@ -331,16 +332,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
tmpDiv.innerHTML = `<label>${title}</label><button>Fix</button>`;
|
tmpDiv.innerHTML = `<label>${title}</label><button>Fix</button>`;
|
||||||
const x = checkResultDiv.appendChild(tmpDiv);
|
const x = checkResultDiv.appendChild(tmpDiv);
|
||||||
x.querySelector("button").addEventListener("click", async () => {
|
x.querySelector("button").addEventListener("click", async () => {
|
||||||
console.dir({ key, value });
|
Logger(`CouchDB Configuration: ${title} -> Set ${key} to ${value}`)
|
||||||
const res = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, undefined, key, value);
|
const res = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, undefined, key, value);
|
||||||
console.dir(res);
|
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
Logger(`${title} successfully updated`, LOG_LEVEL_NOTICE);
|
Logger(`CouchDB Configuration: ${title} successfully updated`, LOG_LEVEL_NOTICE);
|
||||||
checkResultDiv.removeChild(x);
|
checkResultDiv.removeChild(x);
|
||||||
checkConfig();
|
checkConfig();
|
||||||
} else {
|
} else {
|
||||||
Logger(`${title} failed`, LOG_LEVEL_NOTICE);
|
Logger(`CouchDB Configuration: ${title} failed`, LOG_LEVEL_NOTICE);
|
||||||
Logger(res.text);
|
Logger(res.text, LOG_LEVEL_VERBOSE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -350,7 +350,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
["ob-btn-config-info"]
|
["ob-btn-config-info"]
|
||||||
);
|
);
|
||||||
|
|
||||||
addResult("Your configuration is dumped to Log", ["ob-btn-config-info"]);
|
|
||||||
addResult("--Config check--", ["ob-btn-config-head"]);
|
addResult("--Config check--", ["ob-btn-config-head"]);
|
||||||
|
|
||||||
// Admin check
|
// Admin check
|
||||||
@@ -451,6 +450,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
}
|
}
|
||||||
addResult("--Done--", ["ob-btn-config-head"]);
|
addResult("--Done--", ["ob-btn-config-head"]);
|
||||||
addResult("If you have some trouble with Connection-check even though all Config-check has been passed, Please check your reverse proxy's configuration.", ["ob-btn-config-info"]);
|
addResult("If you have some trouble with Connection-check even though all Config-check has been passed, Please check your reverse proxy's configuration.", ["ob-btn-config-info"]);
|
||||||
|
Logger(`Checking configuration done`, LOG_LEVEL_INFO);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(`Checking configuration failed`, LOG_LEVEL_NOTICE);
|
Logger(`Checking configuration failed`, LOG_LEVEL_NOTICE);
|
||||||
Logger(ex);
|
Logger(ex);
|
||||||
@@ -891,6 +891,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
liveSync: false,
|
liveSync: false,
|
||||||
periodicReplication: false,
|
periodicReplication: false,
|
||||||
syncOnSave: false,
|
syncOnSave: false,
|
||||||
|
syncOnEditorSave: false,
|
||||||
syncOnStart: false,
|
syncOnStart: false,
|
||||||
syncOnFileOpen: false,
|
syncOnFileOpen: false,
|
||||||
syncAfterMerge: false,
|
syncAfterMerge: false,
|
||||||
@@ -904,6 +905,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
batchSave: true,
|
batchSave: true,
|
||||||
periodicReplication: true,
|
periodicReplication: true,
|
||||||
syncOnSave: false,
|
syncOnSave: false,
|
||||||
|
syncOnEditorSave: false,
|
||||||
syncOnStart: true,
|
syncOnStart: true,
|
||||||
syncOnFileOpen: true,
|
syncOnFileOpen: true,
|
||||||
syncAfterMerge: true,
|
syncAfterMerge: true,
|
||||||
@@ -1014,6 +1016,17 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
applyDisplayEnabled();
|
applyDisplayEnabled();
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
new Setting(containerSyncSettingEl)
|
||||||
|
.setName("Sync on Editor Save")
|
||||||
|
.setDesc("When you save file on the editor, sync automatically")
|
||||||
|
.setClass("wizardHidden")
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle.setValue(this.plugin.settings.syncOnEditorSave).onChange(async (value) => {
|
||||||
|
this.plugin.settings.syncOnEditorSave = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
applyDisplayEnabled();
|
||||||
|
})
|
||||||
|
)
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Sync on File Open")
|
.setName("Sync on File Open")
|
||||||
.setDesc("When you open file, sync automatically")
|
.setDesc("When you open file, sync automatically")
|
||||||
@@ -1168,19 +1181,20 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
new Setting(containerSyncSettingEl)
|
if (!this.plugin.settings.watchInternalFileChanges) {
|
||||||
.setName("Scan for hidden files before replication")
|
new Setting(containerSyncSettingEl)
|
||||||
.setDesc("This configuration will be ignored if monitoring changes is enabled.")
|
.setName("Scan for hidden files before replication")
|
||||||
.setClass("wizardHidden")
|
.setClass("wizardHidden")
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.syncInternalFilesBeforeReplication).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.syncInternalFilesBeforeReplication).onChange(async (value) => {
|
||||||
this.plugin.settings.syncInternalFilesBeforeReplication = value;
|
this.plugin.settings.syncInternalFilesBeforeReplication = value;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Scan hidden files periodically")
|
.setName("Scan hidden files periodically")
|
||||||
.setDesc("Seconds, 0 to disable. This configuration will be ignored if monitoring changes is enabled.")
|
.setDesc("Seconds, 0 to disable")
|
||||||
.setClass("wizardHidden")
|
.setClass("wizardHidden")
|
||||||
.addText((text) => {
|
.addText((text) => {
|
||||||
text.setPlaceholder("")
|
text.setPlaceholder("")
|
||||||
@@ -1199,7 +1213,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/";
|
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/";
|
||||||
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$";
|
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$";
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Skip patterns")
|
.setName("Folders and files to ignore")
|
||||||
.setDesc(
|
.setDesc(
|
||||||
"Regular expression, If you use hidden file sync between desktop and mobile, adding `workspace$` is recommended."
|
"Regular expression, If you use hidden file sync between desktop and mobile, adding `workspace$` is recommended."
|
||||||
)
|
)
|
||||||
@@ -1493,10 +1507,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
pluginConfig.passphrase = REDACTED;
|
pluginConfig.passphrase = REDACTED;
|
||||||
pluginConfig.encryptedPassphrase = REDACTED;
|
pluginConfig.encryptedPassphrase = REDACTED;
|
||||||
pluginConfig.encryptedCouchDBConnection = REDACTED;
|
pluginConfig.encryptedCouchDBConnection = REDACTED;
|
||||||
|
pluginConfig.pluginSyncExtendedSetting = {};
|
||||||
|
|
||||||
const msgConfig = `----remote config----
|
const msgConfig = `----remote config----
|
||||||
${stringifyYaml(responseConfig)}
|
${stringifyYaml(responseConfig)}
|
||||||
---- Plug-in config ---
|
---- Plug-in config ---
|
||||||
|
version:${manifestVersion}
|
||||||
${stringifyYaml(pluginConfig)}`;
|
${stringifyYaml(pluginConfig)}`;
|
||||||
console.log(msgConfig);
|
console.log(msgConfig);
|
||||||
await navigator.clipboard.writeText(msgConfig);
|
await navigator.clipboard.writeText(msgConfig);
|
||||||
@@ -1777,8 +1793,8 @@ ${stringifyYaml(pluginConfig)}`;
|
|||||||
dropdown
|
dropdown
|
||||||
.addOptions({ "": "Old Algorithm", "xxhash32": "xxhash32 (Fast)", "xxhash64": "xxhash64 (Fastest)" } as Record<HashAlgorithm, string>)
|
.addOptions({ "": "Old Algorithm", "xxhash32": "xxhash32 (Fast)", "xxhash64": "xxhash64 (Fastest)" } as Record<HashAlgorithm, string>)
|
||||||
.setValue(this.plugin.settings.hashAlg)
|
.setValue(this.plugin.settings.hashAlg)
|
||||||
.onChange(async (value: HashAlgorithm) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.hashAlg = value;
|
this.plugin.settings.hashAlg = value as HashAlgorithm;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -1844,16 +1860,18 @@ ${stringifyYaml(pluginConfig)}`;
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerPluginSettings)
|
if (!this.plugin.settings.watchInternalFileChanges) {
|
||||||
.setName("Scan customization periodically")
|
new Setting(containerPluginSettings)
|
||||||
.setDesc("Scan customization every 1 minute. This configuration will be ignored if monitoring changes of hidden files has been enabled.")
|
.setName("Scan customization periodically")
|
||||||
.addToggle((toggle) =>
|
.setDesc("Scan customization every 1 minute.")
|
||||||
toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => {
|
.addToggle((toggle) =>
|
||||||
this.plugin.settings.autoSweepPluginsPeriodic = value;
|
toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => {
|
||||||
updateDisabledOfDeviceAndVaultName();
|
this.plugin.settings.autoSweepPluginsPeriodic = value;
|
||||||
await this.plugin.saveSettings();
|
updateDisabledOfDeviceAndVaultName();
|
||||||
})
|
await this.plugin.saveSettings();
|
||||||
);
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
new Setting(containerPluginSettings)
|
new Setting(containerPluginSettings)
|
||||||
.setName("Notify customized")
|
.setName("Notify customized")
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.reduce((p, c) => p | c, 0);
|
.reduce((p, c) => p | (c as number), 0 as number);
|
||||||
if (matchingStatus == 0b0000100) {
|
if (matchingStatus == 0b0000100) {
|
||||||
equivalency = "⚖️ Same";
|
equivalency = "⚖️ Same";
|
||||||
canApply = false;
|
canApply = false;
|
||||||
|
|||||||
@@ -3,9 +3,13 @@
|
|||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
import { type PluginDataExDisplay, pluginIsEnumerating, pluginList } from "./CmdConfigSync";
|
import { type PluginDataExDisplay, pluginIsEnumerating, pluginList } from "./CmdConfigSync";
|
||||||
import PluginCombo from "./PluginCombo.svelte";
|
import PluginCombo from "./PluginCombo.svelte";
|
||||||
|
import { Menu } from "obsidian";
|
||||||
|
import { unique } from "./lib/src/utils";
|
||||||
|
import { MODE_SELECTIVE, MODE_AUTOMATIC, MODE_PAUSED, type SYNC_MODE, type PluginSyncSettingEntry } from "./lib/src/types";
|
||||||
|
import { normalizePath } from "./deps";
|
||||||
export let plugin: ObsidianLiveSyncPlugin;
|
export let plugin: ObsidianLiveSyncPlugin;
|
||||||
|
|
||||||
$: hideNotApplicable = true;
|
$: hideNotApplicable = false;
|
||||||
$: thisTerm = plugin.deviceAndVaultName;
|
$: thisTerm = plugin.deviceAndVaultName;
|
||||||
|
|
||||||
const addOn = plugin.addOnConfigSync;
|
const addOn = plugin.addOnConfigSync;
|
||||||
@@ -13,7 +17,7 @@
|
|||||||
let list: PluginDataExDisplay[] = [];
|
let list: PluginDataExDisplay[] = [];
|
||||||
|
|
||||||
let selectNewestPulse = 0;
|
let selectNewestPulse = 0;
|
||||||
let hideEven = true;
|
let hideEven = false;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let applyAllPluse = 0;
|
let applyAllPluse = 0;
|
||||||
let isMaintenanceMode = false;
|
let isMaintenanceMode = false;
|
||||||
@@ -80,6 +84,54 @@
|
|||||||
async function deleteData(data: PluginDataExDisplay): Promise<boolean> {
|
async function deleteData(data: PluginDataExDisplay): Promise<boolean> {
|
||||||
return await addOn.deleteData(data);
|
return await addOn.deleteData(data);
|
||||||
}
|
}
|
||||||
|
function askMode(evt: MouseEvent, title: string, key: string) {
|
||||||
|
const menu = new Menu();
|
||||||
|
menu.addItem((item) => item.setTitle(title).setIsLabel(true));
|
||||||
|
menu.addSeparator();
|
||||||
|
const prevMode = automaticList.get(key) ?? MODE_SELECTIVE;
|
||||||
|
for (const mode of [MODE_SELECTIVE, MODE_AUTOMATIC, MODE_PAUSED]) {
|
||||||
|
menu.addItem((item) => {
|
||||||
|
item.setTitle(`${getIcon(mode as SYNC_MODE)}:${TITLES[mode]}`)
|
||||||
|
.onClick((e) => {
|
||||||
|
if (mode === MODE_AUTOMATIC) {
|
||||||
|
askOverwriteModeForAutomatic(evt, key);
|
||||||
|
} else {
|
||||||
|
setMode(key, mode as SYNC_MODE);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setChecked(prevMode == mode)
|
||||||
|
.setDisabled(prevMode == mode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
menu.showAtMouseEvent(evt);
|
||||||
|
}
|
||||||
|
function applyAutomaticSync(key: string, direction: "pushForce" | "pullForce" | "safe") {
|
||||||
|
setMode(key, MODE_AUTOMATIC);
|
||||||
|
const configDir = normalizePath(plugin.app.vault.configDir);
|
||||||
|
const files = (plugin.settings.pluginSyncExtendedSetting[key]?.files ?? []).map((e) => `${configDir}/${e}`);
|
||||||
|
plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase(direction, true, false, files);
|
||||||
|
}
|
||||||
|
function askOverwriteModeForAutomatic(evt: MouseEvent, key: string) {
|
||||||
|
const menu = new Menu();
|
||||||
|
menu.addItem((item) => item.setTitle("Initial Action").setIsLabel(true));
|
||||||
|
menu.addSeparator();
|
||||||
|
menu.addItem((item) => {
|
||||||
|
item.setTitle(`↑: Overwrite Remote`).onClick((e) => {
|
||||||
|
applyAutomaticSync(key, "pushForce");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addItem((item) => {
|
||||||
|
item.setTitle(`↓: Overwrite Local`).onClick((e) => {
|
||||||
|
applyAutomaticSync(key, "pullForce");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addItem((item) => {
|
||||||
|
item.setTitle(`⇅: Use newer`).onClick((e) => {
|
||||||
|
applyAutomaticSync(key, "safe");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
menu.showAtMouseEvent(evt);
|
||||||
|
}
|
||||||
|
|
||||||
$: options = {
|
$: options = {
|
||||||
thisTerm,
|
thisTerm,
|
||||||
@@ -92,11 +144,84 @@
|
|||||||
plugin,
|
plugin,
|
||||||
isMaintenanceMode,
|
isMaintenanceMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ICON_EMOJI_PAUSED = `⛔`;
|
||||||
|
const ICON_EMOJI_AUTOMATIC = `✨`;
|
||||||
|
const ICON_EMOJI_SELECTIVE = `🔀`;
|
||||||
|
|
||||||
|
const ICONS: { [key: number]: string } = {
|
||||||
|
[MODE_SELECTIVE]: ICON_EMOJI_SELECTIVE,
|
||||||
|
[MODE_PAUSED]: ICON_EMOJI_PAUSED,
|
||||||
|
[MODE_AUTOMATIC]: ICON_EMOJI_AUTOMATIC,
|
||||||
|
};
|
||||||
|
const TITLES: { [key: number]: string } = {
|
||||||
|
[MODE_SELECTIVE]: "Selective",
|
||||||
|
[MODE_PAUSED]: "Ignore",
|
||||||
|
[MODE_AUTOMATIC]: "Automatic",
|
||||||
|
};
|
||||||
|
const PREFIX_PLUGIN_ALL = "PLUGIN_ALL";
|
||||||
|
const PREFIX_PLUGIN_DATA = "PLUGIN_DATA";
|
||||||
|
const PREFIX_PLUGIN_MAIN = "PLUGIN_MAIN";
|
||||||
|
function setMode(key: string, mode: SYNC_MODE) {
|
||||||
|
if (key.startsWith(PREFIX_PLUGIN_ALL + "/")) {
|
||||||
|
setMode(PREFIX_PLUGIN_DATA + key.substring(PREFIX_PLUGIN_ALL.length), mode);
|
||||||
|
setMode(PREFIX_PLUGIN_MAIN + key.substring(PREFIX_PLUGIN_ALL.length), mode);
|
||||||
|
}
|
||||||
|
const files = unique(
|
||||||
|
list
|
||||||
|
.filter((e) => `${e.category}/${e.name}` == key)
|
||||||
|
.map((e) => e.files)
|
||||||
|
.flat()
|
||||||
|
.map((e) => e.filename)
|
||||||
|
);
|
||||||
|
automaticList.set(key, mode);
|
||||||
|
automaticListDisp = automaticList;
|
||||||
|
if (!(key in plugin.settings.pluginSyncExtendedSetting)) {
|
||||||
|
plugin.settings.pluginSyncExtendedSetting[key] = {
|
||||||
|
key,
|
||||||
|
mode,
|
||||||
|
files: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
plugin.settings.pluginSyncExtendedSetting[key].files = files;
|
||||||
|
plugin.settings.pluginSyncExtendedSetting[key].mode = mode;
|
||||||
|
plugin.saveSettingData();
|
||||||
|
}
|
||||||
|
function getIcon(mode: SYNC_MODE) {
|
||||||
|
if (mode in ICONS) {
|
||||||
|
return ICONS[mode];
|
||||||
|
} else {
|
||||||
|
("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let automaticList = new Map<string, SYNC_MODE>();
|
||||||
|
let automaticListDisp = new Map<string, SYNC_MODE>();
|
||||||
|
|
||||||
|
// apply current configuration to the dialogue
|
||||||
|
for (const { key, mode } of Object.values(plugin.settings.pluginSyncExtendedSetting)) {
|
||||||
|
automaticList.set(key, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
automaticListDisp = automaticList;
|
||||||
|
|
||||||
|
let displayKeys: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const extraKeys = Object.keys(plugin.settings.pluginSyncExtendedSetting);
|
||||||
|
displayKeys = [
|
||||||
|
...list,
|
||||||
|
...extraKeys
|
||||||
|
.map((e) => `${e}///`.split("/"))
|
||||||
|
.filter((e) => e[0] && e[1])
|
||||||
|
.map((e) => ({ category: e[0], name: e[1], displayName: e[1] })),
|
||||||
|
]
|
||||||
|
.sort((a, b) => (a.displayName ?? a.name).localeCompare(b.displayName ?? b.name))
|
||||||
|
.reduce((p, c) => ({ ...p, [c.category]: unique(c.category in p ? [...p[c.category], c.displayName ?? c.name] : [c.displayName ?? c.name]) }), {} as Record<string, string[]>);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<h1>Customization sync</h1>
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button on:click={() => scanAgain()}>Scan changes</button>
|
<button on:click={() => scanAgain()}>Scan changes</button>
|
||||||
<button on:click={() => replicate()}>Sync once</button>
|
<button on:click={() => replicate()}>Sync once</button>
|
||||||
@@ -119,15 +244,24 @@
|
|||||||
{#if list.length == 0}
|
{#if list.length == 0}
|
||||||
<div class="center">No Items.</div>
|
<div class="center">No Items.</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each Object.entries(displays) as [key, label]}
|
{#each Object.entries(displays).filter(([key, _]) => key in displayKeys) as [key, label]}
|
||||||
<div>
|
<div>
|
||||||
<h3>{label}</h3>
|
<h3>{label}</h3>
|
||||||
{#each groupBy(filterList(list, [key]), "name") as [name, listX]}
|
{#each displayKeys[key] as name}
|
||||||
|
{@const bindKey = `${key}/${name}`}
|
||||||
|
{@const mode = automaticListDisp.get(bindKey) ?? MODE_SELECTIVE}
|
||||||
<div class="labelrow {hideEven ? 'hideeven' : ''}">
|
<div class="labelrow {hideEven ? 'hideeven' : ''}">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{name}
|
<button class="status" on:click={(evt) => askMode(evt, `${key}/${name}`, bindKey)}>
|
||||||
|
{getIcon(mode)}
|
||||||
|
</button>
|
||||||
|
<span class="name">{name}</span>
|
||||||
</div>
|
</div>
|
||||||
<PluginCombo {...options} list={listX} hidden={false} />
|
{#if mode == MODE_SELECTIVE}
|
||||||
|
<PluginCombo {...options} list={list.filter((e) => e.category == key && e.name == name)} hidden={false} />
|
||||||
|
{:else}
|
||||||
|
<div class="statusnote">{TITLES[mode]}</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -135,20 +269,55 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3>Plugins</h3>
|
<h3>Plugins</h3>
|
||||||
{#each groupBy(filterList(list, ["PLUGIN_MAIN", "PLUGIN_DATA", "PLUGIN_ETC"]), "name") as [name, listX]}
|
{#each groupBy(filterList(list, ["PLUGIN_MAIN", "PLUGIN_DATA", "PLUGIN_ETC"]), "name") as [name, listX]}
|
||||||
|
{@const bindKeyAll = `${PREFIX_PLUGIN_ALL}/${name}`}
|
||||||
|
{@const modeAll = automaticListDisp.get(bindKeyAll) ?? MODE_SELECTIVE}
|
||||||
|
{@const bindKeyMain = `${PREFIX_PLUGIN_MAIN}/${name}`}
|
||||||
|
{@const modeMain = automaticListDisp.get(bindKeyMain) ?? MODE_SELECTIVE}
|
||||||
|
{@const bindKeyData = `${PREFIX_PLUGIN_DATA}/${name}`}
|
||||||
|
{@const modeData = automaticListDisp.get(bindKeyData) ?? MODE_SELECTIVE}
|
||||||
<div class="labelrow {hideEven ? 'hideeven' : ''}">
|
<div class="labelrow {hideEven ? 'hideeven' : ''}">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{name}
|
<button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_ALL}/${name}`, bindKeyAll)}>
|
||||||
|
{getIcon(modeAll)}
|
||||||
|
</button>
|
||||||
|
<span class="name">{name}</span>
|
||||||
</div>
|
</div>
|
||||||
<PluginCombo {...options} list={listX} hidden={true} />
|
{#if modeAll == MODE_SELECTIVE}
|
||||||
</div>
|
<PluginCombo {...options} list={listX} hidden={true} />
|
||||||
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
{/if}
|
||||||
<div class="filetitle">Main</div>
|
|
||||||
<PluginCombo {...options} list={filterList(listX, ["PLUGIN_MAIN"])} hidden={false} />
|
|
||||||
</div>
|
|
||||||
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
|
||||||
<div class="filetitle">Data</div>
|
|
||||||
<PluginCombo {...options} list={filterList(listX, ["PLUGIN_DATA"])} hidden={false} />
|
|
||||||
</div>
|
</div>
|
||||||
|
{#if modeAll == MODE_SELECTIVE}
|
||||||
|
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
||||||
|
<div class="filetitle">
|
||||||
|
<button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_MAIN}/${name}/MAIN`, bindKeyMain)}>
|
||||||
|
{getIcon(modeMain)}
|
||||||
|
</button>
|
||||||
|
<span class="name">MAIN</span>
|
||||||
|
</div>
|
||||||
|
{#if modeMain == MODE_SELECTIVE}
|
||||||
|
<PluginCombo {...options} list={filterList(listX, ["PLUGIN_MAIN"])} hidden={false} />
|
||||||
|
{:else}
|
||||||
|
<div class="statusnote">{TITLES[modeMain]}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
||||||
|
<div class="filetitle">
|
||||||
|
<button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_DATA}/${name}`, bindKeyData)}>
|
||||||
|
{getIcon(modeData)}
|
||||||
|
</button>
|
||||||
|
<span class="name">DATA</span>
|
||||||
|
</div>
|
||||||
|
{#if modeData == MODE_SELECTIVE}
|
||||||
|
<PluginCombo {...options} list={filterList(listX, ["PLUGIN_DATA"])} hidden={false} />
|
||||||
|
{:else}
|
||||||
|
<div class="statusnote">{TITLES[modeData]}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="noterow">
|
||||||
|
<div class="statusnote">{TITLES[modeAll]}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -162,6 +331,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
span.spacer {
|
||||||
|
min-width: 1px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: var(--modal-background);
|
||||||
|
}
|
||||||
.labelrow {
|
.labelrow {
|
||||||
margin-left: 0.4em;
|
margin-left: 0.4em;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -183,6 +361,24 @@
|
|||||||
.labelrow.hideeven:has(.even) {
|
.labelrow.hideeven:has(.even) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.noterow {
|
||||||
|
min-height: 2em;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
button.status {
|
||||||
|
flex-grow: 0;
|
||||||
|
margin: 2px 4px;
|
||||||
|
min-width: 3em;
|
||||||
|
max-width: 4em;
|
||||||
|
}
|
||||||
|
.statusnote {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-right: var(--size-4-12);
|
||||||
|
align-items: center;
|
||||||
|
min-width: 10em;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type LiveSyncForStorageEventManager = Plugin &
|
|||||||
ignoreFiles: string[],
|
ignoreFiles: string[],
|
||||||
} & {
|
} & {
|
||||||
isTargetFile: (file: string | TAbstractFile) => Promise<boolean>,
|
isTargetFile: (file: string | TAbstractFile) => Promise<boolean>,
|
||||||
procFileEvent: (applyBatch?: boolean) => Promise<boolean>,
|
procFileEvent: (applyBatch?: boolean) => Promise<any>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export {
|
|||||||
addIcon, App, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginSettingTab, requestUrl, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder,
|
addIcon, App, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginSettingTab, requestUrl, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder,
|
||||||
parseYaml, ItemView, WorkspaceLeaf
|
parseYaml, ItemView, WorkspaceLeaf
|
||||||
} from "obsidian";
|
} from "obsidian";
|
||||||
export type { DataWriteOptions, PluginManifest, RequestUrlParam, RequestUrlResponse } from "obsidian";
|
export type { DataWriteOptions, PluginManifest, RequestUrlParam, RequestUrlResponse, MarkdownFileInfo } from "obsidian";
|
||||||
import {
|
import {
|
||||||
normalizePath as normalizePath_
|
normalizePath as normalizePath_
|
||||||
} from "obsidian";
|
} from "obsidian";
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export class PluginDialogModal extends Modal {
|
|||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
|
this.titleEl.setText("Customization Sync (Beta2)")
|
||||||
if (this.component == null) {
|
if (this.component == null) {
|
||||||
this.component = new PluginPane({
|
this.component = new PluginPane({
|
||||||
target: contentEl,
|
target: contentEl,
|
||||||
@@ -38,7 +39,7 @@ export class PluginDialogModal extends Modal {
|
|||||||
|
|
||||||
export class InputStringDialog extends Modal {
|
export class InputStringDialog extends Modal {
|
||||||
result: string | false = false;
|
result: string | false = false;
|
||||||
onSubmit: (result: string | boolean) => void;
|
onSubmit: (result: string | false) => void;
|
||||||
title: string;
|
title: string;
|
||||||
key: string;
|
key: string;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
@@ -56,8 +57,7 @@ export class InputStringDialog extends Modal {
|
|||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
|
this.titleEl.setText(this.title);
|
||||||
contentEl.createEl("h1", { text: this.title });
|
|
||||||
// For enter to submit
|
// For enter to submit
|
||||||
const formEl = contentEl.createEl("form");
|
const formEl = contentEl.createEl("form");
|
||||||
new Setting(formEl).setName(this.key).setClass(this.isPassword ? "password-input" : "normal-input").addText((text) =>
|
new Setting(formEl).setName(this.key).setClass(this.isPassword ? "password-input" : "normal-input").addText((text) =>
|
||||||
@@ -144,7 +144,7 @@ export class MessageBox extends Modal {
|
|||||||
timer: ReturnType<typeof setInterval> = undefined;
|
timer: ReturnType<typeof setInterval> = undefined;
|
||||||
defaultButtonComponent: ButtonComponent | undefined;
|
defaultButtonComponent: ButtonComponent | undefined;
|
||||||
|
|
||||||
onSubmit: (result: string | boolean) => void;
|
onSubmit: (result: string | false) => void;
|
||||||
|
|
||||||
constructor(plugin: Plugin, title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout: number, onSubmit: (result: (typeof buttons)[number] | false) => void) {
|
constructor(plugin: Plugin, title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout: number, onSubmit: (result: (typeof buttons)[number] | false) => void) {
|
||||||
super(plugin.app);
|
super(plugin.app);
|
||||||
@@ -175,6 +175,7 @@ export class MessageBox extends Modal {
|
|||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
|
this.titleEl.setText(this.title);
|
||||||
contentEl.addEventListener("click", () => {
|
contentEl.addEventListener("click", () => {
|
||||||
if (this.timer) {
|
if (this.timer) {
|
||||||
clearInterval(this.timer);
|
clearInterval(this.timer);
|
||||||
|
|||||||
2
src/lib
2
src/lib
Submodule src/lib updated: 8872807f47...6548bd3ed7
323
src/main.ts
323
src/main.ts
@@ -1,7 +1,7 @@
|
|||||||
const isDebug = false;
|
const isDebug = false;
|
||||||
|
|
||||||
import { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "./deps";
|
import { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "./deps";
|
||||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, type RequestUrlParam, type RequestUrlResponse, requestUrl } from "./deps";
|
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, type RequestUrlParam, type RequestUrlResponse, requestUrl, type MarkdownFileInfo } from "./deps";
|
||||||
import { type EntryDoc, type LoadedEntry, type ObsidianLiveSyncSettings, type diff_check_result, type diff_result_leaf, type EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, type diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, SALT_OF_PASSPHRASE, type ConfigPassphraseStore, type CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE, type DatabaseConnectingStatus, type EntryHasPath, type DocumentID, type FilePathWithPrefix, type FilePath, type AnyEntry, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT, LOG_LEVEL_VERBOSE } from "./lib/src/types";
|
import { type EntryDoc, type LoadedEntry, type ObsidianLiveSyncSettings, type diff_check_result, type diff_result_leaf, type EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, type diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, SALT_OF_PASSPHRASE, type ConfigPassphraseStore, type CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE, type DatabaseConnectingStatus, type EntryHasPath, type DocumentID, type FilePathWithPrefix, type FilePath, type AnyEntry, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT, LOG_LEVEL_VERBOSE } from "./lib/src/types";
|
||||||
import { type InternalFileInfo, type queueItem, type CacheData, type FileEventItem, FileWatchEventQueueMax } from "./types";
|
import { type InternalFileInfo, type queueItem, type CacheData, type FileEventItem, FileWatchEventQueueMax } from "./types";
|
||||||
import { arrayToChunkedArray, getDocData, isDocContentSame } from "./lib/src/utils";
|
import { arrayToChunkedArray, getDocData, isDocContentSame } from "./lib/src/utils";
|
||||||
@@ -10,15 +10,15 @@ import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
|||||||
import { ConflictResolveModal } from "./ConflictResolveModal";
|
import { ConflictResolveModal } from "./ConflictResolveModal";
|
||||||
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||||
import { applyPatch, cancelAllPeriodicTask, cancelAllTasks, cancelTask, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, flattenObject, path2id, scheduleTask, tryParseJSON, createFile, modifyFile, isValidPath, getAbstractFileByPath, touch, recentlyTouched, isInternalMetadata, isPluginMetadata, stripInternalMetadataPrefix, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, clearTouched, getPath, getPathWithoutPrefix, getPathFromTFile, performRebuildDB } from "./utils";
|
import { applyPatch, cancelAllPeriodicTask, cancelAllTasks, cancelTask, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, flattenObject, path2id, scheduleTask, tryParseJSON, createFile, modifyFile, isValidPath, getAbstractFileByPath, touch, recentlyTouched, isInternalMetadata, isPluginMetadata, stripInternalMetadataPrefix, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, clearTouched, getPath, getPathWithoutPrefix, getPathFromTFile, performRebuildDB, memoIfNotExist, memoObject, retrieveMemoObject, disposeMemoObject, isCustomisationSyncMetadata } from "./utils";
|
||||||
import { encrypt, tryDecrypt } from "./lib/src/e2ee_v2";
|
import { encrypt, tryDecrypt } from "./lib/src/e2ee_v2";
|
||||||
import { balanceChunkPurgedDBs, enableEncryption, isCloudantURI, isErrorOfMissingDoc, isValidRemoteCouchDBURI, purgeUnreferencedChunks } from "./lib/src/utils_couchdb";
|
import { balanceChunkPurgedDBs, enableEncryption, isCloudantURI, isErrorOfMissingDoc, isValidRemoteCouchDBURI, purgeUnreferencedChunks } from "./lib/src/utils_couchdb";
|
||||||
import { getGlobalStore, ObservableStore, observeStores } from "./lib/src/store";
|
import { getGlobalStore, ObservableStore, observeStores } from "./lib/src/store";
|
||||||
import { lockStore, logMessageStore, logStore, type LogEntry } from "./lib/src/stores";
|
import { lockStore, logMessageStore, logStore, type LogEntry } from "./lib/src/stores";
|
||||||
import { setNoticeClass } from "./lib/src/wrapper";
|
import { setNoticeClass } from "./lib/src/wrapper";
|
||||||
import { base64ToString, versionNumberString2Number, base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin";
|
import { base64ToString, versionNumberString2Number, base64ToArrayBuffer, arrayBufferToBase64, writeString } from "./lib/src/strbin";
|
||||||
import { addPrefix, isAcceptedAll, isPlainText, shouldBeIgnored, stripAllPrefixes } from "./lib/src/path";
|
import { addPrefix, isAcceptedAll, isPlainText, shouldBeIgnored, stripAllPrefixes } from "./lib/src/path";
|
||||||
import { isLockAcquired, runWithLock } from "./lib/src/lock";
|
import { isLockAcquired, serialized, skipIfDuplicated } from "./lib/src/lock";
|
||||||
import { Semaphore } from "./lib/src/semaphore";
|
import { Semaphore } from "./lib/src/semaphore";
|
||||||
import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager";
|
import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager";
|
||||||
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/LiveSyncLocalDB";
|
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/LiveSyncLocalDB";
|
||||||
@@ -43,16 +43,29 @@ setGlobalLogFunction((message: any, level?: LOG_LEVEL, key?: string) => {
|
|||||||
});
|
});
|
||||||
logStore.intercept(e => e.slice(Math.min(e.length - 200, 0)));
|
logStore.intercept(e => e.slice(Math.min(e.length - 200, 0)));
|
||||||
|
|
||||||
|
async function fetchByAPI(request: RequestUrlParam): Promise<RequestUrlResponse> {
|
||||||
|
const ret = await requestUrl(request);
|
||||||
|
if (ret.status - (ret.status % 100) !== 200) {
|
||||||
|
const er: Error & { status?: number } = new Error(`Request Error:${ret.status}`);
|
||||||
|
if (ret.json) {
|
||||||
|
er.message = ret.json.reason;
|
||||||
|
er.name = `${ret.json.error ?? ""}:${ret.json.message ?? ""}`;
|
||||||
|
}
|
||||||
|
er.status = ret.status;
|
||||||
|
throw er;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
export default class ObsidianLiveSyncPlugin extends Plugin
|
export default class ObsidianLiveSyncPlugin extends Plugin
|
||||||
implements LiveSyncLocalDBEnv, LiveSyncReplicatorEnv {
|
implements LiveSyncLocalDBEnv, LiveSyncReplicatorEnv {
|
||||||
|
|
||||||
settings: ObsidianLiveSyncSettings;
|
settings!: ObsidianLiveSyncSettings;
|
||||||
localDatabase: LiveSyncLocalDB;
|
localDatabase!: LiveSyncLocalDB;
|
||||||
replicator: LiveSyncDBReplicator;
|
replicator!: LiveSyncDBReplicator;
|
||||||
|
|
||||||
statusBar: HTMLElement;
|
statusBar?: HTMLElement;
|
||||||
suspended: boolean;
|
suspended: boolean = false;
|
||||||
deviceAndVaultName: string;
|
deviceAndVaultName: string = "";
|
||||||
isMobile = false;
|
isMobile = false;
|
||||||
isReady = false;
|
isReady = false;
|
||||||
packageVersion = "";
|
packageVersion = "";
|
||||||
@@ -67,25 +80,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
periodicSyncProcessor = new PeriodicProcessor(this, async () => await this.replicate());
|
periodicSyncProcessor = new PeriodicProcessor(this, async () => await this.replicate());
|
||||||
|
|
||||||
// implementing interfaces
|
// implementing interfaces
|
||||||
kvDB: KeyValueDatabase;
|
kvDB!: KeyValueDatabase;
|
||||||
last_successful_post = false;
|
last_successful_post = false;
|
||||||
getLastPostFailedBySize() {
|
getLastPostFailedBySize() {
|
||||||
return !this.last_successful_post;
|
return !this.last_successful_post;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchByAPI(request: RequestUrlParam): Promise<RequestUrlResponse> {
|
|
||||||
const ret = await requestUrl(request);
|
_unloaded = false;
|
||||||
if (ret.status - (ret.status % 100) !== 200) {
|
|
||||||
const er: Error & { status?: number } = new Error(`Request Error:${ret.status}`);
|
|
||||||
if (ret.json) {
|
|
||||||
er.message = ret.json.reason;
|
|
||||||
er.name = `${ret.json.error ?? ""}:${ret.json.message ?? ""}`;
|
|
||||||
}
|
|
||||||
er.status = ret.status;
|
|
||||||
throw er;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
getDatabase(): PouchDB.Database<EntryDoc> {
|
getDatabase(): PouchDB.Database<EntryDoc> {
|
||||||
return this.localDatabase.localDatabase;
|
return this.localDatabase.localDatabase;
|
||||||
}
|
}
|
||||||
@@ -103,7 +106,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces.";
|
if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces.";
|
||||||
let authHeader = "";
|
let authHeader = "";
|
||||||
if (auth.username && auth.password) {
|
if (auth.username && auth.password) {
|
||||||
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${auth.username}:${auth.password}`));
|
const utf8str = String.fromCharCode.apply(null, [...writeString(`${auth.username}:${auth.password}`)]);
|
||||||
const encoded = window.btoa(utf8str);
|
const encoded = window.btoa(utf8str);
|
||||||
authHeader = "Basic " + encoded;
|
authHeader = "Basic " + encoded;
|
||||||
} else {
|
} else {
|
||||||
@@ -115,11 +118,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
adapter: "http",
|
adapter: "http",
|
||||||
auth,
|
auth,
|
||||||
skip_setup: !performSetup,
|
skip_setup: !performSetup,
|
||||||
fetch: async (url: string | Request, opts: RequestInit) => {
|
fetch: async (url: string | Request, opts?: RequestInit) => {
|
||||||
let size = "";
|
let size = "";
|
||||||
const localURL = url.toString().substring(uri.length);
|
const localURL = url.toString().substring(uri.length);
|
||||||
const method = opts.method ?? "GET";
|
const method = opts?.method ?? "GET";
|
||||||
if (opts.body) {
|
if (opts?.body) {
|
||||||
const opts_length = opts.body.toString().length;
|
const opts_length = opts.body.toString().length;
|
||||||
if (opts_length > 1000 * 1000 * 10) {
|
if (opts_length > 1000 * 1000 * 10) {
|
||||||
// over 10MB
|
// over 10MB
|
||||||
@@ -132,10 +135,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
size = ` (${opts_length})`;
|
size = ` (${opts_length})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!disableRequestURI && typeof url == "string" && typeof (opts.body ?? "") == "string") {
|
if (!disableRequestURI && typeof url == "string" && typeof (opts?.body ?? "") == "string") {
|
||||||
const body = opts.body as string;
|
const body = opts?.body as string;
|
||||||
|
|
||||||
const transformedHeaders = { ...(opts.headers as Record<string, string>) };
|
const transformedHeaders = { ...(opts?.headers as Record<string, string>) };
|
||||||
if (authHeader != "") transformedHeaders["authorization"] = authHeader;
|
if (authHeader != "") transformedHeaders["authorization"] = authHeader;
|
||||||
delete transformedHeaders["host"];
|
delete transformedHeaders["host"];
|
||||||
delete transformedHeaders["Host"];
|
delete transformedHeaders["Host"];
|
||||||
@@ -143,7 +146,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
delete transformedHeaders["Content-Length"];
|
delete transformedHeaders["Content-Length"];
|
||||||
const requestParam: RequestUrlParam = {
|
const requestParam: RequestUrlParam = {
|
||||||
url,
|
url,
|
||||||
method: opts.method,
|
method: opts?.method,
|
||||||
body: body,
|
body: body,
|
||||||
headers: transformedHeaders,
|
headers: transformedHeaders,
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
@@ -151,7 +154,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await this.fetchByAPI(requestParam);
|
const r = await fetchByAPI(requestParam);
|
||||||
if (method == "POST" || method == "PUT") {
|
if (method == "POST" || method == "PUT") {
|
||||||
this.last_successful_post = r.status - (r.status % 100) == 200;
|
this.last_successful_post = r.status - (r.status % 100) == 200;
|
||||||
} else {
|
} else {
|
||||||
@@ -209,9 +212,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
try {
|
try {
|
||||||
const info = await db.info();
|
const info = await db.info();
|
||||||
return { db: db, info: info };
|
return { db: db, info: info };
|
||||||
} catch (ex) {
|
} catch (ex: any) {
|
||||||
let msg = `${ex.name}:${ex.message}`;
|
let msg = `${ex?.name}:${ex?.message}`;
|
||||||
if (ex.name == "TypeError" && ex.message == "Failed to fetch") {
|
if (ex?.name == "TypeError" && ex?.message == "Failed to fetch") {
|
||||||
msg += "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector.";
|
msg += "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector.";
|
||||||
}
|
}
|
||||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||||
@@ -219,7 +222,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
id2path(id: DocumentID, entry: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
|
id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
|
||||||
const tempId = id2path(id, entry);
|
const tempId = id2path(id, entry);
|
||||||
if (stripPrefix && isInternalMetadata(tempId)) {
|
if (stripPrefix && isInternalMetadata(tempId)) {
|
||||||
const out = stripInternalMetadataPrefix(tempId);
|
const out = stripInternalMetadataPrefix(tempId);
|
||||||
@@ -234,18 +237,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
return getPathWithoutPrefix(entry);
|
return getPathWithoutPrefix(entry);
|
||||||
}
|
}
|
||||||
async path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID> {
|
async path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID> {
|
||||||
const destPath = addPrefix(filename, prefix);
|
const destPath = addPrefix(filename, prefix ?? "");
|
||||||
return await path2id(destPath, this.settings.usePathObfuscation ? this.settings.passphrase : "");
|
return await path2id(destPath, this.settings.usePathObfuscation ? this.settings.passphrase : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
createPouchDBInstance<T>(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database<T> {
|
createPouchDBInstance<T extends object>(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database<T> {
|
||||||
|
const optionPass = options ?? {};
|
||||||
if (this.settings.useIndexedDBAdapter) {
|
if (this.settings.useIndexedDBAdapter) {
|
||||||
options.adapter = "indexeddb";
|
optionPass.adapter = "indexeddb";
|
||||||
//@ts-ignore :missing def
|
//@ts-ignore :missing def
|
||||||
options.purged_infos_limit = 1;
|
optionPass.purged_infos_limit = 1;
|
||||||
return new PouchDB(name + "-indexeddb", options);
|
return new PouchDB(name + "-indexeddb", optionPass);
|
||||||
}
|
}
|
||||||
return new PouchDB(name, options);
|
return new PouchDB(name, optionPass);
|
||||||
}
|
}
|
||||||
beforeOnUnload(db: LiveSyncLocalDB): void {
|
beforeOnUnload(db: LiveSyncLocalDB): void {
|
||||||
this.kvDB.close();
|
this.kvDB.close();
|
||||||
@@ -258,6 +262,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
this.replicator = new LiveSyncDBReplicator(this);
|
this.replicator = new LiveSyncDBReplicator(this);
|
||||||
}
|
}
|
||||||
async onResetDatabase(db: LiveSyncLocalDB): Promise<void> {
|
async onResetDatabase(db: LiveSyncLocalDB): Promise<void> {
|
||||||
|
const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
|
||||||
|
localStorage.removeItem(lsKey);
|
||||||
await this.kvDB.destroy();
|
await this.kvDB.destroy();
|
||||||
this.kvDB = await OpenKeyValueDatabase(db.dbname + "-livesync-kv");
|
this.kvDB = await OpenKeyValueDatabase(db.dbname + "-livesync-kv");
|
||||||
this.replicator = new LiveSyncDBReplicator(this);
|
this.replicator = new LiveSyncDBReplicator(this);
|
||||||
@@ -320,7 +326,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showHistory(file: TFile | FilePathWithPrefix, id: DocumentID) {
|
showHistory(file: TFile | FilePathWithPrefix, id?: DocumentID) {
|
||||||
new DocumentHistoryModal(this.app, this, file, id).open();
|
new DocumentHistoryModal(this.app, this, file, id).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +340,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
const target = await askSelectString(this.app, "File to view History", notesList);
|
const target = await askSelectString(this.app, "File to view History", notesList);
|
||||||
if (target) {
|
if (target) {
|
||||||
const targetId = notes.find(e => e.dispPath == target);
|
const targetId = notes.find(e => e.dispPath == target);
|
||||||
this.showHistory(targetId.path, undefined);
|
this.showHistory(targetId.path, targetId.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async pickFileForResolve() {
|
async pickFileForResolve() {
|
||||||
@@ -349,7 +355,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
Logger("There are no conflicted documents", LOG_LEVEL_NOTICE);
|
Logger("There are no conflicted documents", LOG_LEVEL_NOTICE);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const target = await askSelectString(this.app, "File to view History", notesList);
|
const target = await askSelectString(this.app, "File to resolve conflict", notesList);
|
||||||
if (target) {
|
if (target) {
|
||||||
const targetItem = notes.find(e => e.dispPath == target);
|
const targetItem = notes.find(e => e.dispPath == target);
|
||||||
await this.resolveConflicted(targetItem.path);
|
await this.resolveConflicted(targetItem.path);
|
||||||
@@ -363,6 +369,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
await this.addOnHiddenFileSync.resolveConflictOnInternalFile(target);
|
await this.addOnHiddenFileSync.resolveConflictOnInternalFile(target);
|
||||||
} else if (isPluginMetadata(target)) {
|
} else if (isPluginMetadata(target)) {
|
||||||
await this.resolveConflictByNewerEntry(target);
|
await this.resolveConflictByNewerEntry(target);
|
||||||
|
} else if (isCustomisationSyncMetadata(target)) {
|
||||||
|
await this.resolveConflictByNewerEntry(target);
|
||||||
} else {
|
} else {
|
||||||
await this.showIfConflicted(target);
|
await this.showIfConflicted(target);
|
||||||
}
|
}
|
||||||
@@ -451,8 +459,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.realizeSettingSyncMode();
|
|
||||||
this.registerWatchEvents();
|
this.registerWatchEvents();
|
||||||
|
await this.realizeSettingSyncMode();
|
||||||
|
this.swapSaveCommand();
|
||||||
if (this.settings.syncOnStart) {
|
if (this.settings.syncOnStart) {
|
||||||
this.replicator.openReplication(this.settings, false, false);
|
this.replicator.openReplication(this.settings, false, false);
|
||||||
}
|
}
|
||||||
@@ -474,7 +483,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
notes.push({ path: this.getPath(doc), mtime: doc.mtime });
|
notes.push({ path: this.getPath(doc), mtime: doc.mtime });
|
||||||
}
|
}
|
||||||
if (notes.length > 0) {
|
if (notes.length > 0) {
|
||||||
Logger(`Some files have been left conflicted! Please resolve them by "Pick a file to resolve conflict". The list is written in the log.`, LOG_LEVEL_NOTICE);
|
this.askInPopup(`conflicting-detected-on-safety`, `Some files have been left conflicted! Press {HERE} to resolve them, or you can do it later by "Pick a file to resolve conflict`, (anchor) => {
|
||||||
|
anchor.text = "HERE";
|
||||||
|
anchor.addEventListener("click", () => {
|
||||||
|
// @ts-ignore
|
||||||
|
this.app.commands.executeCommandById("obsidian-livesync:livesync-all-conflictcheck");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Logger(`Some files have been left conflicted! Please resolve them by "Pick a file to resolve conflict". The list is written in the log.`, LOG_LEVEL_VERBOSE);
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
Logger(`Conflicted: ${note.path}`);
|
Logger(`Conflicted: ${note.path}`);
|
||||||
}
|
}
|
||||||
@@ -512,6 +529,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
if (last_version && Number(last_version) < VER) {
|
if (last_version && Number(last_version) < VER) {
|
||||||
this.settings.liveSync = false;
|
this.settings.liveSync = false;
|
||||||
this.settings.syncOnSave = false;
|
this.settings.syncOnSave = false;
|
||||||
|
this.settings.syncOnEditorSave = false;
|
||||||
this.settings.syncOnStart = false;
|
this.settings.syncOnStart = false;
|
||||||
this.settings.syncOnFileOpen = false;
|
this.settings.syncOnFileOpen = false;
|
||||||
this.settings.syncAfterMerge = false;
|
this.settings.syncAfterMerge = false;
|
||||||
@@ -572,15 +590,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-dump",
|
id: "livesync-dump",
|
||||||
name: "Dump information of this doc ",
|
name: "Dump information of this doc ",
|
||||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
|
||||||
this.localDatabase.getDBEntry(getPathFromTFile(view.file), {}, true, false);
|
const file = view.file;
|
||||||
|
if (!file) return;
|
||||||
|
this.localDatabase.getDBEntry(getPathFromTFile(file), {}, true, false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-checkdoc-conflicted",
|
id: "livesync-checkdoc-conflicted",
|
||||||
name: "Resolve if conflicted.",
|
name: "Resolve if conflicted.",
|
||||||
editorCallback: async (editor: Editor, view: MarkdownView) => {
|
editorCallback: async (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
|
||||||
await this.showIfConflicted(getPathFromTFile(view.file));
|
const file = view.file;
|
||||||
|
if (!file) return;
|
||||||
|
await this.showIfConflicted(getPathFromTFile(file));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -617,8 +639,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-history",
|
id: "livesync-history",
|
||||||
name: "Show history",
|
name: "Show history",
|
||||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
|
||||||
this.showHistory(view.file, null);
|
if (view.file) this.showHistory(view.file, null);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
@@ -720,6 +742,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
}
|
}
|
||||||
cancelAllPeriodicTask();
|
cancelAllPeriodicTask();
|
||||||
cancelAllTasks();
|
cancelAllTasks();
|
||||||
|
this._unloaded = true;
|
||||||
Logger("unloading plugin");
|
Logger("unloading plugin");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,7 +875,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
(async () => await this.realizeSettingSyncMode())();
|
(async () => await this.realizeSettingSyncMode())();
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSettings() {
|
|
||||||
|
async saveSettingData() {
|
||||||
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName();
|
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName();
|
||||||
|
|
||||||
localStorage.setItem(lsKey, this.deviceAndVaultName || "");
|
localStorage.setItem(lsKey, this.deviceAndVaultName || "");
|
||||||
@@ -882,6 +906,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
await this.saveData(settings);
|
await this.saveData(settings);
|
||||||
this.localDatabase.settings = this.settings;
|
this.localDatabase.settings = this.settings;
|
||||||
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
|
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings() {
|
||||||
|
await this.saveSettingData();
|
||||||
this.triggerRealizeSettingSyncMode();
|
this.triggerRealizeSettingSyncMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -889,7 +917,38 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
registerFileWatchEvents() {
|
registerFileWatchEvents() {
|
||||||
this.vaultManager = new StorageEventManagerObsidian(this)
|
this.vaultManager = new StorageEventManagerObsidian(this)
|
||||||
}
|
}
|
||||||
|
_initialCallback: any;
|
||||||
|
swapSaveCommand() {
|
||||||
|
Logger("Modifying callback of the save command", LOG_LEVEL_VERBOSE);
|
||||||
|
const saveCommandDefinition = (this.app as any).commands?.commands?.[
|
||||||
|
"editor:save-file"
|
||||||
|
];
|
||||||
|
const save = saveCommandDefinition?.callback;
|
||||||
|
if (typeof save === "function") {
|
||||||
|
this._initialCallback = save;
|
||||||
|
saveCommandDefinition.callback = () => {
|
||||||
|
scheduleTask("syncOnEditorSave", 250, () => {
|
||||||
|
if (this._unloaded) {
|
||||||
|
Logger("Unload and remove the handler.", LOG_LEVEL_VERBOSE);
|
||||||
|
saveCommandDefinition.callback = this._initialCallback;
|
||||||
|
} else {
|
||||||
|
Logger("Sync on Editor Save.", LOG_LEVEL_VERBOSE);
|
||||||
|
if (this.settings.syncOnEditorSave) {
|
||||||
|
this.replicate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
save();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
|
const _this = this;
|
||||||
|
//@ts-ignore
|
||||||
|
window.CodeMirrorAdapter.commands.save = () => {
|
||||||
|
//@ts-ignore
|
||||||
|
_this.app.commands.executeCommandById('editor:save-file');
|
||||||
|
};
|
||||||
|
}
|
||||||
registerWatchEvents() {
|
registerWatchEvents() {
|
||||||
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
||||||
this.registerDomEvent(document, "visibilitychange", this.watchWindowVisibility);
|
this.registerDomEvent(document, "visibilitychange", this.watchWindowVisibility);
|
||||||
@@ -949,7 +1008,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cancelTask("applyBatchAuto");
|
cancelTask("applyBatchAuto");
|
||||||
const ret = await runWithLock("procFiles", true, async () => {
|
const ret = await skipIfDuplicated("procFiles", async () => {
|
||||||
do {
|
do {
|
||||||
const queue = this.vaultManager.fetchEvent();
|
const queue = this.vaultManager.fetchEvent();
|
||||||
if (queue === false) break;
|
if (queue === false) break;
|
||||||
@@ -1004,9 +1063,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
watchWorkspaceOpen(file: TFile) {
|
watchWorkspaceOpen(file: TFile | null) {
|
||||||
if (this.settings.suspendFileWatching) return;
|
if (this.settings.suspendFileWatching) return;
|
||||||
if (!this.isReady) return;
|
if (!this.isReady) return;
|
||||||
|
if (!file) return;
|
||||||
this.watchWorkspaceOpenAsync(file);
|
this.watchWorkspaceOpenAsync(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1167,17 +1227,25 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
if (docEntry._deleted || docEntry.deleted) {
|
if (docEntry._deleted || docEntry.deleted) {
|
||||||
// This occurs not only when files are deleted, but also when conflicts are resolved.
|
// This occurs not only when files are deleted, but also when conflicts are resolved.
|
||||||
// We have to check no other revisions are left.
|
// We have to check no other revisions are left.
|
||||||
const lastDocs = await this.localDatabase.getDBEntry(path);
|
const existDoc = await this.localDatabase.getDBEntry(path, { conflicts: true });
|
||||||
if (path != file.path) {
|
if (path != file.path) {
|
||||||
Logger(`delete skipped: ${file.path} :Not exactly matched`, LOG_LEVEL_VERBOSE);
|
Logger(`delete skipped: ${file.path} :Not exactly matched`, LOG_LEVEL_VERBOSE);
|
||||||
}
|
}
|
||||||
if (lastDocs === false) {
|
if (existDoc === false) {
|
||||||
await this.deleteVaultItem(file);
|
await this.deleteVaultItem(file);
|
||||||
} else {
|
} else {
|
||||||
// it perhaps delete some revisions.
|
if (existDoc._conflicts) {
|
||||||
// may be we have to reload this
|
if (this.settings.writeDocumentsIfConflicted) {
|
||||||
await this.pullFile(path, null, true);
|
Logger(`Delete: ${file.path}: Conflicted revision has been deleted, but there were more conflicts. `, LOG_LEVEL_INFO);
|
||||||
Logger(`delete skipped:${file.path}`, LOG_LEVEL_VERBOSE);
|
await this.pullFile(path, null, true);
|
||||||
|
} else {
|
||||||
|
Logger(`Delete: ${file.path}: Conflicted revision has been deleted, but there were more conflicts...`);
|
||||||
|
this.queueConflictedOnlyActiveFile(file);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger(`Delete: ${file.path}: Conflict revision has been deleted and resolved`);
|
||||||
|
await this.pullFile(path, null, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1261,7 +1329,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
const path = getPath(entry);
|
const path = getPath(entry);
|
||||||
try {
|
try {
|
||||||
const releaser = await semaphore.acquire(1);
|
const releaser = await semaphore.acquire(1);
|
||||||
runWithLock(`dbchanged-${path}`, false, async () => {
|
serialized(`dbchanged-${path}`, async () => {
|
||||||
Logger(`Applying ${path} (${entry._id}: ${entry._rev}) change...`, LOG_LEVEL_VERBOSE);
|
Logger(`Applying ${path} (${entry._id}: ${entry._rev}) change...`, LOG_LEVEL_VERBOSE);
|
||||||
await this.handleDBChangedAsync(entry);
|
await this.handleDBChangedAsync(entry);
|
||||||
Logger(`Applied ${path} (${entry._id}:${entry._rev}) change...`);
|
Logger(`Applied ${path} (${entry._id}:${entry._rev}) change...`);
|
||||||
@@ -1274,6 +1342,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
this.dbChangeProcRunning = false;
|
this.dbChangeProcRunning = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
queueConflictedOnlyActiveFile(file: TFile) {
|
||||||
|
if (!this.settings.checkConflictOnlyOnOpen) {
|
||||||
|
this.queueConflictedCheck(file);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const af = this.app.workspace.getActiveFile();
|
||||||
|
if (af && af.path == file.path) {
|
||||||
|
this.queueConflictedCheck(file);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
async handleDBChangedAsync(change: EntryBody) {
|
async handleDBChangedAsync(change: EntryBody) {
|
||||||
|
|
||||||
const targetFile = getAbstractFileByPath(this.getPathWithoutPrefix(change));
|
const targetFile = getAbstractFileByPath(this.getPathWithoutPrefix(change));
|
||||||
@@ -1286,29 +1367,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
} else if (targetFile instanceof TFile) {
|
} else if (targetFile instanceof TFile) {
|
||||||
const doc = change;
|
const doc = change;
|
||||||
const file = targetFile;
|
const file = targetFile;
|
||||||
const queueConflictCheck = () => {
|
|
||||||
if (!this.settings.checkConflictOnlyOnOpen) {
|
|
||||||
this.queueConflictedCheck(file);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
const af = app.workspace.getActiveFile();
|
|
||||||
if (af && af.path == file.path) {
|
|
||||||
this.queueConflictedCheck(file);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (this.settings.writeDocumentsIfConflicted) {
|
if (this.settings.writeDocumentsIfConflicted) {
|
||||||
await this.doc2storage(doc, file);
|
await this.doc2storage(doc, file);
|
||||||
queueConflictCheck();
|
this.queueConflictedOnlyActiveFile(file);
|
||||||
} else {
|
} else {
|
||||||
const d = await this.localDatabase.getDBEntryMeta(this.getPath(change), { conflicts: true }, true);
|
const d = await this.localDatabase.getDBEntryMeta(this.getPath(change), { conflicts: true }, true);
|
||||||
if (d && !d._conflicts) {
|
if (d && !d._conflicts) {
|
||||||
await this.doc2storage(doc, file);
|
await this.doc2storage(doc, file);
|
||||||
} else {
|
} else {
|
||||||
if (!queueConflictCheck()) {
|
if (!this.queueConflictedOnlyActiveFile(file)) {
|
||||||
Logger(`${this.getPath(change)} is conflicted, write to the storage has been pended.`, LOG_LEVEL_NOTICE);
|
Logger(`${this.getPath(change)} is conflicted, write to the storage has been postponed.`, LOG_LEVEL_NOTICE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1347,10 +1415,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
if (queue.missingChildren.length == 0) {
|
if (queue.missingChildren.length == 0) {
|
||||||
queue.done = true;
|
queue.done = true;
|
||||||
if (isInternalMetadata(queue.entry._id)) {
|
if (isInternalMetadata(queue.entry._id) && this.settings.syncInternalFiles) {
|
||||||
//system file
|
//system file
|
||||||
const filename = this.getPathWithoutPrefix(queue.entry);
|
const filename = this.getPathWithoutPrefix(queue.entry);
|
||||||
this.addOnHiddenFileSync.procInternalFile(filename);
|
this.isTargetFile(filename).then((ret) => ret ? this.addOnHiddenFileSync.procInternalFile(filename) : Logger(`Skipped (Not target:${filename})`, LOG_LEVEL_VERBOSE));
|
||||||
} else if (isValidPath(this.getPath(queue.entry))) {
|
} else if (isValidPath(this.getPath(queue.entry))) {
|
||||||
this.handleDBChanged(queue.entry);
|
this.handleDBChanged(queue.entry);
|
||||||
} else {
|
} else {
|
||||||
@@ -1392,7 +1460,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
if (!await this.isTargetFile(path)) return;
|
if (!await this.isTargetFile(path)) return;
|
||||||
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
|
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
|
||||||
// Do not handle internal files if the feature has not been enabled.
|
// Do not handle internal files if the feature has not been enabled.
|
||||||
if (isInternalMetadata(doc._id) && !this.settings.syncInternalFiles) return;
|
if (isInternalMetadata(doc._id) && !this.settings.syncInternalFiles) {
|
||||||
|
Logger(`Skipped: ${path} (${doc._id}, ${doc._rev}) Hidden file sync is disabled.`, LOG_LEVEL_VERBOSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isCustomisationSyncMetadata(doc._id) && !this.settings.usePluginSync) {
|
||||||
|
Logger(`Skipped: ${path} (${doc._id}, ${doc._rev}) Customization sync is disabled.`, LOG_LEVEL_VERBOSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// It is better for your own safety, not to handle the following files
|
// It is better for your own safety, not to handle the following files
|
||||||
const ignoreFiles = [
|
const ignoreFiles = [
|
||||||
"_design/replicate",
|
"_design/replicate",
|
||||||
@@ -1466,12 +1541,22 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
|
|
||||||
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") {
|
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") {
|
||||||
if (this.settings.suspendParseReplicationResult) {
|
if (this.settings.suspendParseReplicationResult) {
|
||||||
|
if (isInternalMetadata(change._id) && !this.settings.syncInternalFiles) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isCustomisationSyncMetadata(change._id) && !this.settings.usePluginSync) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!change.path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const newQueue = {
|
const newQueue = {
|
||||||
entry: change,
|
entry: change,
|
||||||
missingChildren: [] as string[],
|
missingChildren: [] as string[],
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
};
|
};
|
||||||
Logger(`Processing scheduled: ${change.path}`, LOG_LEVEL_INFO);
|
Logger(`Processing scheduled: ${change.path}`, LOG_LEVEL_INFO);
|
||||||
|
this.queuedFiles = this.queuedFiles.filter(e => e.entry.path != change.path);
|
||||||
this.queuedFiles.push(newQueue);
|
this.queuedFiles.push(newQueue);
|
||||||
this.saveQueuedFiles();
|
this.saveQueuedFiles();
|
||||||
continue;
|
continue;
|
||||||
@@ -1638,7 +1723,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
if (this.replicator.remoteLockedAndDeviceNotAccepted) {
|
if (this.replicator.remoteLockedAndDeviceNotAccepted) {
|
||||||
if (this.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
|
if (this.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
|
||||||
Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
|
Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
|
||||||
await runWithLock("cleanup", true, async () => {
|
await skipIfDuplicated("cleanup", async () => {
|
||||||
const count = await purgeUnreferencedChunks(this.localDatabase.localDatabase, true);
|
const count = await purgeUnreferencedChunks(this.localDatabase.localDatabase, true);
|
||||||
const message = `The remote database has been cleaned up.
|
const message = `The remote database has been cleaned up.
|
||||||
To synchronize, this device must be also cleaned up. ${count} chunk(s) will be erased from this device.
|
To synchronize, this device must be also cleaned up. ${count} chunk(s) will be erased from this device.
|
||||||
@@ -1901,6 +1986,9 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
const ret = [] as Diff[];
|
const ret = [] as Diff[];
|
||||||
do {
|
do {
|
||||||
const d = src.shift();
|
const d = src.shift();
|
||||||
|
if (d === undefined) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
const pieces = d[1].split(/([^\n]*\n)/).filter(f => f != "");
|
const pieces = d[1].split(/([^\n]*\n)/).filter(f => f != "");
|
||||||
if (typeof (d) == "undefined") {
|
if (typeof (d) == "undefined") {
|
||||||
break;
|
break;
|
||||||
@@ -2161,18 +2249,11 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
Logger(`could not get old revisions, automatically used newer one:${path}`, LOG_LEVEL_NOTICE);
|
Logger(`could not get old revisions, automatically used newer one:${path}`, LOG_LEVEL_NOTICE);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// first, check for same contents and deletion status.
|
|
||||||
if (leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted) {
|
const isSame = leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted;
|
||||||
let leaf = leftLeaf;
|
const isBinary = !isPlainText(path);
|
||||||
if (leftLeaf.mtime > rightLeaf.mtime) {
|
const alwaysNewer = this.settings.resolveConflictsByNewerFile;
|
||||||
leaf = rightLeaf;
|
if (isSame || isBinary || alwaysNewer) {
|
||||||
}
|
|
||||||
await this.localDatabase.deleteDBEntry(path, { rev: leaf.rev });
|
|
||||||
await this.pullFile(path, null, true);
|
|
||||||
Logger(`automatically merged:${path}`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (this.settings.resolveConflictsByNewerFile) {
|
|
||||||
const lMtime = ~~(leftLeaf.mtime / 1000);
|
const lMtime = ~~(leftLeaf.mtime / 1000);
|
||||||
const rMtime = ~~(rightLeaf.mtime / 1000);
|
const rMtime = ~~(rightLeaf.mtime / 1000);
|
||||||
let loser = leftLeaf;
|
let loser = leftLeaf;
|
||||||
@@ -2181,7 +2262,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
}
|
}
|
||||||
await this.localDatabase.deleteDBEntry(path, { rev: loser.rev });
|
await this.localDatabase.deleteDBEntry(path, { rev: loser.rev });
|
||||||
await this.pullFile(path, null, true);
|
await this.pullFile(path, null, true);
|
||||||
Logger(`Automatically merged (newerFileResolve) :${path}`, LOG_LEVEL_NOTICE);
|
Logger(`Automatically merged (${isSame ? "same," : ""}${isBinary ? "binary," : ""}${alwaysNewer ? "alwaysNewer" : ""}) :${path}`, LOG_LEVEL_NOTICE);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// make diff.
|
// make diff.
|
||||||
@@ -2197,7 +2278,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
}
|
}
|
||||||
|
|
||||||
showMergeDialog(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean> {
|
showMergeDialog(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean> {
|
||||||
return runWithLock("resolve-conflict:" + filename, false, () =>
|
return serialized("resolve-conflict:" + filename, () =>
|
||||||
new Promise((res, rej) => {
|
new Promise((res, rej) => {
|
||||||
Logger("open conflict dialog", LOG_LEVEL_VERBOSE);
|
Logger("open conflict dialog", LOG_LEVEL_VERBOSE);
|
||||||
new ConflictResolveModal(this.app, filename, conflictCheckResult, async (selected) => {
|
new ConflictResolveModal(this.app, filename, conflictCheckResult, async (selected) => {
|
||||||
@@ -2276,7 +2357,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
}
|
}
|
||||||
|
|
||||||
async showIfConflicted(filename: FilePathWithPrefix) {
|
async showIfConflicted(filename: FilePathWithPrefix) {
|
||||||
await runWithLock("conflicted", false, async () => {
|
await serialized("conflicted", async () => {
|
||||||
const conflictCheckResult = await this.getConflictedStatus(filename);
|
const conflictCheckResult = await this.getConflictedStatus(filename);
|
||||||
if (conflictCheckResult === false) {
|
if (conflictCheckResult === false) {
|
||||||
//nothing to do.
|
//nothing to do.
|
||||||
@@ -2428,7 +2509,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
};
|
};
|
||||||
//upsert should locked
|
//upsert should locked
|
||||||
const msg = `DB <- STORAGE (${datatype}) `;
|
const msg = `DB <- STORAGE (${datatype}) `;
|
||||||
const isNotChanged = await runWithLock("file-" + fullPath, false, async () => {
|
const isNotChanged = await serialized("file-" + fullPath, async () => {
|
||||||
if (recentlyTouched(file)) {
|
if (recentlyTouched(file)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2597,7 +2678,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
return this.localDatabase.isTargetFile(filepath);
|
return this.localDatabase.isTargetFile(filepath);
|
||||||
}
|
}
|
||||||
async dryRunGC() {
|
async dryRunGC() {
|
||||||
await runWithLock("cleanup", true, async () => {
|
await skipIfDuplicated("cleanup", async () => {
|
||||||
const remoteDBConn = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.isMobile)
|
const remoteDBConn = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.isMobile)
|
||||||
if (typeof (remoteDBConn) == "string") {
|
if (typeof (remoteDBConn) == "string") {
|
||||||
Logger(remoteDBConn);
|
Logger(remoteDBConn);
|
||||||
@@ -2611,7 +2692,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
|
|
||||||
async dbGC() {
|
async dbGC() {
|
||||||
// Lock the remote completely once.
|
// Lock the remote completely once.
|
||||||
await runWithLock("cleanup", true, async () => {
|
await skipIfDuplicated("cleanup", async () => {
|
||||||
this.getReplicator().markRemoteLocked(this.settings, true, true);
|
this.getReplicator().markRemoteLocked(this.settings, true, true);
|
||||||
const remoteDBConn = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.isMobile)
|
const remoteDBConn = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.isMobile)
|
||||||
if (typeof (remoteDBConn) == "string") {
|
if (typeof (remoteDBConn) == "string") {
|
||||||
@@ -2626,5 +2707,41 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
Logger("The remote database has been cleaned up! Other devices will be cleaned up on the next synchronisation.")
|
Logger("The remote database has been cleaned up! Other devices will be cleaned up on the next synchronisation.")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void) {
|
||||||
|
|
||||||
|
const fragment = createFragment((doc) => {
|
||||||
|
|
||||||
|
const [beforeText, afterText] = dialogText.split("{HERE}", 2);
|
||||||
|
doc.createEl("span", null, (a) => {
|
||||||
|
a.appendText(beforeText);
|
||||||
|
a.appendChild(a.createEl("a", null, (anchor) => {
|
||||||
|
anchorCallback(anchor);
|
||||||
|
}));
|
||||||
|
|
||||||
|
a.appendText(afterText);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const popupKey = "popup-" + key;
|
||||||
|
scheduleTask(popupKey, 1000, async () => {
|
||||||
|
const popup = await memoIfNotExist(popupKey, () => new Notice(fragment, 0));
|
||||||
|
//@ts-ignore
|
||||||
|
const isShown = popup?.noticeEl?.isShown();
|
||||||
|
if (!isShown) {
|
||||||
|
memoObject(popupKey, new Notice(fragment, 0));
|
||||||
|
}
|
||||||
|
scheduleTask(popupKey + "-close", 20000, () => {
|
||||||
|
const popup = retrieveMemoObject<Notice>(popupKey);
|
||||||
|
if (!popup)
|
||||||
|
return;
|
||||||
|
//@ts-ignore
|
||||||
|
if (popup?.noticeEl?.isShown()) {
|
||||||
|
popup.hide();
|
||||||
|
}
|
||||||
|
disposeMemoObject(popupKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
src/utils.ts
12
src/utils.ts
@@ -3,9 +3,10 @@ import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDa
|
|||||||
|
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "./lib/src/types";
|
import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "./lib/src/types";
|
||||||
import { CHeader, ICHeader, ICHeaderLength, PSCHeader } from "./types";
|
import { CHeader, ICHeader, ICHeaderLength, ICXHeader, PSCHeader } from "./types";
|
||||||
import { InputStringDialog, PopoverSelectString } from "./dialogs";
|
import { InputStringDialog, PopoverSelectString } from "./dialogs";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
|
import { writeString } from "./lib/src/strbin";
|
||||||
|
|
||||||
// For backward compatibility, using the path for determining id.
|
// For backward compatibility, using the path for determining id.
|
||||||
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
|
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||||
@@ -387,6 +388,9 @@ export function isChunk(str: string): boolean {
|
|||||||
export function isPluginMetadata(str: string): boolean {
|
export function isPluginMetadata(str: string): boolean {
|
||||||
return str.startsWith(PSCHeader);
|
return str.startsWith(PSCHeader);
|
||||||
}
|
}
|
||||||
|
export function isCustomisationSyncMetadata(str: string): boolean {
|
||||||
|
return str.startsWith(ICXHeader);
|
||||||
|
}
|
||||||
|
|
||||||
export const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
|
export const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
|
||||||
return new Promise((res) => {
|
return new Promise((res) => {
|
||||||
@@ -440,7 +444,7 @@ export class PeriodicProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const _requestToCouchDBFetch = async (baseUri: string, username: string, password: string, path?: string, body?: string | any, method?: string) => {
|
export const _requestToCouchDBFetch = async (baseUri: string, username: string, password: string, path?: string, body?: string | any, method?: string) => {
|
||||||
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${username}:${password}`));
|
const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]);
|
||||||
const encoded = window.btoa(utf8str);
|
const encoded = window.btoa(utf8str);
|
||||||
const authHeader = "Basic " + encoded;
|
const authHeader = "Basic " + encoded;
|
||||||
const transformedHeaders: Record<string, string> = { authorization: authHeader, "content-type": "application/json" };
|
const transformedHeaders: Record<string, string> = { authorization: authHeader, "content-type": "application/json" };
|
||||||
@@ -456,7 +460,7 @@ export const _requestToCouchDBFetch = async (baseUri: string, username: string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const _requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string, path?: string, body?: any, method?: string) => {
|
export const _requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string, path?: string, body?: any, method?: string) => {
|
||||||
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${username}:${password}`));
|
const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]);
|
||||||
const encoded = window.btoa(utf8str);
|
const encoded = window.btoa(utf8str);
|
||||||
const authHeader = "Basic " + encoded;
|
const authHeader = "Basic " + encoded;
|
||||||
const transformedHeaders: Record<string, string> = { authorization: authHeader, origin: origin };
|
const transformedHeaders: Record<string, string> = { authorization: authHeader, origin: origin };
|
||||||
@@ -477,7 +481,7 @@ export const requestToCouchDB = async (baseUri: string, username: string, passwo
|
|||||||
|
|
||||||
export async function performRebuildDB(plugin: ObsidianLiveSyncPlugin, method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice") {
|
export async function performRebuildDB(plugin: ObsidianLiveSyncPlugin, method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice") {
|
||||||
if (method == "localOnly") {
|
if (method == "localOnly") {
|
||||||
await plugin.addOnSetup.fetchLocal();
|
await plugin.addOnSetup.fetchLocalWithKeepLocal();
|
||||||
}
|
}
|
||||||
if (method == "remoteOnly") {
|
if (method == "remoteOnly") {
|
||||||
await plugin.addOnSetup.rebuildRemote();
|
await plugin.addOnSetup.rebuildRemote();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
// "importsNotUsedAsValues": "error",
|
// "importsNotUsedAsValues": "error",
|
||||||
"importHelpers": false,
|
"importHelpers": false,
|
||||||
"alwaysStrict": true,
|
"alwaysStrict": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"es2018",
|
"es2018",
|
||||||
"DOM",
|
"DOM",
|
||||||
|
|||||||
65
updates.md
65
updates.md
@@ -14,34 +14,49 @@ I hope you will give it a try.
|
|||||||
|
|
||||||
#### Minors
|
#### Minors
|
||||||
|
|
||||||
- 0.19.1 to 0.19.14 has been moved into the updates_old.md
|
- 0.19.1 to 0.19.17 has been moved into the updates_old.md
|
||||||
- 0.19.15
|
|
||||||
|
- 0.19.18
|
||||||
- Fixed:
|
- Fixed:
|
||||||
- Now storing files after cleaning up is correct works.
|
- Now the empty (or deleted) file could be conflict-resolved.
|
||||||
- Improved:
|
- 0.19.19
|
||||||
- Cleaning the local database up got incredibly fastened.
|
- Fixed:
|
||||||
Now we can clean instead of fetching again when synchronising with the remote which has been cleaned up.
|
- Resolving conflicted revision has become more robust.
|
||||||
- 0.19.16
|
- LiveSync now try to keep local changes when fetching from the rebuilt remote database.
|
||||||
- Many upgrades on this release. I have tried not to let that happen, if something got corrupted, please feel free to notify me.
|
Local changes now have been kept as a revision and fetched things will be new revisions.
|
||||||
|
- Now, all files will be restored after performing `fetch` immediately.
|
||||||
|
- 0.19.20
|
||||||
- New feature:
|
- New feature:
|
||||||
- (Beta) ignore files handling
|
- `Sync on Editor save` has been implemented
|
||||||
We can use `.gitignore`, `.dockerignore`, and anything you like to filter the synchronising files.
|
- We can start synchronisation when we save from the Obsidian explicitly.
|
||||||
|
- Now we can use the `Hidden file sync` and the `Customization sync` cooperatively.
|
||||||
|
- We can exclude files from `Hidden file sync` which is already handled in Customization sync.
|
||||||
|
- We can ignore specific plugins in Customization sync.
|
||||||
|
- Now the message of leftover conflicted files accepts our click.
|
||||||
|
- We can open `Resolve all conflicted files` in an instant.
|
||||||
|
- Refactored:
|
||||||
|
- Parallelism functions made more explicit.
|
||||||
|
- Type errors have been reduced.
|
||||||
- Fixed:
|
- Fixed:
|
||||||
- Buttons on lock-detected-dialogue now can be shown in narrow-width devices.
|
- Now documents would not be overwritten if they are conflicted.
|
||||||
- Improved:
|
It will be saved as a new conflicted revision.
|
||||||
- Some constant has been flattened to be evaluated.
|
- Some error messages have been fixed.
|
||||||
- The usage of the deprecated API of obsidian has been reduced.
|
- Missing dialogue titles have been shown now.
|
||||||
- Now the indexedDB adapter will be enabled while the importing configuration.
|
- We can click close buttons on mobile now.
|
||||||
- Misc:
|
- Conflicted Customisation sync files will be resolved automatically by their modified time.
|
||||||
- Compiler, framework, and dependencies have been upgraded.
|
- 0.19.21
|
||||||
- Due to standing for these impacts (especially in esbuild and svelte,) terser has been introduced.
|
|
||||||
Feel free to notify your opinion to me! I do not like to obfuscate the code too.
|
|
||||||
- 0.19.17
|
|
||||||
- Fixed:
|
- Fixed:
|
||||||
- Now nested ignore files could be parsed correctly.
|
- Hidden files are no longer handled in the initial replication.
|
||||||
- The unexpected deletion of hidden files in some cases has been corrected.
|
- Report from `Making report` fixed
|
||||||
- Hidden file change is no longer reflected on the device which has made the change itself.
|
- No longer contains customisation sync information.
|
||||||
- Behaviour changed:
|
- Version of LiveSync has been added.
|
||||||
- From this version, the file which has `:` in its name should be ignored even if on Linux devices.
|
- 0.19.22
|
||||||
|
Fixed:
|
||||||
|
- Now the synchronisation will begin without our interaction.
|
||||||
|
- No longer puts the configuration of the remote database into the log while checking configuration.
|
||||||
|
- Some outdated description notes have been removed.
|
||||||
|
- Options that are meaningless depending on other settings configured are now hidden.
|
||||||
|
- Scan for hidden files before replication
|
||||||
|
- Scan customization periodically
|
||||||
|
|
||||||
... To continue on to `updates_old.md`.
|
... To continue on to `updates_old.md`.
|
||||||
|
|||||||
@@ -113,6 +113,34 @@ I hope you will give it a try.
|
|||||||
- Improved:
|
- Improved:
|
||||||
- Now progress is reported while the cleaning up and fetch process.
|
- Now progress is reported while the cleaning up and fetch process.
|
||||||
- Cancelled replication is now detected.
|
- Cancelled replication is now detected.
|
||||||
|
- 0.19.15
|
||||||
|
- Fixed:
|
||||||
|
- Now storing files after cleaning up is correct works.
|
||||||
|
- Improved:
|
||||||
|
- Cleaning the local database up got incredibly fastened.
|
||||||
|
Now we can clean instead of fetching again when synchronising with the remote which has been cleaned up.
|
||||||
|
- 0.19.16
|
||||||
|
- Many upgrades on this release. I have tried not to let that happen, if something got corrupted, please feel free to notify me.
|
||||||
|
- New feature:
|
||||||
|
- (Beta) ignore files handling
|
||||||
|
We can use `.gitignore`, `.dockerignore`, and anything you like to filter the synchronising files.
|
||||||
|
- Fixed:
|
||||||
|
- Buttons on lock-detected-dialogue now can be shown in narrow-width devices.
|
||||||
|
- Improved:
|
||||||
|
- Some constant has been flattened to be evaluated.
|
||||||
|
- The usage of the deprecated API of obsidian has been reduced.
|
||||||
|
- Now the indexedDB adapter will be enabled while the importing configuration.
|
||||||
|
- Misc:
|
||||||
|
- Compiler, framework, and dependencies have been upgraded.
|
||||||
|
- Due to standing for these impacts (especially in esbuild and svelte,) terser has been introduced.
|
||||||
|
Feel free to notify your opinion to me! I do not like to obfuscate the code too.
|
||||||
|
- 0.19.17
|
||||||
|
- Fixed:
|
||||||
|
- Now nested ignore files could be parsed correctly.
|
||||||
|
- The unexpected deletion of hidden files in some cases has been corrected.
|
||||||
|
- Hidden file change is no longer reflected on the device which has made the change itself.
|
||||||
|
- Behaviour changed:
|
||||||
|
- From this version, the file which has `:` in its name should be ignored even if on Linux devices.
|
||||||
### 0.18.0
|
### 0.18.0
|
||||||
|
|
||||||
#### Now, paths of files in the database can now be obfuscated. (Experimental Feature)
|
#### Now, paths of files in the database can now be obfuscated. (Experimental Feature)
|
||||||
|
|||||||
Reference in New Issue
Block a user