forked from jeena/FeedTheMonkey
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
65
src/credentials.rs
Normal file
65
src/credentials.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
use libsecret::{prelude::*, SchemaAttributeType, SchemaFlags, SearchFlags};
|
||||
use std::collections::HashMap;
|
||||
|
||||
const SCHEMA_NAME: &str = "net.jeena.FeedTheMonkey";
|
||||
const ATTR_SERVER: &str = "server-url";
|
||||
const ATTR_USERNAME: &str = "username";
|
||||
const LABEL: &str = "FeedTheMonkey credentials";
|
||||
|
||||
fn schema() -> libsecret::Schema {
|
||||
libsecret::Schema::new(
|
||||
SCHEMA_NAME,
|
||||
SchemaFlags::NONE,
|
||||
HashMap::from([
|
||||
(ATTR_SERVER, SchemaAttributeType::String),
|
||||
(ATTR_USERNAME, SchemaAttributeType::String),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn store_credentials(server_url: &str, username: &str, password: &str) {
|
||||
let schema = schema();
|
||||
let attrs = HashMap::from([
|
||||
(ATTR_SERVER, server_url),
|
||||
(ATTR_USERNAME, username),
|
||||
]);
|
||||
if let Err(e) = libsecret::password_store_sync(
|
||||
Some(&schema),
|
||||
attrs,
|
||||
Some(libsecret::COLLECTION_DEFAULT.as_str()),
|
||||
LABEL,
|
||||
password,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
eprintln!("Failed to store credentials: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_credentials() -> Option<(String, String, String)> {
|
||||
let schema = schema();
|
||||
let items = libsecret::password_search_sync(
|
||||
Some(&schema),
|
||||
HashMap::new(),
|
||||
SearchFlags::LOAD_SECRETS | SearchFlags::UNLOCK,
|
||||
gio::Cancellable::NONE,
|
||||
).ok()?;
|
||||
|
||||
let item = items.into_iter().next()?;
|
||||
let attrs = item.attributes();
|
||||
let server_url = attrs.get(ATTR_SERVER)?.to_string();
|
||||
let username = attrs.get(ATTR_USERNAME)?.to_string();
|
||||
let secret = item.retrieve_secret_sync(gio::Cancellable::NONE).ok()??;
|
||||
let password = secret.text()?.to_string();
|
||||
Some((server_url, username, password))
|
||||
}
|
||||
|
||||
pub fn clear_credentials() {
|
||||
let schema = schema();
|
||||
if let Err(e) = libsecret::password_clear_sync(
|
||||
Some(&schema),
|
||||
HashMap::new(),
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
eprintln!("Failed to clear credentials: {e}");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue