After a successful article refresh, all images referenced in article
content are downloaded in the background so articles can be read
offline. The prefetch only runs when the cache-images setting is
enabled and the connection is not metered.
Read/unread state changes that fail to reach the server (e.g. when
offline) are now persisted to a local queue in
~/.cache/net.jeena.FeedTheMonkey/pending_sync.json. The queue is
flushed at the start of the next successful fetch.
process() was downloading all images before returning, blocking the
article list update for potentially minutes on a first run or after a
cache wipe. Move all network I/O out of process():
- process() now only rewrites src="https://..." to the custom
feedthemonkey-img:/// scheme — it is synchronous and instant.
- The URI scheme handler already downloads and caches on demand, so
images are fetched the first time the WebView requests them and
served from disk on every subsequent view.
This means the article list appears immediately after a server fetch
regardless of how many images need caching.
Instead of rewriting img src to file:// URIs, rewrite to a custom
feedthemonkey-img:/// scheme. A WebKit URI scheme handler is registered
on the WebView's WebContext that:
- Serves the image directly from the cache directory if present.
- On a cache miss (e.g. after the user deletes ~/.cache), spawns a
reqwest download in the tokio runtime, then resumes on the GLib main
loop via glib::spawn_future_local and serves the freshly downloaded
bytes — all transparent to the WebView.
This means deleting the cache directory never results in permanently
broken images; they are silently re-fetched on first access.
Both cache.json (article list) and the images directory are
regeneratable from the server, so they belong in XDG_CACHE_HOME
(~/.cache/net.jeena.FeedTheMonkey/) rather than XDG_DATA_HOME.
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.
On shutdown the full article list (including current read/unread state)
and the ID of the open article are saved to
~/.local/share/net.jeena.FeedTheMonkey/cache.json.
On next launch:
- The cached articles are loaded into the list immediately, before any
network request, so the sidebar is populated and the previously open
article is visible without waiting for the server.
- The article content is injected into the WebView once its base HTML
finishes loading (LoadEvent::Finished), avoiding a race where
window.setArticle() did not yet exist.
- A background refresh then fetches fresh data from the server; if the
previously open article still exists its selection is preserved,
otherwise the first item is selected.
- Network errors during a background refresh show a toast instead of
replacing the visible article list with an error page.
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.
When the server returns an HTML response (wrong URL, redirect to a
login page), the error dialog previously showed the full HTML body.
Now detect HTML responses and show a short actionable message:
- 404 with HTML: 'API endpoint not found. Check your server URL.'
- 401/403 with HTML: 'Wrong username or password.'
- 200 with HTML (no Auth= token): explain the endpoint is not FreshRSS
- Non-HTML bodies are shown as-is (they are already readable)
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
build.rs now runs glib-compile-schemas on data/ so that debug builds
can find the schema without a system-wide install. main.rs sets
GSETTINGS_SCHEMA_DIR from the build-time constant when running in
debug mode.
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.
Back in the day on OSX this has been used to pass the current item
to a application called Speaker which would read the content. That
application is not available anymore so we can remove the
functionality from FeedTheMonkey.
The colors are now closer to the Adwaita-dark mode which I'm using as
my primary theme. The header has been redesigned slightly to appear
more consistent with other default apps too.
Often I wanted to make the sidebar dissapear because I don't need it.
This patch let's you make it dissapear when it's thinner than 200px
and reappear if it's wider.
My old jabs.nu domain doesn't exist anymore, I moved the screenshot
and the logo file included in the readme to a different server.
The screenshot has been renewt to the dark mode one.
When running with AppImage and on Ubuntu I'm getting the error that
QtWebEngine 1.7 is not installed, it seems that per default 1.8 is
installed nowadays.
For some reason the JS never got loaded when it was in it's own
file, therefor I moved it into the HTML where it gets called.
Also when a session id on the server was expired or something, you
weren't able to log out, there is now code which fixes that.
For some reason in the latest Qt versions the webview took over the
focus from the keyboard, once clicked on the webview the arrow
keys wouldn't register up and thus you couldn't navigate with them
anymore.
This patch fixes this problem by using window.location.href and
checkinf for those special urls. This is way easier to use than
WebChannels.
There was no feedback on any login errors when a user provided a
wrong url, username, password or a disabled API. This commit
adds feedback to the user in this cases.
Fixes#15
Untill now the MenuBar was not visible untill you pressed the
alt-key, which made it visible. Sadly after that it was not
possible to hide it again. This patch fixes that.
As one of the first steps to offline capability we download all
images and put them into the JSON stirng and into the content
as data-uris for <img>-tags. This way you can go offline and keep
enjoying pictures in your feeds, at least untill you restart
FeedTheMonkey for now.