Browse Source

Merge remote-tracking branch 'upstream/master' into whiteboards

Peng Xiao 3 years ago
parent
commit
d59e7fdbe8
52 changed files with 283 additions and 2124 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. 16 15
      package.json
  40. 36 40
      src/main/frontend/components/file_sync.cljs
  41. 2 1
      src/main/frontend/components/repo.cljs
  42. 4 2
      src/main/frontend/components/sidebar.css
  43. 4 4
      src/main/frontend/fs/sync.cljs
  44. 9 4
      src/main/frontend/handler/events.cljs
  45. 3 10
      src/main/frontend/handler/file_sync.cljs
  46. 6 3
      src/main/frontend/handler/repo.cljs
  47. 2 3
      src/main/frontend/handler/user.cljs
  48. 1 1
      src/main/frontend/handler/web/nfs.cljs
  49. 4 6
      src/main/frontend/mobile/util.cljs
  50. 10 4
      src/main/frontend/state.cljs
  51. 2 2
      templates/config.edn
  52. 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

+ 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.5",
         "@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

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

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

+ 4 - 4
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]
@@ -2779,7 +2780,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))}

+ 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.5"
+  resolved "https://registry.yarnpkg.com/@logseq/capacitor-file-sync/-/capacitor-file-sync-0.0.5.tgz#e391d3ec9eb65d200fa5af18738913d19a223f39"
+  integrity sha512-3cdpwt5lsEE7occQwJKaalaKGXxgucSDzFNeRkRQMylRehlZskAQtCjgDFR7Wt3tBQZdLZmjpgj7ioYQesWbTA==
+
 "@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==