Merge branch 'master' into enhance/mobile-ux-2

This commit is contained in:
charlie
2022-11-29 11:40:25 +08:00
12 changed files with 234 additions and 206 deletions

View File

@@ -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;
}
}
}

View File

@@ -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).

View File

@@ -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)]

View File

@@ -159,10 +159,6 @@
.title-wrap {
flex: 1;
}
.menu-link {
@apply px-2;
}
}
&.is-enabled-progress-pane {

View File

@@ -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)

View File

@@ -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"

View File

@@ -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)

View File

@@ -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 []

View File

@@ -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)

View File

@@ -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])

View File

@@ -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}
/>
))}

View File

@@ -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