init
This commit is contained in:
parent
f810a0f645
commit
3490cd6ed9
7 changed files with 2799 additions and 0 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -6,3 +6,8 @@
|
|||
/captures
|
||||
.externalNativeBuild
|
||||
.idea/*
|
||||
|
||||
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
|
|
|
|||
2068
Cargo.lock
generated
Normal file
2068
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "next-companion"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
gtk = { package = "gtk4", version = "0.10" }
|
||||
adw = { package = "libadwaita", version = "0.8", features = ["v1_6"] }
|
||||
reqwest = { version = "0.12", features = ["blocking", "rustls-tls"], default-features = false }
|
||||
serde_json = "1"
|
||||
dirs = "5"
|
||||
62
flake.lock
generated
Normal file
62
flake.lock
generated
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1772963539,
|
||||
"narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9dcb002ca1690658be4a04645215baea8b95f31d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1744536153,
|
||||
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773115373,
|
||||
"narHash": "sha256-bfK9FJFcQth6f3ydYggS5m0z2NRGF/PY6Y2XgZDJ6pg=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "1924b4672a2b8e4aee6e6652ec2e59a8d3c5648e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
47
flake.nix
Normal file
47
flake.nix
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
description = "NextCompanion — a minimal GTK4 Nextbike client for Linux";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, rust-overlay, ... }:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
|
||||
runtimeDeps = with pkgs; [
|
||||
gtk4
|
||||
libadwaita
|
||||
glib
|
||||
];
|
||||
buildDeps = with pkgs; [
|
||||
pkg-config
|
||||
rust-bin.stable.latest.default
|
||||
];
|
||||
in
|
||||
{
|
||||
devShells.${system}.default = pkgs.mkShell {
|
||||
buildInputs = buildDeps ++ runtimeDeps;
|
||||
shellHook = ''
|
||||
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath runtimeDeps}:$LD_LIBRARY_PATH"
|
||||
'';
|
||||
};
|
||||
|
||||
packages.${system}.default = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "next-companion";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
nativeBuildInputs = buildDeps;
|
||||
buildInputs = runtimeDeps;
|
||||
};
|
||||
|
||||
apps.${system}.default = {
|
||||
type = "app";
|
||||
program = "${self.packages.${system}.default}/bin/next-companion";
|
||||
};
|
||||
};
|
||||
}
|
||||
1
result
Symbolic link
1
result
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/nix/store/gan72x7dji7c6lwngwfx88my14azg9wn-next-companion-0.1.0
|
||||
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