Compare commits
6 commits
c19c2cbd1d
...
2c5217e744
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c5217e744 | |||
| 9a4bf4b9f8 | |||
| 571d80fa6b | |||
| 81439edf87 | |||
| e5f2d5c941 | |||
| b63549ae0a |
6 changed files with 135 additions and 28 deletions
|
|
@ -32,6 +32,7 @@ template $ArticleRow : Gtk.Box {
|
|||
wrap: true;
|
||||
lines: 2;
|
||||
ellipsize: end;
|
||||
styles ["article-title"]
|
||||
}
|
||||
|
||||
Label excerpt_label {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ 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>
|
||||
|
|
|
|||
19
src/api.rs
19
src/api.rs
|
|
@ -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.contains("user/-/state/com.google/read"));
|
||||
.any(|c| c == "user/-/state/com.google/read");
|
||||
let content = item.summary
|
||||
.as_ref()
|
||||
.and_then(|s| s.content.clone())
|
||||
|
|
@ -249,9 +249,20 @@ fn plain_text_excerpt(html: &str, max_chars: usize) -> String {
|
|||
}
|
||||
}
|
||||
let collapsed: String = out.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
if collapsed.chars().count() <= max_chars {
|
||||
collapsed
|
||||
let decoded = decode_html_entities(&collapsed);
|
||||
if decoded.chars().count() <= max_chars {
|
||||
decoded
|
||||
} else {
|
||||
collapsed.chars().take(max_chars).collect::<String>() + "…"
|
||||
decoded.chars().take(max_chars).collect::<String>() + "…"
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_html_entities(s: &str) -> String {
|
||||
s.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace(""", "\"")
|
||||
.replace("'", "'")
|
||||
.replace("'", "'")
|
||||
.replace(" ", " ")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ mod imp {
|
|||
.sidebar-content row:selected {
|
||||
background-color: alpha(@window_fg_color, 0.22);
|
||||
}
|
||||
.unread-title {
|
||||
font-weight: bold;
|
||||
.article-title {
|
||||
font-size: 1.05em;
|
||||
}"
|
||||
);
|
||||
gtk4::style_context_add_provider_for_display(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ 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]
|
||||
|
|
@ -61,32 +62,98 @@ 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.title_label.set_text(&article.title);
|
||||
self.excerpt_label.set_text(&article.excerpt);
|
||||
self.date_label.set_text(&relative_time(article.published));
|
||||
|
||||
self.update_read_style(article.unread);
|
||||
// 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);
|
||||
}
|
||||
drop(article);
|
||||
|
||||
// Watch for unread state changes
|
||||
// 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 unread = obj.article().unread;
|
||||
if unread {
|
||||
let article = obj.article();
|
||||
let escaped = glib::markup_escape_text(&article.title);
|
||||
if article.unread {
|
||||
title_label.remove_css_class("dim-label");
|
||||
title_label.add_css_class("unread-title");
|
||||
title_label.set_markup(&format!("<b>{escaped}</b>"));
|
||||
} else {
|
||||
title_label.add_css_class("dim-label");
|
||||
title_label.remove_css_class("unread-title");
|
||||
title_label.set_markup(&escaped);
|
||||
}
|
||||
});
|
||||
*self.unread_handler.borrow_mut() = Some((obj.clone(), id));
|
||||
|
|
@ -100,16 +167,6 @@ 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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::<String>()) {
|
||||
win.imp().do_mark_article_unread(id);
|
||||
}
|
||||
});
|
||||
klass.install_action("win.open-in-browser", None, |win, _, _| {
|
||||
win.imp().do_open_in_browser()
|
||||
});
|
||||
|
|
@ -101,6 +106,14 @@ 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>()));
|
||||
});
|
||||
}
|
||||
|
|
@ -234,6 +247,21 @@ 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);
|
||||
|
|
@ -727,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::<ArticleObject>() {
|
||||
|
|
@ -739,7 +770,11 @@ 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;
|
||||
}
|
||||
|
||||
let api = self.api.borrow().clone();
|
||||
let wt = self.write_token.borrow().clone();
|
||||
|
|
@ -808,7 +843,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 label {{ font-size: {level}em; }}"
|
||||
".sidebar-content {{ font-size: {level}em; }}"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue