Add shrink-end-child: false and width-request: 320 on the content
ToolbarView so the divider cannot be dragged all the way to the right
and hide the article view.
Remove shrink-start-child: false so the paned divider can actually be
dragged below the sidebar's natural minimum width. Defer the snap-close
via idle_add_local_once so widget visibility is not changed mid-drag.
When the paned divider is dragged left below 120 px, snap the sidebar
closed instead of leaving a sliver. This replaces the previous
minimum-width approach.
When the sidebar is hidden (via toggle or snap-close), show the refresh
button and primary menu in the content header so they remain reachable.
Both stacks are kept in sync (spinner/button) with their sidebar
counterparts during article fetches.
Add shrink-end-child: false to the Paned so the content panel cannot be
squeezed below its minimum size, and set width-request: 360 on the
content ToolbarView so there is always a visible reading area.
After fetching articles, all remote images referenced in article content
are downloaded to ~/.local/share/net.jeena.FeedTheMonkey/images/ and
their src attributes rewritten to file:// URIs. Subsequent loads of the
same article (including from the cache on the next startup) display
images without a network connection.
Metered-connection awareness: image caching is skipped automatically
when GIO reports the network connection as metered, regardless of the
preference setting.
A "Cache Images" toggle in Preferences lets the user disable caching
entirely (stored in the cache-images GSettings key).
After each refresh, images no longer referenced by any article in the
current unread list are deleted from the cache directory to prevent
unbounded disk growth.
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.
Three bugs fixed:
- No tokio reactor: glib::spawn_future_local does not provide a
tokio context, so reqwest/hyper panicked at runtime. Introduce
src/runtime.rs with a multi-thread tokio Runtime (init() called
from main before the GTK app starts). runtime::spawn() posts the
async result back to GTK via a tokio oneshot channel awaited by
glib::spawn_future_local, which only polls a flag (no I/O).
runtime::spawn_bg() is used for fire-and-forget background calls.
- Enter key didn't submit login: connect_apply on AdwEntryRow only
fires when show-apply-button is true. Switch to connect_entry_activated
which fires on Return in all three login rows.
- Wrong API URL: the app constructed /accounts/ClientLogin directly
off the server host, yielding a 404. Add normalize_base_url() in
api.rs that appends /api/greader.php when the URL doesn't already
contain it, so users can enter just https://rss.example.com.
Add the full application logic on top of the Epic 1 skeleton:
Epic 2 — Authentication
- LoginDialog (AdwDialog, Blueprint template) with server URL,
username, and password fields; emits logged-in signal on submit
- credentials.rs: store/load/clear via libsecret (password_store_sync /
password_search_sync / password_clear_sync, v0_19 feature)
- api.rs: Api::login() parses Auth= token from ClientLogin response;
fetch_write_token() fetches the write token
- Auto-login on startup from stored credentials; logout with
AdwAlertDialog confirmation; login errors shown in AdwAlertDialog
Epic 3 — Article fetching
- model.rs: Article struct and ArticleObject GObject wrapper with
unread property for list store binding
- Api::fetch_unread() deserializes Google Reader JSON, derives unread
from categories, generates plain-text excerpt
- Sidebar uses a GtkStack with placeholder / loading / empty / error /
list pages; AdwSpinnerPaintable while fetching; Try Again button
Epic 4 — Sidebar
- article_row.blp: composite template with feed title, date, title,
and excerpt labels
- ArticleRow GObject subclass; binds ArticleObject, watches unread
notify to apply .dim-label on the title; relative timestamp format
Epic 5 — Content pane
- content.html updated: setArticle(), checkKey(), feedthemonkey: URI
navigation scheme; dark mode via prefers-color-scheme
- content.css: proper article layout, dark mode, code blocks
- WebView loaded from GResource; decide-policy intercepts
feedthemonkey:{next,previous,open} and all external links
Epic 6 — Read state
- Api::mark_read() / mark_unread() via edit-tag endpoint
- Optimistic unread toggle on ArticleObject; background API calls;
mark_unread_guard prevents re-marking on navigation
- AdwToast shown on mark-unread
Epic 7 — Keyboard shortcuts
- GtkShortcutController on window for all shortcuts from the backlog
- shortcuts.blp: AdwShortcutsWindow documenting all shortcuts
- F1 opens shortcuts dialog; Ctrl+W closes window; Ctrl+Q quits
Epic 8 — Zoom
- zoom_in/zoom_out/zoom_reset wired to Ctrl+±/0; zoom level saved to
and restored from GSettings zoom-level key
Epic 9 — Window state persistence
- Window width/height/maximized saved on close, restored on open
- (Sidebar width deferred — AdwNavigationSplitView fraction binding)
Epic 10 — Polish
- AdwAboutDialog with app name, version, GPL-3.0, website
- Logout confirmation AdwAlertDialog with destructive button
- Win.toggle-fullscreen action (F11)
- Api dropped on window close to cancel in-flight requests
Add the full Rust + GTK4 + libadwaita project skeleton:
- Cargo.toml with all dependencies (gtk4 0.11, libadwaita 0.9,
webkit6 0.6, reqwest, serde, tokio, libsecret)
- build.rs that compiles Blueprint .blp files and bundles a GResource
- data/ui/window.blp — AdwApplicationWindow with AdwNavigationSplitView,
sidebar with refresh button/spinner and primary menu,
content page with article menu
- data/resources.gresource.xml bundling UI, HTML, and CSS
- data/net.jeena.FeedTheMonkey.gschema.xml with all GSettings keys
- html/content.html and html/content.css (minimal placeholders)
- src/main.rs, src/app.rs — AdwApplication with APP_ID net.jeena.FeedTheMonkey
- src/window.rs — AdwApplicationWindow GObject subclass loading the
Blueprint template and persisting window size in GSettings
- COPYING (GPL-3.0) restored from master
The app compiles and the binary is ready to open a blank window.