fix: tokio runtime, Enter-to-login, and server URL handling

Three bugs fixed:

- No tokio reactor: glib::spawn_future_local does not provide a
  tokio context, so reqwest/hyper panicked at runtime. Introduce
  src/runtime.rs with a multi-thread tokio Runtime (init() called
  from main before the GTK app starts). runtime::spawn() posts the
  async result back to GTK via a tokio oneshot channel awaited by
  glib::spawn_future_local, which only polls a flag (no I/O).
  runtime::spawn_bg() is used for fire-and-forget background calls.

- Enter key didn't submit login: connect_apply on AdwEntryRow only
  fires when show-apply-button is true. Switch to connect_entry_activated
  which fires on Return in all three login rows.

- Wrong API URL: the app constructed /accounts/ClientLogin directly
  off the server host, yielding a 404. Add normalize_base_url() in
  api.rs that appends /api/greader.php when the URL doesn't already
  contain it, so users can enter just https://rss.example.com.
This commit is contained in:
Jeena 2026-03-20 12:17:27 +00:00
parent d157f3f244
commit 5dee5cc52b
6 changed files with 161 additions and 67 deletions

View file

@ -271,12 +271,7 @@ pub mod imp {
fn auto_login(&self) {
if let Some((server_url, username, password)) = credentials::load_credentials() {
let win_weak = self.obj().downgrade();
glib::spawn_future_local(async move {
if let Some(win) = win_weak.upgrade() {
win.imp().do_login(server_url, username, password, false).await;
}
});
self.do_login(server_url, username, password, false);
} else {
self.show_login_dialog();
}
@ -291,39 +286,49 @@ pub mod imp {
let username = args[2].get::<String>().unwrap();
let password = args[3].get::<String>().unwrap();
if let Some(win) = win_weak.upgrade() {
glib::spawn_future_local(async move {
win.imp().do_login(server_url, username, password, true).await;
});
win.imp().do_login(server_url, username, password, true);
}
None
});
dialog.present(Some(win.upcast_ref::<gtk4::Widget>()));
}
pub async fn do_login(
&self,
server_url: String,
username: String,
password: String,
store: bool,
) {
match Api::login(&server_url, &username, &password).await {
Ok(api) => {
if store {
credentials::store_credentials(&server_url, &username, &password);
fn do_login(&self, server_url: String, username: String, password: String, store: bool) {
let win_weak = self.obj().downgrade();
crate::runtime::spawn(
async move { Api::login(&server_url, &username, &password).await
.map(|api| (api, server_url, username, password)) },
move |result| {
let Some(win) = win_weak.upgrade() else { return };
match result {
Ok((api, server_url, username, password)) => {
if store {
credentials::store_credentials(&server_url, &username, &password);
}
// Fetch write token in background (non-critical)
let api_clone = api.clone();
let win_weak2 = win.downgrade();
crate::runtime::spawn(
async move { api_clone.fetch_write_token().await },
move |wt_result| {
if let Some(win) = win_weak2.upgrade() {
match wt_result {
Ok(wt) => *win.imp().write_token.borrow_mut() = Some(wt),
Err(e) => eprintln!("Write token error: {e}"),
}
}
},
);
*win.imp().api.borrow_mut() = Some(api);
win.imp().fetch_articles();
}
Err(e) => {
win.imp().show_login_dialog();
win.imp().show_error_dialog("Login Failed", &e);
}
}
match api.fetch_write_token().await {
Ok(wt) => *self.write_token.borrow_mut() = Some(wt),
Err(e) => eprintln!("Write token error: {e}"),
}
*self.api.borrow_mut() = Some(api);
self.fetch_articles().await;
}
Err(e) => {
self.show_login_dialog();
self.show_error_dialog("Login Failed", &e);
}
}
},
);
}
fn show_error_dialog(&self, title: &str, body: &str) {
@ -364,42 +369,44 @@ pub mod imp {
// ── Fetch articles ────────────────────────────────────────────────────
fn do_reload(&self) {
let win_weak = self.obj().downgrade();
glib::spawn_future_local(async move {
if let Some(win) = win_weak.upgrade() {
win.imp().fetch_articles().await;
}
});
self.fetch_articles();
}
pub async fn fetch_articles(&self) {
fn fetch_articles(&self) {
let api = self.api.borrow().clone();
let Some(api) = api else { return };
self.refresh_stack.set_visible_child_name("spinner");
self.sidebar_content.set_visible_child_name("loading");
match api.fetch_unread().await {
Ok(articles) => {
let store = self.article_store.borrow();
let store = store.as_ref().unwrap();
store.remove_all();
for a in articles {
store.append(&ArticleObject::new(a));
let win_weak = self.obj().downgrade();
crate::runtime::spawn(
async move { api.fetch_unread().await },
move |result| {
let Some(win) = win_weak.upgrade() else { return };
let imp = win.imp();
match result {
Ok(articles) => {
let store = imp.article_store.borrow();
let store = store.as_ref().unwrap();
store.remove_all();
for a in articles {
store.append(&ArticleObject::new(a));
}
if store.n_items() == 0 {
imp.sidebar_content.set_visible_child_name("empty");
} else {
imp.sidebar_content.set_visible_child_name("list");
}
}
Err(e) => {
imp.error_status.set_description(Some(&e));
imp.sidebar_content.set_visible_child_name("error");
}
}
if store.n_items() == 0 {
self.sidebar_content.set_visible_child_name("empty");
} else {
self.sidebar_content.set_visible_child_name("list");
}
}
Err(e) => {
self.error_status.set_description(Some(&e));
self.sidebar_content.set_visible_child_name("error");
}
}
self.refresh_stack.set_visible_child_name("button");
imp.refresh_stack.set_visible_child_name("button");
},
);
}
// ── Read state ────────────────────────────────────────────────────────
@ -408,7 +415,7 @@ pub mod imp {
let api = self.api.borrow().clone();
let wt = self.write_token.borrow().clone();
if let (Some(api), Some(wt)) = (api, wt) {
glib::spawn_future_local(async move {
crate::runtime::spawn_bg(async move {
if let Err(e) = api.mark_read(&wt, &item_id).await {
eprintln!("mark_read error: {e}");
}
@ -437,7 +444,7 @@ pub mod imp {
let wt = self.write_token.borrow().clone();
if let (Some(api), Some(wt)) = (api, wt) {
let id_clone = id.clone();
glib::spawn_future_local(async move {
crate::runtime::spawn_bg(async move {
if let Err(e) = api.mark_unread(&wt, &id_clone).await {
eprintln!("mark_unread error: {e}");
}