Compare commits

..

No commits in common. "2c5217e744307758e25b69d871949536b8d3f11e" and "c19c2cbd1d7ff348ed401123a90174eb0d88d528" have entirely different histories.

6 changed files with 28 additions and 135 deletions

View file

@ -32,7 +32,6 @@ template $ArticleRow : Gtk.Box {
wrap: true;
lines: 2;
ellipsize: end;
styles ["article-title"]
}
Label excerpt_label {

View file

@ -46,9 +46,6 @@ corresponding .blp file and regenerate this file with blueprint-compiler.
<property name="wrap">true</property>
<property name="lines">2</property>
<property name="ellipsize">3</property>
<style>
<class name="article-title"/>
</style>
</object>
</child>
<child>

View file

@ -148,7 +148,7 @@ impl Api {
Ok(stream.items.into_iter().map(|item| {
let unread = !item.categories.as_deref().unwrap_or_default()
.iter()
.any(|c| c == "user/-/state/com.google/read");
.any(|c| c.contains("user/-/state/com.google/read"));
let content = item.summary
.as_ref()
.and_then(|s| s.content.clone())
@ -249,20 +249,9 @@ fn plain_text_excerpt(html: &str, max_chars: usize) -> String {
}
}
let collapsed: String = out.split_whitespace().collect::<Vec<_>>().join(" ");
let decoded = decode_html_entities(&collapsed);
if decoded.chars().count() <= max_chars {
decoded
if collapsed.chars().count() <= max_chars {
collapsed
} else {
decoded.chars().take(max_chars).collect::<String>() + ""
collapsed.chars().take(max_chars).collect::<String>() + ""
}
}
fn decode_html_entities(s: &str) -> String {
s.replace("&amp;", "&")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&quot;", "\"")
.replace("&apos;", "'")
.replace("&#39;", "'")
.replace("&nbsp;", " ")
}

View file

@ -71,8 +71,8 @@ mod imp {
.sidebar-content row:selected {
background-color: alpha(@window_fg_color, 0.22);
}
.article-title {
font-size: 1.05em;
.unread-title {
font-weight: bold;
}"
);
gtk4::style_context_add_provider_for_display(

View file

@ -44,7 +44,6 @@ mod imp {
pub bindings: RefCell<Vec<glib::Binding>>,
pub unread_handler: RefCell<Option<(ArticleObject, glib::SignalHandlerId)>>,
pub context_menu: std::cell::OnceCell<gtk4::Popover>,
}
#[glib::object_subclass]
@ -62,98 +61,32 @@ mod imp {
}
}
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 ObjectImpl for ArticleRow {}
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(&gtk4::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.title_label.set_text(&article.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!("<b>{escaped}</b>"));
} else {
self.title_label.add_css_class("dim-label");
self.title_label.set_markup(&escaped);
}
self.update_read_style(article.unread);
drop(article);
// Connect handler for future unread state changes.
// Watch for 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 {
let unread = obj.article().unread;
if unread {
title_label.remove_css_class("dim-label");
title_label.set_markup(&format!("<b>{escaped}</b>"));
title_label.add_css_class("unread-title");
} else {
title_label.add_css_class("dim-label");
title_label.set_markup(&escaped);
title_label.remove_css_class("unread-title");
}
});
*self.unread_handler.borrow_mut() = Some((obj.clone(), id));
@ -167,6 +100,16 @@ mod imp {
b.unbind();
}
}
fn update_read_style(&self, unread: bool) {
if unread {
self.title_label.remove_css_class("dim-label");
self.title_label.add_css_class("unread-title");
} else {
self.title_label.add_css_class("dim-label");
self.title_label.remove_css_class("unread-title");
}
}
}
}

View file

@ -77,11 +77,6 @@ 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::<String>()) {
win.imp().do_mark_article_unread(id);
}
});
klass.install_action("win.open-in-browser", None, |win, _, _| {
win.imp().do_open_in_browser()
});
@ -106,14 +101,6 @@ pub mod imp {
});
klass.install_action("win.preferences", None, |win, _, _| {
let dialog = crate::preferences_dialog::PreferencesDialog::new();
let win_weak = win.downgrade();
dialog.connect_closed(move |_| {
if let Some(win) = win_weak.upgrade() {
let imp = win.imp();
*imp.filter_rules.borrow_mut() = crate::filters::load_rules();
imp.reload_current_article();
}
});
dialog.present(Some(win.upcast_ref::<gtk4::Widget>()));
});
}
@ -247,21 +234,6 @@ pub mod imp {
obj.set_unread(false);
}
fn reload_current_article(&self) {
let id = self.current_article_id.borrow().clone();
let Some(id) = id else { return };
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::<ArticleObject>() {
if obj.article().id == id {
self.load_article_in_webview(&obj.article().clone());
break;
}
}
}
}
}
fn load_article_in_webview(&self, article: &crate::model::Article) {
let rules = self.filter_rules.borrow();
let content = crate::filters::apply(&rules, &article.id, &article.link, &article.content);
@ -755,11 +727,8 @@ 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);
}
fn do_mark_article_unread(&self, id: String) {
// Find the ArticleObject in the store and set unread=true.
// 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::<ArticleObject>() {
@ -770,11 +739,7 @@ pub mod imp {
}
}
// 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;
}
*self.mark_unread_guard.borrow_mut() = true;
let api = self.api.borrow().clone();
let wt = self.write_token.borrow().clone();
@ -843,7 +808,7 @@ pub mod imp {
fn update_sidebar_zoom(&self, level: f64) {
if let Some(css) = self.sidebar_zoom_css.get() {
css.load_from_string(&format!(
".sidebar-content {{ font-size: {level}em; }}"
".sidebar-content label {{ font-size: {level}em; }}"
));
}
}