refactor: simplify navigation handling and improve event logging

This commit is contained in:
charlie
2026-05-13 16:54:51 +08:00
parent a79ea5a743
commit 6e92a32c45
5 changed files with 114 additions and 136 deletions

View File

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

View File

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

View File

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

View File

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

View File

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