implement phase 1~3
This commit is contained in:
parent
de16a4f185
commit
c60f3b2ab0
1 changed files with 913 additions and 0 deletions
148
src/app.rs
Normal file
148
src/app.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
use std::sync::mpsc::{self, Sender};
|
||||
use std::time::Duration;
|
||||
|
||||
use eframe::egui;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::countries::{get_countries, Country};
|
||||
use crate::nordvpn::{self, CmdResult, Poller};
|
||||
use crate::ui::country_selector::CountrySelector;
|
||||
|
||||
pub struct App {
|
||||
config: Config,
|
||||
countries: Vec<Country>,
|
||||
poller: Poller,
|
||||
cmd_tx: Sender<CmdResult>,
|
||||
cmd_rx: mpsc::Receiver<CmdResult>,
|
||||
selector: CountrySelector,
|
||||
/// Last connect/disconnect result shown inline below the status bar.
|
||||
notification: Option<String>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
let config = Config::load();
|
||||
let countries = get_countries(&config.external_app);
|
||||
let poller = Poller::new(config.external_app.clone(), config.refresh_rate);
|
||||
let (cmd_tx, cmd_rx) = mpsc::channel();
|
||||
|
||||
Self {
|
||||
config,
|
||||
countries,
|
||||
poller,
|
||||
cmd_tx,
|
||||
cmd_rx,
|
||||
selector: CountrySelector::default(),
|
||||
notification: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_connect(&self, nordvpn_key: String) {
|
||||
let app = self.config.external_app.clone();
|
||||
let tx = self.cmd_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let _ = tx.send(nordvpn::connect(&app, &nordvpn_key));
|
||||
});
|
||||
}
|
||||
|
||||
fn spawn_disconnect(&self) {
|
||||
let app = self.config.external_app.clone();
|
||||
let tx = self.cmd_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let _ = tx.send(nordvpn::disconnect(&app));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
// Drain completed command results.
|
||||
while let Ok(result) = self.cmd_rx.try_recv() {
|
||||
self.notification = Some(match result {
|
||||
CmdResult::Success { output, .. } => output.trim().to_string(),
|
||||
CmdResult::Failure { cmd, message } => {
|
||||
format!("Error running '{cmd}': {message}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Repaint often enough to stay live without busy-looping.
|
||||
ctx.request_repaint_after(Duration::from_secs(1));
|
||||
|
||||
let status = self.poller.status.lock().unwrap().clone();
|
||||
|
||||
// ── Status panel ────────────────────────────────────────────────────
|
||||
egui::TopBottomPanel::top("status_panel")
|
||||
.min_height(40.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.strong(&status.status);
|
||||
if let Some(country) = &status.country {
|
||||
ui.separator();
|
||||
ui.label(country);
|
||||
}
|
||||
if let Some(ip) = &status.ip {
|
||||
ui.separator();
|
||||
ui.label(ip);
|
||||
}
|
||||
if let Some(uptime) = &status.uptime {
|
||||
ui.separator();
|
||||
ui.weak(uptime);
|
||||
}
|
||||
});
|
||||
if let Some(note) = &self.notification {
|
||||
ui.add_space(2.0);
|
||||
ui.weak(note);
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
});
|
||||
|
||||
// ── Bottom bar ──────────────────────────────────────────────────────
|
||||
egui::TopBottomPanel::bottom("bottom_bar")
|
||||
.min_height(36.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Disconnect").clicked() {
|
||||
self.notification = None;
|
||||
self.spawn_disconnect();
|
||||
}
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.button("Configure").clicked() {
|
||||
// Phase 7
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── Left panel — country selector ───────────────────────────────────
|
||||
let panel_width = ctx.screen_rect().width() * 0.3;
|
||||
let connect_key =
|
||||
egui::SidePanel::left("country_selector")
|
||||
.exact_width(panel_width)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
self.selector.show(
|
||||
ui,
|
||||
&self.countries,
|
||||
&self.config.favorites,
|
||||
status.country.as_deref(),
|
||||
)
|
||||
})
|
||||
.inner;
|
||||
|
||||
if let Some(key) = connect_key {
|
||||
self.notification = None;
|
||||
self.spawn_connect(key);
|
||||
}
|
||||
|
||||
// ── Central panel — world map placeholder ───────────────────────────
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.weak("World map — Phase 5");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue