fjordgard

A desktop clock application
Log | Files | Refs | README | LICENSE

commit 517d3016c8e7eb589cdf0aadb4aba0f9c6460d8d
parent 49085bf580622d1182b4248601d7c9c78a3c63c6
Author: Sylvia Ivory <git@sivory.net>
Date:   Wed, 18 Jun 2025 22:50:12 -0700

Implement geocode search

Diffstat:
Msrc/main.rs | 12++++++++++--
Msrc/settings.rs | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
2 files changed, 96 insertions(+), 17 deletions(-)

diff --git a/src/main.rs b/src/main.rs @@ -1,9 +1,10 @@ -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, rc::Rc, sync::Arc}; use chrono::{ DateTime, Local, format::{Item, StrftimeItems}, }; +use fjordgard_weather::MeteoClient; use iced::{ Color, Element, Font, Length, Size, Subscription, Task, font::Weight, @@ -23,6 +24,7 @@ mod settings; struct Fjordgard { config: Rc<RefCell<Config>>, + meteo: Arc<MeteoClient>, time: DateTime<Local>, background: BackgroundKind, format_string: String, @@ -62,9 +64,12 @@ impl Fjordgard { .parse_to_owned() .unwrap(); + let meteo = MeteoClient::new(None).unwrap(); + ( Self { config: Rc::new(RefCell::new(config)), + meteo: Arc::new(meteo), time: Local::now(), background: BackgroundKind::Color(Color::from_rgb8(255, 255, 255)), format_string, @@ -109,7 +114,10 @@ impl Fjordgard { ..Default::default() }); - self.settings_window = Some(settings::Settings::new(self.config.clone())); + self.settings_window = Some(settings::Settings::new( + self.config.clone(), + self.meteo.clone(), + )); open.map(Message::SettingsOpened) } else { diff --git a/src/settings.rs b/src/settings.rs @@ -1,11 +1,9 @@ -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, rc::Rc, sync::Arc}; +use fjordgard_weather::{MeteoClient, model::Location}; use iced::{ Background, Border, Color, Element, Length, Task, Theme, - widget::{ - button, column, combo_box, container, row, scrollable, text, text_input, vertical_space, - }, - window, + widget::{button, column, combo_box, container, row, scrollable, text, text_input, tooltip}, }; use rfd::{AsyncFileDialog, FileHandle}; use strum::VariantArray; @@ -20,7 +18,8 @@ pub enum WeatherLocation { Coordinates, } -pub struct Location { +#[derive(Debug, Clone)] +pub struct LocationRow { name: String, latitude: f64, longitude: f64, @@ -28,6 +27,7 @@ pub struct Location { pub struct Settings { config: Rc<RefCell<Config>>, + meteo: Arc<MeteoClient>, backgrounds: combo_box::State<BackgroundMode>, locations: combo_box::State<WeatherLocation>, file_selector_open: bool, @@ -41,7 +41,8 @@ pub struct Settings { latitude: String, longitude: String, - location_results: Vec<Location>, + location_results: Vec<LocationRow>, + location_fetch_error: Option<String>, } #[derive(Debug, Clone)] @@ -52,6 +53,8 @@ pub enum Message { Location(WeatherLocation), Name(String), NameSubmitted, + Geocode(Result<Vec<Location>, String>), + LocationSelected(LocationRow), Latitude(String), Longitude(String), Save, @@ -60,7 +63,7 @@ pub enum Message { } impl Settings { - pub fn new(config: Rc<RefCell<Config>>) -> Self { + pub fn new(config: Rc<RefCell<Config>>, meteo: Arc<MeteoClient>) -> Self { let original_config = config.borrow().clone(); let location = original_config.location; @@ -89,6 +92,7 @@ impl Settings { Self { config, + meteo, backgrounds: combo_box::State::new(BackgroundMode::VARIANTS.to_vec()), locations: combo_box::State::new(WeatherLocation::VARIANTS.to_vec()), file_selector_open: false, @@ -103,6 +107,7 @@ impl Settings { name, location_results: vec![], + location_fetch_error: None, } } @@ -129,6 +134,48 @@ impl Settings { self.name = name; Task::none() } + Message::NameSubmitted => { + self.location_fetch_error = None; + let meteo = self.meteo.clone(); + let name = self.name.clone(); + + Task::future(async move { meteo.geocode(&name, None).await }) + .map(|r| Message::Geocode(r.map_err(|e| e.to_string()))) + } + Message::Geocode(locations) => { + match locations { + Err(s) => { + self.location_fetch_error = Some(s); + } + Ok(res) => { + self.location_results = res + .iter() + .map(|l| { + let level1 = if let Some(admin1) = &l.admin1 { + format!(", {admin1}") + } else { + String::new() + }; + + LocationRow { + name: format!("{}{level1}, {}", l.name, l.country), + latitude: l.latitude, + longitude: l.longitude, + } + }) + .collect() + } + }; + + Task::none() + } + Message::LocationSelected(loc) => { + self.name = loc.name; + self.latitude = loc.latitude.to_string(); + self.longitude = loc.longitude.to_string(); + + Task::none() + } Message::Latitude(latitude) => { self.latitude = latitude; Task::none() @@ -229,10 +276,39 @@ impl Settings { "{} ({}, {})", res.name, res.latitude, res.longitude ))) - .style(button::text), + .style(button::text) + .on_press_with(|| Message::LocationSelected(res.clone())), ) } + let location_style = if self.location_fetch_error.is_some() { + save_message = None; + text_input_error + } else { + text_input::default + }; + + let mut location_row: Element<Message> = row![ + text("Location").width(Length::FillPortion(1)), + text_input("", &self.name) + .width(Length::FillPortion(2)) + .on_input_maybe(name) + .on_submit(Message::NameSubmitted) + .style(location_style) + ] + .into(); + + if let Some(err) = &self.location_fetch_error { + location_row = tooltip( + location_row, + container(err.as_ref()) + .padding(5) + .style(container::rounded_box), + tooltip::Position::Top, + ) + .into() + }; + scrollable( container( column![ @@ -272,12 +348,7 @@ impl Settings { .on_input_maybe(longitude) .style(longitude_style) ], - row![ - text("Location").width(Length::FillPortion(1)), - text_input("", &self.name) - .width(Length::FillPortion(2)) - .on_input_maybe(name), - ], + location_row, scrollable(results) .height(Length::Fixed( 64.0 * (self.location_results.len().clamp(0, 1) as f32)