final polish
This commit is contained in:
parent
dade4e204b
commit
b23c89cc1f
6 changed files with 66 additions and 26 deletions
63
src/app.rs
63
src/app.rs
|
|
@ -8,7 +8,7 @@ use std::collections::HashMap;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::countries::{get_countries, Country};
|
use crate::countries::{get_countries, Country};
|
||||||
use crate::flags;
|
use crate::flags;
|
||||||
use crate::nordvpn::{self, CmdResult, Poller};
|
use crate::nordvpn::{self, CmdResult, Poller, StatusInfo};
|
||||||
use crate::ui::config_dialog::{ConfigDialog, DialogResult};
|
use crate::ui::config_dialog::{ConfigDialog, DialogResult};
|
||||||
use crate::ui::country_selector::CountrySelector;
|
use crate::ui::country_selector::CountrySelector;
|
||||||
use crate::world_map::WorldMap;
|
use crate::world_map::WorldMap;
|
||||||
|
|
@ -28,10 +28,12 @@ pub struct App {
|
||||||
config_dialog: Option<ConfigDialog>,
|
config_dialog: Option<ConfigDialog>,
|
||||||
/// Last connect/disconnect result shown inline below the status bar.
|
/// Last connect/disconnect result shown inline below the status bar.
|
||||||
notification: Option<String>,
|
notification: Option<String>,
|
||||||
|
/// A connect key that is awaiting user confirmation before being dispatched.
|
||||||
|
pending_connect: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
let config = Config::load();
|
let config = Config::load();
|
||||||
let countries = get_countries(&config.external_app);
|
let countries = get_countries(&config.external_app);
|
||||||
let poller = Poller::new(config.external_app.clone(), config.refresh_rate);
|
let poller = Poller::new(config.external_app.clone(), config.refresh_rate);
|
||||||
|
|
@ -53,6 +55,7 @@ impl App {
|
||||||
world_map: WorldMap::new(),
|
world_map: WorldMap::new(),
|
||||||
config_dialog: None,
|
config_dialog: None,
|
||||||
notification: None,
|
notification: None,
|
||||||
|
pending_connect: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,6 +67,16 @@ impl App {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If already connected, queue a confirmation prompt; otherwise connect immediately.
|
||||||
|
fn request_connect(&mut self, key: String, status: &StatusInfo) {
|
||||||
|
if status.is_connected() {
|
||||||
|
self.pending_connect = Some(key);
|
||||||
|
} else {
|
||||||
|
self.notification = None;
|
||||||
|
self.spawn_connect(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn spawn_disconnect(&self) {
|
fn spawn_disconnect(&self) {
|
||||||
let app = self.config.external_app.clone();
|
let app = self.config.external_app.clone();
|
||||||
let tx = self.cmd_tx.clone();
|
let tx = self.cmd_tx.clone();
|
||||||
|
|
@ -162,7 +175,7 @@ impl eframe::App for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Left panel — country selector ───────────────────────────────────
|
// ── Left panel — country selector ───────────────────────────────────
|
||||||
let panel_width = ctx.screen_rect().width() * 0.3;
|
let panel_width = ctx.content_rect().width() * 0.3;
|
||||||
let connect_key =
|
let connect_key =
|
||||||
egui::SidePanel::left("country_selector")
|
egui::SidePanel::left("country_selector")
|
||||||
.exact_width(panel_width)
|
.exact_width(panel_width)
|
||||||
|
|
@ -179,16 +192,52 @@ impl eframe::App for App {
|
||||||
.inner;
|
.inner;
|
||||||
|
|
||||||
if let Some(key) = connect_key {
|
if let Some(key) = connect_key {
|
||||||
self.notification = None;
|
self.request_connect(key, &status);
|
||||||
self.spawn_connect(key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Central panel — world map ────────────────────────────────────────
|
// ── Central panel — world map ────────────────────────────────────────
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
if let Some(key) = self.world_map.show(ui, &self.countries, status.country.as_deref()) {
|
if let Some(key) = self.world_map.show(ui, &self.countries, status.country.as_deref()) {
|
||||||
self.notification = None;
|
self.request_connect(key, &status);
|
||||||
self.spawn_connect(key);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Confirmation modal ───────────────────────────────────────────────
|
||||||
|
if let Some(key) = self.pending_connect.clone() {
|
||||||
|
let label = self
|
||||||
|
.countries
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.nordvpn_key == key)
|
||||||
|
.map(|c| c.name.as_str())
|
||||||
|
.unwrap_or(key.as_str());
|
||||||
|
|
||||||
|
let mut confirmed = false;
|
||||||
|
let mut cancelled = false;
|
||||||
|
|
||||||
|
egui::Window::new("Confirm")
|
||||||
|
.collapsible(false)
|
||||||
|
.resizable(false)
|
||||||
|
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.label(format!("Switch server to {}?", label));
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui.button("Connect").clicked() {
|
||||||
|
confirmed = true;
|
||||||
|
}
|
||||||
|
if ui.button("Cancel").clicked() {
|
||||||
|
cancelled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if confirmed {
|
||||||
|
self.notification = None;
|
||||||
|
self.spawn_connect(key);
|
||||||
|
self.pending_connect = None;
|
||||||
|
} else if cancelled {
|
||||||
|
self.pending_connect = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
// TODO: remove after phase 7 — dead_code is expected while modules are being built up
|
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod config;
|
mod config;
|
||||||
mod countries;
|
mod countries;
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@ use std::time::Duration;
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct StatusInfo {
|
pub struct StatusInfo {
|
||||||
pub status: String,
|
pub status: String,
|
||||||
pub server: Option<String>,
|
#[allow(dead_code)] pub server: Option<String>,
|
||||||
pub country: Option<String>,
|
pub country: Option<String>,
|
||||||
pub ip: Option<String>,
|
pub ip: Option<String>,
|
||||||
pub city: Option<String>,
|
#[allow(dead_code)] pub city: Option<String>,
|
||||||
pub protocol: Option<String>,
|
#[allow(dead_code)] pub protocol: Option<String>,
|
||||||
pub transfer: Option<String>,
|
#[allow(dead_code)] pub transfer: Option<String>,
|
||||||
pub uptime: Option<String>,
|
pub uptime: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ impl StatusInfo {
|
||||||
/// Result sent back to the UI thread after a connect/disconnect command.
|
/// Result sent back to the UI thread after a connect/disconnect command.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum CmdResult {
|
pub enum CmdResult {
|
||||||
Success { cmd: String, output: String },
|
Success { #[allow(dead_code)] cmd: String, output: String },
|
||||||
Failure { cmd: String, message: String },
|
Failure { cmd: String, message: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ use eframe::egui;
|
||||||
pub trait MapProjection: Send + Sync {
|
pub trait MapProjection: Send + Sync {
|
||||||
fn project(&self, lon: f64, lat: f64, rect: egui::Rect) -> egui::Pos2;
|
fn project(&self, lon: f64, lat: f64, rect: egui::Rect) -> egui::Pos2;
|
||||||
fn unproject(&self, pos: egui::Pos2, rect: egui::Rect) -> (f64, f64);
|
fn unproject(&self, pos: egui::Pos2, rect: egui::Rect) -> (f64, f64);
|
||||||
fn name(&self) -> &'static str;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Equirectangular;
|
pub struct Equirectangular;
|
||||||
|
|
@ -22,9 +21,6 @@ impl MapProjection for Equirectangular {
|
||||||
(lon, lat)
|
(lon, lat)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"Equirectangular"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,6 @@ impl CountrySelector {
|
||||||
connected_country: Option<&str>,
|
connected_country: Option<&str>,
|
||||||
flags: &HashMap<String, egui::TextureHandle>,
|
flags: &HashMap<String, egui::TextureHandle>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let mut clicked = None;
|
|
||||||
|
|
||||||
// ── Toggle + search bar ─────────────────────────────────────────────
|
// ── Toggle + search bar ─────────────────────────────────────────────
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.selectable_value(&mut self.view_mode, ViewMode::List, "List");
|
ui.selectable_value(&mut self.view_mode, ViewMode::List, "List");
|
||||||
|
|
@ -71,11 +69,9 @@ impl CountrySelector {
|
||||||
|
|
||||||
// ── Virtual-scrolled content ────────────────────────────────────────
|
// ── Virtual-scrolled content ────────────────────────────────────────
|
||||||
match self.view_mode {
|
match self.view_mode {
|
||||||
ViewMode::List => clicked = show_list(ui, &favs, &rest, connected_country, flags),
|
ViewMode::List => show_list(ui, &favs, &rest, connected_country, flags),
|
||||||
ViewMode::Grid => clicked = show_grid(ui, &favs, &rest, connected_country, flags),
|
ViewMode::Grid => show_grid(ui, &favs, &rest, connected_country, flags),
|
||||||
}
|
}
|
||||||
|
|
||||||
clicked
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,8 @@ impl WorldMap {
|
||||||
let fill = if is_hovered { brightened(base, 50) } else { base };
|
let fill = if is_hovered { brightened(base, 50) } else { base };
|
||||||
|
|
||||||
render_fill(&painter, shape, &*self.projection, virtual_rect, fill);
|
render_fill(&painter, shape, &*self.projection, virtual_rect, fill);
|
||||||
render_border(&painter, shape, &*self.projection, virtual_rect, COLOR_BORDER);
|
let stroke_w = (0.8 * self.zoom).min(2.0);
|
||||||
|
render_border(&painter, shape, &*self.projection, virtual_rect, COLOR_BORDER, stroke_w);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tooltip ──────────────────────────────────────────────────────────
|
// ── Tooltip ──────────────────────────────────────────────────────────
|
||||||
|
|
@ -193,6 +194,7 @@ fn render_border(
|
||||||
projection: &dyn MapProjection,
|
projection: &dyn MapProjection,
|
||||||
rect: egui::Rect,
|
rect: egui::Rect,
|
||||||
color: egui::Color32,
|
color: egui::Color32,
|
||||||
|
stroke_width: f32,
|
||||||
) {
|
) {
|
||||||
if shape.outer_ring.len() < 2 {
|
if shape.outer_ring.len() < 2 {
|
||||||
return;
|
return;
|
||||||
|
|
@ -202,7 +204,7 @@ fn render_border(
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&[lon, lat]| projection.project(lon, lat, rect))
|
.map(|&[lon, lat]| projection.project(lon, lat, rect))
|
||||||
.collect();
|
.collect();
|
||||||
painter.add(egui::Shape::closed_line(pts, egui::Stroke::new(0.8, color)));
|
painter.add(egui::Shape::closed_line(pts, egui::Stroke::new(stroke_width, color)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue