use gtk4::glib; use gtk4::subclass::prelude::ObjectSubclassIsExt; glib::wrapper! { pub struct ArticleRow(ObjectSubclass) @extends gtk4::Box, gtk4::Widget, @implements gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Orientable; } impl ArticleRow { pub fn new() -> Self { glib::Object::new() } pub fn bind(&self, obj: &crate::model::ArticleObject) { self.imp().bind(obj); } pub fn unbind(&self) { self.imp().unbind(); } } mod imp { use super::*; use crate::model::ArticleObject; use gtk4::prelude::*; use gtk4::subclass::prelude::*; use gtk4::CompositeTemplate; use glib::object::ObjectExt; use std::cell::RefCell; #[derive(CompositeTemplate, Default)] #[template(resource = "/net/jeena/FeedTheMonkey/ui/article_row.ui")] pub struct ArticleRow { #[template_child] pub feed_title_label: TemplateChild, #[template_child] pub date_label: TemplateChild, #[template_child] pub title_label: TemplateChild, #[template_child] pub excerpt_label: TemplateChild, pub bindings: RefCell>, pub unread_handler: RefCell>, pub context_menu: std::cell::OnceCell, } #[glib::object_subclass] impl ObjectSubclass for ArticleRow { const NAME: &'static str = "ArticleRow"; type Type = super::ArticleRow; type ParentType = gtk4::Box; fn class_init(klass: &mut Self::Class) { klass.bind_template(); } fn instance_init(obj: &glib::subclass::InitializingObject) { obj.init_template(); } } 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); // 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(); let escaped = glib::markup_escape_text(&article.title); if article.unread { title_label.remove_css_class("dim-label"); title_label.set_markup(&format!("{escaped}")); } else { title_label.add_css_class("dim-label"); title_label.set_markup(&escaped); } }); *self.unread_handler.borrow_mut() = Some((obj.clone(), id)); } pub fn unbind(&self) { if let Some((obj, id)) = self.unread_handler.borrow_mut().take() { obj.disconnect(id); } for b in self.bindings.borrow_mut().drain(..) { b.unbind(); } } } } fn relative_time(unix: i64) -> String { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs() as i64; let diff = now - unix; if diff < 60 { "just now".to_string() } else if diff < 3600 { let m = diff / 60; format!("{m}m ago") } else if diff < 86400 { let h = diff / 3600; format!("{h}h ago") } else if diff < 172800 { "Yesterday".to_string() } else { let d = diff / 86400; format!("{d}d ago") } }