Browse Source

add favorite button and block context actions

Tienson Qin 1 week ago
parent
commit
9af504e568

+ 32 - 5
ios/App/App/NativeTopBarPlugin.swift

@@ -12,6 +12,7 @@ public class NativeTopBarPlugin: CAPPlugin, CAPBridgedPlugin {
     private class NativeTopBarButton: UIButton {
         var buttonId: String = ""
         override var intrinsicContentSize: CGSize {
+            // Keep a consistent tap target; icon size is controlled via SF Symbol configuration
             CGSize(width: 36, height: 32)
         }
     }
@@ -24,7 +25,7 @@ public class NativeTopBarPlugin: CAPPlugin, CAPBridgedPlugin {
             return nav
         }
         if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
-            return appDelegate.navController // or appDelegate.mainNavController
+            return appDelegate.navController
         }
         return nil
     }
@@ -70,11 +71,12 @@ public class NativeTopBarPlugin: CAPPlugin, CAPBridgedPlugin {
                     button.setTitle(title, for: .normal)
                     button.setTitleColor(nav.navigationBar.tintColor, for: .normal)
                     button.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
-                    button.addTarget(self, action: #selector(titleTapped(_:)), for: .touchUpInside)
+                    button.addTarget(self, action: #selector(self.titleTapped(_:)), for: .touchUpInside)
                     topVC.navigationItem.titleView = button
                 } else {
                     topVC.navigationItem.title = title
                 }
+
                 topVC.navigationItem.leftBarButtonItems = self.buildButtons(from: leftButtons)
                 topVC.navigationItem.rightBarButtonItems = self.buildButtons(from: rightButtons)
             }
@@ -83,6 +85,8 @@ public class NativeTopBarPlugin: CAPPlugin, CAPBridgedPlugin {
         }
     }
 
+    // MARK: - Button building
+
     private func buildButtons(from array: [JSObject]) -> [UIBarButtonItem] {
         return array.compactMap { obj in
             guard let id = obj["id"] as? String else { return nil }
@@ -90,14 +94,19 @@ public class NativeTopBarPlugin: CAPPlugin, CAPBridgedPlugin {
 
             let button = NativeTopBarButton(type: .system)
             button.buttonId = id
-            if let image = UIImage(systemName: systemIconName) {
+
+            // Size: small / medium / large -> SF Symbol pointSize
+            let symbolConfig = symbolConfiguration(for: obj)
+            if let image = UIImage(systemName: systemIconName, withConfiguration: symbolConfig) {
                 button.setImage(image, for: .normal)
             }
+
+            // Per-button color: prefers "tintColor", then "color"
             button.tintColor = tintColor(for: obj)
             button.imageView?.contentMode = .scaleAspectFit
             button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
 
-            // Use a fixed-size container via frames to avoid Auto Layout conflicts with the bar wrapper.
+            // Fixed tap target; icon itself is sized by SF Symbol config
             let container = UIView(frame: CGRect(x: 0, y: 0, width: 36, height: 32))
             button.frame = container.bounds
             button.autoresizingMask = [.flexibleWidth, .flexibleHeight]
@@ -109,12 +118,30 @@ public class NativeTopBarPlugin: CAPPlugin, CAPBridgedPlugin {
     }
 
     private func tintColor(for obj: JSObject) -> UIColor {
-        if let hex = obj["tintColor"] as? String {
+        if let hex = (obj["tintColor"] as? String) ?? (obj["color"] as? String) {
             return hex.toUIColor(defaultColor: .label)
         }
         return .label
     }
 
+    private func symbolConfiguration(for obj: JSObject) -> UIImage.SymbolConfiguration {
+        let sizeString = (obj["size"] as? String)?.lowercased()
+
+        let pointSize: CGFloat
+        switch sizeString {
+        case "small":
+            pointSize = 8
+        case "large":
+            pointSize = 19
+        default: // "medium" or nil
+            pointSize = 15
+        }
+
+        return UIImage.SymbolConfiguration(pointSize: pointSize, weight: .semibold)
+    }
+
+    // MARK: - Actions
+
     @objc private func buttonTapped(_ sender: NativeTopBarButton) {
         notifyListeners("buttonTapped", data: ["id": sender.buttonId])
     }

+ 75 - 9
src/main/mobile/components/header.cljs

@@ -9,6 +9,7 @@
             [frontend.db.async :as db-async]
             [frontend.db.conn :as db-conn]
             [frontend.flows :as flows]
+            [frontend.handler.notification :as notification]
             [frontend.handler.page :as page-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.user :as user-handler]
@@ -19,6 +20,7 @@
             [goog.date :as gdate]
             [logseq.common.util :as common-util]
             [logseq.db :as ldb]
+            [logseq.db.frontend.entity-util :as entity-util]
             [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [missionary.core :as m]
@@ -117,6 +119,41 @@
    {:title "Actions"
     :default-height false}))
 
+(defn open-page-settings
+  [block]
+  (shui/popup-show!
+   nil
+   (fn []
+     [:div.-mx-2
+      (ui/menu-link
+       {:on-click shui/popup-hide!}
+       [:span.text-lg.flex.gap-2.items-center
+        (shui/tabler-icon "copy" {:class "opacity-80" :size 22})
+        "Copy"])
+
+      (ui/menu-link
+       {:on-click #(-> (shui/dialog-confirm!
+                        (str "⚠️ Are you sure you want to delete this "
+                             (if (entity-util/page? block) "page" "block")
+                             "?"))
+                       (p/then
+                        (fn []
+                          (shui/popup-hide!)
+                          (some->
+                           (:block/uuid block)
+                           (page-handler/<delete!
+                            (fn []
+                              ;; FIXME: empty screen, wrong route state
+                              (mobile-state/redirect-to-tab! "home"))
+                            {:error-handler
+                             (fn [{:keys [msg]}]
+                               (notification/show! msg :warning))})))))}
+       [:span.text-lg.flex.gap-2.items-center.text-red-700
+        (shui/tabler-icon "trash" {:class "opacity-80" :size 22})
+        "Delete"])])
+   {:title "Actions"
+    :default-height false}))
+
 (defn- open-graph-switcher! []
   (ui-component/open-popup!
    (fn []
@@ -137,12 +174,23 @@
                       "sync" (shui/popup-show! nil
                                                (rtc-indicator/details)
                                                {})
+                      "favorite" (when-let [id (state/get-current-page)]
+                                   (when (common-util/uuid-string? id)
+                                     (when-let [block (db/entity [:block/uuid (uuid id)])]
+                                       (let [favorited? (page-handler/favorited? (str (:block/uuid block)))]
+                                         (if favorited?
+                                           (page-handler/<unfavorite-page! id)
+                                           (page-handler/<favorite-page! id))))))
+                      "page-setting" (when-let [id (state/get-current-page)]
+                                       (when (common-util/uuid-string? id)
+                                         (when-let [block (db/entity [:block/uuid (uuid id)])]
+                                           (open-page-settings block))))
 
                       nil)))
     (reset! native-top-bar-listener? true)))
 
 (defn- configure-native-top-bar!
-  [repo {:keys [tab title route-name sync-color]}]
+  [repo {:keys [tab title route-name route-view sync-color favorited?]}]
   (when (mobile-util/native-ios?)
     (let [hidden? (and (= tab "search")
                        (not= route-name :page))
@@ -151,12 +199,18 @@
                               (user-handler/logged-in?))
           base {:title title
                 :hidden (boolean hidden?)}
+          page? (= route-name :page)
           right-buttons (cond
                           (= tab "home")
-                          (cond-> [{:id "calendar" :systemIcon "calendar"}]
-                            rtc-indicator?
+                          (cond-> []
+                            (nil? route-view)
+                            (conj {:id "calendar" :systemIcon "calendar"})
+                            (and rtc-indicator? (not page?))
                             (conj {:id "sync" :systemIcon "circle.fill" :color sync-color
-                                   :size "small"}))
+                                   :size "small"})
+                            page?
+                            (into [{:id "page-setting" :systemIcon "ellipsis"}
+                                   {:id "favorite" :systemIcon (if favorited? "star.fill" "star")}]))
 
                           (= tab "settings")
                           [{:id "settings-actions" :systemIcon "ellipsis"}]
@@ -174,13 +228,21 @@
                           (db-conn/get-short-repo-name current-repo)
                           "Select a Graph")
         route-name (get-in route-match [:data :name])
+        route-view (get-in route-match [:data :view])
         detail-info (hooks/use-flow-state (m/watch rtc-indicator/*detail-info))
         _ (hooks/use-flow-state flows/current-login-user-flow)
         online? (hooks/use-flow-state flows/network-online-event-flow)
         rtc-state (:rtc-state detail-info)
-        sync-color (if (and online? (= :open rtc-state))
-                     "green"
-                     "yellow")]
+        unpushed-block-update-count (:pending-local-ops detail-info)
+        pending-asset-ops           (:pending-asset-ops detail-info)
+        sync-color (if (and online?
+                            (= :open rtc-state)
+                            (zero? unpushed-block-update-count)
+                            (zero? pending-asset-ops))
+                     ;; green
+                     "#16A34A"
+                     ;; yellow
+                     "#CA8A04")]
     (hooks/use-effect!
      (fn []
        (when (mobile-util/native-ios?)
@@ -189,6 +251,8 @@
                          (let [id (get-in route-match [:parameters :path :name])]
                            (when (common-util/uuid-string? id)
                              (db-async/<get-block current-repo (uuid id) {:children? false}))))
+                 favorited? (when block
+                              (page-handler/favorited? (str (:block/uuid block))))
                  title (cond block
                              (:block/title block)
                              (= tab "home")
@@ -202,9 +266,11 @@
              :hidden? (and (= tab "search")
                            (not= route-name :page))
              :route-name route-name
-             :sync-color sync-color})))
+             :route-view route-view
+             :sync-color sync-color
+             :favorited? favorited?})))
        nil)
-     [tab short-repo-name route-match])
+     [tab short-repo-name route-match sync-color])
 
     [:<>]))
 

+ 188 - 0
src/main/mobile/components/header.cljs.~a71c8a938c271cd90172765cbf361f9a0efb5d85~

@@ -0,0 +1,188 @@
+(ns mobile.components.header
+  "App top header"
+  (:require [clojure.string :as string]
+            [frontend.common.missionary :as c.m]
+            [frontend.components.repo :as repo]
+            [frontend.components.rtc.indicator :as rtc-indicator]
+            [frontend.date :as date]
+            [frontend.db :as db]
+            [frontend.db.conn :as db-conn]
+            [frontend.handler.page :as page-handler]
+            [frontend.handler.user :as user-handler]
+            [frontend.mobile.util :as mobile-util]
+            [frontend.state :as state]
+            [frontend.ui :as ui]
+            [frontend.util :as util]
+            [goog.date :as gdate]
+            [logseq.db :as ldb]
+            [logseq.shui.hooks :as hooks]
+            [logseq.shui.ui :as shui]
+            [missionary.core :as m]
+            [mobile.components.ui :as ui-component]
+            [mobile.components.ui-silk :as ui-silk]
+            [mobile.state :as mobile-state]
+            [promesa.core :as p]
+            [rum.core :as rum]))
+
+(rum/defc app-graphs-select
+  []
+  (let [current-repo (state/get-current-repo)
+        short-repo-name (if current-repo
+                          (db-conn/get-short-repo-name current-repo)
+                          "Select a Graph")]
+    [:.app-graph-select
+     (shui/button
+      {:variant :text
+       :size :sm
+       :on-click (fn [e]
+                   (shui/popup-show! (.-target e)
+                                     (fn [{:keys [id]}]
+                                       (repo/repos-dropdown-content {:contentid id}))
+                                     {:id :switch-graph
+                                      :default-height false
+                                      :content-props {:class "repos-list"}}))}
+      [:span.flex.items-center.pt-1
+       [:span.overflow-hidden.text-ellipsis.block.text-base
+        {:style {:max-width "40vw"}}
+        short-repo-name]])]))
+
+(rum/defc journal-calendar-btn
+  []
+  (shui/button
+   {:variant :text
+    :size :sm
+    :on-click (fn []
+                (let [apply-date! (fn [date]
+                                    (let [page-name (date/journal-name (gdate/Date. (js/Date. date)))]
+                                      (if-let [journal (db/get-page page-name)]
+                                        (mobile-state/open-block-modal! journal)
+                                        (-> (page-handler/<create! page-name {:redirect? false})
+                                            (p/then #(mobile-state/open-block-modal! (db/get-page page-name)))))))]
+                  (-> (.showDatePicker mobile-util/ui-local)
+                      (p/then (fn [^js e] (some-> e (.-value) (apply-date!)))))))}
+   [:span.mt-1
+    (shui/tabler-icon "calendar-month" {:size 24})]))
+
+(rum/defc rtc-indicator-btn
+  []
+  (let [repo (state/get-current-repo)]
+    [:div.flex.flex-row.items-center.gap-2
+     (when (and repo
+                (ldb/get-graph-rtc-uuid (db/get-db))
+                (user-handler/logged-in?))
+       (rtc-indicator/indicator))]))
+
+(rum/defc menu-button
+  []
+  (shui/button
+   {:variant :text
+    :size :sm
+    :on-pointer-down (fn [e]
+                       (util/stop e)
+                       (mobile-state/close-block-modal!)
+                       (mobile-state/open-left-sidebar!))}
+   [:span.mt-2
+    (shui/tabler-icon "menu" {:size 24})]))
+
+(rum/defc log
+  []
+  (let [[error-only? set-error-only!] (hooks/use-state false)
+        [reversed? set-reversed!] (hooks/use-state false)
+        [show-worker-log? set-show-worker-log!] (hooks/use-state false)
+        [worker-records set-worker-records!] (hooks/use-state [])]
+    (hooks/use-effect!
+     #(c.m/run-task*
+       (m/sp
+         (set-worker-records! (c.m/<? (state/<invoke-db-worker :thread-api/mobile-logs)))))
+     [])
+    [:div.flex.flex-col.gap-1.p-2.ls-debug-log
+     [:div.flex.flex-row.justify-between
+      [:div.text-lg.font-medium.mb-2 "Full log: "]
+
+      (shui/button
+       {:variant :ghost
+        :size :sm
+        :on-click (fn []
+                    (util/copy-to-clipboard! (str (string/join "\n\n" @mobile-state/*log)
+                                                  "\n\n================================================================\n\n"
+                                                  (string/join "\n\n" worker-records))))}
+       "Copy")]
+
+     [:div.flex.flex-row.gap-2
+      (shui/button
+       {:size :sm
+        :on-click (fn []
+                    (set-error-only! (not error-only?)))}
+       (if error-only?
+         "All"
+         "Errors only"))
+
+      (shui/button
+       {:size :sm
+        :on-click (fn []
+                    (set-reversed! (not reversed?)))}
+       (if reversed?
+         "New record first"
+         "Old record first"))
+
+      (shui/button
+       {:size :sm
+        :on-click (fn []
+                    (set-show-worker-log! (not show-worker-log?)))}
+       (if show-worker-log?
+         "UI logs"
+         "worker logs"))]
+
+     (let [records (cond->> (if show-worker-log? worker-records @mobile-state/*log)
+                     error-only?
+                     (filter (fn [record] (contains? #{:error :severe} (:level record))))
+                     reversed?
+                     reverse)]
+       [:ul
+        (for [record records]
+          [:li (str (:level record) " " (:message record))])])]))
+
+(rum/defc header
+  [tab login?]
+  (ui-silk/app-silk-topbar
+   (cond-> {:title [:span.capitalize (str tab)]
+            :props {:class (str tab)}}
+     (= tab "home")
+     (assoc
+      :left-render (menu-button)
+      :title (app-graphs-select)
+      :right-render [:div.flex.items-center.gap-1
+                     (journal-calendar-btn)
+                     (rtc-indicator-btn)]
+      :center-title? true)
+
+     (= tab "settings")
+     (assoc
+      :right-render
+      [:<>
+       (shui/button
+        {:variant :icon :size :sm
+         :on-click (fn []
+                     (ui-component/open-popup!
+                      (fn []
+                        [:div.-mx-2
+                         (when login?
+                           (ui/menu-link {:on-click #(user-handler/logout)}
+                                         [:span.text-lg.flex.gap-2.items-center.text-red-700
+                                          (shui/tabler-icon "logout" {:class "opacity-80" :size 22})
+                                          "Logout"]))
+                         (ui/menu-link {:on-click #(js/window.open "https://github.com/logseq/db-test/issues")}
+                                       [:span.text-lg.flex.gap-2.items-center
+                                        (shui/tabler-icon "bug" {:class "opacity-70" :size 22})
+                                        "Report bug"])
+                         (ui/menu-link {:on-click (fn []
+                                                    (mobile-state/set-popup! nil)
+                                                    (mobile-state/set-popup!
+                                                     {:open? true
+                                                      :content-fn (fn [] (log))}))}
+                                       [:span.text-lg.flex.gap-2.items-center
+                                        "Check log"])])
+                      {:title "Actions"
+                       :default-height false
+                       :type :action-sheet}))}
+        (shui/tabler-icon "dots" {:size 23}))]))))