init
This commit is contained in:
parent
f810a0f645
commit
3490cd6ed9
7 changed files with 2799 additions and 0 deletions
605
src/main.rs
Normal file
605
src/main.rs
Normal file
|
|
@ -0,0 +1,605 @@
|
|||
use adw::prelude::*;
|
||||
use adw::{Application, ApplicationWindow, BottomSheet, Clamp, HeaderBar, NavigationPage, NavigationView};
|
||||
use gtk::{
|
||||
Box, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow, Spinner, Stack,
|
||||
};
|
||||
use gtk::gio;
|
||||
use gtk::glib;
|
||||
use std::cell::RefCell;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
const API_KEY: &str = "3IaBlP9OZw14dvES";
|
||||
const BASE_URL: &str = "https://api.nextbike.net";
|
||||
|
||||
// ── Bike model ────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Bike {
|
||||
id: String,
|
||||
code: String,
|
||||
electric_lock: bool,
|
||||
}
|
||||
|
||||
// ── Persistent login key ──────────────────────────────────────────────────────
|
||||
|
||||
fn config_path() -> PathBuf {
|
||||
let mut p = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
p.push("next-companion");
|
||||
p.push("loginkey");
|
||||
p
|
||||
}
|
||||
|
||||
fn load_loginkey() -> Option<String> {
|
||||
fs::read_to_string(config_path())
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
fn save_loginkey(key: &str) {
|
||||
let path = config_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
let _ = fs::write(&path, key);
|
||||
}
|
||||
|
||||
fn clear_loginkey() {
|
||||
let _ = fs::remove_file(config_path());
|
||||
}
|
||||
|
||||
// ── API calls (blocking — run via gio::spawn_blocking) ────────────────────────
|
||||
|
||||
fn api_login(phone: &str, pin: &str) -> Result<String, String> {
|
||||
let resp = reqwest::blocking::Client::new()
|
||||
.post(format!("{BASE_URL}/api/login.json"))
|
||||
.form(&[("apikey", API_KEY), ("mobile", phone), ("pin", pin)])
|
||||
.send()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_str(&resp.text().map_err(|e| e.to_string())?)
|
||||
.map_err(|e| e.to_string())?;
|
||||
json["user"]["loginkey"]
|
||||
.as_str()
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| "Invalid credentials".to_string())
|
||||
}
|
||||
|
||||
fn api_get_rentals(loginkey: &str) -> Result<Vec<Bike>, String> {
|
||||
let resp = reqwest::blocking::Client::new()
|
||||
.post(format!("{BASE_URL}/api/getOpenRentals.json"))
|
||||
.form(&[("apikey", API_KEY), ("loginkey", loginkey)])
|
||||
.send()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_str(&resp.text().map_err(|e| e.to_string())?)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let arr = json["rentalCollection"]
|
||||
.as_array()
|
||||
.ok_or_else(|| "No rental data".to_string())?;
|
||||
Ok(arr
|
||||
.iter()
|
||||
.map(|b| Bike {
|
||||
id: b["bike"].as_str().unwrap_or("").to_string(),
|
||||
code: b["code"].as_str().unwrap_or("").to_string(),
|
||||
electric_lock: b["electric_lock"].as_str().map_or(false, |s| s == "true"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn api_rent(loginkey: &str, bike_id: &str) -> Result<(), String> {
|
||||
reqwest::blocking::Client::new()
|
||||
.post(format!("{BASE_URL}/api/rent.json"))
|
||||
.form(&[("apikey", API_KEY), ("loginkey", loginkey), ("bike", bike_id)])
|
||||
.send()
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_return(loginkey: &str, bike_id: &str, station_id: &str) -> Result<(), String> {
|
||||
reqwest::blocking::Client::new()
|
||||
.post(format!("{BASE_URL}/api/return.json"))
|
||||
.form(&[
|
||||
("apikey", API_KEY),
|
||||
("bike", bike_id),
|
||||
("loginkey", loginkey),
|
||||
("station", station_id),
|
||||
("comment", ""),
|
||||
])
|
||||
.send()
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Async helpers (run on GLib main context) ──────────────────────────────────
|
||||
|
||||
async fn load_rentals(key: String, bikes: Rc<RefCell<Vec<Bike>>>, bikes_list: ListBox, list_stack: Stack) {
|
||||
let result = match gio::spawn_blocking(move || api_get_rentals(&key)).await {
|
||||
Ok(r) => r,
|
||||
Err(_) => return,
|
||||
};
|
||||
if let Ok(new_bikes) = result {
|
||||
while let Some(child) = bikes_list.first_child() {
|
||||
bikes_list.remove(&child);
|
||||
}
|
||||
for bike in &new_bikes {
|
||||
let text = format!(
|
||||
"Bike {} · code: {}{}",
|
||||
bike.id,
|
||||
bike.code,
|
||||
if bike.electric_lock { " ⚡" } else { "" }
|
||||
);
|
||||
let lbl = Label::builder()
|
||||
.label(&text)
|
||||
.xalign(0.0)
|
||||
.margin_top(14)
|
||||
.margin_bottom(14)
|
||||
.margin_start(12)
|
||||
.margin_end(12)
|
||||
.build();
|
||||
let row = ListBoxRow::new();
|
||||
row.set_child(Some(&lbl));
|
||||
bikes_list.append(&row);
|
||||
}
|
||||
list_stack.set_visible_child_name(if new_bikes.is_empty() { "empty" } else { "list" });
|
||||
*bikes.borrow_mut() = new_bikes;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() -> glib::ExitCode {
|
||||
let app = Application::builder()
|
||||
.application_id("org.nextbike.NextCompanion")
|
||||
.build();
|
||||
app.connect_activate(build_ui);
|
||||
app.run()
|
||||
}
|
||||
|
||||
// ── UI ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn build_ui(app: &Application) {
|
||||
let loginkey: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(load_loginkey()));
|
||||
let bikes: Rc<RefCell<Vec<Bike>>> = Rc::new(RefCell::new(vec![]));
|
||||
let return_bike: Rc<RefCell<Option<Bike>>> = Rc::new(RefCell::new(None));
|
||||
|
||||
// ── Login page ────────────────────────────────────────────────────────────
|
||||
let phone_entry = Entry::builder()
|
||||
.placeholder_text("Phone number")
|
||||
.build();
|
||||
let pin_entry = Entry::builder()
|
||||
.placeholder_text("PIN")
|
||||
.visibility(false)
|
||||
.build();
|
||||
let login_err = Label::builder()
|
||||
.css_classes(["error"])
|
||||
.wrap(true)
|
||||
.visible(false)
|
||||
.build();
|
||||
let login_btn = Button::builder()
|
||||
.label("Sign In")
|
||||
.css_classes(["suggested-action", "pill"])
|
||||
.build();
|
||||
let login_spinner = Spinner::new();
|
||||
|
||||
let login_form = Box::builder()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(12)
|
||||
.margin_top(24).margin_bottom(24).margin_start(12).margin_end(12)
|
||||
.build();
|
||||
login_form.append(&phone_entry);
|
||||
login_form.append(&pin_entry);
|
||||
login_form.append(&login_err);
|
||||
login_form.append(&login_btn);
|
||||
login_form.append(&login_spinner);
|
||||
|
||||
let login_clamp = Clamp::builder().maximum_size(400).build();
|
||||
login_clamp.set_child(Some(&login_form));
|
||||
|
||||
let login_body = Box::builder().orientation(Orientation::Vertical).build();
|
||||
login_body.append(&HeaderBar::new());
|
||||
login_body.append(&login_clamp);
|
||||
|
||||
let login_page = NavigationPage::builder()
|
||||
.title("Sign In")
|
||||
.child(&login_body)
|
||||
.build();
|
||||
|
||||
// ── Main page ─────────────────────────────────────────────────────────────
|
||||
let bikes_list = ListBox::builder()
|
||||
.css_classes(["boxed-list"])
|
||||
.selection_mode(gtk::SelectionMode::None)
|
||||
.margin_top(8).margin_bottom(8).margin_start(12).margin_end(12)
|
||||
.build();
|
||||
let empty_label = Label::builder()
|
||||
.label("No active rentals")
|
||||
.margin_top(48)
|
||||
.css_classes(["dim-label"])
|
||||
.build();
|
||||
let list_stack = Stack::new();
|
||||
list_stack.add_named(&empty_label, Some("empty"));
|
||||
list_stack.add_named(&bikes_list, Some("list"));
|
||||
list_stack.set_visible_child_name("empty");
|
||||
|
||||
let scroll = ScrolledWindow::builder().vexpand(true).child(&list_stack).build();
|
||||
|
||||
let rent_btn = Button::builder()
|
||||
.label("Rent a Bike")
|
||||
.css_classes(["suggested-action", "pill"])
|
||||
.margin_top(8).margin_bottom(12).margin_start(12).margin_end(12)
|
||||
.build();
|
||||
|
||||
let main_hdr = HeaderBar::new();
|
||||
let logout_btn = Button::builder()
|
||||
.icon_name("system-log-out-symbolic")
|
||||
.tooltip_text("Logout")
|
||||
.build();
|
||||
let refresh_btn = Button::builder()
|
||||
.icon_name("view-refresh-symbolic")
|
||||
.tooltip_text("Refresh")
|
||||
.build();
|
||||
main_hdr.pack_end(&logout_btn);
|
||||
main_hdr.pack_start(&refresh_btn);
|
||||
|
||||
// ── Return sheet (bottom sheet) ───────────────────────────────────────────
|
||||
let station_entry = Entry::builder()
|
||||
.placeholder_text("Station number")
|
||||
.input_purpose(gtk::InputPurpose::Digits)
|
||||
.build();
|
||||
let ret_err = Label::builder()
|
||||
.css_classes(["error"])
|
||||
.wrap(true)
|
||||
.visible(false)
|
||||
.build();
|
||||
let ret_submit = Button::builder()
|
||||
.label("Return Bike")
|
||||
.css_classes(["destructive-action", "pill"])
|
||||
.build();
|
||||
let ret_spinner = Spinner::new();
|
||||
let electric_msg = Label::builder()
|
||||
.label("This bike has an electric lock.\nJust close the lock to return it.")
|
||||
.wrap(true)
|
||||
.justify(gtk::Justification::Center)
|
||||
.margin_top(24)
|
||||
.build();
|
||||
|
||||
let manual_form = Box::builder()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(12)
|
||||
.build();
|
||||
manual_form.append(&station_entry);
|
||||
manual_form.append(&ret_err);
|
||||
manual_form.append(&ret_submit);
|
||||
manual_form.append(&ret_spinner);
|
||||
|
||||
let ret_inner = Stack::new();
|
||||
ret_inner.add_named(&manual_form, Some("manual"));
|
||||
ret_inner.add_named(&electric_msg, Some("electric"));
|
||||
|
||||
let sheet_title = Label::builder()
|
||||
.css_classes(["title-4"])
|
||||
.label("Return Bike")
|
||||
.xalign(0.0)
|
||||
.build();
|
||||
let sheet_box = Box::builder()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(16)
|
||||
.margin_top(8)
|
||||
.margin_bottom(24)
|
||||
.margin_start(16)
|
||||
.margin_end(16)
|
||||
.build();
|
||||
sheet_box.append(&sheet_title);
|
||||
sheet_box.append(&ret_inner);
|
||||
|
||||
let bottom_sheet = BottomSheet::builder()
|
||||
.show_drag_handle(true)
|
||||
.sheet(&sheet_box)
|
||||
.build();
|
||||
|
||||
let main_content = Box::builder().orientation(Orientation::Vertical).build();
|
||||
main_content.append(&scroll);
|
||||
main_content.append(&rent_btn);
|
||||
bottom_sheet.set_content(Some(&main_content));
|
||||
|
||||
let main_body = Box::builder().orientation(Orientation::Vertical).build();
|
||||
main_body.append(&main_hdr);
|
||||
main_body.append(&bottom_sheet);
|
||||
|
||||
let main_page = NavigationPage::builder()
|
||||
.title("NextCompanion")
|
||||
.child(&main_body)
|
||||
.build();
|
||||
|
||||
// ── Rent page ─────────────────────────────────────────────────────────────
|
||||
let bike_entry = Entry::builder()
|
||||
.placeholder_text("Bike number")
|
||||
.input_purpose(gtk::InputPurpose::Digits)
|
||||
.build();
|
||||
let rent_err = Label::builder()
|
||||
.css_classes(["error"])
|
||||
.wrap(true)
|
||||
.visible(false)
|
||||
.build();
|
||||
let rent_submit = Button::builder()
|
||||
.label("Rent")
|
||||
.css_classes(["suggested-action", "pill"])
|
||||
.build();
|
||||
let rent_spinner = Spinner::new();
|
||||
|
||||
let rent_form = Box::builder()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(12)
|
||||
.margin_top(24).margin_bottom(24).margin_start(12).margin_end(12)
|
||||
.build();
|
||||
rent_form.append(&bike_entry);
|
||||
rent_form.append(&rent_err);
|
||||
rent_form.append(&rent_submit);
|
||||
rent_form.append(&rent_spinner);
|
||||
|
||||
let rent_clamp = Clamp::builder().maximum_size(400).build();
|
||||
rent_clamp.set_child(Some(&rent_form));
|
||||
|
||||
let rent_body = Box::builder().orientation(Orientation::Vertical).build();
|
||||
rent_body.append(&HeaderBar::new());
|
||||
rent_body.append(&rent_clamp);
|
||||
|
||||
let rent_page = NavigationPage::builder()
|
||||
.title("Rent Bike")
|
||||
.child(&rent_body)
|
||||
.build();
|
||||
|
||||
// ── Navigation view ───────────────────────────────────────────────────────
|
||||
let nav = NavigationView::new();
|
||||
nav.push(&main_page);
|
||||
if loginkey.borrow().is_none() {
|
||||
nav.push(&login_page);
|
||||
}
|
||||
|
||||
// ── Window ────────────────────────────────────────────────────────────────
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.title("NextCompanion")
|
||||
.default_width(390)
|
||||
.default_height(700)
|
||||
.content(&nav)
|
||||
.build();
|
||||
|
||||
// ── Login button ──────────────────────────────────────────────────────────
|
||||
{
|
||||
let phone = phone_entry.clone();
|
||||
let pin = pin_entry.clone();
|
||||
let err = login_err.clone();
|
||||
let spinner = login_spinner.clone();
|
||||
let btn = login_btn.clone();
|
||||
let nav = nav.clone();
|
||||
let loginkey = loginkey.clone();
|
||||
let bikes = bikes.clone();
|
||||
let bikes_list = bikes_list.clone();
|
||||
let list_stack = list_stack.clone();
|
||||
login_btn.connect_clicked(move |_| {
|
||||
let p = phone.text().to_string();
|
||||
let n = pin.text().to_string();
|
||||
if p.is_empty() || n.is_empty() {
|
||||
err.set_label("Phone and PIN are required");
|
||||
err.set_visible(true);
|
||||
return;
|
||||
}
|
||||
err.set_visible(false);
|
||||
spinner.set_spinning(true);
|
||||
btn.set_sensitive(false);
|
||||
|
||||
let spinner = spinner.clone();
|
||||
let btn = btn.clone();
|
||||
let err = err.clone();
|
||||
let nav = nav.clone();
|
||||
let loginkey = loginkey.clone();
|
||||
let bikes = bikes.clone();
|
||||
let bikes_list = bikes_list.clone();
|
||||
let list_stack = list_stack.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
let result = match gio::spawn_blocking(move || api_login(&p, &n)).await {
|
||||
Ok(r) => r,
|
||||
Err(_) => Err("Internal error".to_string()),
|
||||
};
|
||||
spinner.set_spinning(false);
|
||||
btn.set_sensitive(true);
|
||||
match result {
|
||||
Ok(key) => {
|
||||
save_loginkey(&key);
|
||||
*loginkey.borrow_mut() = Some(key.clone());
|
||||
nav.pop();
|
||||
load_rentals(key, bikes, bikes_list, list_stack).await;
|
||||
}
|
||||
Err(e) => {
|
||||
err.set_label(&e);
|
||||
err.set_visible(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Logout button ─────────────────────────────────────────────────────────
|
||||
{
|
||||
let nav = nav.clone();
|
||||
let login_page = login_page.clone();
|
||||
let loginkey = loginkey.clone();
|
||||
logout_btn.connect_clicked(move |_| {
|
||||
clear_loginkey();
|
||||
*loginkey.borrow_mut() = None;
|
||||
nav.push(&login_page);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Refresh button ────────────────────────────────────────────────────────
|
||||
{
|
||||
let loginkey = loginkey.clone();
|
||||
let bikes = bikes.clone();
|
||||
let bikes_list = bikes_list.clone();
|
||||
let list_stack = list_stack.clone();
|
||||
refresh_btn.connect_clicked(move |_| {
|
||||
if let Some(key) = loginkey.borrow().clone() {
|
||||
let bikes = bikes.clone();
|
||||
let bikes_list = bikes_list.clone();
|
||||
let list_stack = list_stack.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
load_rentals(key, bikes, bikes_list, list_stack).await;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Open rent page ────────────────────────────────────────────────────────
|
||||
{
|
||||
let nav = nav.clone();
|
||||
let rent_page = rent_page.clone();
|
||||
let bike_entry = bike_entry.clone();
|
||||
rent_btn.connect_clicked(move |_| {
|
||||
bike_entry.set_text("");
|
||||
nav.push(&rent_page);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Rent submit ───────────────────────────────────────────────────────────
|
||||
{
|
||||
let loginkey = loginkey.clone();
|
||||
let entry = bike_entry.clone();
|
||||
let err = rent_err.clone();
|
||||
let spinner = rent_spinner.clone();
|
||||
let btn = rent_submit.clone();
|
||||
let nav = nav.clone();
|
||||
let bikes = bikes.clone();
|
||||
let bikes_list = bikes_list.clone();
|
||||
let list_stack = list_stack.clone();
|
||||
rent_submit.connect_clicked(move |_| {
|
||||
let id = entry.text().to_string();
|
||||
if id.is_empty() {
|
||||
err.set_label("Enter a bike number");
|
||||
err.set_visible(true);
|
||||
return;
|
||||
}
|
||||
if let Some(key) = loginkey.borrow().clone() {
|
||||
err.set_visible(false);
|
||||
spinner.set_spinning(true);
|
||||
btn.set_sensitive(false);
|
||||
|
||||
let spinner = spinner.clone();
|
||||
let btn = btn.clone();
|
||||
let err = err.clone();
|
||||
let nav = nav.clone();
|
||||
let bikes = bikes.clone();
|
||||
let bikes_list = bikes_list.clone();
|
||||
let list_stack = list_stack.clone();
|
||||
let key_reload = key.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
let result = match gio::spawn_blocking(move || api_rent(&key, &id)).await {
|
||||
Ok(r) => r,
|
||||
Err(_) => Err("Internal error".to_string()),
|
||||
};
|
||||
spinner.set_spinning(false);
|
||||
btn.set_sensitive(true);
|
||||
if let Err(e) = result {
|
||||
err.set_label(&e);
|
||||
err.set_visible(true);
|
||||
} else {
|
||||
nav.pop();
|
||||
load_rentals(key_reload, bikes, bikes_list, list_stack).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Click rental row → open return bottom sheet ───────────────────────────
|
||||
{
|
||||
let bottom_sheet = bottom_sheet.clone();
|
||||
let bikes = bikes.clone();
|
||||
let return_bike = return_bike.clone();
|
||||
let ret_inner = ret_inner.clone();
|
||||
let station_entry = station_entry.clone();
|
||||
let ret_err = ret_err.clone();
|
||||
bikes_list.connect_row_activated(move |_, row| {
|
||||
let idx = row.index() as usize;
|
||||
let bike = bikes.borrow().get(idx).cloned();
|
||||
if let Some(bike) = bike {
|
||||
station_entry.set_text("");
|
||||
ret_err.set_visible(false);
|
||||
ret_inner.set_visible_child_name(if bike.electric_lock { "electric" } else { "manual" });
|
||||
*return_bike.borrow_mut() = Some(bike);
|
||||
bottom_sheet.set_open(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Return submit ─────────────────────────────────────────────────────────
|
||||
{
|
||||
let loginkey = loginkey.clone();
|
||||
let return_bike = return_bike.clone();
|
||||
let entry = station_entry.clone();
|
||||
let err = ret_err.clone();
|
||||
let spinner = ret_spinner.clone();
|
||||
let btn = ret_submit.clone();
|
||||
let bottom_sheet = bottom_sheet.clone();
|
||||
let bikes = bikes.clone();
|
||||
let bikes_list = bikes_list.clone();
|
||||
let list_stack = list_stack.clone();
|
||||
ret_submit.connect_clicked(move |_| {
|
||||
let station = entry.text().to_string();
|
||||
if station.is_empty() {
|
||||
err.set_label("Enter a station number");
|
||||
err.set_visible(true);
|
||||
return;
|
||||
}
|
||||
if let (Some(key), Some(bike)) =
|
||||
(loginkey.borrow().clone(), return_bike.borrow().clone())
|
||||
{
|
||||
err.set_visible(false);
|
||||
spinner.set_spinning(true);
|
||||
btn.set_sensitive(false);
|
||||
|
||||
let spinner = spinner.clone();
|
||||
let btn = btn.clone();
|
||||
let err = err.clone();
|
||||
let bottom_sheet = bottom_sheet.clone();
|
||||
let bikes = bikes.clone();
|
||||
let bikes_list = bikes_list.clone();
|
||||
let list_stack = list_stack.clone();
|
||||
let key_reload = key.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
let result = match gio::spawn_blocking(move || {
|
||||
api_return(&key, &bike.id, &station)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(_) => Err("Internal error".to_string()),
|
||||
};
|
||||
spinner.set_spinning(false);
|
||||
btn.set_sensitive(true);
|
||||
if let Err(e) = result {
|
||||
err.set_label(&e);
|
||||
err.set_visible(true);
|
||||
} else {
|
||||
bottom_sheet.set_open(false);
|
||||
load_rentals(key_reload, bikes, bikes_list, list_stack).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Initial rentals load ──────────────────────────────────────────────────
|
||||
if let Some(key) = loginkey.borrow().clone() {
|
||||
let bikes = bikes.clone();
|
||||
let bikes_list = bikes_list.clone();
|
||||
let list_stack = list_stack.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
load_rentals(key, bikes, bikes_list, list_stack).await;
|
||||
});
|
||||
}
|
||||
|
||||
window.present();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue