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
105
src/login_dialog.rs
Normal file
105
src/login_dialog.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
use gtk4::glib;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct LoginDialog(ObjectSubclass<imp::LoginDialog>)
|
||||
@extends libadwaita::Dialog, gtk4::Widget,
|
||||
@implements gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget;
|
||||
}
|
||||
|
||||
impl LoginDialog {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new()
|
||||
}
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::subclass::prelude::*;
|
||||
use gtk4::CompositeTemplate;
|
||||
use libadwaita::prelude::*;
|
||||
use libadwaita::subclass::prelude::*;
|
||||
|
||||
#[derive(CompositeTemplate, Default)]
|
||||
#[template(resource = "/net/jeena/FeedTheMonkey/ui/login_dialog.ui")]
|
||||
pub struct LoginDialog {
|
||||
#[template_child]
|
||||
pub server_url_row: TemplateChild<libadwaita::EntryRow>,
|
||||
#[template_child]
|
||||
pub username_row: TemplateChild<libadwaita::EntryRow>,
|
||||
#[template_child]
|
||||
pub password_row: TemplateChild<libadwaita::PasswordEntryRow>,
|
||||
#[template_child]
|
||||
pub login_button: TemplateChild<libadwaita::ButtonRow>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for LoginDialog {
|
||||
const NAME: &'static str = "LoginDialog";
|
||||
type Type = super::LoginDialog;
|
||||
type ParentType = libadwaita::Dialog;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for LoginDialog {
|
||||
fn signals() -> &'static [glib::subclass::Signal] {
|
||||
use std::sync::OnceLock;
|
||||
static SIGNALS: OnceLock<Vec<glib::subclass::Signal>> = OnceLock::new();
|
||||
SIGNALS.get_or_init(|| {
|
||||
vec![
|
||||
glib::subclass::Signal::builder("logged-in")
|
||||
.param_types([
|
||||
String::static_type(),
|
||||
String::static_type(),
|
||||
String::static_type(),
|
||||
])
|
||||
.build(),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let obj_weak = self.obj().downgrade();
|
||||
self.login_button.connect_activated(move |_| {
|
||||
if let Some(dialog) = obj_weak.upgrade() {
|
||||
dialog.imp().on_login_clicked();
|
||||
}
|
||||
});
|
||||
|
||||
// Also trigger on Enter in password row
|
||||
let obj_weak2 = self.obj().downgrade();
|
||||
self.password_row.connect_apply(move |_| {
|
||||
if let Some(dialog) = obj_weak2.upgrade() {
|
||||
dialog.imp().on_login_clicked();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl LoginDialog {
|
||||
fn on_login_clicked(&self) {
|
||||
let server_url = self.server_url_row.text().trim().to_string();
|
||||
let username = self.username_row.text().trim().to_string();
|
||||
let password = self.password_row.text().to_string();
|
||||
|
||||
if server_url.is_empty() || username.is_empty() || password.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.obj().close();
|
||||
self.obj().emit_by_name::<()>("logged-in", &[&server_url, &username, &password]);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for LoginDialog {}
|
||||
impl AdwDialogImpl for LoginDialog {}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue