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
This commit is contained in:
Jeena 2026-03-20 11:57:06 +00:00
parent 8db0b16954
commit 813dda3579
22 changed files with 1838 additions and 42 deletions

View file

@ -1,4 +1,5 @@
use gtk4::prelude::*;
use libadwaita::prelude::*;
use crate::window::FeedTheMonkeyWindow;
@ -51,6 +52,46 @@ mod imp {
gio::resources_register(&resource);
let window = FeedTheMonkeyWindow::new(app.upcast_ref());
// Shortcuts overlay
let builder = gtk4::Builder::from_resource(
"/net/jeena/FeedTheMonkey/ui/shortcuts.ui",
);
let overlay: gtk4::ShortcutsWindow = builder.object("help_overlay").unwrap();
window.set_help_overlay(Some(&overlay));
setup_shortcuts(&window);
// About action on app
let app_weak = app.downgrade();
let about_action = gio::SimpleAction::new("about", None);
about_action.connect_activate(move |_, _| {
if let Some(app) = app_weak.upgrade() {
let win = app.active_window();
let dialog = libadwaita::AboutDialog::builder()
.application_name("FeedTheMonkey")
.application_icon("feedthemonkey")
.version("3.0.0")
.copyright("© Jeena Paradies")
.license_type(gtk4::License::Gpl30)
.website("https://github.com/jeena/FeedTheMonkey")
.developer_name("Jeena Paradies")
.build();
dialog.present(win.as_ref().map(|w| w.upcast_ref::<gtk4::Widget>()));
}
});
app.add_action(&about_action);
// Quit action
let app_weak = app.downgrade();
let quit_action = gio::SimpleAction::new("quit", None);
quit_action.connect_activate(move |_, _| {
if let Some(app) = app_weak.upgrade() {
app.quit();
}
});
app.add_action(&quit_action);
window.present();
}
}
@ -58,3 +99,39 @@ mod imp {
impl GtkApplicationImpl for FeedTheMonkeyApp {}
impl AdwApplicationImpl for FeedTheMonkeyApp {}
}
fn setup_shortcuts(window: &FeedTheMonkeyWindow) {
use gtk4::gdk::{Key, ModifierType};
let controller = gtk4::ShortcutController::new();
controller.set_scope(gtk4::ShortcutScope::Global);
let add = |controller: &gtk4::ShortcutController,
key: Key,
mods: ModifierType,
action_name: &str| {
let trigger = gtk4::KeyvalTrigger::new(key, mods);
let action = gtk4::NamedAction::new(action_name);
let shortcut = gtk4::Shortcut::new(Some(trigger), Some(action));
controller.add_shortcut(shortcut);
};
add(&controller, Key::j, ModifierType::empty(), "win.next-article");
add(&controller, Key::Right, ModifierType::empty(), "win.next-article");
add(&controller, Key::k, ModifierType::empty(), "win.prev-article");
add(&controller, Key::Left, ModifierType::empty(), "win.prev-article");
add(&controller, Key::r, ModifierType::empty(), "win.reload");
add(&controller, Key::u, ModifierType::empty(), "win.mark-unread");
add(&controller, Key::Return, ModifierType::empty(), "win.open-in-browser");
add(&controller, Key::n, ModifierType::empty(), "win.open-in-browser");
add(&controller, Key::plus, ModifierType::CONTROL_MASK, "win.zoom-in");
add(&controller, Key::equal, ModifierType::CONTROL_MASK, "win.zoom-in");
add(&controller, Key::minus, ModifierType::CONTROL_MASK, "win.zoom-out");
add(&controller, Key::_0, ModifierType::CONTROL_MASK, "win.zoom-reset");
add(&controller, Key::F11, ModifierType::empty(), "win.toggle-fullscreen");
add(&controller, Key::w, ModifierType::CONTROL_MASK, "window.close");
add(&controller, Key::q, ModifierType::CONTROL_MASK, "app.quit");
add(&controller, Key::F1, ModifierType::empty(), "win.show-help-overlay");
window.add_controller(controller);
}