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:
parent
8db0b16954
commit
813dda3579
22 changed files with 1838 additions and 42 deletions
76
src/model.rs
Normal file
76
src/model.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
use gtk4::glib;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::subclass::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Article {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub feed_title: String,
|
||||
pub author: String,
|
||||
pub link: String,
|
||||
pub published: i64,
|
||||
pub content: String,
|
||||
pub excerpt: String,
|
||||
pub unread: bool,
|
||||
}
|
||||
|
||||
// ── GObject wrapper ──────────────────────────────────────────────────────────
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ArticleObject(ObjectSubclass<imp::ArticleObject>);
|
||||
}
|
||||
|
||||
impl ArticleObject {
|
||||
pub fn new(article: Article) -> Self {
|
||||
let obj: Self = glib::Object::new();
|
||||
*obj.imp().article.borrow_mut() = article;
|
||||
obj
|
||||
}
|
||||
|
||||
pub fn article(&self) -> std::cell::Ref<'_, Article> {
|
||||
self.imp().article.borrow()
|
||||
}
|
||||
|
||||
pub fn set_unread(&self, unread: bool) {
|
||||
self.imp().article.borrow_mut().unread = unread;
|
||||
self.notify("unread");
|
||||
}
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ArticleObject {
|
||||
pub article: RefCell<Article>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ArticleObject {
|
||||
const NAME: &'static str = "ArticleObject";
|
||||
type Type = super::ArticleObject;
|
||||
}
|
||||
|
||||
impl ObjectImpl for ArticleObject {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
use std::sync::OnceLock;
|
||||
static PROPS: OnceLock<Vec<glib::ParamSpec>> = OnceLock::new();
|
||||
PROPS.get_or_init(|| {
|
||||
vec![
|
||||
glib::ParamSpecBoolean::builder("unread")
|
||||
.read_only()
|
||||
.build(),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"unread" => self.article.borrow().unread.to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue