diff --git a/src/article_row.rs b/src/article_row.rs index 23c6c1b..bb239b2 100644 --- a/src/article_row.rs +++ b/src/article_row.rs @@ -44,6 +44,7 @@ mod imp { pub bindings: RefCell>, pub unread_handler: RefCell>, + pub context_menu: std::cell::OnceCell, } #[glib::object_subclass] @@ -61,20 +62,88 @@ mod imp { } } - impl ObjectImpl for ArticleRow {} + impl ObjectImpl for ArticleRow { + fn constructed(&self) { + self.parent_constructed(); + self.setup_context_menu(); + } + + fn dispose(&self) { + if let Some(popover) = self.context_menu.get() { + popover.unparent(); + } + } + } impl WidgetImpl for ArticleRow {} impl BoxImpl for ArticleRow {} impl ArticleRow { + fn setup_context_menu(&self) { + let button = gtk4::Button::with_label("Mark as Unread"); + button.set_has_frame(false); + + let popover = gtk4::Popover::new(); + popover.set_child(Some(&button)); + popover.set_parent(&*self.obj()); + self.context_menu.set(popover.clone()).ok(); + + // Close popover and activate action when button is clicked. + let imp_weak = self.downgrade(); + let popover_weak = popover.downgrade(); + button.connect_clicked(move |_| { + if let Some(popover) = popover_weak.upgrade() { + popover.popdown(); + } + let Some(imp) = imp_weak.upgrade() else { return }; + let handler = imp.unread_handler.borrow(); + let Some((obj, _)) = handler.as_ref() else { return }; + let article_id = obj.article().id.clone(); + drop(handler); + imp.obj() + .activate_action( + "win.mark-article-unread", + Some(&article_id.to_variant()), + ) + .ok(); + }); + + // Right-click gesture to show the popover at cursor position. + let gesture = gtk4::GestureClick::new(); + gesture.set_button(3); + let popover_weak2 = popover.downgrade(); + gesture.connect_pressed(move |gesture, _, x, y| { + gesture.set_state(gtk4::EventSequenceState::Claimed); + let Some(popover) = popover_weak2.upgrade() else { return }; + popover.set_pointing_to(Some(>k4::gdk::Rectangle::new( + x as i32, + y as i32, + 1, + 1, + ))); + popover.popup(); + }); + self.obj().add_controller(gesture); + } + pub fn bind(&self, obj: &ArticleObject) { let article = obj.article(); self.feed_title_label.set_text(&article.feed_title); self.excerpt_label.set_text(&article.excerpt); self.date_label.set_text(&relative_time(article.published)); + + // Set initial bold state directly (using Pango markup to avoid + // CSS specificity issues with the zoom font-size provider). + let escaped = glib::markup_escape_text(&article.title); + if article.unread { + self.title_label.remove_css_class("dim-label"); + self.title_label.set_markup(&format!("{escaped}")); + } else { + self.title_label.add_css_class("dim-label"); + self.title_label.set_markup(&escaped); + } drop(article); - // Register handler first, then trigger it for the initial state. - // Using Pango markup for bold avoids CSS specificity issues. + // Connect handler for future unread state changes. let title_label = self.title_label.clone(); let id = obj.connect_notify_local(Some("unread"), move |obj, _| { let article = obj.article(); @@ -88,7 +157,6 @@ mod imp { } }); *self.unread_handler.borrow_mut() = Some((obj.clone(), id)); - obj.notify("unread"); } pub fn unbind(&self) { @@ -99,7 +167,6 @@ mod imp { b.unbind(); } } - } } diff --git a/src/window.rs b/src/window.rs index 683e046..c3c0699 100644 --- a/src/window.rs +++ b/src/window.rs @@ -77,6 +77,11 @@ pub mod imp { klass.install_action("win.reload", None, |win, _, _| win.imp().do_reload()); klass.install_action("win.logout", None, |win, _, _| win.imp().do_logout()); klass.install_action("win.mark-unread", None, |win, _, _| win.imp().do_mark_unread()); + klass.install_action("win.mark-article-unread", Some(glib::VariantTy::STRING), |win, _, param| { + if let Some(id) = param.and_then(|p| p.get::()) { + win.imp().do_mark_article_unread(id); + } + }); klass.install_action("win.open-in-browser", None, |win, _, _| { win.imp().do_open_in_browser() }); @@ -750,8 +755,11 @@ pub mod imp { fn do_mark_unread(&self) { let id = self.current_article_id.borrow().clone(); let Some(id) = id else { return }; + self.do_mark_article_unread(id); + } - // Find the ArticleObject in the store and set unread=true + fn do_mark_article_unread(&self, id: String) { + // Find the ArticleObject in the store and set unread=true. if let Some(store) = self.article_store.borrow().as_ref() { for i in 0..store.n_items() { if let Some(obj) = store.item(i).and_downcast::() { @@ -762,7 +770,11 @@ pub mod imp { } } - *self.mark_unread_guard.borrow_mut() = true; + // If this is the currently displayed article, guard against it + // being immediately re-marked read when the selection fires. + if self.current_article_id.borrow().as_deref() == Some(&*id) { + *self.mark_unread_guard.borrow_mut() = true; + } let api = self.api.borrow().clone(); let wt = self.write_token.borrow().clone();