ui: overhaul sidebar, add content filters and state improvements

Sidebar layout:
- Replace AdwNavigationSplitView with GtkPaned for a resizable sidebar
  with a persistent width stored in GSettings.
- Apply navigation-sidebar CSS class to the content Stack only (not the
  ToolbarView) so both header bars share the same colour and height.
- Override Adwaita's automatic paned-first-child header tint and gap via
  application-level CSS.
- Remove the gap between the sidebar header and the first list item.
- Add toggle-sidebar button and F9 shortcut; sidebar visibility and width
  are persisted across restarts.

Loading indicator:
- Replace the large AdwSpinner status page + header Stack with a small
  Gtk.Spinner (16×16) in the header Stack so the header height never
  changes during loading.

Article row:
- Add hexpand to title and excerpt labels so text reflows when the
  sidebar is resized.

Content:
- Inline CSS into the HTML template at load time (/*INJECT_CSS*/
  placeholder) so WebKit does not need a custom URI scheme handler.
- Fix max-width centering and padding for article body and header.
- Fix embedded video/iframe auto-opening in browser by checking
  NavigationType::LinkClicked instead of is_user_gesture().

Content filters:
- Add Preferences dialog with a TextView for content-rewrite rules
  stored in GSettings (content-filters key).
- Rule format: "domain find replace [find replace …]" one per line.
- Rules are applied to article HTML before display and reloaded on
  every refresh.

Shortcuts:
- Add Ctrl+W to close, Ctrl+Q to quit, F1 for keyboard shortcuts
  overlay, j/k and arrow-key navigation via a capture-phase controller
  so keys work regardless of which widget has focus.

Misc:
- Set window title to "FeedTheMonkey" (fixes Hyprland title bar).
- Update About dialog website URL.
This commit is contained in:
Jeena 2026-03-21 01:13:01 +00:00
parent 141f9ee32d
commit 8fd52dd8a0
16 changed files with 680 additions and 320 deletions

75
src/preferences_dialog.rs Normal file
View file

@ -0,0 +1,75 @@
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use gtk4::{gio, glib};
glib::wrapper! {
pub struct PreferencesDialog(ObjectSubclass<imp::PreferencesDialog>)
@extends libadwaita::Dialog, gtk4::Widget,
@implements gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget;
}
impl PreferencesDialog {
pub fn new() -> Self {
glib::Object::builder().build()
}
}
pub mod imp {
use super::*;
use gtk4::CompositeTemplate;
use libadwaita::subclass::prelude::*;
#[derive(CompositeTemplate, Default)]
#[template(resource = "/net/jeena/FeedTheMonkey/ui/preferences_dialog.ui")]
pub struct PreferencesDialog {
#[template_child]
pub filters_text_view: TemplateChild<gtk4::TextView>,
}
#[glib::object_subclass]
impl ObjectSubclass for PreferencesDialog {
const NAME: &'static str = "PreferencesDialog";
type Type = super::PreferencesDialog;
type ParentType = libadwaita::Dialog;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for PreferencesDialog {
fn constructed(&self) {
self.parent_constructed();
self.load();
// Save on every text change
let obj_weak = self.obj().downgrade();
self.filters_text_view.buffer().connect_changed(move |_| {
if let Some(obj) = obj_weak.upgrade() {
obj.imp().save();
}
});
}
}
impl PreferencesDialog {
fn load(&self) {
let settings = gio::Settings::new("net.jeena.FeedTheMonkey");
let text = settings.string("content-filters");
self.filters_text_view.buffer().set_text(&text);
}
fn save(&self) {
let buf = self.filters_text_view.buffer();
let text = buf.text(&buf.start_iter(), &buf.end_iter(), false);
let settings = gio::Settings::new("net.jeena.FeedTheMonkey");
settings.set_string("content-filters", &text).ok();
}
}
impl WidgetImpl for PreferencesDialog {}
impl AdwDialogImpl for PreferencesDialog {}
}