FeedTheMonkey/src/login_dialog.rs
Jeena 5dee5cc52b 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.
2026-03-20 12:17:27 +00:00

124 lines
4 KiB
Rust

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();
// Login button
let obj_weak = self.obj().downgrade();
self.login_button.connect_activated(move |_| {
if let Some(dialog) = obj_weak.upgrade() {
dialog.imp().on_login_clicked();
}
});
// Enter in any row submits the form (connect_entry_activated fires on Return)
for weak in [
self.server_url_row.downgrade(),
self.username_row.downgrade(),
] {
let obj_weak = self.obj().downgrade();
weak.upgrade().unwrap().connect_entry_activated(move |_| {
if let Some(dialog) = obj_weak.upgrade() {
dialog.imp().on_login_clicked();
}
});
}
let obj_weak2 = self.obj().downgrade();
self.password_row.connect_entry_activated(move |_| {
if let Some(dialog) = obj_weak2.upgrade() {
dialog.imp().on_login_clicked();
}
});
}
}
impl LoginDialog {
fn on_login_clicked(&self) {
let raw_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 raw_url.is_empty() || username.is_empty() || password.is_empty() {
return;
}
// Prepend https:// if no scheme given
let server_url = if raw_url.starts_with("http://") || raw_url.starts_with("https://") {
raw_url
} else {
format!("https://{raw_url}")
};
self.obj().close();
self.obj().emit_by_name::<()>("logged-in", &[&server_url, &username, &password]);
}
}
impl WidgetImpl for LoginDialog {}
impl AdwDialogImpl for LoginDialog {}
}