diff --git a/android/app/src/main/java/com/logseq/app/FsWatcher.java b/android/app/src/main/java/com/logseq/app/FsWatcher.java index 2fc8872da1..839e613883 100644 --- a/android/app/src/main/java/com/logseq/app/FsWatcher.java +++ b/android/app/src/main/java/com/logseq/app/FsWatcher.java @@ -1,7 +1,5 @@ package com.logseq.app; -import android.annotation.SuppressLint; -import android.os.Build; import android.system.ErrnoException; import android.system.Os; import android.system.StructStat; @@ -12,8 +10,9 @@ import android.net.Uri; import java.io.*; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; import java.util.regex.Pattern; import java.io.File; @@ -26,9 +25,9 @@ import com.getcapacitor.PluginCall; @CapacitorPlugin(name = "FsWatcher") public class FsWatcher extends Plugin { - - List observers; private String mPath; + private PollingFsWatcher mWatcher; + private Thread mThread; @Override public void load() { @@ -37,14 +36,11 @@ public class FsWatcher extends Plugin { @PluginMethod() public void watch(PluginCall call) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - call.reject("Android version not supported"); - return; - } String pathParam = call.getString("path"); // check file:// or no scheme uris Uri u = Uri.parse(pathParam); Log.i("FsWatcher", "watching " + u); + // TODO: handle context:// uri if (u.getScheme() == null || u.getScheme().equals("file")) { File pathObj; try { @@ -56,32 +52,15 @@ public class FsWatcher extends Plugin { mPath = pathObj.getAbsolutePath(); - int mask = FileObserver.CLOSE_WRITE | - FileObserver.MOVE_SELF | FileObserver.MOVED_FROM | FileObserver.MOVED_TO | - FileObserver.DELETE | FileObserver.DELETE_SELF | FileObserver.CREATE; - - if (observers != null) { + if (mWatcher != null) { call.reject("already watching"); return; } - observers = new ArrayList<>(); - observers.add(new SingleFileObserver(pathObj, mask)); - // NOTE: only watch first level of directory - File[] files = pathObj.listFiles(); - if (files != null) { - for (File file : files) { - String filename = file.getName(); - if (file.isDirectory() && !filename.startsWith(".") && !filename.equals("bak") && !filename.equals("version-files") && !filename.equals("node_modules")) { - observers.add(new SingleFileObserver(file, mask)); - } - } - } + mWatcher = new PollingFsWatcher(mPath); + mThread = new Thread(mWatcher); + mThread.start(); - this.initialNotify(pathObj); - - for (int i = 0; i < observers.size(); i++) - observers.get(i).startWatching(); call.resolve(); } else { call.reject(u.getScheme() + " scheme not supported"); @@ -92,77 +71,69 @@ public class FsWatcher extends Plugin { public void unwatch(PluginCall call) { Log.i("FsWatcher", "unwatch all..."); - if (observers != null) { - for (int i = 0; i < observers.size(); ++i) - observers.get(i).stopWatching(); - observers.clear(); - observers = null; + if (mWatcher != null) { + mThread.interrupt(); + mWatcher = null; } call.resolve(); } - public void initialNotify(File pathObj) { - this.initialNotify(pathObj, 2); - } - - public void initialNotify(File pathObj, int maxDepth) { - if (maxDepth == 0) { - return; - } - File[] files = pathObj.listFiles(); - if (files != null) { - for (File file : files) { - String filename = file.getName(); - if (file.isDirectory() && !filename.startsWith(".") && !filename.equals("bak") && !filename.equals("version-files") && !filename.equals("node_modules")) { - this.initialNotify(file, maxDepth - 1); - } else if (file.isFile() - && Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", - file.getName())) { - this.onObserverEvent(FileObserver.CREATE, file.getAbsolutePath()); - } - } - } - } - // add, change, unlink events - public void onObserverEvent(int event, String path) { + public void onObserverEvent(int event, String path, SimpleFileMetadata metadata) { JSObject obj = new JSObject(); String content = null; File f = new File(path); + + boolean shouldRead = false; + if (Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown|excalidraw)$", f.getName())) { + shouldRead = true; + } + obj.put("path", Uri.fromFile(f)); obj.put("dir", Uri.fromFile(new File(mPath))); + JSObject stat; switch (event) { - case FileObserver.CLOSE_WRITE: + case FileObserver.MODIFY: obj.put("event", "change"); - try { - obj.put("stat", getFileStat(path)); - content = getFileContents(f); - } catch (IOException | ErrnoException e) { - e.printStackTrace(); + stat = new JSObject(); + stat.put("mtime", metadata.mtime); + stat.put("ctime", metadata.ctime); + stat.put("size", metadata.size); + obj.put("stat", stat); + if (shouldRead) { + try { + content = getFileContents(f); + } catch (IOException e) { + Log.e("FsWatcher", "error reading modified file"); + e.printStackTrace(); + } } + Log.i("FsWatcher", "prepare event " + obj); obj.put("content", content); break; - case FileObserver.MOVED_TO: case FileObserver.CREATE: obj.put("event", "add"); - try { - obj.put("stat", getFileStat(path)); - content = getFileContents(f); - } catch (IOException | ErrnoException e) { - e.printStackTrace(); + stat = new JSObject(); + stat.put("mtime", metadata.mtime); + stat.put("ctime", metadata.ctime); + stat.put("size", metadata.size); + obj.put("stat", stat); + if (shouldRead) { + try { + content = getFileContents(f); + } catch (IOException e) { + Log.e("FsWatcher", "error reading new file"); + e.printStackTrace(); + } } - Log.i("FsWatcher", "prepare event " + obj); obj.put("content", content); break; - case FileObserver.MOVE_SELF: - case FileObserver.MOVED_FROM: case FileObserver.DELETE: - case FileObserver.DELETE_SELF: if (f.exists()) { - Log.i("FsWatcher", "abandon notification due to file exists"); + Log.i("FsWatcher", "abandon delete notification due to file exists"); return; } else { obj.put("event", "unlink"); @@ -193,59 +164,120 @@ public class FsWatcher extends Plugin { return outputStream.toString("utf-8"); } - public static JSObject getFileStat(final String path) throws ErrnoException { - File file = new File(path); - StructStat stat = Os.stat(path); - JSObject obj = new JSObject(); - obj.put("atime", stat.st_atime); - obj.put("mtime", stat.st_mtime); - obj.put("ctime", stat.st_ctime); - obj.put("size", file.length()); - return obj; - } + public class SimpleFileMetadata { + public long mtime; + public long ctime; + public long size; + public long ino; - private class SingleFileObserver extends FileObserver { - private final String mPath; - - public SingleFileObserver(String path, int mask) { - super(path, mask); - mPath = path; + public SimpleFileMetadata(File file) throws ErrnoException { + StructStat stat = Os.stat(file.getPath()); + mtime = stat.st_mtime; + ctime = stat.st_ctime; + size = stat.st_size; + ino = stat.st_ino; } - @SuppressLint("NewApi") - public SingleFileObserver(File path, int mask) { - super(path, mask); - mPath = path.getAbsolutePath(); + public boolean equals(SimpleFileMetadata other) { + return mtime == other.mtime && ctime == other.ctime && size == other.size && ino == other.ino; + } + } + + + public class PollingFsWatcher implements Runnable { + private String mPath; + private Map metaDb; + + public PollingFsWatcher(String path) { + metaDb = new HashMap(); + + File dir = new File(path); + try { + mPath = dir.getCanonicalPath(); + } catch (IOException e) { + e.printStackTrace(); + } } @Override - public void onEvent(int event, String path) { - if (path != null && !path.equals("graphs-txid.edn") && !path.equals("broken-config.edn")) { - Log.d("FsWatcher", "got path=" + mPath + "/" + path + " event=" + event); - // TODO: handle newly created directory - if (Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", path)) { - String fullPath = mPath + "/" + path; - if (event == FileObserver.MOVE_SELF || event == FileObserver.MOVED_FROM || - event == FileObserver.DELETE || event == FileObserver.DELETE_SELF) { - Log.d("FsWatcher", "defer delete notification for " + path); - Thread timer = new Thread() { - @Override - public void run() { - try { - // delay 500ms then send, enough for most syncing net disks - Thread.sleep(500); - FsWatcher.this.onObserverEvent(event, fullPath); - } catch (InterruptedException e) { - e.printStackTrace(); - } + public void run() { + while (!Thread.currentThread().isInterrupted()) { + try { + this.tick(); + Thread.sleep(2000); // The same as iOS fswatcher, 2s interval + } catch (InterruptedException e) { + // e.printStackTrace(); + Log.i("FsWatcher", "interrupted, unwatch"); + break; + } + } + + } + + private void tick() { + Map newMetaDb = new HashMap(); + + Stack paths = new Stack(); + paths.push(mPath); + while (!paths.isEmpty()) { + String dir = paths.pop(); + File curr = new File(dir); + + File[] files = curr.listFiles(); + if (files != null) { + for (File file : files) { + String filename = file.getName(); + if (file.isDirectory()) { + if (!filename.startsWith(".") && !filename.equals("bak") && !filename.equals("version-files") && !filename.equals("node_modules")) { + paths.push(file.getAbsolutePath()); } - }; - timer.start(); - } else { - FsWatcher.this.onObserverEvent(event, fullPath); + } else if (file.isFile() && !filename.equals("graphs-txid.edn") && !filename.equals("broken-config.edn")) { + try { + SimpleFileMetadata metadata = new SimpleFileMetadata(file); + newMetaDb.put(file.getAbsolutePath(), metadata); + } catch (ErrnoException e) { + } + } } } } + this.updateMetaDb(newMetaDb); + } + + private void updateMetaDb(Map newMetaDb) { + for (Map.Entry entry : newMetaDb.entrySet()) { + String path = entry.getKey(); + SimpleFileMetadata newMeta = entry.getValue(); + SimpleFileMetadata oldMeta = metaDb.remove(path); + if (oldMeta == null) { + // new file + onObserverEvent(FileObserver.CREATE, path, newMeta); + Log.d("FsWatcher", "create " + path); + } else if (!oldMeta.equals(newMeta)) { + // file changed + onObserverEvent(FileObserver.MODIFY, path, newMeta); + Log.d("FsWatcher", "changed " + path); + } + } + for (String path : metaDb.keySet()) { + // file deleted + Thread timer = new Thread() { + @Override + public void run() { + try { + // delay 500ms then send, enough for most syncing net disks + Thread.sleep(500); + onObserverEvent(FileObserver.DELETE, path, null); + Log.d("FsWatcher", "deleted " + path); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + timer.start(); + } + + this.metaDb = newMetaDb; } } } diff --git a/shadow-cljs.edn b/shadow-cljs.edn index 5d191e6cea..b93911f5e4 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -35,7 +35,8 @@ :redef false}} :closure-defines {goog.debug.LOGGING_ENABLED true frontend.config/ENABLE-PLUGINS #shadow/env ["ENABLE_PLUGINS" :as :bool :default true] - frontend.config/ENABLE-FILE-SYNC-PRODUCTION #shadow/env ["ENABLE_FILE_SYNC_PRODUCTION" :as :bool :default true]} + frontend.config/ENABLE-FILE-SYNC-PRODUCTION #shadow/env ["ENABLE_FILE_SYNC_PRODUCTION" :as :bool :default true] + frontend.config/TEST #shadow/env ["CI" :as :bool :default false]} ;; NOTE: electron, browser/mobile-app use different asset-paths. ;; For browser/mobile-app devs, assets are located in /static/js(via HTTP root). diff --git a/src/main/frontend/components/file_sync.cljs b/src/main/frontend/components/file_sync.cljs index be0dc3535b..1e96da99de 100644 --- a/src/main/frontend/components/file_sync.cljs +++ b/src/main/frontend/components/file_sync.cljs @@ -264,10 +264,13 @@ (storage/set :ui/file-sync-active-file-list? list-active?))) [list-active?]) - [:div.cp__file-sync-indicator-progress-pane - {:ref *el-ref - :class (when (and syncing? progressing?) "is-progress-active")} - (let [idle-&-no-active? (and idle? no-active-files?)] + (let [idle-&-no-active? (and idle? no-active-files?) + waiting? (not (or (not online?) + idle-&-no-active? + syncing?))] + [:div.cp__file-sync-indicator-progress-pane + {:ref *el-ref + :class (when (and syncing? progressing?) "is-progress-active")} [:div.a [:div.al [:strong @@ -285,30 +288,31 @@ :else "Waiting..." )]] [:div.ar - (when queuing? (sync-now))]]) + (when queuing? (sync-now))]] - [:div.b.dark:text-gray-200 - [:div.bl - [:span.flex.items-center - (if no-active-files? - [:span.opacity-100.pr-1 "Successfully processed"] - [:span.opacity-60.pr-1 "Processed"])] + (when-not waiting? + [:div.b.dark:text-gray-200 + [:div.bl + [:span.flex.items-center + (if no-active-files? + [:span.opacity-100.pr-1 "Successfully processed"] + [:span.opacity-60.pr-1 "Processed"])] - (first tip-b&p)] + (first tip-b&p)] - [:div.br - [:small.opacity-50 - (when syncing? - (calc-time-left))]]] + [:div.br + [:small.opacity-50 + (when syncing? + (calc-time-left))]]]) - [:div.c - (second tip-b&p) - (when (or history-files? (not no-active-files?)) - [:span.inline-flex.ml-1.active:opacity-50 - {:on-click #(set-list-active? (not list-active?))} - (if list-active? - (ui/icon "chevron-up" {:style {:font-size 24}}) - (ui/icon "chevron-left" {:style {:font-size 24}}))])]])) + [:div.c + (second tip-b&p) + (when (or history-files? (not no-active-files?)) + [:span.inline-flex.ml-1.active:opacity-50 + {:on-click #(set-list-active? (not list-active?))} + (if list-active? + (ui/icon "chevron-up" {:style {:font-size 24}}) + (ui/icon "chevron-left" {:style {:font-size 24}}))])]]))) (defn- sort-files [files] @@ -409,7 +413,6 @@ (str "status-of-" (and (keyword? status) (name status)))])} (when (and (not config/publishing?) (user-handler/logged-in?)) - (ui/dropdown-with-links ;; trigger (fn [{:keys [toggle-fn]}] @@ -428,21 +431,21 @@ (ui/icon "cloud-off" {:size ui/icon-size})])) ;; links - (cond-> [] + (cond-> (vec + (when-not (and no-active-files? idle?) + (cond + need-password? + [{:title [:div.file-item.flex.items-center.leading-none.pt-3 + {:style {:margin-left -8}} + (ui/icon "lock" {:size 20}) [:span.pl-1.font-semibold "Password is required"]] + :options {:on-click fs-sync/sync-need-password!}}] + + ;; head of upcoming sync + (not no-active-files?) + [{:title [:div.file-item.is-first ""] + :options {:class "is-first-placeholder"}}]))) synced-file-graph? (concat - (when-not (and no-active-files? idle?) - (cond - need-password? - [{:title [:div.file-item.flex.items-center.leading-none.pt-3 - (ui/icon "lock" {:size 20}) [:span.pl-1.font-semibold "Password is required"]] - :options {:on-click fs-sync/sync-need-password!}}] - - ;; head of upcoming sync - (not no-active-files?) - [{:title [:div.file-item.is-first ""] - :options {:class "is-first-placeholder"}}])) - (map (fn [f] {:title [:div.file-item {:key (str "downloading-" f)} (gp-util/safe-decode-uri-component f)] diff --git a/src/main/frontend/components/file_sync.css b/src/main/frontend/components/file_sync.css index 965b44cbf8..82376190b0 100644 --- a/src/main/frontend/components/file_sync.css +++ b/src/main/frontend/components/file_sync.css @@ -159,10 +159,6 @@ .title-wrap { flex: 1; } - - .menu-link { - @apply px-2; - } } &.is-enabled-progress-pane { diff --git a/src/main/frontend/config.cljs b/src/main/frontend/config.cljs index b8828f50e8..ed94c5ff07 100644 --- a/src/main/frontend/config.cljs +++ b/src/main/frontend/config.cljs @@ -18,7 +18,8 @@ (reset! state/publishing? publishing?) -(def test? false) +(goog-define TEST false) +(def test? TEST) (goog-define ENABLE-FILE-SYNC-PRODUCTION false) diff --git a/src/main/frontend/dicts.cljc b/src/main/frontend/dicts.cljc index ccb651fb6a..5f9dffeba4 100644 --- a/src/main/frontend/dicts.cljc +++ b/src/main/frontend/dicts.cljc @@ -1300,7 +1300,7 @@ :left-side-bar/new-whiteboard "Nouveau tableau blanc" :linked-references/filter-search "Rechercher dans les pages liées" :on-boarding/add-graph "Ajouter un graphe" - :on-boarding/demo-graph "Il s'agit d'un graphe de démo, les changements ne seront pas enregistrés à moins que vous n'ouvrir un dossier local." + :on-boarding/demo-graph "Il s'agit d'un graphe de démo, les changements ne seront pas enregistrés à moins que vous n'ouvriez un dossier local." :on-boarding/new-graph-desc-1 "Logseq supporte à la fois le Markdown et l'Org-mode. Vous pouvez ouvrir un dossier existant ou en créer un nouveau sur cet appareil. Vos données seront enregistrées uniquement sur cet appareil." :on-boarding/new-graph-desc-2 "Après avoir ouvert votre dossier, cela créera 3 sous-dossiers :" :on-boarding/new-graph-desc-3 "/journals - contient vos pages du journal" @@ -1408,8 +1408,8 @@ :settings-page/enable-tooltip "Astuces" :settings-page/enable-whiteboards "Tableaux blancs" :settings-page/export-theme "Exporter le theme" - :settings-page/filename-format "Format de nm de fichier" - :settings-page/git-commit-delay "Délai (secondes) des commit Git automatiques" + :settings-page/filename-format "Format de nom de fichier" + :settings-page/git-commit-delay "Délai (secondes) des commits Git automatiques" :settings-page/git-confirm "Vous devez redémarrer l'application après avoir mis à jour le dossier Git" :settings-page/git-desc "est utilisé pour gérer les versions de pages, vous pouvez cliquer sur..." :settings-page/git-switcher-label "Activer les commits Git automatiques" @@ -1420,9 +1420,9 @@ :settings-page/preferred-outdenting "Mise en retrait logique" :settings-page/shortcut-settings "Personnaliser les raccourcis" :settings-page/show-brackets "Montrer les parenthèses, crochets et accolades" - :settings-page/spell-checker "Vérification autographique" + :settings-page/spell-checker "Vérification orthographique" :settings-page/sync "Synchronisation" - :settings-page/tab-advanced "Advancé" + :settings-page/tab-advanced "Avancé" :settings-page/tab-assets "Pièces-jointes" :settings-page/tab-editor "Éditeur" :settings-page/tab-features "Fonctionnalités" diff --git a/src/main/frontend/handler/events.cljs b/src/main/frontend/handler/events.cljs index e7cf1d0375..68e0998b02 100644 --- a/src/main/frontend/handler/events.cljs +++ b/src/main/frontend/handler/events.cljs @@ -76,7 +76,7 @@ (async/go (async/c (persist-var/load-vars))) (async/ id-token parse-jwt almost-expired?) - (-> access-token parse-jwt almost-expired?)) - (go - ;; id-token or access-token expired - ( resp :body (as-> $ (set-tokens! (:id_token $) (:access_token $) (:refresh_token $))) - (#(state/pub-event! [:user/login]))) + (#(state/pub-event! [:user/fetch-info-and-graphs]))) (debug/pprint "login-callback" resp))))) (defn logout [] diff --git a/src/main/frontend/ui.cljs b/src/main/frontend/ui.cljs index 55a0834785..07d80a4581 100644 --- a/src/main/frontend/ui.cljs +++ b/src/main/frontend/ui.cljs @@ -250,10 +250,11 @@ content]] [:div.ml-4.flex-shrink-0.flex [:button.inline-flex.text-gray-400.focus:outline-none.focus:text-gray-500.transition.ease-in-out.duration-150.notification-close-button - {:on-click (fn [] + {:aria-label "Close" + :on-click (fn [] (notification/clear! uid))} - (icon "x" {:fill "currentColor"})]]]]]]]))) + (icon "x" {:fill "currentColor"})]]]]]]]))) (declare button) diff --git a/tldraw/apps/tldraw-logseq/src/components/ContextBar/ContextBar.tsx b/tldraw/apps/tldraw-logseq/src/components/ContextBar/ContextBar.tsx index 7326bebb07..e75b1855f0 100644 --- a/tldraw/apps/tldraw-logseq/src/components/ContextBar/ContextBar.tsx +++ b/tldraw/apps/tldraw-logseq/src/components/ContextBar/ContextBar.tsx @@ -29,7 +29,7 @@ const _ContextBar: TLContextBarComponent = ({ shapes, offsets, hidden }) const elm = rContextBar.current if (!elm) return const size = rSize.current ?? [0, 0] - const [x, y] = getContextBarTranslation(size, { ...offsets, bottom: offsets.bottom - 32 }) + const [x, y] = getContextBarTranslation(size, offsets) elm.style.setProperty('transform', `translateX(${x}px) translateY(${y}px)`) }, [offsets]) diff --git a/tldraw/packages/react/src/components/Canvas/Canvas.tsx b/tldraw/packages/react/src/components/Canvas/Canvas.tsx index c4edb062fd..352aee4681 100644 --- a/tldraw/packages/react/src/components/Canvas/Canvas.tsx +++ b/tldraw/packages/react/src/components/Canvas/Canvas.tsx @@ -110,6 +110,7 @@ export const Canvas = observer(function Renderer({ const selectedShapesSet = React.useMemo(() => new Set(selectedShapes || []), [selectedShapes]) const erasingShapesSet = React.useMemo(() => new Set(erasingShapes || []), [erasingShapes]) const singleSelectedShape = selectedShapes?.length === 1 ? selectedShapes[0] : undefined + const selectedOrHoveredShape = hoveredShape || singleSelectedShape return (
@@ -138,7 +139,7 @@ export const Canvas = observer(function Renderer({ isSelected={selectedShapesSet.has(shape)} isErasing={erasingShapesSet.has(shape)} meta={meta} - zIndex={1000 + i} + zIndex={selectedOrHoveredShape === shape ? 10000 : 1000 + i} onEditingEnd={onEditingEnd} /> ))} diff --git a/tldraw/packages/react/src/index.ts b/tldraw/packages/react/src/index.ts index 4095c63638..e29ea4b01e 100644 --- a/tldraw/packages/react/src/index.ts +++ b/tldraw/packages/react/src/index.ts @@ -14,7 +14,7 @@ export function getContextBarTranslation(barSize: number[], offset: TLOffset) { let y = 0 if (offset.top < 116) { // Show on bottom - y = offset.height / 2 + 72 + y = offset.height / 2 + 40 // Too far down, move up if (offset.bottom < 140) { y += offset.bottom - 140