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

View file

@ -27,6 +27,7 @@ template $ArticleRow : Gtk.Box {
}
Label title_label {
hexpand: true;
xalign: 0;
wrap: true;
lines: 2;
@ -34,6 +35,7 @@ template $ArticleRow : Gtk.Box {
}
Label excerpt_label {
hexpand: true;
xalign: 0;
ellipsize: end;
lines: 1;

View file

@ -41,6 +41,7 @@ corresponding .blp file and regenerate this file with blueprint-compiler.
</child>
<child>
<object class="GtkLabel" id="title_label">
<property name="hexpand">true</property>
<property name="xalign">0</property>
<property name="wrap">true</property>
<property name="lines">2</property>
@ -49,6 +50,7 @@ corresponding .blp file and regenerate this file with blueprint-compiler.
</child>
<child>
<object class="GtkLabel" id="excerpt_label">
<property name="hexpand">true</property>
<property name="xalign">0</property>
<property name="ellipsize">3</property>
<property name="lines">1</property>

View file

@ -0,0 +1,29 @@
using Gtk 4.0;
using Adw 1;
template $PreferencesDialog : Adw.Dialog {
title: _("Preferences");
content-width: 500;
content-height: 400;
Adw.ToolbarView {
[top]
Adw.HeaderBar {}
Adw.PreferencesPage {
Adw.PreferencesGroup {
title: _("Content Filters");
description: _("One rule per line: domain find replace [find replace …]\n\nExample:\n www.imycomic.com -150x150.jpg .jpg");
TextView filters_text_view {
monospace: true;
wrap-mode: word;
top-margin: 8;
bottom-margin: 8;
left-margin: 8;
right-margin: 8;
}
}
}
}
}

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
DO NOT EDIT!
This file was @generated by blueprint-compiler. Instead, edit the
corresponding .blp file and regenerate this file with blueprint-compiler.
-->
<interface>
<requires lib="gtk" version="4.0"/>
<template class="PreferencesDialog" parent="AdwDialog">
<property name="title" translatable="yes">Preferences</property>
<property name="content-width">500</property>
<property name="content-height">400</property>
<child>
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar"></object>
</child>
<child>
<object class="AdwPreferencesPage">
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">Content Filters</property>
<property name="description" translatable="yes">One rule per line: domain find replace [find replace …]
Example:
www.imycomic.com -150x150.jpg .jpg</property>
<child>
<object class="GtkTextView" id="filters_text_view">
<property name="monospace">true</property>
<property name="wrap-mode">2</property>
<property name="top-margin">8</property>
<property name="bottom-margin">8</property>
<property name="left-margin">8</property>
<property name="right-margin">8</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -79,6 +79,11 @@ ShortcutsWindow help_overlay {
accelerator: "<Control>0";
}
ShortcutsShortcut {
title: _("Toggle sidebar");
accelerator: "F9";
}
ShortcutsShortcut {
title: _("Toggle fullscreen");
accelerator: "F11";

View file

@ -97,6 +97,12 @@ corresponding .blp file and regenerate this file with blueprint-compiler.
<property name="accelerator">&lt;Control&gt;0</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Toggle sidebar</property>
<property name="accelerator">F9</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Toggle fullscreen</property>

View file

@ -7,121 +7,140 @@ template $FeedTheMonkeyWindow : Adw.ApplicationWindow {
default-height: 600;
Adw.ToastOverlay toast_overlay {
Adw.NavigationSplitView split_view {
sidebar: Adw.NavigationPage {
title: _("FeedTheMonkey");
Paned paned {
focusable: false;
shrink-start-child: false;
resize-start-child: false;
Adw.ToolbarView {
[top]
Adw.HeaderBar {
[start]
Stack refresh_stack {
StackPage {
name: "button";
child: Button refresh_button {
icon-name: "view-refresh-symbolic";
tooltip-text: _("Refresh");
action-name: "win.reload";
};
}
start-child: Adw.ToolbarView sidebar_toolbar {
top-bar-style: raised;
StackPage {
name: "spinner";
child: Adw.Spinner {};
}
[top]
Adw.HeaderBar {
show-start-title-buttons: false;
show-end-title-buttons: false;
title-widget: Box {};
[start]
Stack refresh_stack {
StackPage {
name: "button";
child: Button refresh_button {
icon-name: "view-refresh-symbolic";
tooltip-text: _("Refresh");
action-name: "win.reload";
};
}
[end]
MenuButton menu_button {
icon-name: "open-menu-symbolic";
primary: true;
menu-model: primary_menu;
StackPage {
name: "spinner";
child: Spinner {
spinning: true;
width-request: 16;
height-request: 16;
};
}
}
Stack sidebar_content {
StackPage {
name: "placeholder";
child: Adw.StatusPage {
icon-name: "rss-symbolic";
title: _("FeedTheMonkey");
description: _("Log in to load your articles");
};
}
[end]
MenuButton menu_button {
icon-name: "open-menu-symbolic";
primary: true;
menu-model: primary_menu;
}
}
StackPage {
name: "loading";
child: Adw.StatusPage {
paintable: Adw.SpinnerPaintable {};
title: _("Loading…");
};
}
Stack sidebar_content {
styles ["sidebar-content"]
StackPage {
name: "empty";
child: Adw.StatusPage {
icon-name: "rss-symbolic";
title: _("No Unread Articles");
};
}
StackPage {
name: "placeholder";
child: Adw.StatusPage {
icon-name: "rss-symbolic";
title: _("FeedTheMonkey");
description: _("Log in to load your articles");
};
}
StackPage {
name: "error";
child: Adw.StatusPage error_status {
icon-name: "network-error-symbolic";
title: _("Could Not Load Articles");
StackPage {
name: "loading";
child: Adw.StatusPage {
title: _("Loading…");
};
}
Button {
label: _("Try Again");
halign: center;
action-name: "win.reload";
styles ["pill", "suggested-action"]
}
};
}
StackPage {
name: "empty";
child: Adw.StatusPage {
icon-name: "rss-symbolic";
title: _("No Unread Articles");
};
}
StackPage {
name: "list";
child: ScrolledWindow {
hscrollbar-policy: never;
ListView article_list_view {
single-click-activate: true;
}
};
}
StackPage {
name: "error";
child: Adw.StatusPage error_status {
icon-name: "network-error-symbolic";
title: _("Could Not Load Articles");
Button {
label: _("Try Again");
halign: center;
action-name: "win.reload";
styles ["pill", "suggested-action"]
}
};
}
StackPage {
name: "list";
child: ScrolledWindow {
hscrollbar-policy: never;
ListView article_list_view {
single-click-activate: false;
show-separators: true;
}
};
}
}
};
content: Adw.NavigationPage content_page {
title: _("FeedTheMonkey");
end-child: Adw.ToolbarView {
top-bar-style: raised;
Adw.ToolbarView {
top-bar-style: raised;
[top]
Adw.HeaderBar {
[end]
MenuButton article_menu_button {
icon-name: "view-more-symbolic";
menu-model: article_menu;
visible: false;
}
[top]
Adw.HeaderBar {
[start]
Button toggle_sidebar_button {
icon-name: "sidebar-show-symbolic";
tooltip-text: _("Toggle Sidebar");
action-name: "win.toggle-sidebar";
}
Stack content_stack {
StackPage {
name: "empty";
child: Adw.StatusPage {
icon-name: "document-open-symbolic";
title: _("No Article Selected");
};
}
title-widget: Adw.WindowTitle {
title: _("FeedTheMonkey");
};
StackPage {
name: "webview";
child: WebKit.WebView web_view {};
}
[end]
MenuButton article_menu_button {
icon-name: "view-more-symbolic";
menu-model: article_menu;
visible: false;
}
}
Stack content_stack {
StackPage {
name: "empty";
child: Adw.StatusPage {
icon-name: "document-open-symbolic";
title: _("No Article Selected");
};
}
StackPage {
name: "webview";
child: WebKit.WebView web_view {};
}
}
};
@ -137,6 +156,13 @@ menu primary_menu {
}
}
section {
item {
label: _("Preferences");
action: "win.preferences";
}
}
section {
item {
label: _("Keyboard Shortcuts");

View file

@ -12,166 +12,182 @@ corresponding .blp file and regenerate this file with blueprint-compiler.
<child>
<object class="AdwToastOverlay" id="toast_overlay">
<child>
<object class="AdwNavigationSplitView" id="split_view">
<property name="sidebar">
<object class="AdwNavigationPage">
<property name="title" translatable="yes">FeedTheMonkey</property>
<child>
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<child type="start">
<object class="GtkStack" id="refresh_stack">
<child>
<object class="GtkStackPage">
<property name="name">button</property>
<property name="child">
<object class="GtkButton" id="refresh_button">
<property name="icon-name">view-refresh-symbolic</property>
<property name="tooltip-text" translatable="yes">Refresh</property>
<property name="action-name">win.reload</property>
</object>
</property>
<object class="GtkPaned" id="paned">
<property name="focusable">false</property>
<property name="shrink-start-child">false</property>
<property name="resize-start-child">false</property>
<property name="start-child">
<object class="AdwToolbarView" id="sidebar_toolbar">
<property name="top-bar-style">1</property>
<child type="top">
<object class="AdwHeaderBar">
<property name="show-start-title-buttons">false</property>
<property name="show-end-title-buttons">false</property>
<property name="title-widget">
<object class="GtkBox"></object>
</property>
<child type="start">
<object class="GtkStack" id="refresh_stack">
<child>
<object class="GtkStackPage">
<property name="name">button</property>
<property name="child">
<object class="GtkButton" id="refresh_button">
<property name="icon-name">view-refresh-symbolic</property>
<property name="tooltip-text" translatable="yes">Refresh</property>
<property name="action-name">win.reload</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">spinner</property>
<property name="child">
<object class="AdwSpinner"></object>
</property>
</object>
</child>
</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton" id="menu_button">
<property name="icon-name">open-menu-symbolic</property>
<property name="primary">true</property>
<property name="menu-model">primary_menu</property>
<child>
<object class="GtkStackPage">
<property name="name">spinner</property>
<property name="child">
<object class="GtkSpinner">
<property name="spinning">true</property>
<property name="width-request">16</property>
<property name="height-request">16</property>
</object>
</property>
</object>
</child>
</object>
</child>
<child type="end">
<object class="GtkMenuButton" id="menu_button">
<property name="icon-name">open-menu-symbolic</property>
<property name="primary">true</property>
<property name="menu-model">primary_menu</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkStack" id="sidebar_content">
<style>
<class name="sidebar-content"/>
</style>
<child>
<object class="GtkStack" id="sidebar_content">
<child>
<object class="GtkStackPage">
<property name="name">placeholder</property>
<property name="child">
<object class="AdwStatusPage">
<property name="icon-name">rss-symbolic</property>
<property name="title" translatable="yes">FeedTheMonkey</property>
<property name="description" translatable="yes">Log in to load your articles</property>
</object>
</property>
<object class="GtkStackPage">
<property name="name">placeholder</property>
<property name="child">
<object class="AdwStatusPage">
<property name="icon-name">rss-symbolic</property>
<property name="title" translatable="yes">FeedTheMonkey</property>
<property name="description" translatable="yes">Log in to load your articles</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="AdwStatusPage">
<property name="paintable">
<object class="AdwSpinnerPaintable"></object>
</property>
<property name="title" translatable="yes">Loading…</property>
</object>
</property>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="AdwStatusPage">
<property name="title" translatable="yes">Loading…</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">empty</property>
<property name="child">
<object class="AdwStatusPage">
<property name="icon-name">rss-symbolic</property>
<property name="title" translatable="yes">No Unread Articles</property>
</object>
</property>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">empty</property>
<property name="child">
<object class="AdwStatusPage">
<property name="icon-name">rss-symbolic</property>
<property name="title" translatable="yes">No Unread Articles</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">error</property>
<property name="child">
<object class="AdwStatusPage" id="error_status">
<property name="icon-name">network-error-symbolic</property>
<property name="title" translatable="yes">Could Not Load Articles</property>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Try Again</property>
<property name="halign">3</property>
<property name="action-name">win.reload</property>
<style>
<class name="pill"/>
<class name="suggested-action"/>
</style>
</object>
</child>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">error</property>
<property name="child">
<object class="AdwStatusPage" id="error_status">
<property name="icon-name">network-error-symbolic</property>
<property name="title" translatable="yes">Could Not Load Articles</property>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Try Again</property>
<property name="halign">3</property>
<property name="action-name">win.reload</property>
<style>
<class name="pill"/>
<class name="suggested-action"/>
</style>
</object>
</property>
</child>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">list</property>
<property name="child">
<object class="GtkScrolledWindow">
<property name="hscrollbar-policy">2</property>
<child>
<object class="GtkListView" id="article_list_view">
<property name="single-click-activate">true</property>
</object>
</child>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">list</property>
<property name="child">
<object class="GtkScrolledWindow">
<property name="hscrollbar-policy">2</property>
<child>
<object class="GtkListView" id="article_list_view">
<property name="single-click-activate">false</property>
<property name="show-separators">true</property>
</object>
</property>
</child>
</object>
</child>
</property>
</object>
</child>
</object>
</child>
</object>
</property>
<property name="content">
<object class="AdwNavigationPage" id="content_page">
<property name="title" translatable="yes">FeedTheMonkey</property>
<property name="end-child">
<object class="AdwToolbarView">
<property name="top-bar-style">1</property>
<child type="top">
<object class="AdwHeaderBar">
<child type="start">
<object class="GtkButton" id="toggle_sidebar_button">
<property name="icon-name">sidebar-show-symbolic</property>
<property name="tooltip-text" translatable="yes">Toggle Sidebar</property>
<property name="action-name">win.toggle-sidebar</property>
</object>
</child>
<property name="title-widget">
<object class="AdwWindowTitle">
<property name="title" translatable="yes">FeedTheMonkey</property>
</object>
</property>
<child type="end">
<object class="GtkMenuButton" id="article_menu_button">
<property name="icon-name">view-more-symbolic</property>
<property name="menu-model">article_menu</property>
<property name="visible">false</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwToolbarView">
<property name="top-bar-style">1</property>
<child type="top">
<object class="AdwHeaderBar">
<child type="end">
<object class="GtkMenuButton" id="article_menu_button">
<property name="icon-name">view-more-symbolic</property>
<property name="menu-model">article_menu</property>
<property name="visible">false</property>
<object class="GtkStack" id="content_stack">
<child>
<object class="GtkStackPage">
<property name="name">empty</property>
<property name="child">
<object class="AdwStatusPage">
<property name="icon-name">document-open-symbolic</property>
<property name="title" translatable="yes">No Article Selected</property>
</object>
</child>
</property>
</object>
</child>
<child>
<object class="GtkStack" id="content_stack">
<child>
<object class="GtkStackPage">
<property name="name">empty</property>
<property name="child">
<object class="AdwStatusPage">
<property name="icon-name">document-open-symbolic</property>
<property name="title" translatable="yes">No Article Selected</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">webview</property>
<property name="child">
<object class="WebKitWebView" id="web_view"></object>
</property>
</object>
</child>
<object class="GtkStackPage">
<property name="name">webview</property>
<property name="child">
<object class="WebKitWebView" id="web_view"></object>
</property>
</object>
</child>
</object>
@ -190,6 +206,12 @@ corresponding .blp file and regenerate this file with blueprint-compiler.
<attribute name="action">win.logout</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Preferences</attribute>
<attribute name="action">win.preferences</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Keyboard Shortcuts</attribute>