mirror of
https://github.com/logseq/logseq.git
synced 2026-05-29 15:09:41 +00:00
refactor: simplify navigation handling and improve event logging
This commit is contained in:
@@ -7,7 +7,6 @@ import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebView
|
||||
import android.widget.FrameLayout
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
@@ -34,7 +33,6 @@ import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -55,14 +53,14 @@ object ComposeHost {
|
||||
fun applyNavigation(navigationType: String?, path: String?) {
|
||||
val type = (navigationType ?: "push").lowercase()
|
||||
val safePath = path?.takeIf { it.isNotBlank() } ?: "/"
|
||||
navEvents.tryEmit(NavigationEvent(type, safePath))
|
||||
if (!navEvents.tryEmit(NavigationEvent(type, safePath))) {
|
||||
Log.w("ComposeHost", "Dropped navigation event: type=$type path=$safePath")
|
||||
}
|
||||
}
|
||||
|
||||
fun renderWithSystemInsets(
|
||||
activity: Activity,
|
||||
webView: WebView,
|
||||
onBackRequested: () -> Unit,
|
||||
onExit: () -> Unit = { activity.finish() }
|
||||
webView: WebView
|
||||
) {
|
||||
WebViewSnapshotManager.registerWindow(activity.window)
|
||||
val root = activity.findViewById<FrameLayout>(android.R.id.content)
|
||||
@@ -76,9 +74,7 @@ object ComposeHost {
|
||||
setContent {
|
||||
ComposeNavigationHost(
|
||||
navEvents = navEvents,
|
||||
webView = webView,
|
||||
onBackRequested = onBackRequested,
|
||||
onExit = onExit
|
||||
webView = webView
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -105,9 +101,7 @@ private fun routeFor(path: String): String =
|
||||
@Composable
|
||||
private fun ComposeNavigationHost(
|
||||
navEvents: SharedFlow<NavigationEvent>,
|
||||
webView: WebView,
|
||||
onBackRequested: () -> Unit,
|
||||
onExit: () -> Unit
|
||||
webView: WebView
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
@@ -273,9 +267,7 @@ private fun HandleNavigationEvents(
|
||||
}
|
||||
|
||||
"pop" -> {
|
||||
if (!navController.popBackStack()) {
|
||||
// Already at root; nothing to pop.
|
||||
}
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
"reset" -> {
|
||||
|
||||
@@ -86,7 +86,7 @@ class LiquidTabsPlugin : Plugin() {
|
||||
ensureNav()
|
||||
currentTabId?.let { id ->
|
||||
tabsState.find { it.id == id }?.let { tab ->
|
||||
handleSelection(tab, reselected = false)
|
||||
handleSelection(tab, reselected = false, notify = false)
|
||||
}
|
||||
} ?: hideSearchUi()
|
||||
adjustWebViewPadding()
|
||||
@@ -112,7 +112,7 @@ class LiquidTabsPlugin : Plugin() {
|
||||
}
|
||||
nav.post {
|
||||
val reselected = currentTabId == tab.id
|
||||
handleSelection(tab, reselected)
|
||||
handleSelection(tab, reselected, notify = false)
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
@@ -169,7 +169,7 @@ class LiquidTabsPlugin : Plugin() {
|
||||
currentId = currentTabId,
|
||||
onSelect = { tab ->
|
||||
val reselected = tab.id == currentTabId
|
||||
handleSelection(tab, reselected)
|
||||
handleSelection(tab, reselected, notify = true)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -178,7 +178,7 @@ class LiquidTabsPlugin : Plugin() {
|
||||
return nav
|
||||
}
|
||||
|
||||
private fun handleSelection(tab: TabSpec, reselected: Boolean) {
|
||||
private fun handleSelection(tab: TabSpec, reselected: Boolean, notify: Boolean) {
|
||||
currentTabId = tab.id
|
||||
if (tab.role == "search") {
|
||||
showSearchUi()
|
||||
@@ -186,7 +186,9 @@ class LiquidTabsPlugin : Plugin() {
|
||||
hideSearchUi()
|
||||
}
|
||||
|
||||
notifyListeners("tabSelected", JSObject().put("id", tab.id).put("reselected", reselected))
|
||||
if (notify) {
|
||||
notifyListeners("tabSelected", JSObject().put("id", tab.id).put("reselected", reselected))
|
||||
}
|
||||
}
|
||||
|
||||
private fun adjustWebViewPadding() {
|
||||
@@ -539,7 +541,7 @@ class LiquidTabsPlugin : Plugin() {
|
||||
val id = obj.optString("id", "")
|
||||
if (id.isBlank()) continue
|
||||
val title = obj.optString("title", "")
|
||||
val subtitle = obj.optString("subtitle", null)
|
||||
val subtitle = if (obj.has("subtitle") && !obj.isNull("subtitle")) obj.optString("subtitle") else null
|
||||
result.add(SearchResult(id, title, subtitle))
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -6,24 +6,20 @@ import android.content.Context;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebView;
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.activity.EdgeToEdge;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
import androidx.activity.OnBackPressedDispatcher;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import ee.forgr.capacitor_navigation_bar.CapgoNavigationBarPlugin;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
private NavigationCoordinator navigationCoordinator = new NavigationCoordinator();
|
||||
private final NavigationCoordinator navigationCoordinator = new NavigationCoordinator();
|
||||
private BroadcastReceiver routeChangeReceiver;
|
||||
|
||||
@Override
|
||||
@@ -46,13 +42,15 @@ public class MainActivity extends BridgeActivity {
|
||||
|
||||
applyLogseqTheme();
|
||||
|
||||
// Let Compose host the WebView with system bar padding for safe areas and handle back.
|
||||
ComposeHost.INSTANCE.renderWithSystemInsets(this, webView, () -> {
|
||||
sendJsBack(webView);
|
||||
return null;
|
||||
}, () -> {
|
||||
finish();
|
||||
return null;
|
||||
// Let Compose host the WebView with system bar padding for safe areas.
|
||||
// Android back is still delegated to JS from the Activity back dispatcher.
|
||||
ComposeHost.INSTANCE.renderWithSystemInsets(this, webView);
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
handleNativeBack();
|
||||
}
|
||||
});
|
||||
|
||||
routeChangeReceiver = new BroadcastReceiver() {
|
||||
@@ -66,23 +64,11 @@ public class MainActivity extends BridgeActivity {
|
||||
}
|
||||
};
|
||||
IntentFilter filter = new IntentFilter(UILocal.ACTION_ROUTE_CHANGED);
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(routeChangeReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||
} else {
|
||||
registerReceiver(routeChangeReceiver, filter);
|
||||
}
|
||||
|
||||
// initNavigationBarBgColor();
|
||||
|
||||
ContextCompat.registerReceiver(this, routeChangeReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED);
|
||||
new Timer().schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
bridge.eval("window.dispatchEvent(new Event('sendIntentReceived'))", new ValueCallback<String>() {
|
||||
@Override
|
||||
public void onReceiveValue(String s) {
|
||||
//
|
||||
}
|
||||
});
|
||||
bridge.eval("window.dispatchEvent(new Event('sendIntentReceived'))", s -> {});
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
@@ -139,23 +125,13 @@ public class MainActivity extends BridgeActivity {
|
||||
}
|
||||
}
|
||||
|
||||
public void initNavigationBarBgColor() {
|
||||
CapgoNavigationBarPlugin navigationBarPlugin = new CapgoNavigationBarPlugin();
|
||||
JSObject data = new JSObject();
|
||||
data.put("color", "transparent");
|
||||
|
||||
PluginCall call = new PluginCall(null, null, null, "t", data);
|
||||
navigationBarPlugin.setNavigationBarColor(call);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
overridePendingTransition(0, R.anim.byebye);
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
private void handleNativeBack() {
|
||||
Log.d("onBackPressed", "Debug");
|
||||
|
||||
WebView webView = getBridge().getWebView();
|
||||
@@ -165,9 +141,10 @@ public class MainActivity extends BridgeActivity {
|
||||
sendJsBack(webView);
|
||||
} else {
|
||||
// Fallback if for some reason there is no webview
|
||||
super.onBackPressed();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
@@ -175,12 +152,7 @@ public class MainActivity extends BridgeActivity {
|
||||
String type = intent.getType();
|
||||
if (Intent.ACTION_SEND.equals(action) && type != null) {
|
||||
bridge.getActivity().setIntent(intent);
|
||||
bridge.eval("window.dispatchEvent(new Event('sendIntentReceived'))", new ValueCallback<String>() {
|
||||
@Override
|
||||
public void onReceiveValue(String s) {
|
||||
//
|
||||
}
|
||||
});
|
||||
bridge.eval("window.dispatchEvent(new Event('sendIntentReceived'))", s -> {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[frontend.handler.route :as route-handler]
|
||||
[frontend.mobile.util :as mobile-util]
|
||||
[frontend.state :as state]
|
||||
[goog.object :as gobj]
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.shui.dialog.core :as shui-dialog]
|
||||
[promesa.core :as p]
|
||||
@@ -19,6 +20,8 @@
|
||||
(defonce ^:private pending-navigation (atom nil))
|
||||
(defonce ^:private hooks-installed? (atom false))
|
||||
|
||||
(declare notify-route-payload!)
|
||||
|
||||
;; Track whether the latest change came from a native back gesture / popstate.
|
||||
(.addEventListener js/window "popstate" (fn [_]
|
||||
(reset! navigation-source :pop)))
|
||||
@@ -109,6 +112,57 @@
|
||||
[stack]
|
||||
(-> @stack-history (get stack) :history last))
|
||||
|
||||
(defn- native-route-payload!
|
||||
[navigation-type stack path route]
|
||||
(when (and (mobile-util/native-platform?)
|
||||
mobile-util/ui-local)
|
||||
(let [payload (cond-> {:navigationType navigation-type
|
||||
:push (= navigation-type "push")
|
||||
:stack stack
|
||||
:path path}
|
||||
route (assoc :route route))]
|
||||
(notify-route-payload! payload))))
|
||||
|
||||
(defn- prepare-browser-route-restore!
|
||||
[navigation-type stack]
|
||||
(case navigation-type
|
||||
"pop" (reset! navigation-source :pop)
|
||||
"replace" (record-navigation-intent! {:type :replace :stack stack})
|
||||
"push" (record-navigation-intent! {:type :push :stack stack})
|
||||
nil))
|
||||
|
||||
(defn- restore-stack-entry!
|
||||
"Restore a stack entry into route state and native navigation.
|
||||
|
||||
If the entry has a real reitit route, update the browser hash and let the
|
||||
router callback report to native. If the entry is a virtual tab root
|
||||
(`graphs`, `capture`, `search`, etc.), there is no valid reitit route to
|
||||
generate, so update route state and notify native directly."
|
||||
[stack {:keys [path route route-match] :as _entry} navigation-type]
|
||||
(let [route-match (or route-match (:route-match (stack-defaults stack)))
|
||||
route (or route (:route (stack-defaults stack)))
|
||||
path (or path (current-path))
|
||||
path (if (string/blank? path) "/" path)
|
||||
route-name (:to route)]
|
||||
(set-current-stack! stack)
|
||||
(route-handler/set-route-match! route-match)
|
||||
(swap! initialised-stacks assoc stack true)
|
||||
(if route-name
|
||||
(if (= path (current-path))
|
||||
;; No router callback will fire if the hash is already at the target.
|
||||
;; Still keep the native animation/mirror stack in sync.
|
||||
(do
|
||||
(reset! navigation-source nil)
|
||||
(reset! pending-navigation nil)
|
||||
(native-route-payload! navigation-type stack path route))
|
||||
(do
|
||||
(prepare-browser-route-restore! navigation-type stack)
|
||||
(orig-replace-state route-name (:path-params route) (:query-params route))))
|
||||
(do
|
||||
(reset! navigation-source nil)
|
||||
(reset! pending-navigation nil)
|
||||
(native-route-payload! navigation-type stack path nil)))))
|
||||
|
||||
(defn- remember-route!
|
||||
[stack nav-type route path route-match]
|
||||
(when stack
|
||||
@@ -213,26 +267,12 @@
|
||||
"Activate a stack and restore its last known route."
|
||||
[stack]
|
||||
(when stack
|
||||
(let [stack (ensure-stack stack)
|
||||
current @active-stack]
|
||||
(set-current-stack! stack)
|
||||
(let [stack (ensure-stack stack)]
|
||||
(when-let [{:keys [path route route-match]} (stack-top stack)]
|
||||
(let [route-match (or route-match (:route-match (stack-defaults stack)))
|
||||
path (or path (current-path))]
|
||||
(route-handler/set-route-match! route-match)
|
||||
(when (= current "search")
|
||||
;; reset to :home
|
||||
(orig-replace-state :home nil nil))
|
||||
(notify-route-change!
|
||||
{:route {:to (or (get-in route [:data :name])
|
||||
(get-in route-match [:data :name]))
|
||||
:path-params (or (:path-params route)
|
||||
(get-in route-match [:parameters :path]))
|
||||
:query-params (or (:query-params route)
|
||||
(get-in route-match [:parameters :query]))}
|
||||
:path path
|
||||
:stack stack
|
||||
:push false}))))))
|
||||
(restore-stack-entry!
|
||||
stack
|
||||
{:path path :route route :route-match route-match}
|
||||
"replace")))))
|
||||
|
||||
(defn pop-modal!
|
||||
[]
|
||||
@@ -259,32 +299,19 @@
|
||||
(let [stack (current-stack)
|
||||
{:keys [history]} (get @stack-history stack)
|
||||
history (vec history)]
|
||||
;; back to search root
|
||||
(when (and
|
||||
(mobile-util/native-android?)
|
||||
(= stack "search")
|
||||
(= (count history) 2))
|
||||
(.showSearchUiNative ^js (.. js/Capacitor -Plugins -LiquidTabsPlugin)))
|
||||
(when (>= (count history) 1)
|
||||
(let [root-history? (= (count history) 1)
|
||||
new-history (if root-history?
|
||||
history
|
||||
(subvec history 0 (dec (count history))))
|
||||
{:keys [route-match]} (peek new-history)
|
||||
route-match (or route-match (:route-match (stack-defaults stack)))
|
||||
route-name (get-in route-match [:data :name])
|
||||
path-params (get-in route-match [:parameters :path])
|
||||
query-params (get-in route-match [:parameters :query])]
|
||||
|
||||
(when (> (count history) 1)
|
||||
;; Back to the Android native search root.
|
||||
(when (and (mobile-util/native-android?)
|
||||
(= stack "search")
|
||||
(= (count history) 2))
|
||||
(when-let [^js plugin (some-> js/Capacitor .-Plugins .-LiquidTabsPlugin)]
|
||||
(when (gobj/get plugin "showSearchUiNative")
|
||||
(.showSearchUiNative plugin))))
|
||||
(let [new-history (subvec history 0 (dec (count history)))
|
||||
entry (peek new-history)]
|
||||
(swap! stack-history assoc stack {:history new-history})
|
||||
|
||||
;; Pretend this came from a pop for next-navigation!
|
||||
(reset! navigation-source :pop)
|
||||
|
||||
;; Use *original* replace-state to avoid recording a :replace intent.
|
||||
(orig-replace-state route-name path-params query-params)
|
||||
|
||||
(route-handler/set-route-match! route-match)))))
|
||||
(restore-stack-entry! stack entry "pop")
|
||||
true))))
|
||||
|
||||
(defn pop-to-root!
|
||||
"Pop current or given stack back to its root entry and notify navigation."
|
||||
@@ -293,25 +320,9 @@
|
||||
(when stack
|
||||
(let [{:keys [history]} (get @stack-history stack)
|
||||
root (or (first history) (stack-defaults stack))
|
||||
{:keys [route route-match path]} root
|
||||
route-match (or route-match (:route-match (stack-defaults stack)))
|
||||
path (or path (current-path))
|
||||
route (or route {:to (get-in route-match [:data :name])
|
||||
:path-params (get-in route-match [:parameters :path])
|
||||
:query-params (get-in route-match [:parameters :query])})]
|
||||
root (merge (stack-defaults stack) root)]
|
||||
(swap! stack-history assoc stack {:history [root]})
|
||||
(set-current-stack! stack)
|
||||
;; Use original replace-state to avoid recording a push intent.
|
||||
(orig-replace-state (get-in route-match [:data :name])
|
||||
(get-in route-match [:parameters :path])
|
||||
(get-in route-match [:parameters :query]))
|
||||
(route-handler/set-route-match! route-match)
|
||||
(notify-route-change!
|
||||
{:route route
|
||||
:route-match route-match
|
||||
:path path
|
||||
:stack stack
|
||||
:push false})))))
|
||||
(restore-stack-entry! stack root "replace")))))
|
||||
|
||||
(defn ^:export install-native-bridge!
|
||||
[]
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
(defonce *tab (atom "home"))
|
||||
(defn set-tab! [tab]
|
||||
(let [prev @*tab]
|
||||
;; When leaving the search tab, clear its stack so reopening starts fresh.
|
||||
(when (and (= prev "search")
|
||||
(not= tab "search"))
|
||||
(reset! *search-input "")
|
||||
(mobile-nav/reset-stack-history! "search"))
|
||||
(reset! *tab tab)
|
||||
(mobile-nav/switch-stack! tab)))
|
||||
(when-not (= prev tab)
|
||||
;; When leaving the search tab, clear its stack so reopening starts fresh.
|
||||
(when (and (= prev "search")
|
||||
(not= tab "search"))
|
||||
(reset! *search-input "")
|
||||
(mobile-nav/reset-stack-history! "search"))
|
||||
(reset! *tab tab)
|
||||
(mobile-nav/switch-stack! tab))))
|
||||
(defn use-tab [] (r/use-atom *tab))
|
||||
|
||||
(defonce *popup-data (atom nil))
|
||||
|
||||
Reference in New Issue
Block a user