mirror of
https://github.com/logseq/logseq.git
synced 2026-05-29 06:59:36 +00:00
Merge branch 'master' into enhance/mobile-ux-2
This commit is contained in:
@@ -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<SingleFileObserver> 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<String, SimpleFileMetadata> 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<String, SimpleFileMetadata> newMetaDb = new HashMap();
|
||||
|
||||
Stack<String> 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<String, SimpleFileMetadata> newMetaDb) {
|
||||
for (Map.Entry<String, SimpleFileMetadata> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -159,10 +159,6 @@
|
||||
.title-wrap {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menu-link {
|
||||
@apply px-2;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-enabled-progress-pane {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
(async/go (async/<! (p->c (persist-var/load-vars)))
|
||||
(async/<! (sync/<sync-stop))))
|
||||
|
||||
(defmethod handle :user/login [[_]]
|
||||
(defmethod handle :user/fetch-info-and-graphs [[_]]
|
||||
(state/set-state! [:ui/loading? :login] false)
|
||||
(async/go
|
||||
(let [result (async/<! (sync/<user-info sync/remoteapi))]
|
||||
@@ -353,7 +353,8 @@
|
||||
(state/set-modal! #(git-component/file-specific-version path hash content)))
|
||||
|
||||
;; Hook on a graph is ready to be shown to the user.
|
||||
;; It's different from :graph/resotred, as :graph/restored is for window reloaded
|
||||
;; It's different from :graph/restored, as :graph/restored is for window reloaded
|
||||
;; FIXME: config may not be loaded when the graph is ready.
|
||||
(defmethod handle :graph/ready
|
||||
[[_ repo]]
|
||||
(when (config/local-db? repo)
|
||||
@@ -363,14 +364,15 @@
|
||||
(state/pub-event! [:graph/dir-gone dir]))))
|
||||
;; FIXME: an ugly implementation for redirecting to page on new window is restored
|
||||
(repo-handler/graph-ready! repo)
|
||||
(js/setTimeout
|
||||
(fn []
|
||||
(let [filename-format (state/get-filename-format repo)]
|
||||
(when (and (util/electron?)
|
||||
(not (config/demo-graph?))
|
||||
(not= filename-format :triple-lowbar))
|
||||
(state/pub-event! [:ui/notify-outdated-filename-format []]))))
|
||||
3000))
|
||||
(when-not config/test?
|
||||
(js/setTimeout
|
||||
(fn []
|
||||
(let [filename-format (state/get-filename-format repo)]
|
||||
(when (and (util/electron?)
|
||||
(not (config/demo-graph?))
|
||||
(not= filename-format :triple-lowbar))
|
||||
(state/pub-event! [:ui/notify-outdated-filename-format []]))))
|
||||
3000)))
|
||||
|
||||
(defmethod handle :notification/show [[_ {:keys [content status clear?]}]]
|
||||
(notification/show! content status clear?))
|
||||
@@ -769,13 +771,13 @@
|
||||
"We suggest you upgrade now to avoid potential bugs."]
|
||||
(when (seq paths)
|
||||
[:p
|
||||
"For example, the files below have reserved characters that can't be synced on some platforms."])]
|
||||
]
|
||||
"For example, the files below have reserved characters that can't be synced on some platforms."])]]
|
||||
(ui/button
|
||||
"Update filename format"
|
||||
:on-click (fn []
|
||||
(notification/clear-all!)
|
||||
(state/set-modal!
|
||||
"Update filename format"
|
||||
:aria-label "Update filename format"
|
||||
:on-click (fn []
|
||||
(notification/clear-all!)
|
||||
(state/set-modal!
|
||||
(fn [_] (conversion-component/files-breaking-changed))
|
||||
{:id :filename-format-panel :center? true})))
|
||||
(when (seq paths)
|
||||
|
||||
@@ -133,24 +133,15 @@
|
||||
(set-tokens! (:id_token (:body resp)) (:access_token (:body resp)))))))))
|
||||
|
||||
(defn restore-tokens-from-localstorage
|
||||
"Restore id-token, access-token, refresh-token from localstorage,
|
||||
and refresh id-token&access-token if necessary.
|
||||
return nil when tokens are not available."
|
||||
"Refresh id-token&access-token, pull latest repos, returns nil when tokens are not available."
|
||||
[]
|
||||
(println "restore-tokens-from-localstorage")
|
||||
(let [id-token (js/localStorage.getItem "id-token")
|
||||
access-token (js/localStorage.getItem "access-token")
|
||||
refresh-token (js/localStorage.getItem "refresh-token")]
|
||||
(let [refresh-token (js/localStorage.getItem "refresh-token")]
|
||||
(when refresh-token
|
||||
(set-tokens! id-token access-token refresh-token)
|
||||
(when (or (nil? id-token) (nil? access-token)
|
||||
(-> id-token parse-jwt almost-expired?)
|
||||
(-> access-token parse-jwt almost-expired?))
|
||||
(go
|
||||
;; id-token or access-token expired
|
||||
(<! (<refresh-id-token&access-token))
|
||||
;; refresh remote graph list by pub login event
|
||||
(when (user-uuid) (state/pub-event! [:user/login])))))))
|
||||
(go
|
||||
(<! (<refresh-id-token&access-token))
|
||||
;; refresh remote graph list by pub login event
|
||||
(when (user-uuid) (state/pub-event! [:user/fetch-info-and-graphs]))))))
|
||||
|
||||
(defn login-callback [code]
|
||||
(state/set-state! [:ui/loading? :login] true)
|
||||
@@ -161,7 +152,7 @@
|
||||
(-> 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 []
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const _ContextBar: TLContextBarComponent<Shape> = ({ 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])
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ export const Canvas = observer(function Renderer<S extends TLReactShape>({
|
||||
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 (
|
||||
<div ref={rContainer} className={`tl-container ${className ?? ''}`}>
|
||||
@@ -138,7 +139,7 @@ export const Canvas = observer(function Renderer<S extends TLReactShape>({
|
||||
isSelected={selectedShapesSet.has(shape)}
|
||||
isErasing={erasingShapesSet.has(shape)}
|
||||
meta={meta}
|
||||
zIndex={1000 + i}
|
||||
zIndex={selectedOrHoveredShape === shape ? 10000 : 1000 + i}
|
||||
onEditingEnd={onEditingEnd}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user