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

BIN
data/gschemas.compiled Normal file

Binary file not shown.

View file

@ -2,6 +2,9 @@
<gresources>
<gresource prefix="/net/jeena/FeedTheMonkey">
<file preprocess="xml-stripblanks">ui/window.ui</file>
<file preprocess="xml-stripblanks">ui/login_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/article_row.ui</file>
<file preprocess="xml-stripblanks">ui/shortcuts.ui</file>
<file>html/content.html</file>
<file>html/content.css</file>
</gresource>

42
data/ui/article_row.blp Normal file
View file

@ -0,0 +1,42 @@
using Gtk 4.0;
using Adw 1;
template $ArticleRow : Gtk.Box {
orientation: vertical;
margin-top: 12;
margin-bottom: 12;
margin-start: 12;
margin-end: 12;
spacing: 4;
Box {
orientation: horizontal;
spacing: 4;
Label feed_title_label {
hexpand: true;
xalign: 0;
ellipsize: end;
styles ["dim-label", "caption"]
}
Label date_label {
xalign: 1;
styles ["dim-label", "caption"]
}
}
Label title_label {
xalign: 0;
wrap: true;
lines: 2;
ellipsize: end;
}
Label excerpt_label {
xalign: 0;
ellipsize: end;
lines: 1;
styles ["dim-label", "caption"]
}
}

62
data/ui/article_row.ui Normal file
View file

@ -0,0 +1,62 @@
<?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="ArticleRow" parent="GtkBox">
<property name="orientation">1</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="spacing">4</property>
<child>
<object class="GtkBox">
<property name="orientation">0</property>
<property name="spacing">4</property>
<child>
<object class="GtkLabel" id="feed_title_label">
<property name="hexpand">true</property>
<property name="xalign">0</property>
<property name="ellipsize">3</property>
<style>
<class name="dim-label"/>
<class name="caption"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel" id="date_label">
<property name="xalign">1</property>
<style>
<class name="dim-label"/>
<class name="caption"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel" id="title_label">
<property name="xalign">0</property>
<property name="wrap">true</property>
<property name="lines">2</property>
<property name="ellipsize">3</property>
</object>
</child>
<child>
<object class="GtkLabel" id="excerpt_label">
<property name="xalign">0</property>
<property name="ellipsize">3</property>
<property name="lines">1</property>
<style>
<class name="dim-label"/>
<class name="caption"/>
</style>
</object>
</child>
</template>
</interface>

41
data/ui/login_dialog.blp Normal file
View file

@ -0,0 +1,41 @@
using Gtk 4.0;
using Adw 1;
template $LoginDialog : Adw.Dialog {
title: _("Log In");
content-width: 360;
Adw.ToolbarView {
[top]
Adw.HeaderBar {}
Adw.Clamp {
margin-top: 12;
margin-bottom: 24;
margin-start: 12;
margin-end: 12;
Adw.PreferencesGroup {
Adw.EntryRow server_url_row {
title: _("Server URL");
input-hints: no_spellcheck;
input-purpose: url;
}
Adw.EntryRow username_row {
title: _("Username");
input-hints: no_spellcheck;
}
Adw.PasswordEntryRow password_row {
title: _("Password");
}
Adw.ButtonRow login_button {
title: _("Log In");
styles ["suggested-action"]
}
}
}
}
}

58
data/ui/login_dialog.ui Normal file
View file

@ -0,0 +1,58 @@
<?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="LoginDialog" parent="AdwDialog">
<property name="title" translatable="yes">Log In</property>
<property name="content-width">360</property>
<child>
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar"></object>
</child>
<child>
<object class="AdwClamp">
<property name="margin-top">12</property>
<property name="margin-bottom">24</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwEntryRow" id="server_url_row">
<property name="title" translatable="yes">Server URL</property>
<property name="input-hints">2</property>
<property name="input-purpose">5</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="username_row">
<property name="title" translatable="yes">Username</property>
<property name="input-hints">2</property>
</object>
</child>
<child>
<object class="AdwPasswordEntryRow" id="password_row">
<property name="title" translatable="yes">Password</property>
</object>
</child>
<child>
<object class="AdwButtonRow" id="login_button">
<property name="title" translatable="yes">Log In</property>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

107
data/ui/shortcuts.blp Normal file
View file

@ -0,0 +1,107 @@
using Gtk 4.0;
using Adw 1;
ShortcutsWindow help_overlay {
modal: true;
ShortcutsSection {
section-name: "shortcuts";
max-height: 10;
ShortcutsGroup {
title: _("Navigation");
ShortcutsShortcut {
title: _("Next article");
accelerator: "j Right";
}
ShortcutsShortcut {
title: _("Previous article");
accelerator: "k Left";
}
}
ShortcutsGroup {
title: _("Article");
ShortcutsShortcut {
title: _("Open in browser");
accelerator: "Return n";
}
ShortcutsShortcut {
title: _("Mark as unread");
accelerator: "u";
}
ShortcutsShortcut {
title: _("Reload articles");
accelerator: "r";
}
}
ShortcutsGroup {
title: _("View");
ShortcutsShortcut {
title: _("Scroll down");
accelerator: "space Page_Down";
}
ShortcutsShortcut {
title: _("Scroll up");
accelerator: "Page_Up";
}
ShortcutsShortcut {
title: _("Scroll to top");
accelerator: "Home";
}
ShortcutsShortcut {
title: _("Scroll to bottom");
accelerator: "End";
}
ShortcutsShortcut {
title: _("Zoom in");
accelerator: "<Control>plus";
}
ShortcutsShortcut {
title: _("Zoom out");
accelerator: "<Control>minus";
}
ShortcutsShortcut {
title: _("Reset zoom");
accelerator: "<Control>0";
}
ShortcutsShortcut {
title: _("Toggle fullscreen");
accelerator: "F11";
}
}
ShortcutsGroup {
title: _("Application");
ShortcutsShortcut {
title: _("Keyboard shortcuts");
accelerator: "F1";
}
ShortcutsShortcut {
title: _("Close window");
accelerator: "<Control>w";
}
ShortcutsShortcut {
title: _("Quit");
accelerator: "<Control>q";
}
}
}
}

134
data/ui/shortcuts.ui Normal file
View file

@ -0,0 +1,134 @@
<?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"/>
<object class="GtkShortcutsWindow" id="help_overlay">
<property name="modal">true</property>
<child>
<object class="GtkShortcutsSection">
<property name="section-name">shortcuts</property>
<property name="max-height">10</property>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes">Navigation</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Next article</property>
<property name="accelerator">j Right</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Previous article</property>
<property name="accelerator">k Left</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes">Article</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Open in browser</property>
<property name="accelerator">Return n</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Mark as unread</property>
<property name="accelerator">u</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Reload articles</property>
<property name="accelerator">r</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes">View</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Scroll down</property>
<property name="accelerator">space Page_Down</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Scroll up</property>
<property name="accelerator">Page_Up</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Scroll to top</property>
<property name="accelerator">Home</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Scroll to bottom</property>
<property name="accelerator">End</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Zoom in</property>
<property name="accelerator">&lt;Control&gt;plus</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Zoom out</property>
<property name="accelerator">&lt;Control&gt;minus</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Reset zoom</property>
<property name="accelerator">&lt;Control&gt;0</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Toggle fullscreen</property>
<property name="accelerator">F11</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes">Application</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Keyboard shortcuts</property>
<property name="accelerator">F1</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Close window</property>
<property name="accelerator">&lt;Control&gt;w</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Quit</property>
<property name="accelerator">&lt;Control&gt;q</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

View file

@ -1,5 +1,6 @@
using Gtk 4.0;
using Adw 1;
using WebKit 6.0;
template $FeedTheMonkeyWindow : Adw.ApplicationWindow {
default-width: 900;
@ -38,10 +39,56 @@ template $FeedTheMonkeyWindow : Adw.ApplicationWindow {
}
}
Adw.StatusPage placeholder_page {
icon-name: "rss-symbolic";
title: _("FeedTheMonkey");
description: _("Log in to load your articles");
Stack sidebar_content {
StackPage {
name: "placeholder";
child: Adw.StatusPage {
icon-name: "rss-symbolic";
title: _("FeedTheMonkey");
description: _("Log in to load your articles");
};
}
StackPage {
name: "loading";
child: Adw.StatusPage {
paintable: Adw.SpinnerPaintable {};
title: _("Loading…");
};
}
StackPage {
name: "empty";
child: Adw.StatusPage {
icon-name: "rss-symbolic";
title: _("No Unread Articles");
};
}
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: true;
}
};
}
}
}
};
@ -62,9 +109,19 @@ template $FeedTheMonkeyWindow : Adw.ApplicationWindow {
}
}
Adw.StatusPage {
icon-name: "document-open-symbolic";
title: _("No Article Selected");
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 {};
}
}
}
};

View file

@ -54,10 +54,80 @@ corresponding .blp file and regenerate this file with blueprint-compiler.
</object>
</child>
<child>
<object class="AdwStatusPage" id="placeholder_page">
<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 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>
</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>
</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>
</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>
</object>
</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">true</property>
</object>
</child>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
@ -82,9 +152,26 @@ corresponding .blp file and regenerate this file with blueprint-compiler.
</object>
</child>
<child>
<object class="AdwStatusPage">
<property name="icon-name">document-open-symbolic</property>
<property name="title" translatable="yes">No Article Selected</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>
</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>
</child>
</object>