diff --git a/ios/App/App/LiquidTabsPlugin.swift b/ios/App/App/LiquidTabsPlugin.swift index 823e6b7d7a..c055c2d026 100644 --- a/ios/App/App/LiquidTabsPlugin.swift +++ b/ios/App/App/LiquidTabsPlugin.swift @@ -44,14 +44,23 @@ public class LiquidTabsPlugin: CAPPlugin, CAPBridgedPlugin { let systemImage = dict["systemImage"] as? String ?? "square" let roleStr = dict["role"] as? String ?? "normal" - let role: LiquidTab.Role = (roleStr == "search") ? .search : .normal + let role: LiquidTab.Role + + switch roleStr { + case "search": + role = .search + case "action": + role = .action + default: + role = .normal + } return LiquidTab(id: id, title: title, systemImage: systemImage, role: role) } DispatchQueue.main.async { self.store.tabs = tabs - if let firstId = tabs.first?.id { + if let firstId = tabs.first(where: { !$0.isActionButton })?.id ?? tabs.first?.id { self.store.selectedId = firstId } } @@ -68,6 +77,11 @@ public class LiquidTabsPlugin: CAPPlugin, CAPBridgedPlugin { } DispatchQueue.main.async { + if let tab = self.store.tab(for: id), tab.isActionButton { + LiquidTabsPlugin.shared?.notifyTabSelected(id: id) + return + } + self.store.selectedId = id } diff --git a/ios/App/App/LiquidTabsRootView.swift b/ios/App/App/LiquidTabsRootView.swift index 610b011ced..c11ac54301 100644 --- a/ios/App/App/LiquidTabsRootView.swift +++ b/ios/App/App/LiquidTabsRootView.swift @@ -121,6 +121,19 @@ private struct LiquidTabs26View: View { private let maxMainTabs = 6 + private func tab(for selection: LiquidTabsTabSelection) -> LiquidTab? { + guard let id = store.tabId(for: selection) else { return nil } + return store.tab(for: id) + } + + @discardableResult + private func handleActionIfNeeded(selection: LiquidTabsTabSelection) -> Bool { + guard let tab = tab(for: selection), tab.isActionButton else { return false } + + LiquidTabsPlugin.shared?.notifyTabSelected(id: tab.id) + return true + } + // Proxy binding to intercept re-taps private var tabSelectionProxy: Binding { Binding( @@ -128,7 +141,7 @@ private struct LiquidTabs26View: View { set: { newValue in if newValue == selectedTab { handleRetap(on: newValue) - } else { + } else if !handleActionIfNeeded(selection: newValue) { selectedTab = newValue } } @@ -146,24 +159,27 @@ private struct LiquidTabs26View: View { private func initialSelection() -> LiquidTabsTabSelection { if let id = store.selectedId, + let tab = store.tab(for: id), + !tab.isActionButton, let sel = store.selection(forId: id) { return sel } - if !store.tabs.isEmpty { - return .content(0) + if let firstIndex = store.tabs.prefix(maxMainTabs).firstIndex(where: { !$0.isActionButton }) { + return .content(firstIndex) } return .search } private func focusSearchField() { - // Drive focus (and keyboard) only through searchFocused. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { isSearchFocused = true } } + // MARK: - Body + var body: some View { if store.tabs.isEmpty { // bootstrap webview so JS can configure tabs @@ -175,29 +191,46 @@ private struct LiquidTabs26View: View { Color.logseqBackground.ignoresSafeArea() TabView(selection: tabSelectionProxy) { - // Dynamic main tabs using Tab(...) API + // Dynamic main tabs – all use Tab(...) ForEach(Array(store.tabs.prefix(maxMainTabs).enumerated()), id: \.element.id) { index, tab in Tab( - tab.title, - systemImage: tab.systemImage, - value: LiquidTabsTabSelection.content(index) + tab.title, + systemImage: tab.systemImage, + value: LiquidTabsTabSelection.content(index) ) { - NativeNavHost(navController: navController) - .ignoresSafeArea() - .background(Color.logseqBackground) + if tab.isActionButton { + // Capture / action tab: no real content, acts like a plain button + NavigationStack { + VStack(spacing: 20) { + Text("Capture") + .font(.largeTitle) + .fontWeight(.bold) + .padding(.top, 120) + + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.logseqBackground) + .ignoresSafeArea() + } + } else { + NativeNavHost(navController: navController) + .ignoresSafeArea() + .background(Color.logseqBackground) + } } } // Search Tab Tab(value: .search, role: .search) { SearchTabHost26( - navController: navController, - selectedTab: $selectedTab, - firstTabId: store.tabs.first?.id, - store: store + navController: navController, + selectedTab: $selectedTab, + firstTabId: store.tabs.first?.id, + store: store ) - .ignoresSafeArea() + .ignoresSafeArea() } } .searchable( @@ -249,8 +282,6 @@ private struct LiquidTabs26View: View { switch newValue { case .search: - // Every time we switch to the search tab, re-focus the search - // field so the search bar auto-focuses and keyboard appears. DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { hackShowKeyboard = true } @@ -262,13 +293,14 @@ private struct LiquidTabs26View: View { focusSearchField() case .content: - // Leaving search tab – drop focus and stop hack keyboard. isSearchFocused = false hackShowKeyboard = false } } .onChange(of: store.selectedId) { newId in guard let id = newId, + let tab = store.tab(for: id), + !tab.isActionButton, let newSelection = store.selection(forId: id) else { return } @@ -282,6 +314,7 @@ private struct LiquidTabs26View: View { } } + // Search host for 26+ // Only responsible for cancel behaviour and tab switching. // It does NOT own the focus anymore. @@ -342,11 +375,16 @@ private struct LiquidTabs16View: View { TabView(selection: Binding( get: { - store.selectedId ?? store.firstTab?.id + store.selectedId ?? store.tabs.first(where: { !$0.isActionButton })?.id ?? store.firstTab?.id }, set: { newValue in guard let id = newValue else { return } + if let tab = store.tab(for: id), tab.isActionButton { + LiquidTabsPlugin.shared?.notifyTabSelected(id: id) + return + } + // Re-tap: pop to root if id == store.selectedId { navController.popToRootViewController(animated: true) @@ -386,7 +424,8 @@ private struct LiquidTabs16View: View { } .onAppear { if store.selectedId == nil { - store.selectedId = store.tabs.first?.id + store.selectedId = store.tabs.first(where: { !$0.isActionButton })?.id + ?? store.tabs.first?.id } let appearance = UITabBarAppearance() diff --git a/ios/App/App/LiquidTabsStore.swift b/ios/App/App/LiquidTabsStore.swift index ec95733c15..fa934d8275 100644 --- a/ios/App/App/LiquidTabsStore.swift +++ b/ios/App/App/LiquidTabsStore.swift @@ -10,6 +10,20 @@ struct LiquidTab: Identifiable, Equatable { enum Role { case normal case search + case action + } +} + +extension LiquidTab { + /// Tabs that should behave like plain buttons instead of driving selection. + /// Defaults to the existing capture tab unless explicitly marked as an action. + var isActionButton: Bool { + switch role { + case .action: + return true + default: + return id == "capture" + } } } diff --git a/src/main/mobile/bottom_tabs.cljs b/src/main/mobile/bottom_tabs.cljs index c38d9242d7..5913beaa39 100644 --- a/src/main/mobile/bottom_tabs.cljs +++ b/src/main/mobile/bottom_tabs.cljs @@ -120,5 +120,5 @@ (configure-tabs [{:id "home" :title "Home" :systemImage "house" :role "normal"} {:id "favorites" :title "Favorites" :systemImage "star" :role "normal"} - {:id "capture" :title "Capture" :systemImage "tray" :role "normal"} + {:id "capture" :title "Capture" :systemImage "tray" :role "action"} {:id "settings" :title "Settings" :systemImage "gear" :role "normal"}])) diff --git a/src/main/mobile/components/popup.cljs b/src/main/mobile/components/popup.cljs index 2c08dc8a9c..c0aad1f080 100644 --- a/src/main/mobile/components/popup.cljs +++ b/src/main/mobile/components/popup.cljs @@ -7,6 +7,7 @@ [logseq.shui.popup.core :as shui-popup] [logseq.shui.ui :as shui] [mobile.state :as mobile-state] + [promesa.core :as p] [rum.core :as rum])) (defonce *last-popup? (atom nil)) @@ -51,10 +52,15 @@ (editor-handler/quick-add-open-last-block!)) dismissing? - (when (some? @mobile-state/*popup-data) - (state/pub-event! [:mobile/clear-edit]) - (mobile-state/set-popup! nil) - (reset! *last-popup-data nil)) + (do + (when (mobile-state/quick-add-open?) + (when-let [tab @mobile-state/*tab] + (mobile-state/set-tab! tab))) + (when (some? @mobile-state/*popup-data) + (p/do! + (mobile-state/set-popup! nil) + (reset! *last-popup-data nil) + (state/pub-event! [:mobile/clear-edit])))) :else nil)))