Browse Source

Merge branch 'master' into feat/whiteboards-create-button

Konstantinos Kaloutas 3 years ago
parent
commit
7bd31def16
58 changed files with 415 additions and 2211 deletions
  1. 1 0
      .gitignore
  2. 2 1
      android/app/build.gradle
  3. 3 2
      android/app/capacitor.build.gradle
  4. 1 0
      android/app/src/main/AndroidManifest.xml
  5. 0 21
      android/app/src/main/assets/capacitor.config.json
  6. 4 0
      android/app/src/main/assets/capacitor.plugins.json
  7. 0 351
      android/app/src/main/java/com/logseq/app/FileSync.java
  8. 1 2
      android/app/src/main/java/com/logseq/app/MainActivity.java
  9. 6 0
      android/app/src/main/res/drawable/splash_centered.xml
  10. 4 0
      android/app/src/main/res/values/colors.xml
  11. 9 4
      android/app/src/main/res/values/styles.xml
  12. 5 2
      android/build.gradle
  13. 3 0
      android/capacitor.settings.gradle
  14. 0 1
      android/file-sync/.gitignore
  15. 0 36
      android/file-sync/build.gradle
  16. 0 0
      android/file-sync/consumer-rules.pro
  17. 0 21
      android/file-sync/proguard-rules.pro
  18. 0 5
      android/file-sync/src/main/AndroidManifest.xml
  19. 0 33
      android/file-sync/src/main/java/com/logseq/sync/FileMeta.java
  20. 0 34
      android/file-sync/src/main/java/com/logseq/sync/RSFileSync.java
  21. BIN
      android/file-sync/src/main/jniLibs/arm64-v8a/librsapi.so
  22. BIN
      android/file-sync/src/main/jniLibs/armeabi-v7a/librsapi.so
  23. BIN
      android/file-sync/src/main/jniLibs/x86/librsapi.so
  24. BIN
      android/file-sync/src/main/jniLibs/x86_64/librsapi.so
  25. 1 1
      android/gradle/wrapper/gradle-wrapper.properties
  26. 1 1
      android/settings.gradle
  27. 14 12
      android/variables.gradle
  28. 5 0
      capacitor.config.ts
  29. 0 32
      ios/App/App.xcodeproj/project.pbxproj
  30. 4 19
      ios/App/App/AppDelegate.swift
  31. 0 110
      ios/App/App/FileSync/AgeEncryption.swift
  32. 0 211
      ios/App/App/FileSync/Extensions.swift
  33. 0 25
      ios/App/App/FileSync/FileSync.m
  34. 0 596
      ios/App/App/FileSync/FileSync.swift
  35. 0 36
      ios/App/App/FileSync/Payload.swift
  36. 0 381
      ios/App/App/FileSync/SyncClient.swift
  37. 0 5
      ios/App/App/Info.plist
  38. 12 4
      ios/App/Podfile
  39. 1 1
      libs/src/LSPlugin.ts
  40. 16 15
      package.json
  41. 36 40
      src/main/frontend/components/file_sync.cljs
  42. 2 1
      src/main/frontend/components/repo.cljs
  43. 2 2
      src/main/frontend/components/sidebar.css
  44. 31 31
      src/main/frontend/dicts.cljc
  45. 3 3
      src/main/frontend/encrypt.cljs
  46. 1 1
      src/main/frontend/fs.cljs
  47. 27 35
      src/main/frontend/fs/capacitor_fs.cljs
  48. 69 14
      src/main/frontend/fs/sync.cljs
  49. 9 4
      src/main/frontend/handler/events.cljs
  50. 3 10
      src/main/frontend/handler/file_sync.cljs
  51. 6 3
      src/main/frontend/handler/repo.cljs
  52. 2 3
      src/main/frontend/handler/user.cljs
  53. 1 1
      src/main/frontend/handler/web/nfs.cljs
  54. 4 6
      src/main/frontend/mobile/util.cljs
  55. 10 4
      src/main/frontend/state.cljs
  56. 2 2
      templates/config.edn
  57. 6 6
      templates/tutorial-pl.md
  58. 108 83
      yarn.lock

+ 1 - 0
.gitignore

@@ -47,3 +47,4 @@ startup.png
 ~*~
 
 ios/App/App/capacitor.config.json
+android/app/src/main/assets/capacitor.config.json

+ 2 - 1
android/app/build.gradle

@@ -30,6 +30,8 @@ repositories {
 }
 
 dependencies {
+    implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
+    implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
     implementation fileTree(include: ['*.jar'], dir: 'libs')
     implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
     implementation project(':capacitor-android')
@@ -38,7 +40,6 @@ dependencies {
     androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
     androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
     implementation project(':capacitor-cordova-android-plugins')
-    implementation project(':file-sync')
 }
 
 apply from: 'capacitor.build.gradle'

+ 3 - 2
android/app/capacitor.build.gradle

@@ -2,8 +2,8 @@
 
 android {
   compileOptions {
-      sourceCompatibility JavaVersion.VERSION_1_8
-      targetCompatibility JavaVersion.VERSION_1_8
+      sourceCompatibility JavaVersion.VERSION_11
+      targetCompatibility JavaVersion.VERSION_11
   }
 }
 
@@ -18,6 +18,7 @@ dependencies {
     implementation project(':capacitor-share')
     implementation project(':capacitor-splash-screen')
     implementation project(':capacitor-status-bar')
+    implementation project(':logseq-capacitor-file-sync')
     implementation project(':capacitor-voice-recorder')
     implementation project(':send-intent')
 

+ 1 - 0
android/app/src/main/AndroidManifest.xml

@@ -12,6 +12,7 @@
         android:theme="@style/AppTheme">
 
         <activity
+            android:exported="true"
             android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
             android:name="com.logseq.app.MainActivity"
             android:label="@string/title_activity_main"

+ 0 - 21
android/app/src/main/assets/capacitor.config.json

@@ -1,21 +0,0 @@
-{
-	"appId": "com.logseq.app",
-	"appName": "Logseq",
-	"bundledWebRuntime": false,
-	"webDir": "public",
-	"plugins": {
-		"SplashScreen": {
-			"launchShowDuration": 500,
-			"launchAutoHide": false,
-			"androidScaleType": "CENTER_CROP",
-			"splashImmersive": false,
-			"backgroundColor": "#002b36"
-		},
-		"Keyboard": {
-			"resize": "none"
-		}
-	},
-	"ios": {
-		"scheme": "Logseq"
-	}
-}

+ 4 - 0
android/app/src/main/assets/capacitor.plugins.json

@@ -35,6 +35,10 @@
 		"pkg": "@capacitor/status-bar",
 		"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
 	},
+	{
+		"pkg": "@logseq/capacitor-file-sync",
+		"classpath": "com.logseq.app.FileSyncPlugin"
+	},
 	{
 		"pkg": "capacitor-voice-recorder",
 		"classpath": "com.tchvu3.capacitorvoicerecorder.VoiceRecorder"

+ 0 - 351
android/app/src/main/java/com/logseq/app/FileSync.java

@@ -1,351 +0,0 @@
-package com.logseq.app;
-
-import android.net.Uri;
-import android.util.Log;
-
-import com.getcapacitor.JSArray;
-import com.getcapacitor.JSObject;
-import com.getcapacitor.Plugin;
-import com.getcapacitor.annotation.CapacitorPlugin;
-import com.getcapacitor.PluginCall;
-import com.getcapacitor.PluginMethod;
-import com.logseq.sync.FileMeta;
-import com.logseq.sync.RSFileSync;
-
-import org.json.JSONException;
-
-import java.util.List;
-
-@CapacitorPlugin(name = "FileSync")
-public class FileSync extends Plugin {
-
-    @Override
-    public void load() {
-        super.load();
-
-        Log.i("FileSync", "Android plugin loaded");
-    }
-
-    @PluginMethod()
-    public void keygen(PluginCall call) {
-        call.setKeepAlive(true);
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                String[] keyPairs = RSFileSync.keygen();
-                JSObject data = new JSObject();
-                data.put("secretKey", keyPairs[0]);
-                data.put("publicKey", keyPairs[1]);
-                call.resolve(data);
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void setKey(PluginCall call) {
-        String secretKey = call.getString("secretKey");
-        String publicKey = call.getString("publicKey");
-        long code = RSFileSync.setKeys(secretKey, publicKey);
-        if (code != -1) {
-            JSObject ret = new JSObject();
-            ret.put("ok", true);
-            call.resolve(ret);
-        } else {
-            call.reject("invalid setKey call");
-        }
-    }
-
-    @PluginMethod()
-    public void setEnv(PluginCall call) {
-        String env = call.getString("env");
-        if (env == null) {
-            call.reject("required parameter: env");
-            return;
-        }
-        this.setKey(call);
-        long code = RSFileSync.setEnvironment(env);
-        if (code != -1) {
-            JSObject ret = new JSObject();
-            ret.put("ok", true);
-            call.resolve(ret);
-        } else {
-            call.reject("invalid setEnv call");
-        }
-    }
-
-    @PluginMethod()
-    public void encryptFnames(PluginCall call) {
-        call.setKeepAlive(true);
-
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                List<String> filePaths = null;
-                try {
-                    filePaths = call.getArray("filePaths").toList();
-                } catch (JSONException e) {
-                    e.printStackTrace();
-                    return;
-                }
-
-                for (int i = 0; i < filePaths.size(); i++) {
-                    String filePath = filePaths.get(i);
-                    filePaths.set(i, Uri.decode(filePath));
-                }
-
-                String[] raw;
-                raw = RSFileSync.encryptFilenames(filePaths);
-                if (raw != null) {
-                    JSObject ret = new JSObject();
-                    ret.put("value", JSArray.from(raw));
-                    call.resolve(ret);
-                }
-
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void decryptFnames(PluginCall call) {
-        call.setKeepAlive(true);
-
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                JSArray filePaths = call.getArray("filePaths");
-                String[] raw;
-                try {
-                    raw = RSFileSync.decryptFilenames(filePaths.toList());
-                    for (int i = 0; i < raw.length; i++) {
-                        raw[i] = Uri.encode(raw[i], "/");
-                    }
-                    if (raw != null) {
-                        JSObject ret = new JSObject();
-                        ret.put("value", JSArray.from(raw));
-                        call.resolve(ret);
-                    }
-                } catch (JSONException e) {
-                    e.printStackTrace();
-                    call.reject("cannot decrypt fnames: " + e.toString());
-                }
-            }
-        };
-        runner.start();
-    }
-
-    //@PluginMethod(returnType = PluginMethod.RETURN_CALLBACK)
-    @PluginMethod()
-    public void getLocalFilesMeta(PluginCall call) throws JSONException {
-        String basePath = call.getString("basePath");
-        List<String> filePaths = call.getArray("filePaths").toList();
-
-
-        call.setKeepAlive(true);
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                for (int i = 0; i < filePaths.size(); i++) {
-                    String filePath = filePaths.get(i);
-                    filePaths.set(i, Uri.decode(filePath));
-                }
-
-                FileMeta[] metas = RSFileSync.getLocalFilesMeta(basePath, filePaths);
-                if (metas == null) {
-                    call.reject(RSFileSync.getLastError());
-                    return;
-                }
-                JSObject dict = new JSObject();
-                for (FileMeta meta : metas) {
-                    if (meta == null) {
-                        continue;
-                    }
-                    Log.i("FileSync", "got meta " + meta.toString());
-                    JSObject item = new JSObject();
-                    item.put("md5", meta.md5);
-                    item.put("size", meta.size);
-                    item.put("encryptedFname", meta.encryptedFilename);
-
-                    item.put("mtime", meta.mtime); // not used for now
-                    dict.put(Uri.encode(meta.filePath, "/"), item);
-                }
-                JSObject ret = new JSObject();
-                ret.put("result", dict);
-                call.resolve(ret);
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void getLocalAllFilesMeta(PluginCall call) {
-        call.setKeepAlive(true);
-
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                String basePath = call.getString("basePath");
-                FileMeta[] metas = RSFileSync.getLocalAllFilesMeta(basePath);
-                if (metas == null) {
-                    call.reject(RSFileSync.getLastError());
-                    return;
-                }
-                JSObject dict = new JSObject();
-                for (FileMeta meta : metas) {
-                    JSObject item = new JSObject();
-                    item.put("md5", meta.md5);
-                    item.put("size", meta.size);
-                    item.put("encryptedFname", meta.encryptedFilename);
-
-                    item.put("mtime", meta.mtime); // not used for now
-                    dict.put(Uri.encode(meta.filePath, "/"), item);
-                }
-                JSObject ret = new JSObject();
-                ret.put("result", dict);
-                call.resolve(ret);
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void deleteLocalFiles(PluginCall call) throws JSONException {
-        String basePath = call.getString("basePath");
-        List<String> filePaths = call.getArray("filePaths").toList();
-        for (int i = 0; i < filePaths.size(); i++) {
-            filePaths.set(i, Uri.decode(filePaths.get(i)));
-        }
-
-        RSFileSync.deleteLocalFiles(basePath, filePaths);
-
-        JSObject ret = new JSObject();
-        ret.put("ok", true);
-        call.resolve(ret);
-    }
-
-    @PluginMethod()
-    public void updateLocalFiles(PluginCall call) throws JSONException {
-        String basePath = call.getString("basePath");
-        List<String> filePaths = call.getArray("filePaths").toList();
-        String graphUUID = call.getString("graphUUID");
-        String token = call.getString("token");
-
-        for (int i = 0; i < filePaths.size(); i++) {
-            filePaths.set(i, Uri.decode(filePaths.get(i)));
-        }
-
-        call.setKeepAlive(true);
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                long code = RSFileSync.updateLocalFiles(basePath, filePaths, graphUUID, token);
-                if (code != -1) {
-                    JSObject ret = new JSObject();
-                    ret.put("ok", true);
-                    call.resolve(ret);
-                } else {
-                    call.reject(RSFileSync.getLastError());
-                }
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void updateLocalVersionFiles(PluginCall call) throws JSONException {
-        String basePath = call.getString("basePath");
-        List<String> filePaths = call.getArray("filePaths").toList();
-        String graphUUID = call.getString("graphUUID");
-        String token = call.getString("token");
-
-        for (int i = 0; i < filePaths.size(); i++) {
-            filePaths.set(i, Uri.decode(filePaths.get(i)));
-        }
-
-        call.setKeepAlive(true);
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                long code = RSFileSync.updateLocalVersionFiles(basePath, filePaths, graphUUID, token);
-                if (code != -1) {
-                    JSObject ret = new JSObject();
-                    ret.put("ok", true);
-                    call.resolve(ret);
-                } else {
-                    call.reject(RSFileSync.getLastError());
-                }
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void deleteRemoteFiles(PluginCall call) throws JSONException {
-        List<String> filePaths = call.getArray("filePaths").toList();
-        String graphUUID = call.getString("graphUUID");
-        String token = call.getString("token");
-        long txid = call.getInt("txid").longValue();
-
-        for (int i = 0; i < filePaths.size(); i++) {
-            filePaths.set(i, Uri.decode(filePaths.get(i)));
-        }
-
-        call.setKeepAlive(true);
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                long code = RSFileSync.deleteRemoteFiles(filePaths, graphUUID, token, txid);
-                if (code != -1) {
-                    JSObject ret = new JSObject();
-                    ret.put("ok", true);
-                    ret.put("txid", code);
-                    call.resolve(ret);
-                } else {
-                    call.reject(RSFileSync.getLastError());
-                }
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void updateRemoteFiles(PluginCall call) throws JSONException {
-        String basePath = call.getString("basePath");
-        List<String> filePaths = call.getArray("filePaths").toList();
-        String graphUUID = call.getString("graphUUID");
-        String token = call.getString("token");
-        long txid = call.getInt("txid").longValue();
-        // NOTE: fnameEncryption is ignored. since it's always on.
-
-        for (int i = 0; i < filePaths.size(); i++) {
-            filePaths.set(i, Uri.decode(filePaths.get(i)));
-        }
-
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                long code = RSFileSync.updateRemoteFiles(basePath, filePaths, graphUUID, token, txid);
-                if (code != -1) {
-                    JSObject ret = new JSObject();
-                    ret.put("ok", true);
-                    ret.put("txid", code);
-                    call.resolve(ret);
-                } else {
-                    call.reject(RSFileSync.getLastError());
-                }
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void ageEncryptWithPassphrase(PluginCall call) {
-        call.reject("unimplemented");
-    }
-
-    @PluginMethod()
-    public void ageDecryptWithPassphrase(PluginCall call) {
-        call.reject("unimplemented");
-    }
-}

+ 1 - 2
android/app/src/main/java/com/logseq/app/MainActivity.java

@@ -12,10 +12,9 @@ import java.util.TimerTask;
 public class MainActivity extends BridgeActivity {
     @Override
     public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
         registerPlugin(FolderPicker.class);
         registerPlugin(FsWatcher.class);
-        registerPlugin(FileSync.class);
+        super.onCreate(savedInstanceState);
 
         new Timer().schedule(new TimerTask() {
             @Override

+ 6 - 0
android/app/src/main/res/drawable/splash_centered.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list  xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@color/logoPrimary" />
+    <item android:drawable="@drawable/splash" android:gravity="center" />
+</layer-list>
+

+ 4 - 0
android/app/src/main/res/values/colors.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="logoPrimary">#002b36</color>
+</resources>

+ 9 - 4
android/app/src/main/res/values/styles.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<resources>
+<resources xmlns:tools="http://schemas.android.com/tools">
 
     <!-- Base application theme. -->
     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
@@ -9,14 +9,19 @@
         <item name="colorAccent">@color/colorAccent</item>
     </style>
 
-    <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
+    <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
         <item name="windowActionBar">false</item>
         <item name="windowNoTitle">true</item>
         <item name="android:background">@null</item>
         <item name="android:windowIsTranslucent">true</item>
     </style>
 
-    <style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
-        <item name="android:background">@drawable/splash</item>
+    <!-- App Starting -->
+    <style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
+        <item name="android:windowBackground">@drawable/splash_centered</item>
+
+        <item name="windowSplashScreenAnimationDuration">200</item>
+        <item name="windowSplashScreenIconBackgroundColor">@color/logoPrimary</item>
+        <item name="postSplashScreenTheme">@style/AppTheme</item>
     </style>
 </resources>

+ 5 - 2
android/build.gradle

@@ -5,10 +5,11 @@ buildscript {
     repositories {
         google()
         jcenter()
+        mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:4.2.1'
-        classpath 'com.google.gms:google-services:4.3.5'
+        classpath 'com.android.tools.build:gradle:7.2.1'
+        classpath 'com.google.gms:google-services:4.3.13'
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
@@ -21,9 +22,11 @@ allprojects {
     repositories {
         google()
         jcenter()
+        mavenCentral()
     }
 }
 
 task clean(type: Delete) {
     delete rootProject.buildDir
 }
+

+ 3 - 0
android/capacitor.settings.gradle

@@ -29,6 +29,9 @@ project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capa
 include ':capacitor-status-bar'
 project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
 
+include ':logseq-capacitor-file-sync'
+project(':logseq-capacitor-file-sync').projectDir = new File('../node_modules/@logseq/capacitor-file-sync/android')
+
 include ':capacitor-voice-recorder'
 project(':capacitor-voice-recorder').projectDir = new File('../node_modules/capacitor-voice-recorder/android')
 

+ 0 - 1
android/file-sync/.gitignore

@@ -1 +0,0 @@
-/build

+ 0 - 36
android/file-sync/build.gradle

@@ -1,36 +0,0 @@
-plugins {
-    id 'com.android.library'
-}
-
-android {
-    compileSdkVersion 30
-
-    defaultConfig {
-        minSdkVersion 21
-        targetSdkVersion 30
-        versionCode 1
-        versionName "1.0"
-
-        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
-        consumerProguardFiles "consumer-rules.pro"
-    }
-
-    buildTypes {
-        release {
-            minifyEnabled false
-            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
-        }
-    }
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
-    }
-}
-
-dependencies {
-
-    implementation 'com.android.support:appcompat-v7:28.0.0'
-    testImplementation 'junit:junit:4.+'
-    androidTestImplementation 'com.android.support.test:runner:1.0.2'
-    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
-}

+ 0 - 0
android/file-sync/consumer-rules.pro


+ 0 - 21
android/file-sync/proguard-rules.pro

@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-#   http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-#   public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile

+ 0 - 5
android/file-sync/src/main/AndroidManifest.xml

@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.logseq.sync">
-
-</manifest>

+ 0 - 33
android/file-sync/src/main/java/com/logseq/sync/FileMeta.java

@@ -1,33 +0,0 @@
-package com.logseq.sync;
-
-public class FileMeta {
-    public String filePath;
-    public long size;
-    public long mtime;
-    public String md5;
-    public String encryptedFilename;
-
-    public FileMeta(String filePath, long size, long mtime, String md5) {
-        this.filePath = filePath;
-        this.size = size;
-        this.mtime = mtime;
-        this.md5 = md5;
-        this.encryptedFilename = encryptedFilename;
-    }
-
-    public FileMeta(long size, long mtime, String md5) {
-        this.size = size;
-        this.mtime = mtime;
-        this.md5 = md5;
-        this.encryptedFilename = null;
-    }
-
-    public String toString() {
-        return "FileMeta{" +
-                "size=" + size +
-                ", mtime=" + mtime +
-                ", md5='" + md5 + '\'' +
-                ", encryptedFilename='" + encryptedFilename + '\'' +
-                '}';
-    }
-}

+ 0 - 34
android/file-sync/src/main/java/com/logseq/sync/RSFileSync.java

@@ -1,34 +0,0 @@
-package com.logseq.sync;
-
-import java.util.List;
-
-public class RSFileSync {
-    static {
-        System.loadLibrary("rsapi");
-    }
-
-    public static native String getLastError();
-
-    public static native String[] keygen();
-
-    public static native long setEnvironment(String env);
-    public static native long setKeys(String secretKey, String publicKey);
-
-    public static native String[] encryptFilenames(List<String> filenames);
-    public static native String[] decryptFilenames(List<String> encryptedFilenames);
-
-    public static native FileMeta[] getLocalFilesMeta(String basePath, List<String> filePaths);
-    public static native FileMeta[] getLocalAllFilesMeta(String basePath);
-
-    public static native long renameLocalFile(String basePath, String oldPath, String newPath);
-
-    public static native void deleteLocalFiles(String basePath, List<String> filePaths);
-
-    public static native long updateLocalFiles(String basePath, List<String> filePaths, String graphUUID, String token);
-
-    public static native long updateLocalVersionFiles(String basePath, List<String> filePaths, String graphUUID, String token);
-
-    public static native long deleteRemoteFiles(List<String> filePaths, String graphUUID, String token, long txid);
-
-    public static native long updateRemoteFiles(String basePath, List<String> filePaths, String graphUUID, String token, long txid);
-}

BIN
android/file-sync/src/main/jniLibs/arm64-v8a/librsapi.so


BIN
android/file-sync/src/main/jniLibs/armeabi-v7a/librsapi.so


BIN
android/file-sync/src/main/jniLibs/x86/librsapi.so


BIN
android/file-sync/src/main/jniLibs/x86_64/librsapi.so


+ 1 - 1
android/gradle/wrapper/gradle-wrapper.properties

@@ -1,5 +1,5 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists

+ 1 - 1
android/settings.gradle

@@ -3,4 +3,4 @@ include ':capacitor-cordova-android-plugins'
 project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
 
 apply from: 'capacitor.settings.gradle'
-include ':file-sync'
+

+ 14 - 12
android/variables.gradle

@@ -1,14 +1,16 @@
 ext {
-    minSdkVersion = 21
-    compileSdkVersion = 30
-    targetSdkVersion = 30
-    androidxActivityVersion = '1.2.0'
-    androidxAppCompatVersion = '1.2.0'
-    androidxCoordinatorLayoutVersion = '1.1.0'
-    androidxCoreVersion = '1.3.2'
-    androidxFragmentVersion = '1.3.0'
-    junitVersion = '4.13.1'
-    androidxJunitVersion = '1.1.2'
-    androidxEspressoCoreVersion = '3.3.0'
-    cordovaAndroidVersion = '7.0.0'
+    minSdkVersion = 22
+    compileSdkVersion = 32
+    targetSdkVersion = 32
+    androidxActivityVersion = '1.4.0'
+    androidxAppCompatVersion = '1.4.2'
+    androidxCoordinatorLayoutVersion = '1.2.0'
+    androidxCoreVersion = '1.8.0'
+    androidxFragmentVersion = '1.4.1'
+    junitVersion = '4.13.2'
+    androidxJunitVersion = '1.1.3'
+    androidxEspressoCoreVersion = '3.4.0'
+    cordovaAndroidVersion = '10.1.1'
+    coreSplashScreenVersion = '1.0.0-rc01'
+    androidxWebkitVersion = '1.4.0'
 }

+ 5 - 0
capacitor.config.ts

@@ -20,6 +20,11 @@ const config: CapacitorConfig = {
   },
   ios: {
     scheme: 'Logseq'
+  },
+  cordova: {
+    staticPlugins: [
+      '@logseq/capacitor-file-sync', // AgeEncryption requires static link
+    ]
   }
 }
 

+ 0 - 32
ios/App/App.xcodeproj/project.pbxproj

@@ -25,14 +25,8 @@
 		D32752BE275496C60039291C /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D32752BD275496C60039291C /* CloudKit.framework */; };
 		D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A09275C92880003FBDC /* FileContainer.swift */; };
 		D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A0B275C928F0003FBDC /* FileContainer.m */; };
-		FE443F1C27FF5420007ECE65 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE443F1B27FF5420007ECE65 /* Extensions.swift */; };
-		FE443F1E27FF54AA007ECE65 /* Payload.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE443F1D27FF54AA007ECE65 /* Payload.swift */; };
-		FE443F2027FF54C9007ECE65 /* SyncClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE443F1F27FF54C9007ECE65 /* SyncClient.swift */; };
 		FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF327BDFEDE00F3206B /* FsWatcher.swift */; };
 		FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF527BDFEF500F3206B /* FsWatcher.m */; };
-		FE8C946B27FD762700C8017B /* FileSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE8C946927FD762700C8017B /* FileSync.swift */; };
-		FE8C946C27FD762700C8017B /* FileSync.m in Sources */ = {isa = PBXBuildFile; fileRef = FE8C946A27FD762700C8017B /* FileSync.m */; };
-		FEE688A428448F8C0019510E /* AgeEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE688A328448F8C0019510E /* AgeEncryption.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -87,14 +81,8 @@
 		D3D62A09275C92880003FBDC /* FileContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileContainer.swift; sourceTree = "<group>"; };
 		D3D62A0B275C928F0003FBDC /* FileContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileContainer.m; sourceTree = "<group>"; };
 		DE5650F4AD4E2242AB9C012D /* Pods-Logseq.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Logseq.debug.xcconfig"; path = "Target Support Files/Pods-Logseq/Pods-Logseq.debug.xcconfig"; sourceTree = "<group>"; };
-		FE443F1B27FF5420007ECE65 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
-		FE443F1D27FF54AA007ECE65 /* Payload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Payload.swift; sourceTree = "<group>"; };
-		FE443F1F27FF54C9007ECE65 /* SyncClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncClient.swift; sourceTree = "<group>"; };
 		FE647FF327BDFEDE00F3206B /* FsWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FsWatcher.swift; sourceTree = "<group>"; };
 		FE647FF527BDFEF500F3206B /* FsWatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FsWatcher.m; sourceTree = "<group>"; };
-		FE8C946927FD762700C8017B /* FileSync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSync.swift; sourceTree = "<group>"; };
-		FE8C946A27FD762700C8017B /* FileSync.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileSync.m; sourceTree = "<group>"; };
-		FEE688A328448F8C0019510E /* AgeEncryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgeEncryption.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -154,7 +142,6 @@
 				50B271D01FEDC1A000F3C39B /* public */,
 				7435D10B2704659F00AB88E0 /* FolderPicker.swift */,
 				FE647FF327BDFEDE00F3206B /* FsWatcher.swift */,
-				FE443F1A27FF53A2007ECE65 /* FileSync */,
 				FE647FF527BDFEF500F3206B /* FsWatcher.m */,
 				7435D10E2704660B00AB88E0 /* FolderPicker.m */,
 				D3D62A09275C92880003FBDC /* FileContainer.swift */,
@@ -193,19 +180,6 @@
 			path = Pods;
 			sourceTree = "<group>";
 		};
-		FE443F1A27FF53A2007ECE65 /* FileSync */ = {
-			isa = PBXGroup;
-			children = (
-				FE8C946927FD762700C8017B /* FileSync.swift */,
-				FE443F1F27FF54C9007ECE65 /* SyncClient.swift */,
-				FE443F1D27FF54AA007ECE65 /* Payload.swift */,
-				FE443F1B27FF5420007ECE65 /* Extensions.swift */,
-				FE8C946A27FD762700C8017B /* FileSync.m */,
-				FEE688A328448F8C0019510E /* AgeEncryption.swift */,
-			);
-			path = FileSync;
-			sourceTree = "<group>";
-		};
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
@@ -356,20 +330,14 @@
 			buildActionMask = 2147483647;
 			files = (
 				504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
-				FE443F1E27FF54AA007ECE65 /* Payload.swift in Sources */,
 				5FF8632C283B5BFD0047731B /* Utils.m in Sources */,
-				FEE688A428448F8C0019510E /* AgeEncryption.swift in Sources */,
-				FE8C946B27FD762700C8017B /* FileSync.swift in Sources */,
 				FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */,
 				5FF8632A283B5ADB0047731B /* Utils.swift in Sources */,
 				D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */,
 				D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */,
-				FE443F1C27FF5420007ECE65 /* Extensions.swift in Sources */,
-				FE8C946C27FD762700C8017B /* FileSync.m in Sources */,
 				7435D10F2704660B00AB88E0 /* FolderPicker.m in Sources */,
 				7435D10C2704659F00AB88E0 /* FolderPicker.swift in Sources */,
 				FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */,
-				FE443F2027FF54C9007ECE65 /* SyncClient.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 4 - 19
ios/App/App/AppDelegate.swift

@@ -36,14 +36,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
         // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
     }
 
-    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
-            
-            var success = true
-            if CAPBridge.handleOpenUrl(url, options) {
-                success = ApplicationDelegateProxy.shared.application(app, open: url, options: options)
-            }
-            return success
-        }
+    func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
+        return ApplicationDelegateProxy.shared.application(application, open: url, options: options)
+    }
 
     func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
         // Called when the app was launched with an activity, including Universal Links.
@@ -52,15 +47,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
         return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
     }
 
-    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
-        super.touchesBegan(touches, with: event)
-
-        let statusBarRect = UIApplication.shared.statusBarFrame
-        guard let touchPoint = event?.allTouches?.first?.location(in: self.window) else { return }
-
-        if statusBarRect.contains(touchPoint) {
-            NotificationCenter.default.post(name: .capacitorStatusBarTapped, object: nil)
-        }
-    }
-
 }
+

+ 0 - 110
ios/App/App/FileSync/AgeEncryption.swift

@@ -1,110 +0,0 @@
-//
-//  AgeEncryption.swift
-//  Logseq
-//
-//  Created by Mono Wang on 5/30/R4.
-//
-
-import Foundation
-import AgeEncryption
-
-public enum AgeEncryption {
-    public static func keygen() -> (String, String) {
-        let cSecretKey = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
-        let cPublicKey = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
-        
-        rust_age_encryption_keygen(cSecretKey, cPublicKey);
-        let secretKey = String(cString: cSecretKey.pointee!)
-        let publicKey = String(cString: cPublicKey.pointee!)
-        
-        rust_age_encryption_free_str(cSecretKey.pointee!)
-        rust_age_encryption_free_str(cPublicKey.pointee!)
-        cSecretKey.deallocate()
-        cPublicKey.deallocate()
-
-        return (secretKey, publicKey)
-    }
-    
-    public static func toRawX25519Key(_ secretKey: String) -> Data? {
-        let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
-
-        let ret = rust_age_encryption_to_raw_x25519_key(secretKey.cString(using: .utf8), cOutput)
-        if ret >= 0 {
-            let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: 32)
-            let rawKey = Data.init(buffer: cOutputBuf)
-            rust_age_encryption_free_vec(cOutput.pointee, ret)
-            cOutput.deallocate()
-            return rawKey
-        } else {
-            return nil
-        }
-    }
-    
-    public static func encryptWithPassphrase(_ plaintext: Data, _ passphrase: String, armor: Bool) -> Data? {
-        plaintext.withUnsafeBytes { (cPlaintext) in
-            let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
-
-            let ret = rust_age_encrypt_with_user_passphrase(passphrase.cString(using: .utf8), cPlaintext.bindMemory(to: CChar.self).baseAddress, Int32(plaintext.count), armor ? 1 : 0, cOutput)
-            if ret > 0 {
-                let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
-                let ciphertext = Data.init(buffer: cOutputBuf)
-                rust_age_encryption_free_vec(cOutput.pointee, ret)
-                cOutput.deallocate()
-                return ciphertext
-            } else {
-                return nil
-            }
-        }
-    }
-
-    public static func decryptWithPassphrase(_ ciphertext: Data, _ passphrase: String) -> Data? {
-        ciphertext.withUnsafeBytes { (cCiphertext) in
-            let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
-
-            let ret = rust_age_decrypt_with_user_passphrase(passphrase.cString(using: .utf8), cCiphertext.bindMemory(to: CChar.self).baseAddress, Int32(ciphertext.count), cOutput)
-            if ret > 0 {
-                let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
-                let plaintext = Data.init(buffer: cOutputBuf)
-                rust_age_encryption_free_vec(cOutput.pointee, ret)
-                cOutput.deallocate()
-                return plaintext
-            } else {
-                return nil
-            }
-        }
-    }
-    
-    public static func encryptWithX25519(_ plaintext: Data, _ publicKey: String, armor: Bool) -> Data? {
-        plaintext.withUnsafeBytes { (cPlaintext) in
-            let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
-            
-            let ret = rust_age_encrypt_with_x25519(publicKey.cString(using: .utf8), cPlaintext.bindMemory(to: CChar.self).baseAddress, Int32(plaintext.count), armor ? 1 : 0, cOutput)
-            if ret > 0 {
-                let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
-                let ciphertext = Data.init(buffer: cOutputBuf)
-                rust_age_encryption_free_vec(cOutput.pointee, ret)
-                cOutput.deallocate()
-                return ciphertext
-            } else {
-                return nil
-            }
-        }
-    }
-    
-    public static func decryptWithX25519(_ ciphertext: Data, _ secretKey: String) -> Data? {
-        ciphertext.withUnsafeBytes { (cCiphertext) in
-            let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
-
-            let ret = rust_age_decrypt_with_x25519(secretKey.cString(using: .utf8), cCiphertext.bindMemory(to: CChar.self).baseAddress, Int32(ciphertext.count), cOutput)
-            if ret >= 0 {
-                let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
-                let plaintext = Data.init(buffer: cOutputBuf)
-                rust_age_encryption_free_vec(cOutput.pointee, ret)
-                cOutput.deallocate()
-                return plaintext
-            } else {
-                return nil
-            }
-        }
-    }
-}

+ 0 - 211
ios/App/App/FileSync/Extensions.swift

@@ -1,211 +0,0 @@
-//
-//  Extensions.swift
-//  Logseq
-//
-//  Created by Mono Wang on 4/8/R4.
-//
-
-import Foundation
-import CryptoKit
-
-// via https://github.com/krzyzanowskim/CryptoSwift
-extension Array where Element == UInt8 {
-  public init(hex: String) {
-      self = Array.init()
-      self.reserveCapacity(hex.unicodeScalars.lazy.underestimatedCount)
-      var buffer: UInt8?
-      var skip = hex.hasPrefix("0x") ? 2 : 0
-      for char in hex.unicodeScalars.lazy {
-          guard skip == 0 else {
-              skip -= 1
-              continue
-          }
-          guard char.value >= 48 && char.value <= 102 else {
-              removeAll()
-              return
-          }
-          let v: UInt8
-          let c: UInt8 = UInt8(char.value)
-          switch c {
-          case let c where c <= 57:
-              v = c - 48
-          case let c where c >= 65 && c <= 70:
-              v = c - 55
-          case let c where c >= 97:
-              v = c - 87
-          default:
-              removeAll()
-              return
-          }
-          if let b = buffer {
-              append(b << 4 | v)
-              buffer = nil
-          } else {
-              buffer = v
-          }
-      }
-      if let b = buffer {
-          append(b)
-      }
-  }
-}
-
-extension Data {
-    public init?(hexEncoded: String) {
-        self.init(Array<UInt8>(hex: hexEncoded))
-    }
-    
-    var hexDescription: String {
-        return map { String(format: "%02hhx", $0) }.joined()
-    }
-    
-    var MD5: String {
-        let computed = Insecure.MD5.hash(data: self)
-        return computed.map { String(format: "%02hhx", $0) }.joined()
-    }
-}
-
-extension String {
-    var MD5: String {
-        let computed = Insecure.MD5.hash(data: self.data(using: .utf8)!)
-        return computed.map { String(format: "%02hhx", $0) }.joined()
-    }
-    
-    static func random(length: Int) -> String {
-        let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
-        return String((0..<length).map{ _ in letters.randomElement()! })
-    }
-    
-    func fnameEncrypt(rawKey: Data) -> String? {
-        guard !self.isEmpty else {
-            return nil
-        }
-        guard let raw = self.data(using: .utf8) else {
-            return nil
-        }
-        
-        let key = SymmetricKey(data: rawKey)
-        let nonce = try! ChaChaPoly.Nonce(data: Data(repeating: 0, count: 12))
-        guard let sealed = try? ChaChaPoly.seal(raw, using: key, nonce: nonce) else { return nil }
-        
-        // strip nonce here, since it's all zero
-        return "e." + (sealed.ciphertext + sealed.tag).hexDescription
-
-    }
-    
-    func fnameDecrypt(rawKey: Data) -> String? {
-        // well-formated, non-empty encrypted string
-        guard self.hasPrefix("e.") && self.count > 36 else {
-            return nil
-        }
-        
-        let encryptedHex = self.suffix(from: self.index(self.startIndex, offsetBy: 2))
-        guard let encryptedRaw = Data(hexEncoded: String(encryptedHex)) else {
-            // invalid hex
-            return nil
-        }
-        
-        let key = SymmetricKey(data: rawKey)
-        let nonce = Data(repeating: 0, count: 12)
-
-        guard let sealed = try? ChaChaPoly.SealedBox(combined: nonce + encryptedRaw) else {
-            return nil
-        }
-        guard let outputRaw = try? ChaChaPoly.open(sealed, using: key) else {
-            return nil
-        }
-        return String(data: outputRaw, encoding: .utf8)
-    }
-}
-
-extension URL {
-    func relativePath(from base: URL) -> String? {
-        // Ensure that both URLs represent files:
-        guard self.isFileURL && base.isFileURL else {
-            return nil
-        }
-        
-        // Remove/replace "." and "..", make paths absolute:
-        let destComponents = self.standardized.pathComponents
-        let baseComponents = base.standardized.pathComponents
-        
-        // Find number of common path components:
-        var i = 0
-        while i < destComponents.count && i < baseComponents.count
-                && destComponents[i] == baseComponents[i] {
-            i += 1
-        }
-        
-        // Build relative path:
-        var relComponents = Array(repeating: "..", count: baseComponents.count - i)
-        relComponents.append(contentsOf: destComponents[i...])
-        return relComponents.joined(separator: "/")
-    }
-    
-    // Download a remote URL to a file
-    func download(toFile file: URL, completion: @escaping (Error?) -> Void) {
-        let task = URLSession.shared.downloadTask(with: self) {
-            (tempURL, response, error) in
-            // Early exit on error
-            guard let tempURL = tempURL else {
-                completion(error)
-                return
-            }
-            
-            if let response = response! as? HTTPURLResponse {
-                if response.statusCode == 404 {
-                    completion(NSError(domain: "",
-                                       code: response.statusCode,
-                                       userInfo: [NSLocalizedDescriptionKey: "remote file not found"]))
-                    return
-                }
-                if response.statusCode != 200 {
-                    completion(NSError(domain: "",
-                                       code: response.statusCode,
-                                       userInfo: [NSLocalizedDescriptionKey: "invalid http status code"]))
-                    return
-                }
-            }
-            
-            do {
-                // Remove any existing document at file
-                if FileManager.default.fileExists(atPath: file.path) {
-                    try FileManager.default.removeItem(at: file)
-                } else {
-                    let baseURL = file.deletingLastPathComponent()
-                    try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true, attributes: nil)
-                }
-                let rawData = try Data(contentsOf: tempURL)
-                guard let decryptedRawData = maybeDecrypt(rawData) else {
-                    throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "can not decrypt remote file"])
-                }
-                try decryptedRawData.write(to: file, options: .atomic)
-
-                completion(nil)
-            }
-            
-            // Handle potential file system errors
-            catch {
-                completion(error)
-            }
-        }
-        
-        // Start the download
-        task.resume()
-    }
-}
-
-// MARK: Crypto helper
-
-extension SymmetricKey {
-    public init(passwordString keyString: String) throws {
-        guard let keyData = keyString.data(using: .utf8) else {
-            print("ERROR: Could not create raw Data from String")
-            throw CryptoKitError.incorrectParameterSize
-        }
-        // SymmetricKeySize.bits256
-        let keyDigest = SHA256.hash(data: keyData)
-        
-        self.init(data: keyDigest)
-    }
-}

+ 0 - 25
ios/App/App/FileSync/FileSync.m

@@ -1,25 +0,0 @@
-//
-//  FileSync.m
-//  Logseq
-//
-//  Created by Mono Wang on 2/24/R4.
-//
-
-#import <Capacitor/Capacitor.h>
-
-CAP_PLUGIN(FileSync, "FileSync",
-           CAP_PLUGIN_METHOD(setEnv, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(keygen, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(getLocalFilesMeta, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(getLocalAllFilesMeta, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(renameLocalFile, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(deleteLocalFiles, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(updateLocalFiles, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(deleteRemoteFiles, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(updateRemoteFiles, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(encryptFnames, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(decryptFnames, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(decryptWithPassphrase, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(encryptWithPassphrase, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(updateLocalVersionFiles, CAPPluginReturnPromise);
-)

+ 0 - 596
ios/App/App/FileSync/FileSync.swift

@@ -1,596 +0,0 @@
-//
-//  FileSync.swift
-//  Logseq
-//
-//  Created by Mono Wang on 2/24/R4.
-//
-
-import Capacitor
-import Foundation
-import AWSMobileClient
-import CryptoKit
-
-
-// MARK: Global variable
-
-// Defualts to dev
-var URL_BASE = URL(string: "https://api-dev.logseq.com/file-sync/")!
-var BUCKET: String = "logseq-file-sync-bucket"
-var REGION: String = "us-east-2"
-
-var ENCRYPTION_SECRET_KEY: String? = nil
-var ENCRYPTION_PUBLIC_KEY: String? = nil
-var FNAME_ENCRYPTION_KEY: Data? = nil
-
-
-// MARK: Helpers
-
-
-@inline(__always) func fnameEncryptionEnabled() -> Bool {
-    guard let _ = FNAME_ENCRYPTION_KEY else {
-        return false
-    }
-    return true
-}
-
-// MARK: encryption helper
-
-func maybeEncrypt(_ plaindata: Data) -> Data! {
-    // avoid encryption twice
-    if plaindata.starts(with: "-----BEGIN AGE ENCRYPTED FILE-----".data(using: .utf8)!) ||
-        plaindata.starts(with: "age-encryption.org/v1\n".data(using: .utf8)!) {
-        return plaindata
-    }
-    if let publicKey = ENCRYPTION_PUBLIC_KEY {
-        // use armor = false, for smaller size
-        if let cipherdata = AgeEncryption.encryptWithX25519(plaindata, publicKey, armor: true) {
-            return cipherdata
-        }
-        return nil // encryption fail
-    }
-    return plaindata
-}
-
-func maybeDecrypt(_ cipherdata: Data) -> Data! {
-    if let secretKey = ENCRYPTION_SECRET_KEY {
-        if cipherdata.starts(with: "-----BEGIN AGE ENCRYPTED FILE-----".data(using: .utf8)!) ||
-            cipherdata.starts(with: "age-encryption.org/v1\n".data(using: .utf8)!) {
-            if let plaindata = AgeEncryption.decryptWithX25519(cipherdata, secretKey) {
-                return plaindata
-            }
-            return nil
-        }
-        // not an encrypted file
-        return cipherdata
-    }
-    return cipherdata
-}
-
-// MARK: Metadata type
-
-public struct SyncMetadata: CustomStringConvertible, Equatable {
-    var md5: String
-    var size: Int
-    var ctime: Int64
-    var mtime: Int64
-
-    public init?(of fileURL: URL) {
-        do {
-            let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey, .fileSizeKey, .contentModificationDateKey,
-                                                                     .creationDateKey])
-            guard fileAttributes.isRegularFile! else {
-                return nil
-            }
-            size = fileAttributes.fileSize ?? 0
-            mtime = Int64((fileAttributes.contentModificationDate?.timeIntervalSince1970 ?? 0.0) * 1000)
-            ctime = Int64((fileAttributes.creationDate?.timeIntervalSince1970 ?? 0.0) * 1000)
-
-            // incremental MD5 checksum
-            let bufferSize = 512 * 1024
-            let file = try FileHandle(forReadingFrom: fileURL)
-            defer {
-                file.closeFile()
-            }
-            var ctx = Insecure.MD5.init()
-            while autoreleasepool(invoking: {
-                let data = file.readData(ofLength: bufferSize)
-                if data.count > 0 {
-                    ctx.update(data: data)
-                    return true // continue
-                } else {
-                    return false // eof
-                }
-            }) {}
-
-            let computed = ctx.finalize()
-            md5 = computed.map { String(format: "%02hhx", $0) }.joined()
-        } catch {
-            return nil
-        }
-    }
-
-    public var description: String {
-        return "SyncMetadata(md5=\(md5), size=\(size), mtime=\(mtime))"
-    }
-}
-
-// MARK: FileSync Plugin
-
-@objc(FileSync)
-public class FileSync: CAPPlugin, SyncDebugDelegate {
-    override public func load() {
-        print("debug FileSync iOS plugin loaded!")
-
-        AWSMobileClient.default().initialize { (userState, error) in
-            guard error == nil else {
-                print("error initializing AWSMobileClient. Error: \(error!.localizedDescription)")
-                return
-            }
-        }
-    }
-
-    // NOTE: for debug, or an activity indicator
-    public func debugNotification(_ message: [String: Any]) {
-        self.notifyListeners("debug", data: message)
-    }
-
-    @objc func keygen(_ call: CAPPluginCall) {
-        let (secretKey, publicKey) = AgeEncryption.keygen()
-        call.resolve(["secretKey": secretKey,
-                      "publicKey": publicKey])
-    }
-
-    @objc func setKey(_ call: CAPPluginCall) {
-        let secretKey = call.getString("secretKey")
-        let publicKey = call.getString("publicKey")
-        if secretKey == nil && publicKey == nil {
-            ENCRYPTION_SECRET_KEY = nil
-            ENCRYPTION_PUBLIC_KEY = nil
-            FNAME_ENCRYPTION_KEY = nil
-            return
-        }
-        guard let secretKey = secretKey, let publicKey = publicKey else {
-            call.reject("both secretKey and publicKey should be provided")
-            return
-        }
-        ENCRYPTION_SECRET_KEY = secretKey
-        ENCRYPTION_PUBLIC_KEY = publicKey
-        FNAME_ENCRYPTION_KEY = AgeEncryption.toRawX25519Key(secretKey)
-
-    }
-
-    @objc func setEnv(_ call: CAPPluginCall) {
-        guard let env = call.getString("env") else {
-            call.reject("required parameter: env")
-            return
-        }
-        self.setKey(call)
-
-        switch env {
-        case "production", "product", "prod":
-            URL_BASE = URL(string: "https://api.logseq.com/file-sync/")!
-            BUCKET = "logseq-file-sync-bucket-prod"
-            REGION = "us-east-1"
-        case "development", "develop", "dev":
-            URL_BASE = URL(string: "https://api-dev.logseq.com/file-sync/")!
-            BUCKET = "logseq-file-sync-bucket"
-            REGION = "us-east-2"
-        default:
-            call.reject("invalid env: \(env)")
-            return
-        }
-
-        self.debugNotification(["event": "setenv:\(env)"])
-        call.resolve(["ok": true])
-    }
-
-    @objc func encryptFnames(_ call: CAPPluginCall) {
-        guard fnameEncryptionEnabled() else {
-            call.reject("fname encryption key not set")
-            return
-        }
-        guard var fnames = call.getArray("filePaths") as? [String] else {
-            call.reject("required parameters: filePaths")
-            return
-        }
-
-        let nFiles = fnames.count
-        fnames = fnames.compactMap { $0.removingPercentEncoding!.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!) }
-        if fnames.count != nFiles {
-            call.reject("cannot encrypt \(nFiles - fnames.count) file names")
-        }
-        call.resolve(["value": fnames])
-    }
-
-    @objc func decryptFnames(_ call: CAPPluginCall) {
-        guard fnameEncryptionEnabled() else {
-            call.reject("fname encryption key not set")
-            return
-        }
-        guard var fnames = call.getArray("filePaths") as? [String] else {
-            call.reject("required parameters: filePaths")
-            return
-        }
-        let nFiles = fnames.count
-        fnames = fnames.compactMap { $0.fnameDecrypt(rawKey: FNAME_ENCRYPTION_KEY!)?.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) }
-        if fnames.count != nFiles {
-            call.reject("cannot decrypt \(nFiles - fnames.count) file names")
-        }
-        call.resolve(["value": fnames])
-    }
-    
-    @objc func encryptWithPassphrase(_ call: CAPPluginCall) {
-        guard let passphrase = call.getString("passphrase"),
-              let content = call.getString("content") else {
-                  call.reject("required parameters: passphrase, content")
-                  return
-              }
-        guard let plaintext = content.data(using: .utf8) else {
-            call.reject("cannot decode ciphertext with utf8")
-            return
-        }
-        call.keepAlive = true
-        DispatchQueue.global(qos: .default).async {
-            if let encrypted = AgeEncryption.encryptWithPassphrase(plaintext, passphrase, armor: true) {
-                call.resolve(["data": String(data: encrypted, encoding: .utf8) as Any])
-            } else {
-                call.reject("cannot encrypt with passphrase")
-            }
-        }
-    }
-    
-    
-    @objc func decryptWithPassphrase(_ call: CAPPluginCall) {
-        guard let passphrase = call.getString("passphrase"),
-              let content = call.getString("content") else {
-                  call.reject("required parameters: passphrase, content")
-                  return
-              }
-        guard let ciphertext = content.data(using: .utf8) else {
-            call.reject("cannot decode ciphertext with utf8")
-            return
-        }
-        call.keepAlive = true
-        DispatchQueue.global(qos: .default).async {
-            if let decrypted = AgeEncryption.decryptWithPassphrase(ciphertext, passphrase) {
-                call.resolve(["data": String(data: decrypted, encoding: .utf8) as Any])
-            } else {
-                call.reject("cannot decrypt with passphrase")
-            }
-        }
-    }
-
-    @objc func getLocalFilesMeta(_ call: CAPPluginCall) {
-        // filePaths are url encoded
-        guard let basePath = call.getString("basePath"),
-              let filePaths = call.getArray("filePaths") as? [String] else {
-                  call.reject("required paremeters: basePath, filePaths")
-                  return
-              }
-        guard let baseURL = URL(string: basePath) else {
-            call.reject("invalid basePath")
-            return
-        }
-
-        var fileMetadataDict: [String: [String: Any]] = [:]
-        for percentFilePath in filePaths {
-            let filePath = percentFilePath.removingPercentEncoding!
-            let url = baseURL.appendingPathComponent(filePath)
-            if let meta = SyncMetadata(of: url) {
-                var metaObj: [String: Any] = ["md5": meta.md5,
-                                              "size": meta.size,
-                                              "mtime": meta.mtime]
-                if fnameEncryptionEnabled() {
-                    metaObj["encryptedFname"] = filePath.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!)
-                }
-
-                fileMetadataDict[percentFilePath] = metaObj
-            }
-        }
-
-        call.resolve(["result": fileMetadataDict])
-    }
-
-    @objc func getLocalAllFilesMeta(_ call: CAPPluginCall) {
-        guard let basePath = call.getString("basePath"),
-              let baseURL = URL(string: basePath) else {
-                  call.reject("invalid basePath")
-                  return
-              }
-
-        var fileMetadataDict: [String: [String: Any]] = [:]
-        if let enumerator = FileManager.default.enumerator(at: baseURL, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsPackageDescendants, .skipsHiddenFiles]) {
-
-            for case let fileURL as URL in enumerator {
-                if !fileURL.isSkipped() {
-                    if let meta = SyncMetadata(of: fileURL) {
-                        let filePath = fileURL.relativePath(from: baseURL)!
-                        var metaObj: [String: Any] = ["md5": meta.md5,
-                                                      "size": meta.size,
-                                                      "mtime": meta.mtime]
-                        if fnameEncryptionEnabled() {
-                            metaObj["encryptedFname"] = filePath.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!)
-                        }
-                        fileMetadataDict[filePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!] = metaObj
-                    }
-                } else if fileURL.isICloudPlaceholder() {
-                    try? FileManager.default.startDownloadingUbiquitousItem(at: fileURL)
-                }
-            }
-        }
-        call.resolve(["result": fileMetadataDict])
-    }
-
-
-    @objc func renameLocalFile(_ call: CAPPluginCall) {
-        guard let basePath = call.getString("basePath"),
-              let baseURL = URL(string: basePath) else {
-                  call.reject("invalid basePath")
-                  return
-              }
-        guard let from = call.getString("from") else {
-            call.reject("invalid from file")
-            return
-        }
-        guard let to = call.getString("to") else {
-            call.reject("invalid to file")
-            return
-        }
-
-        let fromUrl = baseURL.appendingPathComponent(from.removingPercentEncoding!)
-        let toUrl = baseURL.appendingPathComponent(to.removingPercentEncoding!)
-
-        do {
-            try FileManager.default.moveItem(at: fromUrl, to: toUrl)
-        } catch {
-            call.reject("can not rename file: \(error.localizedDescription)")
-            return
-        }
-        call.resolve(["ok": true])
-
-    }
-
-    @objc func deleteLocalFiles(_ call: CAPPluginCall) {
-        guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
-              let filePaths = call.getArray("filePaths") as? [String] else {
-                  call.reject("required paremeters: basePath, filePaths")
-                  return
-              }
-
-        for filePath in filePaths {
-            let fileUrl = baseURL.appendingPathComponent(filePath.removingPercentEncoding!)
-            try? FileManager.default.removeItem(at: fileUrl) // ignore any delete errors
-        }
-        call.resolve(["ok": true])
-    }
-
-    /// remote -> local
-    @objc func updateLocalFiles(_ call: CAPPluginCall) {
-        guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
-              let filePaths = call.getArray("filePaths") as? [String],
-              let graphUUID = call.getString("graphUUID") ,
-              let token = call.getString("token") else {
-                  call.reject("required paremeters: basePath, filePaths, graphUUID, token")
-                  return
-              }
-
-        // [encrypted-fname: original-fname]
-        var encryptedFilePathDict: [String: String] = [:]
-        if fnameEncryptionEnabled() {
-            for filePath in filePaths {
-                if let encryptedPath = filePath.removingPercentEncoding!.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!) {
-                    encryptedFilePathDict[encryptedPath] = filePath
-                } else {
-                    call.reject("cannot decrypt all file names")
-                }
-            }
-        } else {
-            encryptedFilePathDict = Dictionary(uniqueKeysWithValues: filePaths.map { ($0, $0) })
-        }
-
-        let encryptedFilePaths = Array(encryptedFilePathDict.keys)
-
-        let client = SyncClient(token: token, graphUUID: graphUUID)
-        client.delegate = self // receives notification
-
-        client.getFiles(at: encryptedFilePaths) {  (fileURLs, error) in
-            if let error = error {
-                print("debug getFiles error \(error)")
-                self.debugNotification(["event": "download:error", "data": ["message": "error while getting files \(filePaths)"]])
-                call.reject(error.localizedDescription)
-            } else {
-                // handle multiple completionHandlers
-                let group = DispatchGroup()
-
-                var downloaded: [String] = []
-
-                for (encryptedFilePath, remoteFileURL) in fileURLs {
-                    group.enter()
-
-                    let filePath = encryptedFilePathDict[encryptedFilePath]!
-                    // NOTE: fileURLs from getFiles API is percent-encoded
-                    let localFileURL = baseURL.appendingPathComponent(filePath.removingPercentEncoding!)
-                    remoteFileURL.download(toFile: localFileURL) {error in
-                        if let error = error {
-                            self.debugNotification(["event": "download:error", "data": ["message": "error while downloading \(filePath): \(error)"]])
-                            print("debug download \(error) in \(filePath)")
-                        } else {
-                            self.debugNotification(["event": "download:file", "data": ["file": filePath]])
-                            downloaded.append(filePath)
-                        }
-                        group.leave()
-                    }
-                }
-                group.notify(queue: .main) {
-                    self.debugNotification(["event": "download:done"])
-                    call.resolve(["ok": true, "data": downloaded])
-                }
-
-            }
-        }
-    }
-
-    @objc func updateLocalVersionFiles(_ call: CAPPluginCall) {
-        guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
-              let filePaths = call.getArray("filePaths") as? [String],
-              let graphUUID = call.getString("graphUUID") ,
-              let token = call.getString("token") else {
-                  call.reject("required paremeters: basePath, filePaths, graphUUID, token")
-                  return
-              }
-        let client = SyncClient(token: token, graphUUID: graphUUID)
-        client.delegate = self // receives notification
-
-        client.getVersionFiles(at: filePaths) {  (fileURLDict, error) in
-            if let error = error {
-                print("debug getVersionFiles error \(error)")
-                self.debugNotification(["event": "version-download:error", "data": ["message": "error while getting version files \(filePaths)"]])
-                call.reject(error.localizedDescription)
-            } else {
-                // handle multiple completionHandlers
-                let group = DispatchGroup()
-
-                var downloaded: [String] = []
-
-                for (filePath, remoteFileURL) in fileURLDict {
-                    group.enter()
-
-                    // NOTE: fileURLs from getFiles API is percent-encoded
-                    let localFileURL = baseURL.appendingPathComponent("logseq/version-files/").appendingPathComponent(filePath.removingPercentEncoding!)
-                    remoteFileURL.download(toFile: localFileURL) {error in
-                        if let error = error {
-                            self.debugNotification(["event": "version-download:error", "data": ["message": "error while downloading \(filePath): \(error)"]])
-                            print("debug download \(error) in \(filePath)")
-                        } else {
-                            self.debugNotification(["event": "version-download:file", "data": ["file": filePath]])
-                            downloaded.append(filePath)
-                        }
-                        group.leave()
-                    }
-                }
-                group.notify(queue: .main) {
-                    self.debugNotification(["event": "version-download:done"])
-                    call.resolve(["ok": true, "data": downloaded])
-                }
-
-            }
-        }
-    }
-
-    // filePaths: Encrypted file paths
-    @objc func deleteRemoteFiles(_ call: CAPPluginCall) {
-        guard var filePaths = call.getArray("filePaths") as? [String],
-              let graphUUID = call.getString("graphUUID"),
-              let token = call.getString("token"),
-              let txid = call.getInt("txid") else {
-                  call.reject("required paremeters: filePaths, graphUUID, token, txid")
-                  return
-              }
-        guard !filePaths.isEmpty else {
-            call.reject("empty filePaths")
-            return
-        }
-
-        let nFiles = filePaths.count
-        if fnameEncryptionEnabled() {
-            filePaths = filePaths.compactMap { $0.removingPercentEncoding!.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!) }
-        }
-        if filePaths.count != nFiles {
-            call.reject("cannot encrypt all file names")
-        }
-
-        let client = SyncClient(token: token, graphUUID: graphUUID, txid: txid)
-        client.deleteFiles(filePaths) { txid, error in
-            guard error == nil else {
-                call.reject("delete \(error!)")
-                return
-            }
-            guard let txid = txid else {
-                call.reject("missing txid")
-                return
-            }
-            call.resolve(["ok": true, "txid": txid])
-        }
-    }
-
-    /// local -> remote
-    @objc func updateRemoteFiles(_ call: CAPPluginCall) {
-        guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
-              let filePaths = call.getArray("filePaths") as? [String],
-              let graphUUID = call.getString("graphUUID"),
-              let token = call.getString("token"),
-              let txid = call.getInt("txid") else {
-                  call.reject("required paremeters: basePath, filePaths, graphUUID, token, txid")
-                  return
-              }
-        let fnameEncryption = call.getBool("fnameEncryption") ?? false // default to false
-
-        guard !filePaths.isEmpty else {
-            return call.reject("empty filePaths")
-        }
-
-        let client = SyncClient(token: token, graphUUID: graphUUID, txid: txid)
-        client.delegate = self
-
-        // 1. refresh_temp_credential
-        client.getTempCredential() { (credentials, error) in
-            guard error == nil else {
-                self.debugNotification(["event": "upload:error", "data": ["message": "error while refreshing credential: \(error!)"]])
-                call.reject("error(getTempCredential): \(error!)")
-                return
-            }
-
-            var files: [String: URL] = [:]
-            for filePath in filePaths {
-                // NOTE: filePath from js may contain spaces
-                let fileURL = baseURL.appendingPathComponent(filePath.removingPercentEncoding!)
-                files[filePath] = fileURL
-            }
-
-            // 2. upload_temp_file
-            client.uploadTempFiles(files, credentials: credentials!) { (uploadedFileKeyDict, fileMd5Dict, error) in
-                guard error == nil else {
-                    self.debugNotification(["event": "upload:error", "data": ["message": "error while uploading temp files: \(error!)"]])
-                    call.reject("error(uploadTempFiles): \(error!)")
-                    return
-                }
-                // 3. update_files
-                guard !uploadedFileKeyDict.isEmpty else {
-                    self.debugNotification(["event": "upload:error", "data": ["message": "no file to update"]])
-                    call.reject("no file to update")
-                    return
-                }
-
-                // encrypted-file-name: (file-key, md5)
-                var uploadedFileKeyMd5Dict: [String: [String]] = [:]
-
-                if fnameEncryptionEnabled() && fnameEncryption {
-                    for (filePath, fileKey) in uploadedFileKeyDict {
-                        guard let encryptedFilePath = filePath.removingPercentEncoding!.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!) else {
-                            call.reject("cannot encrypt file name")
-                            return
-                        }
-                        uploadedFileKeyMd5Dict[encryptedFilePath] = [fileKey, fileMd5Dict[filePath]!]
-                    }
-                } else {
-                    for (filePath, fileKey) in uploadedFileKeyDict {
-                        uploadedFileKeyMd5Dict[filePath] = [fileKey, fileMd5Dict[filePath]!]
-                    }
-                }
-                client.updateFiles(uploadedFileKeyMd5Dict) { (txid, error) in
-                    guard error == nil else {
-                        self.debugNotification(["event": "upload:error", "data": ["message": "error while updating files: \(error!)"]])
-                        call.reject("error updateFiles: \(error!)")
-                        return
-                    }
-                    guard let txid = txid else {
-                        call.reject("error: missing txid")
-                        return
-                    }
-                    self.debugNotification(["event": "upload:done", "data": ["files": filePaths, "txid": txid]])
-                    call.resolve(["ok": true, "files": uploadedFileKeyDict, "txid": txid])
-                }
-            }
-        }
-    }
-}

+ 0 - 36
ios/App/App/FileSync/Payload.swift

@@ -1,36 +0,0 @@
-//
-//  Payload.swift
-//  Logseq
-//
-//  Created by Mono Wang on 4/8/R4.
-//
-
-import Foundation
-
-struct GetFilesResponse: Decodable {
-    let PresignedFileUrls: [String: String]
-}
-
-struct DeleteFilesResponse: Decodable {
-    let TXId: Int
-    let DeleteSuccFiles: [String]
-    let DeleteFailedFiles: [String: String]
-}
-
-public struct S3Credential: Decodable {
-    let AccessKeyId: String
-    let Expiration: String
-    let SecretKey: String
-    let SessionToken: String
-}
-
-struct GetTempCredentialResponse: Decodable {
-    let Credentials: S3Credential
-    let S3Prefix: String
-}
-
-struct UpdateFilesResponse: Decodable {
-    let TXId: Int
-    let UpdateSuccFiles: [String]
-    let UpdateFailedFiles: [String: String]
-}

+ 0 - 381
ios/App/App/FileSync/SyncClient.swift

@@ -1,381 +0,0 @@
-//
-//  SyncClient.swift
-//  Logseq
-//
-//  Created by Mono Wang on 4/8/R4.
-//
-
-import Foundation
-import AWSMobileClient
-import AWSS3
-
-public protocol SyncDebugDelegate {
-    func debugNotification(_ message: [String: Any])
-}
-
-
-public class SyncClient {
-    private var token: String
-    private var graphUUID: String?
-    private var txid: Int = 0
-    private var s3prefix: String?
-    
-    public var delegate: SyncDebugDelegate? = nil
-    
-    public init(token: String) {
-        self.token = token
-    }
-    
-    public init(token: String, graphUUID: String) {
-        self.token = token
-        self.graphUUID = graphUUID
-    }
-    
-    public init(token: String, graphUUID: String, txid: Int) {
-        self.token = token
-        self.graphUUID = graphUUID
-        self.txid = txid
-    }
-
-    // get_files
-    // => file_path, file_url
-    public func getFiles(at filePaths: [String], completionHandler: @escaping ([String: URL], Error?) -> Void) {
-        let url = URL_BASE.appendingPathComponent("get_files")
-        
-        var request = URLRequest(url: url)
-        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
-        request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
-        request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
-        
-        let payload = [
-            "GraphUUID": self.graphUUID ?? "",
-            "Files": filePaths
-        ] as [String : Any]
-        let bodyData = try? JSONSerialization.data(
-            withJSONObject: payload,
-            options: []
-        )
-        request.httpMethod = "POST"
-        request.httpBody = bodyData
-        
-        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
-            guard error == nil else {
-                completionHandler([:], error)
-                return
-            }
-            
-            if (response as? HTTPURLResponse)?.statusCode != 200 {
-                let body = String(data: data!, encoding: .utf8) ?? "";
-                completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "http error \(body)"]))
-                return
-            }
-            
-            if let data = data {
-                let resp = try? JSONDecoder().decode([String:[String:String]].self, from: data)
-                let files = resp?["PresignedFileUrls"] ?? [:]
-                self.delegate?.debugNotification(["event": "download:prepare"])
-                completionHandler(files.mapValues({ url in URL(string: url)!}), nil)
-            } else {
-                // Handle unexpected error
-                completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
-            }
-        }
-        task.resume()
-    }
-    
-    public func getVersionFiles(at filePaths: [String], completionHandler: @escaping ([String: URL], Error?) -> Void) {
-        let url = URL_BASE.appendingPathComponent("get_version_files")
-        
-        var request = URLRequest(url: url)
-        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
-        request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
-        request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
-        
-        let payload = [
-            "GraphUUID": self.graphUUID ?? "",
-            "Files": filePaths
-        ] as [String : Any]
-        let bodyData = try? JSONSerialization.data(
-            withJSONObject: payload,
-            options: []
-        )
-        request.httpMethod = "POST"
-        request.httpBody = bodyData
-        
-        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
-            guard error == nil else {
-                completionHandler([:], error)
-                return
-            }
-            
-            if (response as? HTTPURLResponse)?.statusCode != 200 {
-                let body = String(data: data!, encoding: .utf8) ?? "";
-                completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "http error \(body)"]))
-                return
-            }
-            
-            if let data = data {
-                let resp = try? JSONDecoder().decode([String:[String:String]].self, from: data)
-                let files = resp?["PresignedFileUrls"] ?? [:]
-                self.delegate?.debugNotification(["event": "version-download:prepare"])
-                completionHandler(files.mapValues({ url in URL(string: url)!}), nil)
-            } else {
-                // Handle unexpected error
-                completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
-            }
-        }
-        task.resume()
-    }
-    
-    
-    public func deleteFiles(_ filePaths: [String], completionHandler: @escaping  (Int?, Error?) -> Void) {
-        let url = URL_BASE.appendingPathComponent("delete_files")
-        
-        var request = URLRequest(url: url)
-        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
-        request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
-        request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
-        
-        let payload = [
-            "GraphUUID": self.graphUUID ?? "",
-            "Files": filePaths,
-            "TXId": self.txid,
-        ] as [String : Any]
-        let bodyData = try? JSONSerialization.data(
-            withJSONObject: payload,
-            options: []
-        )
-        request.httpMethod = "POST"
-        request.httpBody = bodyData
-        
-        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
-            guard error == nil else {
-                completionHandler(nil, error)
-                return
-            }
-            
-            if let response = response as? HTTPURLResponse {
-                let body = String(data: data!, encoding: .utf8) ?? ""
-                
-                if response.statusCode == 409 {
-                    if body.contains("txid_to_validate") {
-                        completionHandler(nil, NSError(domain: "",
-                                                       code: 409,
-                                                       userInfo: [NSLocalizedDescriptionKey: "invalid txid: \(body)"]))
-                        return
-                    }
-                    // fallthrough
-                }
-                if response.statusCode != 200 {
-                    completionHandler(nil, NSError(domain: "",
-                                                   code: response.statusCode,
-                                                   userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
-                    return
-                }
-            }
-            
-            if let data = data {
-                do {
-                    let resp = try JSONDecoder().decode(DeleteFilesResponse.self, from: data)
-                    // TODO: handle api resp?
-                    self.delegate?.debugNotification(["event": "delete"])
-                    completionHandler(resp.TXId, nil)
-                } catch {
-                    completionHandler(nil, error)
-                }
-            } else {
-                // Handle unexpected error
-                completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
-            }
-        }
-        task.resume()
-    }
-    
-    // (txid, error)
-    // filePath => [S3Key, md5]
-    public func updateFiles(_ fileKeyDict: [String: [String]], completionHandler: @escaping  (Int?, Error?) -> Void) {
-        let url = URL_BASE.appendingPathComponent("update_files")
-        
-        var request = URLRequest(url: url)
-        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
-        request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
-        request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
-        
-        let payload = [
-            "GraphUUID": self.graphUUID ?? "",
-            "Files": Dictionary(uniqueKeysWithValues: fileKeyDict.map { ($0, $1) }) as [String: [String]] as Any,
-            "TXId": self.txid,
-        ] as [String : Any]
-        let bodyData = try? JSONSerialization.data(
-            withJSONObject: payload,
-            options: []
-        )
-        request.httpMethod = "POST"
-        request.httpBody = bodyData
-        
-        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
-            guard error == nil else {
-                completionHandler(nil, error)
-                return
-            }
-            
-            if let response = response as? HTTPURLResponse {
-                let body = String(data: data!, encoding: .utf8) ?? ""
-                
-                if response.statusCode == 409 {
-                    if body.contains("txid_to_validate") {
-                        completionHandler(nil, NSError(domain: "",
-                                                       code: 409,
-                                                       userInfo: [NSLocalizedDescriptionKey: "invalid txid: \(body)"]))
-                        return
-                    }
-                    // fallthrough
-                }
-                if response.statusCode != 200 {
-                    completionHandler(nil, NSError(domain: "",
-                                                   code: response.statusCode,
-                                                   userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
-                    return
-                }
-            }
-            
-            if let data = data {
-                let resp = try? JSONDecoder().decode(UpdateFilesResponse.self, from: data)
-                if resp?.UpdateFailedFiles.isEmpty ?? true {
-                    completionHandler(resp?.TXId, nil)
-                } else {
-                    completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "update fail for some files: \(resp?.UpdateFailedFiles.debugDescription)"]))
-                }
-            } else {
-                // Handle unexpected error
-                completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
-            }
-        }
-        task.resume()
-    }
-    
-    public func getTempCredential(completionHandler: @escaping (S3Credential?, Error?) -> Void) {
-        let url = URL_BASE.appendingPathComponent("get_temp_credential")
-        
-        var request = URLRequest(url: url)
-        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
-        request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
-        request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
-        request.httpMethod = "POST"
-        request.httpBody = Data()
-        
-        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
-            guard error == nil else {
-                completionHandler(nil, error)
-                return
-            }
-            if let response = response as? HTTPURLResponse {
-                let body = String(data: data!, encoding: .utf8) ?? ""
-                if response.statusCode == 401 {
-                    completionHandler(nil, NSError(domain: "", code: 401, userInfo: [NSLocalizedDescriptionKey: "unauthorized"]))
-                    return
-                }
-                if response.statusCode != 200 {
-                    completionHandler(nil, NSError(domain: "",
-                                                   code: response.statusCode,
-                                                   userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
-                    return
-                }
-            }
-            if let data = data {
-                let resp = try? JSONDecoder().decode(GetTempCredentialResponse.self, from: data)
-                // NOTE: remove BUCKET prefix here.
-                self.s3prefix = resp?.S3Prefix.replacingOccurrences(of: "\(BUCKET)/", with: "")
-                self.delegate?.debugNotification(["event": "upload:prepare"])
-                completionHandler(resp?.Credentials, nil)
-            } else {
-                // Handle unexpected error
-                completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
-            }
-        }
-        task.resume()
-    }
-    
-    // [filePath, Key]
-    public func uploadTempFiles(_ files: [String: URL], credentials: S3Credential, completionHandler: @escaping ([String: String], [String: String], Error?) -> Void) {
-        let credentialsProvider = AWSBasicSessionCredentialsProvider(
-            accessKey: credentials.AccessKeyId, secretKey: credentials.SecretKey, sessionToken: credentials.SessionToken)
-        var region = AWSRegionType.USEast2
-        if REGION == "us-east-2" {
-            region = .USEast2
-        } else if REGION == "us-east-1" {
-            region = .USEast1
-        } // TODO: string to REGION conversion
-        
-        let configuration = AWSServiceConfiguration(region: region, credentialsProvider: credentialsProvider)
-        configuration?.timeoutIntervalForRequest = 5.0
-        configuration?.timeoutIntervalForResource = 5.0
-        
-        let tuConf = AWSS3TransferUtilityConfiguration()
-        tuConf.bucket = BUCKET
-        //x tuConf.isAccelerateModeEnabled = true
-        
-        let transferKey = String.random(length: 10)
-        AWSS3TransferUtility.register(
-            with: configuration!,
-            transferUtilityConfiguration: tuConf,
-            forKey: transferKey
-        ) { (error) in
-            if let error = error {
-                print("error while register tu \(error)")
-            }
-        }
-        
-        let transferUtility = AWSS3TransferUtility.s3TransferUtility(forKey: transferKey)
-        let uploadExpression = AWSS3TransferUtilityUploadExpression()
-        
-        let group = DispatchGroup()
-        var keyFileDict: [String: String] = [:]
-        var fileKeyDict: [String: String] = [:]
-        var fileMd5Dict: [String: String] = [:]
-        
-        let uploadCompletionHandler = { (task: AWSS3TransferUtilityUploadTask, error: Error?) -> Void in
-            // ignore any errors in first level of handler
-            if let error = error {
-                self.delegate?.debugNotification(["event": "upload:error", "data": ["key": task.key, "error": error.localizedDescription]])
-            }
-            if let HTTPResponse = task.response {
-                if HTTPResponse.statusCode != 200 || task.status != .completed {
-                    print("debug uploading error \(HTTPResponse)")
-                }
-            }
-            
-            // only save successful keys
-            let filePath = keyFileDict[task.key]!
-            fileKeyDict[filePath] = task.key
-            keyFileDict.removeValue(forKey: task.key)
-            self.delegate?.debugNotification(["event": "upload:file", "data": ["file": filePath, "key": task.key]])
-            group.leave() // notify finish upload
-        }
-        
-        for (filePath, fileLocalURL) in files {
-            print("debug, upload temp \(fileLocalURL) \(filePath)")
-            guard let rawData = try? Data(contentsOf: fileLocalURL) else { continue }
-            guard let encryptedRawDat = maybeEncrypt(rawData) else { continue }
-            group.enter()
-            
-            let randFileName = String.random(length: 15).appending(".").appending(fileLocalURL.pathExtension)
-            let key = "\(self.s3prefix!)/ios\(randFileName)"
-
-            keyFileDict[key] = filePath
-            fileMd5Dict[filePath] = rawData.MD5
-            transferUtility?.uploadData(encryptedRawDat, key: key, contentType: "application/octet-stream", expression: uploadExpression, completionHandler: uploadCompletionHandler)
-                .continueWith(block: { (task) in
-                    if let error = task.error {
-                        completionHandler([:], [:], error)
-                    }
-                    return nil
-                })
-        }
-        
-        group.notify(queue: .main) {
-            AWSS3TransferUtility.remove(forKey: transferKey)
-            completionHandler(fileKeyDict, fileMd5Dict, nil)
-        }
-    }
-}

+ 0 - 5
ios/App/App/Info.plist

@@ -50,11 +50,6 @@
 	<true/>
 	<key>LSSupportsOpeningDocumentsInPlace</key>
 	<true/>
-	<key>NSAppTransportSecurity</key>
-	<dict>
-		<key>NSAllowsArbitraryLoads</key>
-		<true/>
-	</dict>
 	<key>NSCameraUsageDescription</key>
 	<string>We will access your camera when you take a photo, and embed it in your note.</string>
 	<key>NSDocumentsFolderUsageDescription</key>

+ 12 - 4
ios/App/Podfile

@@ -1,4 +1,9 @@
-platform :ios, '12.0'
+require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
+
+# pod specs for AgeEncryption
+source 'https://github.com/CocoaPods/Specs.git'
+
+platform :ios, '13.0'
 use_frameworks!
 
 # workaround to avoid Xcode caching of Pods that requires
@@ -18,6 +23,7 @@ def capacitor_pods
   pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
   pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'
   pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
+  pod 'LogseqCapacitorFileSync', :path => '../../node_modules/@logseq/capacitor-file-sync'
   pod 'CapacitorVoiceRecorder', :path => '../../node_modules/capacitor-voice-recorder'
   pod 'SendIntent', :path => '../../node_modules/send-intent'
 end
@@ -25,7 +31,9 @@ end
 target 'Logseq' do
   capacitor_pods
   # Add your Pods here
-  pod 'AWSMobileClient'
-  pod 'AWSS3'
-  pod 'AgeEncryption', :podspec => './LogseqSpecs/AgeEncryption.podspec'
+end
+
+
+post_install do |installer|
+  assertDeploymentTarget(installer)
 end

+ 1 - 1
libs/src/LSPlugin.ts

@@ -386,7 +386,7 @@ export interface IAppProxy {
   queryElementRect: (selector: string) => Promise<DOMRectReadOnly | null>
 
   /**
-   * @deprecated
+   * @deprecated Use `logseq.UI.showMsg` instead
    * @param content
    * @param status
    */

+ 16 - 15
package.json

@@ -5,7 +5,7 @@
     "main": "static/electron.js",
     "devDependencies": {
         "@axe-core/playwright": "^4.4.4",
-        "@capacitor/cli": "3.2.2",
+        "@capacitor/cli": "^4.0.0",
         "@playwright/test": "^1.24.2",
         "@tailwindcss/ui": "0.7.2",
         "@types/gulp": "^4.0.7",
@@ -14,6 +14,7 @@
         "del": "^6.0.0",
         "gulp": "^4.0.2",
         "gulp-clean-css": "^4.3.0",
+        "ip": "1.1.8",
         "npm-run-all": "^4.1.5",
         "playwright": "^1.24.2",
         "postcss": "8.4.16",
@@ -26,8 +27,7 @@
         "stylelint": "^13.8.0",
         "stylelint-config-standard": "^20.0.0",
         "tailwindcss": "2.2.16",
-        "typescript": "^4.4.3",
-        "ip": "1.1.8"
+        "typescript": "^4.4.3"
     },
     "scripts": {
         "watch": "run-p gulp:watch cljs:watch",
@@ -71,20 +71,21 @@
         "postinstall": "yarn tldraw:build"
     },
     "dependencies": {
-        "@capacitor/android": "3.2.2",
-        "@capacitor/app": "1.0.6",
-        "@capacitor/camera": "1.2.1",
-        "@capacitor/clipboard": "^1.0.8",
-        "@capacitor/core": "3.2.2",
-        "@capacitor/filesystem": "1.0.6",
-        "@capacitor/haptics": "^1.1.4",
-        "@capacitor/ios": "3.2.2",
-        "@capacitor/keyboard": "^1.2.0",
-        "@capacitor/share": "^1.1.2",
-        "@capacitor/splash-screen": "1.1.3",
-        "@capacitor/status-bar": "1.0.6",
+        "@capacitor/android": "^4.0.0",
+        "@capacitor/app": "^4.0.0",
+        "@capacitor/camera": "^4.0.0",
+        "@capacitor/clipboard": "^4.0.0",
+        "@capacitor/core": "^4.0.0",
+        "@capacitor/filesystem": "^4.0.0",
+        "@capacitor/haptics": "^4.0.0",
+        "@capacitor/ios": "^4.0.0",
+        "@capacitor/keyboard": "^4.0.0",
+        "@capacitor/share": "^4.0.0",
+        "@capacitor/splash-screen": "^4.0.0",
+        "@capacitor/status-bar": "^4.0.0",
         "@excalidraw/excalidraw": "0.10.0",
         "@kanru/rage-wasm": "0.2.1",
+        "@logseq/capacitor-file-sync": "0.0.6",
         "@logseq/react-tweet-embed": "1.3.1-1",
         "@sentry/react": "^6.18.2",
         "@sentry/tracing": "^6.18.2",

+ 36 - 40
src/main/frontend/components/file_sync.cljs

@@ -323,12 +323,6 @@
             ]}))])))
 
 (rum/defc pick-local-graph-for-sync [graph]
-  (rum/use-effect!
-   (fn []
-     (file-sync-handler/set-wait-syncing-graph graph)
-     #(file-sync-handler/set-wait-syncing-graph nil))
-   [graph])
-
   [:div.cp__file-sync-related-normal-modal
    [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "cloud-download")]]
 
@@ -346,41 +340,43 @@
 
    [:div.-mt-1
     (ui/button
-      (str "Open a local directory")
+      "Open a local directory"
       :class "w-full rounded-t-none py-4"
-      :on-click #(->
-                  (page-handler/ls-dir-files!
-                   (fn [{:keys [url]}]
-                     (file-sync-handler/init-remote-graph url)
-                     ;; TODO: wait for switch done
-                     (js/setTimeout (fn [] (repo-handler/refresh-repos!)) 200))
-
-                   {:empty-dir?-or-pred
-                    (fn [ret]
-                      (let [empty-dir? (nil? (second ret))]
-                        (if-let [root (first ret)]
-
-                          ;; verify directory
-                          (-> (if empty-dir?
-                                (p/resolved nil)
-                                (if (util/electron?)
-                                  (ipc/ipc :readGraphTxIdInfo root)
-                                  (fs-util/read-graphs-txid-info root)))
-
-                              (p/then (fn [^js info]
-                                        (when (and (not empty-dir?)
-                                                   (or (nil? info)
-                                                       (nil? (second info))
-                                                       (not= (second info) (:GraphUUID graph))))
-                                          (if (js/confirm "This directory is not empty, are you sure to sync the remote graph to it? Make sure to back up the directory first.")
-                                            (do
-                                              (state/set-state! :graph/remote-binding? true)
-                                              (p/resolved nil))
-                                            (throw (js/Error. nil)))))))
-
-                          ;; cancel pick a directory
-                          (throw (js/Error. nil)))))})
-                  (p/catch (fn []))))
+      :on-click #(do
+                   (state/close-modal!)
+                   (fs-sync/<sync-stop)
+                   (->
+                    (page-handler/ls-dir-files!
+                     (fn [{:keys [url]}]
+                       (file-sync-handler/init-remote-graph url graph)
+                       (js/setTimeout (fn [] (repo-handler/refresh-repos!)) 200))
+
+                     {:empty-dir?-or-pred
+                      (fn [ret]
+                        (let [empty-dir? (nil? (second ret))]
+                          (if-let [root (first ret)]
+
+                            ;; verify directory
+                            (-> (if empty-dir?
+                                  (p/resolved nil)
+                                  (if (util/electron?)
+                                    (ipc/ipc :readGraphTxIdInfo root)
+                                    (fs-util/read-graphs-txid-info root)))
+
+                                (p/then (fn [^js info]
+                                          (when (and (not empty-dir?)
+                                                     (or (nil? info)
+                                                         (nil? (second info))
+                                                         (not= (second info) (:GraphUUID graph))))
+                                            (if (js/confirm "This directory is not empty, are you sure to sync the remote graph to it? Make sure to back up the directory first.")
+                                              (do
+                                                (state/set-state! :graph/remote-binding? true)
+                                                (p/resolved nil))
+                                              (throw (js/Error. nil)))))))
+
+                            ;; cancel pick a directory
+                            (throw (js/Error. nil)))))})
+                    (p/catch (fn [])))))
     [:p.text-xs.opacity-50.px-1 (ui/icon "alert-circle") " An empty directory or an existing remote graph!"]]])
 
 (defn pick-dest-to-sync-panel [graph]

+ 2 - 1
src/main/frontend/components/repo.cljs

@@ -97,7 +97,8 @@
                                      (state/set-modal! (confirm-fn)))
                                    (do
                                      (repo-handler/remove-repo! repo)
-                                     (file-sync/load-session-graphs))))}
+                                     (file-sync/load-session-graphs)
+                                     (state/pub-event! [:graph/unlinked]))))}
                     (if only-cloud? "Remove" "Unlink")])])]]))
 
 (rum/defc repos < rum/reactive

+ 2 - 2
src/main/frontend/components/sidebar.css

@@ -504,8 +504,8 @@ html[data-theme='dark'] {
   &.closed {
     width: 0 !important;
 
-    .resizer {
-      left: -4px;
+    @screen lg {
+      width: 4px !important;
     }
   }
 

+ 31 - 31
src/main/frontend/dicts.cljc

@@ -4727,11 +4727,11 @@
                           :default "tutorial-pl.md")
         :tutorial/dummy-notes #?(:cljs (rc/inline "dummy-notes-pl.md")
                                  :default "dummy-notes-pl.md")
-        :on-boarding/demo-graph "To jest przykładowy graf, zmiany nie zostaną zapisane dopóki nie otworzysz jakiegoś lokalnego katalogu."
+        :on-boarding/demo-graph "To jest przykładowy graf, zmiany nie zostaną zapisane dopóki nie otworzysz lokalnego katalogu."
         :on-boarding/add-graph "Dodaj graf"
         :on-boarding/open-local-dir "Otwórz lokalny katalog"
         :on-boarding/new-graph-desc-1 "Logseq wspiera zarówno składnię Markdown jak i Org-mode. Możesz otworzyć istniejący już folder lub utworzyć nowy katalog na swoim urządzeniu. Wszystkie dane będą przechowywane tylko na tym urządzeniu."
-        :on-boarding/new-graph-desc-2 "Po otwarciu wybranego katalogu zostaną w nim uworzone trzy dodatkowe katalogi:"
+        :on-boarding/new-graph-desc-2 "Po otwarciu wybranego katalogu zostaną w nim utworzone trzy dodatkowe katalogi:"
         :on-boarding/new-graph-desc-3 "/journals - przechowuje wszystkie strony twojego dziennika"
         :on-boarding/new-graph-desc-4 "/pages - przechowuje wszystkie pozostałe, utworzone przez Ciebie strony"
         :on-boarding/new-graph-desc-5 "/logseq - przechowuje konfigurację, custom.css oraz niektóre meta dane."
@@ -4748,19 +4748,19 @@
         :help/community "Społeczność Discord"
         :help/forum-community "Forum dyskusyjne"
         :help/awesome-logseq "Awesome Logseq"
-        :help/shortcuts "Skróty klawiaturowe"
+        :help/shortcuts "Skróty klawiszowe"
         :help/shortcuts-triggers "Akcja"
         :help/shortcut "Skrót"
         :help/slash-autocomplete "Autouzupełnianie poleceń /"
         :help/block-content-autocomplete "Autouzupełnianie treści bloku"
         :help/reference-autocomplete "Autouzupełnianie referencji bloku"
         :help/block-reference "Referencja bloku"
-        :help/key-commands "Kluczowe"
+        :help/key-commands "Polecenia klawiszowe"
         :help/working-with-lists " (praca z listami)"
         :help/select-nfs-browser "Proszę użyć innej przeglądarki (na przykład ostatniego Chrome), która wspiera otwieranie lokalnych katalogów (NFS feature)"
         :undo "Cofnij"
         :redo "Ponów"
-        :general "General"
+        :general "Ogólne"
         :more "Więcej"
         :search/result-for "Wyniki wyszukiwania dla"
         :search/items "elementów"
@@ -4783,7 +4783,7 @@
         :right-side-bar/favorites "Ulubione"
         :right-side-bar/page-graph "Graf strony"
         :right-side-bar/block-ref "Ref bloku"
-        :right-side-bar/graph-view "Widok gafu"
+        :right-side-bar/graph-view "Widok grafu"
         :right-side-bar/all-pages "Wszystkie strony"
         :right-side-bar/flashcards "Fiszki"
         :right-side-bar/new-page "Nowa strona"
@@ -4797,7 +4797,7 @@
         :format/markdown "Markdown"
         :format/org-mode "Org mode"
         :reference/linked "Powiązane referencje"
-        :reference/unlinked-ref "Niepołączone referencje"
+        :reference/unlinked-ref "Niepowiązane referencje"
         :page/presentation-mode "Tryb prezentacji"
         :page/edit-properties-placeholder "Właściwości"
         :page/delete-success "Strona {1} została usunięta!"
@@ -4812,7 +4812,7 @@
         :page/make-public "Oznacz jako publiczną"
         :page/version-history "Sprawdź historię zmian"
         :page/open-backup-directory "Otwórz katalog z kopiami bezpieczeństwa"
-        :page/make-private "Oznacz jako prywantą"
+        :page/make-private "Oznacz jako prywatną"
         :page/delete "Usuń stronę"
         :page/add-to-favorites "Dodaj do ulubionych"
         :page/unfavorite "Usuń z ulubionych"
@@ -4842,29 +4842,29 @@
         :draw/specify-title "Proszę najpierw określić tytuł strony!"
         :draw/rename-success "Nazwa pliku została zmieniona!"
         :draw/rename-failure "Zmiana nazwy pliku nieudana. Powód: "
-        :draw/title-placeholder "Nienazwany"
+        :draw/title-placeholder "Bez nazwy"
         :draw/save "Zapisz"
         :draw/save-changes "Zapisz zmiany"
         :draw/new-file "Nowy plik"
         :draw/list-files "Wylistuj pliki"
         :draw/delete "Usuń"
         :draw/more-options "Więcej opcji"
-        :draw/back-to-logseq "Powrót do LogSeq"
+        :draw/back-to-logseq "Powrót do Logseq"
         :text/image "Obrazek"
         :asset/confirm-delete "Jesteś pewien, że chcesz usunąć ten {1}?"
         :asset/physical-delete "Usuń ten plik z dysku (operacja nie może zostać cofnięta)"
         :content/copy "Kopiuj"
         :content/cut "Wytnij"
         :content/make-todos "Stwórz {1}s"
-        :content/copy-block-ref "Kopiuje ref bloku"
+        :content/copy-block-ref "Kopiuj ref bloku"
         :content/copy-block-emebed "Kopiuj blok jako embed"
         :content/focus-on-block "Skup się na bloku"
         :content/open-in-sidebar "Otwórz w panelu bocznym"
         :content/copy-as-json "Kopiuj jako JSON"
-        :content/click-to-edit "Kliknij aby edytować"
-        :settings-page/git-desc "jest użyty do kontroli wersji stron, możesz kliknąć pionowe trzy kropki żeby zobaczyć historię zmian strony."
-        :settings-page/git-confirm "Musisz uruchomić ponownie aplikację żeby zastosować zmiany w ustawieniach GITa."
-        :settings-page/git-switcher-label "Włącz Autocommit GITa"
+        :content/click-to-edit "Kliknij, aby edytować"
+        :settings-page/git-desc "jest użyty do kontroli wersji stron, aby zobaczyć historię zmian strony, kliknij na ikonę trzech pionowych kropek."
+        :settings-page/git-confirm "Musisz uruchomić ponownie aplikację żeby zastosować zmiany w ustawieniach Gita."
+        :settings-page/git-switcher-label "Włącz opcję autocommit w Git"
         :settings-page/git-commit-delay "Wykonaj commit co każde [s.]"
         :settings-page/edit-config-edn "Edytuj config.edn"
         :settings-page/edit-custom-css "Edytuj custom.css"
@@ -4874,15 +4874,15 @@
         :settings-page/export-theme "Eksportuj motyw graficzny"
         :settings-page/show-brackets "Pokaż kwadratowe nawiasy"
         :settings-page/spell-checker "Sprawdzanie pisowni"
-        :settings-page/auto-updater "Auto aktualizacje"
+        :settings-page/auto-updater "Automatyczne aktualizacje"
         :settings-page/disable-sentry "Wyślij statystyki użycia aplikacji do Logseq"
         :settings-page/preferred-outdenting "Logiczne zmniejszanie wcięć"
         :settings-page/custom-date-format "Preferowany format daty"
         :settings-page/preferred-file-format "Preferowany format pliku"
         :settings-page/preferred-workflow "Preferowany flow TODOsów"
-        :settings-page/enable-shortcut-tooltip "Włącz dymek podpowiedzi skrótu klawiszowego"
+        :settings-page/enable-shortcut-tooltip "Włącz podpowiedzi skrótu klawiszowego"
         :settings-page/enable-timetracking "Mierzenie czasu"
-        :settings-page/enable-tooltip "Dymki podpowiedzi"
+        :settings-page/enable-tooltip "Podpowiedzi"
         :settings-page/enable-journals "Dzienniki"
         :settings-page/enable-all-pages-public "Publikuj wszystkie strony"
         :settings-page/enable-encryption "Szyfrowanie"
@@ -4917,14 +4917,14 @@
         :delete "Usuń"
         :save "Zapisz"
         :type "Typ"
-        :re-index "Ponownie indeksuj"
+        :re-index "Indeksuj ponownie"
         :re-index-detail "Przebuduj graf od nowa"
-        :re-index-multiple-windows-warning "Musisz zamknąć inne okna zanim rozpoczniesz proces ponownej indeksacji"
-        :re-index-discard-unsaved-changes-warning "Ponowna indeksacja odrzuci zmiany w obecnym grafie a następnie przejdzie po wszystkich plikach zapisanych na dysku. Utracisz wszystkie niezapisane zmiany a sam proces może trochę potrwać. Kontynuować?"
+        :re-index-multiple-windows-warning "Musisz zamknąć inne okna, zanim rozpoczniesz proces ponownej indeksacji"
+        :re-index-discard-unsaved-changes-warning "Ponowna indeksacja odrzuci zmiany w obecnym grafie, a następnie przejdzie po wszystkich plikach zapisanych na dysku. Utracisz wszystkie niezapisane zmiany, a sam proces może trochę potrwać. Kontynuować?"
         :open-new-window "Nowe okno"
         :sync-from-local-files "Odśwież"
         :sync-from-local-files-detail "Importuj zmiany z lokalnych plików"
-        :sync-from-local-changes-detected "Funkcja Odśwież wykrywa i procesuje pliki zmianione na dysku. Wszystkie pliki różniące się od tych w Logseq zostaną ponownie wczytane. Kontynuować?"
+        :sync-from-local-changes-detected "Funkcja Odśwież wykrywa i procesuje pliki zmienione na dysku. Wszystkie pliki różniące się od tych w Logseq zostaną ponownie wczytane. Kontynuować?"
 
         :unlink "Usuń link"
         :search/publishing "Szukaj"
@@ -4968,13 +4968,13 @@
         :settings-of-plugins "Ustawienia pluginu"
         :plugins "Pluginy"
         :themes "Motywy graficzne"
-        :developer-mode-alert "Musisz uruchomić ponownie aplikację żeby włączyć system pluginów. Zrobić to teraz?"
-        :relaunch-confirm-to-work "Powinienem uruchomić ponownie aplikację żeby to zaczęło działać. Zrobić to teraz?"
+        :developer-mode-alert "Aby włączyć system pluginów, aplikacja musi zostać uruchomiona ponownie. Zrobić to teraz?"
+        :relaunch-confirm-to-work "Aby wprowadzić zmiany, aplikacja musi zostać uruchomiona ponownie. Zrobić to teraz?"
         :join-community "Dołącz do społeczności"
         :sponsor-us "Sponsoruj nas"
         :discourse-title "Nasze forum!"
         :discord-title "Nasza grupa Discord!" ;; unused
-        :help-shortcut-title "Kliknij aby sprawdzić skróty klawiszowe i inne takie"
+        :help-shortcut-title "Kliknij, aby sprawdzić skróty klawiszowe oraz inne porady"
         :loading "Ładuję"
         :cloning "Klonuję"
         :parsing-files "Parsuję pliki"
@@ -5015,7 +5015,7 @@
         :plugin/all "Wszystkie"
         :plugin/unpacked "Rozpakowany"
         :plugin/marketplace "Marketplace"
-        :plugin/delete-alert "Czy napeno chcesz usunąć plugin [{1}]?"
+        :plugin/delete-alert "Czy na pewno chcesz usunąć plugin [{1}]?"
         :plugin/open-settings "Otwórz ustawienia"
         :plugin/open-package "Otwórz paczkę"
         :plugin/load-unpacked "Załaduj rozpakowany plugin"
@@ -5025,15 +5025,15 @@
         :plugin/contribute "✨ Napisz i prześlij nowy plugin"
         :plugin/marketplace-tips "Jeżeli plugin po załadowaniu nie działa, spróbuj zrestartować Logseq."
         :plugin/up-to-date "Wszystko jest aktualne"
-        :plugin/custom-js-alert "Found the custom.js file, is it allowed to execute? (If you don't understand the content of this file, it is recommended not to allow execution, which has certain security risks.)"
+        :plugin/custom-js-alert "Wykryto plik custom.js, czy chcesz go wykonać? Ze względów bezpieczeństwa, nie rekomendujemy wykonywania tego pliku, jeżeli nie znasz jego zawartości."
 
         :pdf/copy-ref "Kopiuj ref"
         :pdf/copy-text "Kopiuj tekst"
         :pdf/linked-ref "Połączone odwołania"
-        :pdf/toggle-dashed "Dashed style for area highlight"
+        :pdf/toggle-dashed "Styl kreskowany dla podświetlenia obszaru"
 
         :updater/new-version-install "Nowa wersja została ściągnięta."
-        :updater/quit-and-install "Uruchom ponownie żeby zainstalować"
+        :updater/quit-and-install "Uruchom ponownie, aby zainstalować"
 
         :paginates/pages "{1} wszystkich stron"
         :paginates/prev "← Poprzednia"
@@ -5047,7 +5047,7 @@
         :select.graph/empty-placeholder-description "Brak pasujących grafów. Chcesz dodać nowy?"
         :select.graph/add-graph "Tak, dodaj nowy graf"
 
-        :file-sync/other-user-graph "Obecny lokalny graf jest przypisany do zdalnego grafu innego użytkownika. Nie mo rozpocząć synchronizacji."
+        :file-sync/other-user-graph "Obecny lokalny graf jest przypisany do zdalnego grafu innego użytkownika. Nie można rozpocząć synchronizacji."
         :file-sync/graph-deleted "Obecny zdalny graf został usunięty"
         }
    :tongue/fallback :en})
@@ -5061,7 +5061,7 @@
                 {:label "Afrikaans" :value :af}
                 {:label "Español" :value :es}
                 {:label "Norsk (bokmål)" :value :nb-NO}
-                {:label "Polski" :value :pl}
+                {:label "polski" :value :pl}
                 {:label "Português (Brasileiro)" :value :pt-BR}
                 {:label "Português (Europeu)" :value :pt-PT}
                 {:label "Русский" :value :ru}

+ 3 - 3
src/main/frontend/encrypt.cljs

@@ -97,7 +97,7 @@
             encrypted (ipc/ipc "encrypt-with-passphrase" passphrase raw-content)]
       (utf8/decode encrypted))
 
-    (mobile-util/native-ios?)
+    (mobile-util/native-platform?)
     (p/chain (.encryptWithPassphrase mobile-util/file-sync
                                      (clj->js {:passphrase passphrase :content content}))
              #(js->clj % :keywordize-keys true)
@@ -117,12 +117,12 @@
             decrypted (ipc/ipc "decrypt-with-passphrase" passphrase raw-content)]
       (utf8/decode decrypted))
 
-    (mobile-util/native-ios?)
+    (mobile-util/native-platform?)
     (p/chain (.decryptWithPassphrase mobile-util/file-sync
                                      (clj->js {:passphrase passphrase :content content}))
              #(js->clj % :keywordize-keys true)
              :data)
-    
+
     :else
     (p/let [_ (loader/load :age-encryption)
             lazy-decrypt-with-user-passphrase (resolve 'frontend.extensions.age-encryption/decrypt-with-user-passphrase)

+ 1 - 1
src/main/frontend/fs.cljs

@@ -210,7 +210,7 @@
   [dir path]
   (util/p-handle
    (stat dir path)
-   (fn [_stat] true)
+   (fn [stat] (not (nil? stat)))
    (fn [_e] false)))
 
 (defn dir-exists?

+ 27 - 35
src/main/frontend/fs/capacitor_fs.cljs

@@ -82,36 +82,26 @@
                      (p/let [d (first dirs)
                              files (<readdir d)
                              files (->> files
-                                        (remove (fn [file]
-                                                  (or (string/starts-with? file ".")
-                                                      (and (mobile-util/native-android?)
-                                                           (or (string/includes? file "#")
-                                                               (string/includes? file "%")))
-                                                      (= file "bak")))))
-                             files (->> files
-                                        (map (fn [file]
-                                               ;; TODO: use uri-join
-                                               (str (string/replace d #"/+$" "")
-                                                    "/"
-                                                    (if (mobile-util/native-ios?)
-                                                      (js/encodeURI file)
-                                                      file)))))
-                             files-with-stats (p/all (mapv <stat files))
-                             files-dir (->> files-with-stats
+                                        (remove (fn [{:keys [name  type]}]
+                                                  (or (string/starts-with? name ".")
+                                                      (and (= type "directory")
+                                                           (or (= name "bak")
+                                                               (= name "version-files")))))))
+                             files-dir (->> files
                                             (filterv #(= (:type %) "directory"))
                                             (mapv :uri))
                              files-result
                              (p/all
-                              (->> files-with-stats
+                              (->> files
                                    (filter #(= (:type %) "file"))
                                    (filter
                                     (fn [{:keys [uri]}]
                                       (some #(string/ends-with? uri %)
                                             [".md" ".markdown" ".org" ".edn" ".css"])))
                                    (mapv
-                                    (fn [{:keys [uri] :as file-result}]
+                                    (fn [{:keys [uri] :as file-info}]
                                       (p/chain (<read-file-with-utf8 uri)
-                                               #(assoc file-result :content %))))))]
+                                               #(assoc file-info :content %))))))]
                        (p/recur (concat result files-result)
                                 (concat (rest dirs) files-dir)))))]
     (js->clj result :keywordize-keys true)))
@@ -221,7 +211,7 @@
 
 (defn get-file-path [dir path]
   (let [dir (some-> dir (string/replace #"/+$" ""))
-        dir (if (string/starts-with? dir "/")
+        dir (if (and (not-empty dir) (string/starts-with? dir "/"))
               (do
                 (js/console.trace "WARN: detect absolute path, use URL instead")
                 (str "file://" (js/encodeURI dir)))
@@ -271,13 +261,18 @@
                    (log/error :mkdir! {:path dir
                                        :error error})))))
   (mkdir-recur! [_this dir]
-    (p/let [result (.mkdir Filesystem
-                           (clj->js
-                            {:path dir
-                             ;; :directory (.-ExternalStorage Directory)
-                             :recursive true}))]
-      (js/console.log result)
-      result))
+    (p/let
+     [_ (-> (.mkdir Filesystem
+                    (clj->js
+                     {:path dir
+                      :recursive true}))
+            (p/catch (fn [error]
+                       (log/error :mkdir-recur! {:path dir
+                                                 :error error}))))
+      stat (<stat dir)]
+      (if (= (:type stat) "directory")
+        (p/resolved true)
+        (p/rejected (js/Error. "mkdir-recur! failed")))))
   (readdir [_this dir]                  ; recursive
     (let [dir (if-not (string/starts-with? dir "file://")
                 (str "file://" dir)
@@ -286,13 +281,13 @@
   (unlink! [this repo path _opts]
     (p/let [path (get-file-path nil path)
             repo-url (config/get-local-dir repo)
-            recycle-dir (str repo-url config/app-name "/.recycle") ;; logseq/.recycle
+            recycle-dir (util/safe-path-join repo-url config/app-name ".recycle") ;; logseq/.recycle
             ;; convert url to pure path
             file-name (-> (string/replace path repo-url "")
                           (string/replace "/" "_")
                           (string/replace "\\" "_"))
-            new-path (str recycle-dir "/" file-name)]
-      (protocol/mkdir! this recycle-dir)
+            new-path (str recycle-dir "/" file-name)
+            _ (protocol/mkdir-recur! this recycle-dir)]
       (protocol/rename! this repo path new-path)))
   (rmdir! [_this _dir]
     ;; Too dangerous!!! We'll never implement this.
@@ -330,11 +325,8 @@
          (log/error :copy-file-failed error)))))
   (stat [_this dir path]
     (let [path (get-file-path dir path)]
-      (p/let [result (.stat Filesystem (clj->js
-                                        {:path path
-                                         ;; :directory (.-ExternalStorage Directory)
-                                         }))]
-        result)))
+      (p/chain (.stat Filesystem (clj->js {:path path}))
+               #(js->clj % :keywordize-keys true))))
   (open-dir [_this _ok-handler]
     (p/let [_ (when (mobile-util/native-android?) (android-check-permission))
             {:keys [path localDocumentsPath]} (-> (.pickFolder mobile-util/folder-picker)

+ 69 - 14
src/main/frontend/fs/sync.cljs

@@ -26,7 +26,8 @@
             [frontend.fs :as fs]
             [frontend.encrypt :as encrypt]
             [medley.core :refer [dedupe-by]]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [promesa.core :as p]))
 
 ;;; ### Commentary
 ;; file-sync related local files/dirs:
@@ -181,8 +182,8 @@
   [latest-txid graph-uuid user-uuid repo]
   {:pre [(int? latest-txid) (>= latest-txid 0)]}
   (persist-var/-reset-value! graphs-txid [user-uuid graph-uuid latest-txid] repo)
-  (some-> (persist-var/persist-save graphs-txid)
-          p->c)
+  (p/let [_ (persist-var/persist-save graphs-txid)]
+    (state/pub-event! [:graph/refresh]))
   (when (state/developer-mode?) (assert-local-txid<=remote-txid)))
 
 (defn clear-graphs-txid! [repo]
@@ -652,6 +653,7 @@
   (<get-remote-graph [this graph-name-opt graph-uuid-opt] "get graph info by GRAPH-NAME-OPT or GRAPH-UUID-OPT")
   (<get-remote-file-versions [this graph-uuid filepath] "get file's version list")
   (<list-remote-graphs [this] "list all remote graphs")
+  (<get-deletion-logs [this graph-uuid from-txid] "get deletion logs from FROM-TXID")
   (<get-diff [this graph-uuid from-txid] "get diff from FROM-TXID, return [txns, latest-txid, min-txid]")
   (<create-graph [this graph-name] "create graph")
   (<delete-graph [this graph-uuid] "delete graph")
@@ -1103,9 +1105,9 @@
                        (recur next-continuation-token)))))))]
         (if (instance? ExceptionInfo exp-r)
           exp-r
-          (let [file-meta-list*          (persistent! file-meta-list)
-                encrypted-path-list*     (persistent! encrypted-path-list)
-                path-list-or-exp (<! (<decrypt-fnames rsapi encrypted-path-list*))]
+          (let [file-meta-list*      (persistent! file-meta-list)
+                encrypted-path-list* (persistent! encrypted-path-list)
+                path-list-or-exp     (<! (<decrypt-fnames rsapi encrypted-path-list*))]
             (if (instance? ExceptionInfo path-list-or-exp)
               path-list-or-exp
               (let [encrypted-path->path-map (zipmap encrypted-path-list* path-list-or-exp)]
@@ -1156,6 +1158,23 @@
   (<list-remote-graphs [this]
     (.<request this "list_graphs"))
 
+  (<get-deletion-logs [this graph-uuid from-txid]
+    (go
+      (let [r (<! (.<request this "get_deletion_log" {:GraphUUID graph-uuid :FromTXId from-txid}))]
+        (if (instance? ExceptionInfo r)
+          r
+          (let [txns-with-encrypted-paths (mapv #(update % :path remove-user-graph-uuid-prefix) (:Transactions r))
+                encrypted-paths           (mapv :path txns-with-encrypted-paths)
+                encrypted-path->path-map
+                (zipmap
+                 encrypted-paths
+                 (<! (<decrypt-fnames rsapi encrypted-paths)))
+                txns
+                (mapv
+                 (fn [txn] (update txn :path #(get encrypted-path->path-map %)))
+                 txns-with-encrypted-paths)]
+            txns)))))
+
   (<get-diff [this graph-uuid from-txid]
     ;; TODO: path in transactions should be relative path(now s3 key, which includes graph-uuid and user-uuid)
     (go
@@ -2189,6 +2208,21 @@
          (every? #(instance? FileChangeEvent %) es)]}
   (filterv filter-too-huge-files-aux es))
 
+(defn- filter-local-files-in-deletion-logs
+  [local-all-files-meta deletion-logs]
+  (let [deletion-logs-map (into {} (map (juxt :path identity)) deletion-logs)
+        *keep             (transient #{})
+        *delete           (transient #{})]
+    (doseq [f local-all-files-meta]
+      (let [epoch-long (some-> (get deletion-logs-map (:path f))
+                               :epoch
+                               (* 1000))]
+        (if (and epoch-long (> epoch-long (:last-modified f)))
+          (conj! *delete f)
+          (conj! *keep f))))
+    {:keep   (persistent! *keep)
+     :delete (persistent! *delete)}))
+
 
 (defrecord ^:large-vars/cleanup-todo
     Local->RemoteSyncer [user-uuid graph-uuid base-path repo *sync-state remoteapi
@@ -2206,7 +2240,7 @@
                    true)
                  (or (string/starts-with? (.-dir e) base-path)
                      (string/starts-with? (str "file://" (.-dir e)) base-path)) ; valid path prefix
-                 (not (ignored? e)) ;not ignored
+                 (not (ignored? e))     ;not ignored
                  ;; download files will also trigger file-change-events, ignore them
                  (let [r (not (contains? (:recent-remote->local-files @*sync-state)
                                          (<! (<file-change-event=>recent-remote->local-file-item e))))]
@@ -2252,10 +2286,10 @@
     (<sync-local->remote! [_ es]
       (if (empty? es)
         (go {:succ true})
-        (let [type          (.-type ^FileChangeEvent (first es))
-              es->paths-xf  (comp
-                             (map #(relative-path %))
-                             (remove ignored?))]
+        (let [type         (.-type ^FileChangeEvent (first es))
+              es->paths-xf (comp
+                            (map #(relative-path %))
+                            (remove ignored?))]
           (go
             (let [es*   (<! (<filter-checksum-not-consistent es))
                   _     (when (not= (count es*) (count es))
@@ -2315,7 +2349,9 @@
       (go
         (let [remote-all-files-meta-c      (<get-remote-all-files-meta remoteapi graph-uuid)
               local-all-files-meta-c       (<get-local-all-files-meta rsapi graph-uuid base-path)
-              remote-all-files-meta-or-exp (<! remote-all-files-meta-c)]
+              deletion-logs-c              (<get-deletion-logs remoteapi graph-uuid @*txid)
+              remote-all-files-meta-or-exp (<! remote-all-files-meta-c)
+              deletion-logs                (<! deletion-logs-c)]
           (if (or (storage-exceed-limit? remote-all-files-meta-or-exp)
                   (sync-stop-when-api-flying? remote-all-files-meta-or-exp)
                   (decrypt-exp? remote-all-files-meta-or-exp))
@@ -2326,6 +2362,8 @@
                 {:stop true})
             (let [remote-all-files-meta remote-all-files-meta-or-exp
                   local-all-files-meta  (<! local-all-files-meta-c)
+                  {local-all-files-meta :keep delete-local-files :delete}
+                  (filter-local-files-in-deletion-logs local-all-files-meta deletion-logs)
                   diff-local-files      (diff-file-metadata-sets local-all-files-meta remote-all-files-meta)
                   change-events
                   (sequence
@@ -2341,7 +2379,25 @@
                    (partition-file-change-events 10)
                    (distinct-file-change-events change-events))]
               (println "[full-sync(local->remote)]"
-                       (count (flatten change-events-partitions)) "files need to sync")
+                       (count (flatten change-events-partitions)) "files need to sync and"
+                       (count delete-local-files) "local files need to delete")
+              ;; 1. delete local files
+              (loop [[f & fs] delete-local-files]
+                (when f
+                  (let [relative-p (relative-path f)]
+                    (when-not (<! (<local-file-not-exist? rsapi base-path relative-p))
+                      (let [fake-recent-remote->local-file-item {:remote->local-type :delete
+                                                                 :checksum nil
+                                                                 :path relative-p}]
+                        (swap! *sync-state sync-state--add-recent-remote->local-files
+                               [fake-recent-remote->local-file-item])
+                        (<! (<delete-local-files rsapi graph-uuid base-path [(relative-path f)]))
+                        (go (<! (timeout 5000))
+                            (swap! *sync-state sync-state--remove-recent-remote->local-files
+                                   [fake-recent-remote->local-file-item])))))
+                  (recur fs)))
+
+              ;; 2. upload local files
               (loop [es-partitions change-events-partitions]
                 (if @*stopped
                   {:stop true}
@@ -2779,7 +2835,6 @@
     (go
       (when (and (graph-sync-off? repo) @network-online-cursor)
         (<! (p->c (persist-var/-load graphs-txid)))
-
         (let [[user-uuid graph-uuid txid] @graphs-txid]
           (when (and user-uuid graph-uuid txid
                      (user/logged-in?)

+ 9 - 4
src/main/frontend/handler/events.cljs

@@ -106,7 +106,14 @@
       (route-handler/redirect! {:to :import :query-params {:from "picker"}})
       (route-handler/redirect-to-home!)))
   (repo-handler/refresh-repos!)
-  (file-sync-stop!))
+  (file-sync-restart!))
+
+(defmethod handle :graph/unlinked [_]
+  (repo-handler/refresh-repos!)
+  (file-sync-restart!))
+
+(defmethod handle :graph/refresh [_]
+  (repo-handler/refresh-repos!))
 
 (defn- graph-switch
   ([graph]
@@ -568,9 +575,7 @@
                      (state/close-modal!)
                      (repo-handler/re-index!
                       nfs-handler/rebuild-index!
-                      #(do
-                         (page-handler/create-today-journal!)
-                         (file-sync-restart!)))))]])))
+                      page-handler/create-today-journal!)))]])))
 
 ;; encryption
 (defmethod handle :modal/encryption-setup-dialog [[_ repo-url close-fn]]

+ 3 - 10
src/main/frontend/handler/file_sync.cljs

@@ -93,8 +93,7 @@
   (let [repo (state/get-current-repo)
         user-uuid (user/user-uuid)]
     (sync/update-graphs-txid! 0 graph-uuid user-uuid repo)
-    (swap! refresh-file-sync-component not)
-    (state/pub-event! [:graph/switch repo {:persist? false}])))
+    (swap! refresh-file-sync-component not)))
 
 (defn download-version-file
   ([graph-uuid file-uuid version-uuid]
@@ -165,15 +164,9 @@
             all-version-list))))))
 
 
-(def *wait-syncing-graph (atom nil))
-
-(defn set-wait-syncing-graph
-  [graph]
-  (reset! *wait-syncing-graph graph))
-
 (defn init-remote-graph
-  [local]
-  (when-let [graph (and local @*wait-syncing-graph)]
+  [local graph]
+  (when (and local graph)
     (notification/show!
      (str "Start syncing the remote graph "
           (:GraphName graph)

+ 6 - 3
src/main/frontend/handler/repo.cljs

@@ -525,9 +525,12 @@
 
 (defn refresh-repos!
   []
-  (p/let [repos (get-repos)]
-    (state/set-repos! repos)
-    repos))
+  (p/let [repos (get-repos)
+          repos' (combine-local-&-remote-graphs
+                  repos
+                  (state/get-remote-repos))]
+    (state/set-repos! repos')
+    repos'))
 
 (defn graph-ready!
   "Call electron that the graph is loaded."

+ 2 - 3
src/main/frontend/handler/user.cljs

@@ -7,7 +7,6 @@
             [cljs-time.core :as t]
             [cljs-time.coerce :as tc]
             [cljs-http.client :as http]
-            [lambdaisland.glogi :as log]
             [cljs.core.async :as async :refer [go go-loop <! timeout]]))
 
 (defn set-preferred-format!
@@ -68,11 +67,11 @@
 
 (defn- set-token-to-localstorage!
   ([id-token access-token]
-   (log/info :debug "set-token-to-localstorage!")
+   (prn :debug "set-token-to-localstorage!")
    (js/localStorage.setItem "id-token" id-token)
    (js/localStorage.setItem "access-token" access-token))
   ([id-token access-token refresh-token]
-   (log/info :debug "set-token-to-localstorage!")
+   (prn :debug "set-token-to-localstorage!")
    (js/localStorage.setItem "id-token" id-token)
    (js/localStorage.setItem "access-token" access-token)
    (js/localStorage.setItem "refresh-token" refresh-token)))

+ 1 - 1
src/main/frontend/handler/web/nfs.cljs

@@ -189,7 +189,7 @@
                           (p/let [files (map #(dissoc % :file/file) result)
                                   graphs-txid-meta (util-fs/read-graphs-txid-info dir-name)
                                   graph-uuid (and (vector? graphs-txid-meta) (second graphs-txid-meta))]
-                            (if-let [exists-graph (state/get-sync-graph-by-uuid graph-uuid)]
+                            (if-let [exists-graph (state/get-sync-graph-by-id graph-uuid)]
                               (state/pub-event!
                                [:notification/show
                                 {:content (str "This graph already exists in \"" (:root exists-graph) "\"")

+ 4 - 6
src/main/frontend/mobile/util.cljs

@@ -1,6 +1,7 @@
 (ns frontend.mobile.util
   (:require ["@capacitor/core" :refer [Capacitor registerPlugin]]
             ["@capacitor/splash-screen" :refer [SplashScreen]]
+            ["@logseq/capacitor-file-sync" :refer [FileSync]]
             [clojure.string :as string]
             [promesa.core :as p]))
 
@@ -24,14 +25,11 @@
 (defonce folder-picker (registerPlugin "FolderPicker"))
 (when (native-ios?)
   (defonce ios-utils (registerPlugin "Utils"))
-  (defonce ios-file-container (registerPlugin "FileContainer"))
-  (defonce file-sync (registerPlugin "FileSync")))
+  (defonce ios-file-container (registerPlugin "FileContainer")))
 
-(when (native-android?)
-  (defonce file-sync (registerPlugin "FileSync")))
-
-;; NOTE: both iOS and android share the same FsWatcher API
+;; NOTE: both iOS and android share the same API
 (when (native-platform?)
+  (defonce file-sync FileSync)
   (defonce fs-watcher (registerPlugin "FsWatcher")))
 
 (defn hide-splash []

+ 10 - 4
src/main/frontend/state.cljs

@@ -1780,8 +1780,11 @@ Similar to re-frame subscriptions"
 (defn get-file-sync-manager []
   (:file-sync/sync-manager @state))
 
-(defn get-file-sync-state [repo]
-  (get-in @state [:file-sync/sync-state repo]))
+(defn get-file-sync-state
+  ([]
+   (get-file-sync-state (get-current-repo)))
+  ([repo]
+   (get-in @state [:file-sync/sync-state repo])))
 
 (defn reset-parsing-state!
   []
@@ -1804,10 +1807,13 @@ Similar to re-frame subscriptions"
               {:is-active? is-active?
                :timestamp (inst-ms (js/Date.))}))
 
-(defn get-sync-graph-by-uuid
+(defn get-sync-graph-by-id
   [graph-uuid]
   (when graph-uuid
-    (first (filter #(= graph-uuid (:GraphUUID %))(get-repos)))))
+    (let [graph (first (filter #(= graph-uuid (:GraphUUID %))
+                               (get-repos)))]
+      (when (:url graph)
+        graph))))
 
 ;; (defn get-tldraw-api
 ;;   []

+ 2 - 2
templates/config.edn

@@ -129,12 +129,12 @@
  ;; Whether to show empty bullets for non-document mode (the default mode)
  :ui/show-empty-bullets? false
 
- ;; Pre-defined :view function to use in Query
+ ;; Pre-defined :view function to use with advanced queries
  :query/views
  {:pprint
   (fn [r] [:pre.code (pprint r)])}
 
- ;; Pre-defined :result-transform function to use in Query
+ ;; Pre-defined :result-transform function for use with advanced queries
  :query/result-transforms
  {:sort-by-priority
   (fn [result] (sort-by (fn [h] (get h :block/priority "Z")) result))}

+ 6 - 6
templates/tutorial-pl.md

@@ -3,18 +3,18 @@
 - To jest 3 minutowy samouczek jak używać Logseq. Rozpoczynajmy!
 - Masz tutaj kilka podpowiedzi, które mogą być przydatne podczas pracy z programem.
 #+BEGIN_TIP
-Kliknij aby rozpocząć edycję bloku.
+Kliknij, aby rozpocząć edycję bloku.
 Każdorazowe naciśnięcie `Enter` tworzy nowy blok.
 Zaś `Shift+Enter` tworzy nową linię w tym samym bloku.
 Napisanie `/` pokazuje wszystkie dostępne komendy.
 #+END_TIP
-- 1. Stwórzmy stronę [[Jak pisać zwykłe notatki?]]. Możesz kliknąć ten link aby otworzyć wybraną stronę, albo kliknąć z wciśniętym klawiszem `Shift` aby otworzyć go w panelu bocznym! Powinieneś widzieć teraz zarówno _Połączone referencje_ jak i _Niepołączone referencje_.
-- 2. Odwołajmy się do niektórych bloków strony [[Jak pisać zwykłe notatki?]], możesz kliknąć z `Shift` dowolne odwołanie aby otworzyć je w panelu bocznym. Spróbuj zmienić cokolwiek
+- 1. Stwórzmy stronę [[Jak pisać zwykłe notatki?]].Możesz kliknąć ten link, aby otworzyć wybraną stronę albo kliknąć z wciśniętym klawiszem `Shift`, aby otworzyć go w panelu bocznym! Powinieneś widzieć teraz zarówno _Powiązane referencje_ jak i _Niepowiązane referencje_.
+- 2. Odwołajmy się do niektórych bloków strony [[Jak pisać zwykłe notatki?]], możesz kliknąć z `Shift` dowolne odwołanie, aby otworzyć je w panelu bocznym. Spróbuj zmienić cokolwiek
 w prawym panelu. Zmiany powinny być widoczne w wybranych blokach w centralnej części edytora!
     - ((5f713e91-8a3c-4b04-a33a-c39482428e2d)) : To jest odwołanie do wybranegoo bloku.
     - ((5f713ea8-8cba-403d-ac00-9964b1ec7190)) : To jest odwołanie do innego bloku.
 - 3. Czy Logseq wspiera tagi?
-    - Oczywiście!! To jest #przyład tagu.
+    - Oczywiście! To jest #przyład tagu.
 - 4. Czy Logseq wspiera todosy np. todo/doing/done oraz priorytety?
     - Tak jest! Wciśnij `/` i wybierz ulubione słowo todo lub priorytet z listy (A/B/C).
     - NOW [#A] Zwykły samouczek nt. "Jak pisać zwykłe notatki?"
@@ -23,5 +23,5 @@ w prawym panelu. Zmiany powinny być widoczne w wybranych blokach w centralnej c
 
     - DONE Stworzyć nową stronę
     - CANCELED [#C] Stworzyć stronę z przynajmniej tysiącem innych bloków
-- To by było na tyle! Możesz dodać więcej treści do tego grafu lub otworzyć lokalny katalog aby zaimportować swoje notatki!
-- Możesz także ściągnąć naszą pulpitową aplikację na [Github](https://github.com/logseq/logseq/releases)
+- To by było na tyle! Możesz dodać więcej treści do tego grafu lub otworzyć lokalny katalog, aby zaimportować swoje notatki!
+- Możesz także ściągnąć naszą aplikację desktopową na [Github](https://github.com/logseq/logseq/releases)

+ 108 - 83
yarn.lock

@@ -216,42 +216,20 @@
     "@babel/helper-validator-identifier" "^7.18.6"
     to-fast-properties "^2.0.0"
 
-"@capacitor/[email protected]":
-  version "3.2.2"
-  resolved "https://registry.yarnpkg.com/@capacitor/android/-/android-3.2.2.tgz#0847d62d21c4e7007cf0b34fd95965afa22ba619"
-  integrity sha512-+/qbSZIHPP1VndRUe5c6dgheLV0MgX9CXeUo7el8ODHgVrC2DXKhczgvs+EpTAmvS2VZFxzDeld7AMUmHybTcg==
-
-"@capacitor/[email protected]":
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/@capacitor/app/-/app-1.0.6.tgz#8ae8271c7ee0cb8ff7578d1135846759adff153f"
-  integrity sha512-NjHIs6f4WJQuhabnCkcE6YLyIIn+t4Al5etB/SJGZJwUYRe1yJYtZ4T/KobDIzwwZn9I9de7QbEA5947lGttBQ==
+"@capacitor/android@^4.0.0":
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/@capacitor/android/-/android-4.2.0.tgz#f06539694adbaf189cf38cb039f079bde4f7422b"
+  integrity sha512-LWZhM31DoQuNSW8ZGslJ8gZfOAZS2A5TLq30HP1bn0OQTJGvOFIizQysraVRSOOq5FRptykf2nZWu6WEwoKMlA==
 
-"@capacitor/[email protected]":
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/@capacitor/camera/-/camera-1.2.1.tgz#dda900210242ed7eb732c10da482ee8ec127d1ce"
-  integrity sha512-CVT4ITwCK4AeTpNNlYFb0vuqWVD6490PWHqdaPV977clySbspDbrwPeWFKkx8emBnoNHW5urKN7tz737PD7jcg==
+"@capacitor/app@^4.0.0":
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/app/-/app-4.0.1.tgz#05c60541c427ef30f4762d8f786e70d0ed32fe01"
+  integrity sha512-frPft9TMJL70jWq5jmxwGSBddZVvsWxX5Agj2i19WJerk37aTgljB05HRr/YLg6mF1G/NIXmmFJZDY8MEgirDg==
 
-"@capacitor/[email protected]":
-  version "3.2.2"
-  resolved "https://registry.yarnpkg.com/@capacitor/cli/-/cli-3.2.2.tgz#791c04a562584820b1bf76b9b6bc4d8bcdbd6c01"
-  integrity sha512-/yradyGNGxO+Dj1ppzty3ReK+/Ukmnz0GdAE5nX3zySGSQg0ebZs81gKYDKJaXknQhx1ZZJGlT9D9ORiN3sbhw==
-  dependencies:
-    "@ionic/cli-framework-output" "^2.2.1"
-    "@ionic/utils-fs" "^3.1.5"
-    "@ionic/utils-subprocess" "^2.1.6"
-    "@ionic/utils-terminal" "^2.3.0"
-    commander "^6.0.0"
-    debug "^4.2.0"
-    env-paths "^2.2.0"
-    kleur "^4.1.1"
-    native-run "^1.4.0"
-    open "^7.1.0"
-    plist "^3.0.2"
-    prompts "^2.3.2"
-    semver "^7.3.2"
-    tar "^6.0.5"
-    tslib "^2.1.0"
-    xml2js "^0.4.23"
+"@capacitor/camera@^4.0.0":
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/camera/-/camera-4.1.1.tgz#a361b3595c378666614e3bd3384d158b85e4d7c3"
+  integrity sha512-L/1KLg4IRCAUmwhmu5jIic4U2OLAHFSW5GoExFU9yR8iCJo1SBGSiay2TpU3PpgmJHRtazO6pxKUCyAmDExkhw==
 
 "@capacitor/cli@>= 3 < 4":
   version "3.8.0"
@@ -275,17 +253,33 @@
     tslib "^2.1.0"
     xml2js "^0.4.23"
 
-"@capacitor/clipboard@^1.0.8":
-  version "1.0.8"
-  resolved "https://registry.yarnpkg.com/@capacitor/clipboard/-/clipboard-1.0.8.tgz#5799cad4518618d45501509348a5c20d3ed32647"
-  integrity sha512-q8Fb5imJHQtYA+1sGqqigCrXmf0R1ZGXf/XMRtTznQqm0jhiayUusQN63Rv1YtKIPTJeeVJqy/i6rv72d4GH0Q==
-
-"@capacitor/[email protected]":
-  version "3.2.2"
-  resolved "https://registry.yarnpkg.com/@capacitor/core/-/core-3.2.2.tgz#5926788920ba9117aa735d2941f5b2bdc4a6889a"
-  integrity sha512-xY3VyLbF0vSxQmQto6cI1PqP8idbwmfEAeaj248uuSrIYHeMc1L3RS6UN/yNBDPYPeAfNUwBhVqSTCtuZkEIcg==
+"@capacitor/cli@^4.0.0":
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/@capacitor/cli/-/cli-4.2.0.tgz#626b8ea447a210d38d77eedf3d0dc629bf19beaa"
+  integrity sha512-34L/BoDe3BmWBdrXRoF7lszT27ho6ikMdCeMePohU+cEn4ceqiURN7tnPZRyUlpc/1TO+d8d+qb1QQQSTq2oPA==
   dependencies:
-    tslib "^2.1.0"
+    "@ionic/cli-framework-output" "^2.2.5"
+    "@ionic/utils-fs" "^3.1.6"
+    "@ionic/utils-subprocess" "^2.1.11"
+    "@ionic/utils-terminal" "^2.3.3"
+    commander "^9.3.0"
+    debug "^4.3.4"
+    env-paths "^2.2.0"
+    kleur "^4.1.4"
+    native-run "^1.6.0"
+    open "^8.4.0"
+    plist "^3.0.5"
+    prompts "^2.4.2"
+    rimraf "^3.0.2"
+    semver "^7.3.7"
+    tar "^6.1.11"
+    tslib "^2.4.0"
+    xml2js "^0.4.23"
+
+"@capacitor/clipboard@^4.0.0":
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/clipboard/-/clipboard-4.0.1.tgz#b96ae5563583d12e510745fff03aa23050284948"
+  integrity sha512-DO5fC6ax5Tm/4K77NjxRLu/bdyvO6FDCK38w05CE4LHvi3RF4LTM8EgnmIrEGKxwwbH5VloTeca9Cu6bsMXPiA==
 
 "@capacitor/core@>= 3 < 4":
   version "3.8.0"
@@ -294,40 +288,47 @@
   dependencies:
     tslib "^2.1.0"
 
-"@capacitor/[email protected]":
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/@capacitor/filesystem/-/filesystem-1.0.6.tgz#b837585e6b5d48dc705ee89e49cc7c6aeb8874ec"
-  integrity sha512-8xqUbDZFGBMhgqoBSn9wEd9OBPdHIRegQ9zCCZcpHNf3FFAIby1ck+aDFnoq+Da49xhD6ks1SKCBSxz/26qWTw==
+"@capacitor/core@^4.0.0":
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/@capacitor/core/-/core-4.2.0.tgz#608285d4d8d60388940fc8f25dc1576aa99187e4"
+  integrity sha512-kHKn6693Yt9TWfuJ0Q+fyxYCpYAvVMKdu3t500seMEfdgNlF6BGaX5GbOnXkU4cnM9p+sIhRiwXv8Iqwm0E+NA==
+  dependencies:
+    tslib "^2.1.0"
 
-"@capacitor/haptics@^1.1.4":
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/@capacitor/haptics/-/haptics-1.1.4.tgz#80d5bc4776740a227a8cb83c85fac4db1316db8e"
-  integrity sha512-+pJIb5X7xAcbrWj6rJaV+cwBlv8aFwB1/Ob6EV4atydThuuVSSsAL4hI4ZYlPNOxM6H5s+ZDLj7Pa2os4eFmtg==
+"@capacitor/filesystem@^4.0.0":
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/filesystem/-/filesystem-4.1.1.tgz#8a0b4d543d131cc8a421af2d22a21f4577f09fbe"
+  integrity sha512-qpXIR4kxW2age91EiDFOh5KpZ5SUFs1rgWmNrO60wo+AT3Ok0Dzfol2aLSqvNAenDGUilA44ThJPeE+ArRPVzg==
 
-"@capacitor/[email protected]":
-  version "3.2.2"
-  resolved "https://registry.yarnpkg.com/@capacitor/ios/-/ios-3.2.2.tgz#0417cf89df23f651c6a1e20a8fb98294a918ce1f"
-  integrity sha512-Eq17Y+UDHFmYGaZcObvxHAcHw0fF9TCBAg1f5f6qdV8ab3cKKEUB9xMvoCSZAueBfxFARrD18TsZJKoxh2YsLA==
+"@capacitor/haptics@^4.0.0":
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/haptics/-/haptics-4.0.1.tgz#8113c757f9bce3cd6618f6aadda47e7aa7a92069"
+  integrity sha512-ZLVoLdK1Md/xIRRrDOnrLCKGRg8UENY1ShpKcysPN1z1MgcpvB/9Nbqczm3RH24zyo3MP/7homkqMzUGxPBlwQ==
 
-"@capacitor/keyboard@^1.2.0":
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/@capacitor/keyboard/-/keyboard-1.2.3.tgz#8046d8256e1c72ed3a2dad6ff01dc37a0c415077"
-  integrity sha512-KTSzJkMa6CrmDIDhWfswNPqS7gvYlL25E1gqXbY7w/EhyBjJhEdF0bc6QUFufS+ZVMlcPafc0/E5P+pTfLnKiA==
+"@capacitor/ios@^4.0.0":
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/@capacitor/ios/-/ios-4.2.0.tgz#718aef107044af35f574a32c17230cb638399ed6"
+  integrity sha512-YOncOJHMB1QhgmWBk3Ml8IT+KoN131zlmtCBtx9Szzc6/LvzG/NMojVqfl996k37gRw9sYHrvNP01sn22IijSA==
 
-"@capacitor/share@^1.1.2":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@capacitor/share/-/share-1.1.2.tgz#7e35c2c3bccc9955e852d3dd401afe7bd5a69b7e"
-  integrity sha512-FUTdjA7MAiD1tkGVZ+C3gs7a4fyEhXojDO2HkZ954oupG1cQ51dEJ1xTNnR9BAmCwUJO4sa91cxy7SMyCDPuGg==
+"@capacitor/keyboard@^4.0.0":
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/keyboard/-/keyboard-4.0.1.tgz#36cd5f8aa2ef87a722f318e525d1bda12e7811d1"
+  integrity sha512-JZVci2v9jAKH0sIoNNZzmw/cWGXWf+KneLt0yDr/6YSs/2/tfuH10yOrUOhgrKFkR+fFj/rddTClQXUQ8Rqcrg==
 
-"@capacitor/s[email protected]":
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/@capacitor/splash-screen/-/splash-screen-1.1.3.tgz#024bc91b2b521b9f1d5e86f16a114f294bf326fa"
-  integrity sha512-DJh1w9872UofZWAxPLgd5yWi09W0IoWPVBdVd8Ku1mrPfFvtcMeguZHGLEIo+A6DkdCTWzMrh26TLCIympws2g==
+"@capacitor/share@^4.0.0":
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/share/-/share-4.0.1.tgz#71471eba1f193e1d73c5a507cd8f4b836999f68f"
+  integrity sha512-czqYavgW+rMrlcgZkLHJ4Xwr+7E/T2YxxZ5tVtdqxANlKCw/qUL2tpNx3wphiC8lWj9wLUWFjgrBBDeZSlPYdQ==
 
-"@capacitor/[email protected]":
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/@capacitor/status-bar/-/status-bar-1.0.6.tgz#d58c9087cfc7c7377e8009bea86152e4fdd2939e"
-  integrity sha512-5MGWFq76iiKvHpbZ/Xc0Zig3WZyzWZ62wvC4qxak8OuVHBNG4fA1p/XXY9teQPaU3SupEJHnLkw6Gn1LuDp+ew==
+"@capacitor/splash-screen@^4.0.0":
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/splash-screen/-/splash-screen-4.0.1.tgz#5017a33bade5509e075f010c6381190e6ca6c5ba"
+  integrity sha512-7hklUx69aZDonuLP1R5X4ZTGgZLwX8RTj9/3U1905Kz/XflcT7Rhighbad+uZBaOU+L/8Vm6Y3RlR3rFj4ELVA==
+
+"@capacitor/status-bar@^4.0.0":
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/status-bar/-/status-bar-4.0.1.tgz#d1320add5f4ef383394b94beae45a0c5af7250e1"
+  integrity sha512-BmEyOf3m/hAf8bO4hCX0m5gpQPSgd05mGYl+9E841WUZeJjcYlmiG/EBshAUb2uGCVtyNaG36yPXB0r0Ypg+rw==
 
 "@electron/get@^1.14.1":
   version "1.14.1"
@@ -350,7 +351,7 @@
   resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.10.0.tgz#5329d6fb1b0cca068e2cd34da6d648dd6d0d3fec"
   integrity sha512-0fHc/oX394dHAT7LEacwZ0vh8aeI179plYnfaeLeRHBtHARgmtlmvxcnxd2pxJ0Z1Uj/Cy76oK9MVw/y8P1HhQ==
 
-"@ionic/cli-framework-output@^2.2.1":
+"@ionic/cli-framework-output@^2.2.1", "@ionic/cli-framework-output@^2.2.5":
   version "2.2.5"
   resolved "https://registry.yarnpkg.com/@ionic/cli-framework-output/-/cli-framework-output-2.2.5.tgz#0db9fba7efe0c27bb5085b12ee01f22053e44152"
   integrity sha512-YeDLTnTaE6V4IDUxT8GDIep0GuRIFaR7YZDLANMuuWJZDmnTku6DP+MmQoltBeLmVvz1BAAZgk41xzxdq6H2FQ==
@@ -405,7 +406,7 @@
     debug "^4.0.0"
     tslib "^2.0.1"
 
-"@ionic/utils-subprocess@^2.1.6":
+"@ionic/utils-subprocess@^2.1.11", "@ionic/utils-subprocess@^2.1.6":
   version "2.1.11"
   resolved "https://registry.yarnpkg.com/@ionic/utils-subprocess/-/utils-subprocess-2.1.11.tgz#805b86c066fe3a50f6739551ca228d52f25ffeb2"
   integrity sha512-6zCDixNmZCbMCy5np8klSxOZF85kuDyzZSTTQKQP90ZtYNCcPYmuFSzaqDwApJT4r5L3MY3JrqK1gLkc6xiUPw==
@@ -479,6 +480,11 @@
   resolved "https://registry.yarnpkg.com/@kanru/rage-wasm/-/rage-wasm-0.2.1.tgz#dd8fdd3133992c42bf68c0086d8cad40a13bc329"
   integrity sha512-sYi4F2mL6Mpcz7zbS4myasw11xLBEbgZkDMRVg9jNxTKt6Ct/LT7/vCHDmEzAFcPcPqixD5De6Ql3bJijAX0/w==
 
+"@logseq/[email protected]":
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/@logseq/capacitor-file-sync/-/capacitor-file-sync-0.0.6.tgz#e738ec08c1249ad96560c5f5090a60f4f49d679a"
+  integrity sha512-gnSAm2x9jNAtlQP4r8S5P2a7vRYZkGDIh6wX1/UgldDHPaY//+SWYvtFQQeDEJ1rLU/S+UYnPndlQdMDpu6mqA==
+
 "@logseq/[email protected]":
   version "1.3.1-1"
   resolved "https://registry.yarnpkg.com/@logseq/react-tweet-embed/-/react-tweet-embed-1.3.1-1.tgz#119d22be8234de006fc35c3fa2a36f85634c5be6"
@@ -1915,6 +1921,11 @@ commander@^8.0.0:
   resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
   integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
 
+commander@^9.3.0:
+  version "9.4.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.0.tgz#bc4a40918fefe52e22450c111ecd6b7acce6f11c"
+  integrity sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==
+
 component-emitter@^1.2.1:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
@@ -2267,6 +2278,11 @@ defer-to-connect@^1.0.1:
   resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
   integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==
 
+define-lazy-prop@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
+  integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
+
 define-properties@^1.1.3, define-properties@^1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1"
@@ -3863,7 +3879,7 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2:
     is-data-descriptor "^1.0.0"
     kind-of "^6.0.2"
 
-is-docker@^2.0.0:
+is-docker@^2.0.0, is-docker@^2.1.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
   integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
@@ -4084,7 +4100,7 @@ is-windows@^1.0.1, is-windows@^1.0.2:
   resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
   integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
 
-is-wsl@^2.1.1:
+is-wsl@^2.1.1, is-wsl@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
   integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
@@ -4230,7 +4246,7 @@ kleur@^3.0.3:
   resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
   integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
 
-kleur@^4.1.1:
+kleur@^4.1.1, kleur@^4.1.4:
   version "4.1.5"
   resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
   integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
@@ -4750,7 +4766,7 @@ nanomatch@^1.2.9:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
-native-run@^1.4.0, native-run@^1.5.0:
+native-run@^1.5.0, native-run@^1.6.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/native-run/-/native-run-1.7.0.tgz#01a975e0b8cdd55bc82d5c21a28f0ea8972283a8"
   integrity sha512-tkd0E9hwJcxd9xbpC1Z4KED3bK+bavQJ7p4BRMHtlikrvJX+OwHm7/qwDLzQ4emDEMQUG9rskRdJfo1UoGditA==
@@ -5020,7 +5036,7 @@ once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0:
   dependencies:
     wrappy "1"
 
-open@^7.1.0, open@^7.4.2:
+open@^7.4.2:
   version "7.4.2"
   resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
   integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
@@ -5028,6 +5044,15 @@ open@^7.1.0, open@^7.4.2:
     is-docker "^2.0.0"
     is-wsl "^2.1.1"
 
+open@^8.4.0:
+  version "8.4.0"
+  resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8"
+  integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==
+  dependencies:
+    define-lazy-prop "^2.0.0"
+    is-docker "^2.1.1"
+    is-wsl "^2.2.0"
+
 ordered-read-streams@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e"
@@ -5424,7 +5449,7 @@ [email protected]:
   dependencies:
     playwright-core "1.25.2"
 
-plist@^3.0.2, plist@^3.0.6:
+plist@^3.0.2, plist@^3.0.5, plist@^3.0.6:
   version "3.0.6"
   resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.6.tgz#7cfb68a856a7834bca6dbfe3218eb9c7740145d3"
   integrity sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==
@@ -5847,7 +5872,7 @@ promise-polyfill@^8.2.0:
   resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.3.tgz#2edc7e4b81aff781c88a0d577e5fe9da822107c6"
   integrity sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==
 
-prompts@^2.3.2:
+prompts@^2.3.2, prompts@^2.4.2:
   version "2.4.2"
   resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
   integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==
@@ -6531,7 +6556,7 @@ semver@^6.2.0, semver@^6.3.0:
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
 
-semver@^7.3.2, semver@^7.3.4:
+semver@^7.3.2, semver@^7.3.4, semver@^7.3.7:
   version "7.3.7"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
   integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
@@ -7174,7 +7199,7 @@ [email protected]:
     resolve "^1.20.0"
     tmp "^0.2.1"
 
-tar@^6.0.5, tar@^6.1.11:
+tar@^6.1.11:
   version "6.1.11"
   resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
   integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==