Commit graph

212 commits

Author SHA1 Message Date
41312f48f3 window: only mark articles as read when navigating away
Previously the sidebar immediately showed an article as read the
moment it was selected, while the server was only notified when
the user moved to the next article.  Align the two: mark the
previous article as read in both the sidebar and on the server
at the same time, when the user navigates away from it.
2026-03-28 22:57:04 +00:00
d39c7f824b window: preserve reading position across server reloads
After a server refresh the article list is rebuilt, which previously
always reloaded the WebView and reset the scroll position.

- Disable SingleSelection autoselect so store rebuilds don't trigger
  spurious selection changes.
- Skip the WebView reload in on_article_selected when the same article
  is re-selected, preserving the user's scroll position.
- When the previously-read article is no longer in the server response
  (read on another device), leave the sidebar unselected and keep the
  old article visible so the user can finish reading.
- Handle no-selection state in navigate_by() so j/k still work.
- Show a toast with the unread count after every successful fetch.
2026-03-27 23:35:39 +00:00
07b41c7407 window: focus content area instead of sidebar on startup
Scroll events were going to the sidebar list because GTK's default
focus traversal landed on the article_list_view.  Call grab_focus()
on the WebView at the end of constructed() so the content area
receives input by default.
2026-03-27 23:35:32 +00:00
0917f57dbd filters: apply content filters before image-cache URL rewriting
image_cache::process() percent-encodes image URLs into feedthemonkey-img://
scheme URIs (e.g. /thumbs/ becomes %2Fthumbs%2F). Filters applied after
this step can no longer match the original substrings, so a rule like
  www.stuttmann-karikaturen.de  /thumbs/  /
had no effect when image caching was enabled.

Fix by applying filters to the raw article content before passing it to
image_cache::process(), so the corrected URLs are what get encoded into
the cache scheme. The render-time filter pass in load_article_in_webview
remains as a no-op for already-filtered content and still works correctly
for the non-cached code path.
2026-03-22 11:09:28 +00:00
126bd19770 build: regenerate window.ui from blueprint 2026-03-22 04:22:21 +00:00
60fc9d7cfd window: prevent paned from covering the content area
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.
2026-03-22 04:21:28 +00:00
d75c63a8ce window: fix sidebar snap-collapse when dragged narrow
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.
2026-03-22 04:20:02 +00:00
6d6d928733 window: collapse sidebar when dragged too narrow; keep controls accessible
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.
2026-03-22 02:10:37 +00:00
db41a691e6 window: prevent sidebar from covering the content area
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.
2026-03-22 02:06:57 +00:00
2c5217e744 api: fix unread detection broken by substring match
The check `.contains("user/-/state/com.google/read")` was a substring
match, which also matched "user/-/state/com.google/reading-list" — a
category present on every article fetched from the reading list. This
caused all articles to be treated as read, so nothing ever appeared
bold in the sidebar.

Fix by using == for exact string comparison.
2026-03-22 01:58:51 +00:00
9a4bf4b9f8 article-row: fix bold on initial load, add right-click menu
Set unread bold state directly in bind() instead of relying on
obj.notify("unread"), which was unreliable during list factory binding
(GLib may defer or drop notifications during initial bind).

Also add a right-click context menu on each article row with a single
"Mark as Unread" item. The menu is a GtkPopover positioned at the
cursor. Clicking it activates the new win.mark-article-unread action,
which takes the article ID as a string parameter and reuses the
existing mark-unread logic.

Refactor do_mark_unread() to delegate to the new do_mark_article_unread()
so the behaviour is consistent whether triggered from the toolbar button,
keyboard shortcut, or right-click menu.
2026-03-22 01:51:12 +00:00
571d80fa6b api: decode HTML entities in article excerpts 2026-03-22 01:17:49 +00:00
81439edf87 sidebar: use Pango markup for bold, fix zoom CSS specificity 2026-03-22 00:40:20 +00:00
e5f2d5c941 filters: reload current article immediately when preferences closes 2026-03-22 00:37:10 +00:00
b63549ae0a sidebar: fix unread bold via notify, larger title font, sidebar zoom 2026-03-22 00:34:51 +00:00
c19c2cbd1d window: scale sidebar fonts with zoom level 2026-03-21 13:09:28 +00:00
3bcd6af23c sidebar: use CSS class for bold unread title 2026-03-21 12:51:49 +00:00
a77fa3ae03 sidebar: bold title for unread articles 2026-03-21 12:49:58 +00:00
03b1936740 docs: fix image caching description in README 2026-03-21 12:47:06 +00:00
b280d83234 docs: add features list and river of news description to README 2026-03-21 12:44:31 +00:00
a4a01a6394 docs: add screenshot and trivia section to README 2026-03-21 12:39:41 +00:00
de47b21a52 docs: add app icon to README 2026-03-21 12:32:13 +00:00
8dc71214aa data: replace generated SVG icon with original PNG 2026-03-21 12:27:13 +00:00
dec1bfdc7e docs: remove completed backlog 2026-03-21 12:25:19 +00:00
e17f0c622b login: update compiled UI with server URL hint 2026-03-21 12:24:19 +00:00
88afb27a22 login: add server URL format hint to login dialog 2026-03-21 12:17:55 +00:00
f05eec6d53 docs: fix README intro formatting 2026-03-21 12:02:48 +00:00
68c4f6ccfe docs: clarify server URL format for FreshRSS and Miniflux 2026-03-21 12:01:48 +00:00
ff92499406 docs: name supported servers explicitly instead of implying universal detection 2026-03-21 11:59:00 +00:00
b49cc69c49 api: auto-detect Greader API path for Miniflux and FreshRSS
Miniflux serves the Greader API at the server root while FreshRSS uses
/api/greader.php. Instead of hardcoding the FreshRSS suffix, try the
URL as-is first (works for Miniflux) and fall back to appending
/api/greader.php (works for FreshRSS). The user just enters the server
URL without needing to know the API path.
2026-03-21 11:57:19 +00:00
82aabc080a docs: remove TT-RSS from compatible servers list 2026-03-21 11:53:36 +00:00
6afab6f421 docs: clarify app works with any Greader API server 2026-03-21 11:37:18 +00:00
ed10ba1310 docs: add README with build and runtime dependencies 2026-03-21 11:36:44 +00:00
85b05a14bc data: add app icon, desktop entry, and install script 2026-03-21 03:14:15 +00:00
668c73c8d2 window: show toast instead of login dialog when offline with cached articles 2026-03-21 03:05:19 +00:00
d6858b62a7 webview: disable context menu 2026-03-21 02:49:05 +00:00
9bed643023 window: prefetch images and queue offline read/unread actions
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.
2026-03-21 02:45:45 +00:00
3f759bce2e image-cache: rewrite URLs eagerly, download lazily via scheme handler
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.
2026-03-21 02:37:06 +00:00
00700c3211 image-cache: use custom URI scheme for transparent cache-miss re-download
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.
2026-03-21 01:33:40 +00:00
8e21c80a33 cache: store all cached data under XDG_CACHE_HOME
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.
2026-03-21 01:28:11 +00:00
fda441bebd feature: cache article images for offline reading
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.
2026-03-21 01:19:49 +00:00
183191727b window: persist article list and open article across restarts
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.
2026-03-21 01:13:09 +00:00
8fd52dd8a0 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.
2026-03-21 01:13:01 +00:00
141f9ee32d fix: show human-readable login errors instead of raw HTML
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)
2026-03-20 12:21:01 +00:00
5dee5cc52b fix: tokio runtime, Enter-to-login, and server URL handling
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.
2026-03-20 12:17:27 +00:00
d157f3f244 gitignore: exclude compiled schema file 2026-03-20 11:57:09 +00:00
813dda3579 app: implement Epics 2–10
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
2026-03-20 11:57:06 +00:00
8db0b16954 scaffold: compile GSettings schema at build time for dev runs
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.
2026-03-20 11:36:12 +00:00
3339bb5ec8 scaffold: Epic 1 — project scaffold
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.
2026-03-20 11:22:19 +00:00
3196988c98 scaffold: start v3 rewrite from scratch
Remove all Qt5/C++/QML source files to begin a full rewrite in
Rust + GTK4 + libadwaita. The BACKLOG.md describes the plan.
2026-03-20 11:16:27 +00:00