final polish

This commit is contained in:
ms2k 2026-03-31 22:46:58 +09:00
commit b23c89cc1f
6 changed files with 66 additions and 26 deletions

View file

@ -8,7 +8,7 @@ use std::collections::HashMap;
use crate::config::Config;
use crate::countries::{get_countries, Country};
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::country_selector::CountrySelector;
use crate::world_map::WorldMap;
@ -28,10 +28,12 @@ pub struct App {
config_dialog: Option<ConfigDialog>,
/// Last connect/disconnect result shown inline below the status bar.
notification: Option<String>,
/// A connect key that is awaiting user confirmation before being dispatched.
pending_connect: Option<String>,
}
impl App {
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
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);
@ -53,6 +55,7 @@ impl App {
world_map: WorldMap::new(),
config_dialog: 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) {
let app = self.config.external_app.clone();
let tx = self.cmd_tx.clone();
@ -162,7 +175,7 @@ impl eframe::App for App {
}
// ── 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 =
egui::SidePanel::left("country_selector")
.exact_width(panel_width)
@ -179,16 +192,52 @@ impl eframe::App for App {
.inner;
if let Some(key) = connect_key {
self.notification = None;
self.spawn_connect(key);
self.request_connect(key, &status);
}
// ── Central panel — world map ────────────────────────────────────────
egui::CentralPanel::default().show(ctx, |ui| {
if let Some(key) = self.world_map.show(ui, &self.countries, status.country.as_deref()) {
self.notification = None;
self.spawn_connect(key);
self.request_connect(key, &status);
}
});
// ── 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;
}
}
}
}

View file

@ -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 config;
mod countries;

View file

@ -11,12 +11,12 @@ use std::time::Duration;
#[derive(Debug, Clone, Default)]
pub struct StatusInfo {
pub status: String,
pub server: Option<String>,
#[allow(dead_code)] pub server: Option<String>,
pub country: Option<String>,
pub ip: Option<String>,
pub city: Option<String>,
pub protocol: Option<String>,
pub transfer: Option<String>,
#[allow(dead_code)] pub city: Option<String>,
#[allow(dead_code)] pub protocol: Option<String>,
#[allow(dead_code)] pub transfer: Option<String>,
pub uptime: Option<String>,
}
@ -29,7 +29,7 @@ impl StatusInfo {
/// Result sent back to the UI thread after a connect/disconnect command.
#[derive(Debug)]
pub enum CmdResult {
Success { cmd: String, output: String },
Success { #[allow(dead_code)] cmd: String, output: String },
Failure { cmd: String, message: String },
}

View file

@ -3,7 +3,6 @@ use eframe::egui;
pub trait MapProjection: Send + Sync {
fn project(&self, lon: f64, lat: f64, rect: egui::Rect) -> egui::Pos2;
fn unproject(&self, pos: egui::Pos2, rect: egui::Rect) -> (f64, f64);
fn name(&self) -> &'static str;
}
pub struct Equirectangular;
@ -22,9 +21,6 @@ impl MapProjection for Equirectangular {
(lon, lat)
}
fn name(&self) -> &'static str {
"Equirectangular"
}
}
#[cfg(test)]

View file

@ -36,8 +36,6 @@ impl CountrySelector {
connected_country: Option<&str>,
flags: &HashMap<String, egui::TextureHandle>,
) -> Option<String> {
let mut clicked = None;
// ── Toggle + search bar ─────────────────────────────────────────────
ui.horizontal(|ui| {
ui.selectable_value(&mut self.view_mode, ViewMode::List, "List");
@ -71,11 +69,9 @@ impl CountrySelector {
// ── Virtual-scrolled content ────────────────────────────────────────
match self.view_mode {
ViewMode::List => clicked = show_list(ui, &favs, &rest, connected_country, flags),
ViewMode::Grid => clicked = show_grid(ui, &favs, &rest, connected_country, flags),
ViewMode::List => show_list(ui, &favs, &rest, connected_country, flags),
ViewMode::Grid => show_grid(ui, &favs, &rest, connected_country, flags),
}
clicked
}
}

View file

@ -147,7 +147,8 @@ impl WorldMap {
let fill = if is_hovered { brightened(base, 50) } else { base };
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 ──────────────────────────────────────────────────────────
@ -193,6 +194,7 @@ fn render_border(
projection: &dyn MapProjection,
rect: egui::Rect,
color: egui::Color32,
stroke_width: f32,
) {
if shape.outer_ring.len() < 2 {
return;
@ -202,7 +204,7 @@ fn render_border(
.iter()
.map(|&[lon, lat]| projection.project(lon, lat, rect))
.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)));
}
// ---------------------------------------------------------------------------