main.rs (17351B)
1 use std::{cell::RefCell, rc::Rc, sync::Arc}; 2 3 use chrono::{ 4 DateTime, Local, 5 format::{Item, StrftimeItems}, 6 }; 7 use fjordgard_weather::{ 8 MeteoClient, 9 model::{CurrentVariable, Forecast, ForecastOptions}, 10 }; 11 #[cfg(not(target_arch = "wasm32"))] 12 use iced::font::Weight; 13 use iced::{ 14 Color, Element, Font, Length, Size, Subscription, Task, time, 15 widget::{center, column, container, horizontal_space, row, stack, text}, 16 window, 17 }; 18 19 use background::BackgroundHandle; 20 use config::{BackgroundMode, Config}; 21 use icon::{icon, icon_button}; 22 use log::{debug, error}; 23 24 mod background; 25 mod config; 26 mod icon; 27 mod settings; 28 29 pub struct Fjordgard { 30 config: Rc<RefCell<Config>>, 31 meteo: Arc<MeteoClient>, 32 time: DateTime<Local>, 33 background: BackgroundHandle, 34 format_string: String, 35 format_parsed: Vec<Item<'static>>, 36 37 settings_window: Option<settings::Settings>, 38 settings_id: Option<window::Id>, 39 main_window: window::Id, 40 main_window_size: Size, 41 42 coordinate_pair: Option<(f64, f64)>, 43 forecast_text: String, 44 forecast_icon: String, 45 } 46 47 #[derive(Debug, Clone, Copy)] 48 pub enum MediaControl { 49 Pause, 50 Previous, 51 Next, 52 } 53 54 #[derive(Debug, Clone)] 55 pub enum Message { 56 Tick(DateTime<Local>), 57 Media(MediaControl), 58 OpenSettings, 59 60 SettingsOpened(window::Id), 61 MainWindowOpened, 62 WindowClosed(window::Id), 63 WindowResized((window::Id, Size)), 64 65 Settings(settings::Message), 66 Background(background::Message), 67 68 RequestForecastUpdate, 69 ForecastUpdate(Box<Result<Forecast, String>>), 70 } 71 72 #[cfg(target_arch = "wasm32")] 73 fn window_open(_settings: window::Settings) -> (window::Id, Task<window::Id>) { 74 let id = window::Id::unique(); 75 76 (id, Task::done(id)) 77 } 78 79 impl Fjordgard { 80 fn new() -> (Self, Task<Message>) { 81 let settings = window::Settings::default(); 82 let main_window_size = settings.size; 83 84 #[cfg(not(target_arch = "wasm32"))] 85 let (id, open) = window::open(settings); 86 #[cfg(target_arch = "wasm32")] 87 let (id, open) = window_open(settings); 88 89 let config = Config::load().unwrap(); 90 91 let format_string = config.time_format.clone(); 92 let format_parsed = StrftimeItems::new_lenient(&format_string) 93 .parse_to_owned() 94 .unwrap(); 95 96 let meteo = MeteoClient::new(None).unwrap(); 97 let (background, task) = BackgroundHandle::new(&config, main_window_size); 98 99 ( 100 Self { 101 config: Rc::new(RefCell::new(config)), 102 meteo: Arc::new(meteo), 103 time: Local::now(), 104 background, 105 format_string, 106 format_parsed, 107 108 settings_window: None, 109 settings_id: None, 110 main_window: id, 111 main_window_size, 112 113 coordinate_pair: None, 114 forecast_text: String::from("Weather unknown"), 115 forecast_icon: String::from("icons/weather/100-0.svg"), 116 }, 117 Task::batch([ 118 open.map(|_| Message::MainWindowOpened), 119 task.map(Message::Background), 120 Task::done(Message::RequestForecastUpdate), 121 ]), 122 ) 123 } 124 125 #[cfg(not(target_arch = "wasm32"))] 126 fn title(&self, window_id: window::Id) -> String { 127 if window_id == self.main_window { 128 String::from("Fjordgard") 129 } else { 130 String::from("Settings - Fjordgard") 131 } 132 } 133 134 #[cfg(target_arch = "wasm32")] 135 fn title(&self) -> String { 136 String::from("Fjordgard") 137 } 138 139 fn update(&mut self, msg: Message) -> Task<Message> { 140 match msg { 141 Message::Tick(time) => { 142 self.time = time; 143 144 Task::none() 145 } 146 Message::Media(action) => match action { 147 MediaControl::Next => { 148 Task::done(Message::Background(background::Message::RequestUnsplash(1))) 149 } 150 MediaControl::Previous => Task::done(Message::Background( 151 background::Message::RequestUnsplash(-1), 152 )), 153 MediaControl::Pause => { 154 Task::done(Message::Background(background::Message::PauseUnsplash)) 155 } 156 }, 157 Message::OpenSettings => { 158 if self.settings_window.is_none() { 159 #[cfg(not(target_arch = "wasm32"))] 160 let (_id, open) = window::open(window::Settings { 161 level: window::Level::AlwaysOnTop, 162 size: Size::new(350.0, 450.0), 163 ..Default::default() 164 }); 165 166 #[cfg(target_arch = "wasm32")] 167 let (_id, open) = window_open(window::Settings::default()); 168 169 self.settings_window = Some(settings::Settings::new( 170 self.config.clone(), 171 self.meteo.clone(), 172 )); 173 174 open.map(Message::SettingsOpened) 175 } else { 176 Task::none() 177 } 178 } 179 Message::WindowClosed(id) => { 180 if self.main_window == id { 181 iced::exit() 182 } else { 183 self.settings_window = None; 184 Task::none() 185 } 186 } 187 Message::WindowResized((id, size)) => { 188 if self.main_window != id { 189 return Task::none(); 190 } 191 192 self.main_window_size = size; 193 194 Task::none() 195 } 196 Message::Settings(settings::Message::Committed) => { 197 let config = self.config.borrow(); 198 let config_format = &config.time_format; 199 200 if &self.format_string != config_format { 201 self.format_string = config_format.clone(); 202 self.format_parsed = StrftimeItems::new_lenient(config_format) 203 .parse_to_owned() 204 .unwrap(); 205 } 206 207 let background_task = self 208 .background 209 .load_config(&config, self.main_window_size) 210 .map(Message::Background); 211 212 let new_pair = config.location.as_ref().map(|l| (l.latitude, l.longitude)); 213 214 if new_pair != self.coordinate_pair { 215 self.coordinate_pair = new_pair; 216 Task::batch([background_task, Task::done(Message::RequestForecastUpdate)]) 217 } else { 218 background_task 219 } 220 } 221 #[cfg(target_arch = "wasm32")] 222 Message::Settings(settings::Message::ToBackground(msg)) => { 223 Task::done(Message::Background(msg)) 224 } 225 Message::Settings(settings::Message::CloseSettings) => { 226 #[cfg_attr(target_arch = "wasm32", allow(unused_variables))] 227 if let Some(id) = self.settings_id { 228 self.settings_id = None; 229 self.settings_window = None; 230 231 #[cfg(not(target_arch = "wasm32"))] 232 { 233 window::close(id) 234 } 235 #[cfg(target_arch = "wasm32")] 236 { 237 Task::none() 238 } 239 } else { 240 Task::none() 241 } 242 } 243 Message::Settings(msg) => { 244 if let Some(settings) = &mut self.settings_window { 245 settings.update(msg).map(Message::Settings) 246 } else { 247 Task::none() 248 } 249 } 250 Message::Background(msg) => self.background.update(msg).map(Message::Background), 251 Message::SettingsOpened(id) => { 252 debug!("settings window opened"); 253 self.settings_id = Some(id); 254 Task::none() 255 } 256 Message::MainWindowOpened => { 257 debug!("main window opened"); 258 Task::none() 259 } 260 Message::RequestForecastUpdate => { 261 let config = self.config.borrow(); 262 if let Some(location) = &config.location { 263 let meteo = self.meteo.clone(); 264 let (latitude, longitude) = (location.latitude, location.longitude); 265 266 Task::future(async move { 267 meteo 268 .forecast_single( 269 latitude, 270 longitude, 271 Some(ForecastOptions { 272 current: Some(vec![ 273 CurrentVariable::Temperature2m, 274 CurrentVariable::IsDay, 275 CurrentVariable::WeatherCode, 276 ]), 277 ..Default::default() 278 }), 279 ) 280 .await 281 }) 282 .map(|r| Message::ForecastUpdate(Box::new(r.map_err(|e| e.to_string())))) 283 } else { 284 self.forecast_text = String::from("Weather unknown"); 285 self.forecast_icon = String::from("icons/weather/100-0.svg"); 286 287 Task::none() 288 } 289 } 290 Message::ForecastUpdate(res) => match *res { 291 Err(e) => { 292 error!("failed to load forecast: {e}"); 293 Task::none() 294 } 295 Ok(forecast) => { 296 let forecast = || -> Option<(String, String)> { 297 let current = forecast.current?; 298 let units = forecast.current_units?; 299 300 let temperature = current.data.get(&CurrentVariable::Temperature2m)?; 301 let temperature_units = units.get(&CurrentVariable::Temperature2m)?; 302 303 let is_day = *current.data.get(&CurrentVariable::IsDay)? as u64; 304 let weather_code = *current.data.get(&CurrentVariable::WeatherCode)? as u64; 305 306 let condition_text = match weather_code { 307 0 => { 308 if is_day == 0 { 309 "Clear" 310 } else { 311 "Sunny" 312 } 313 } 314 1 => { 315 if is_day == 0 { 316 "Mainly clear" 317 } else { 318 "Mainly sunny" 319 } 320 } 321 2 => "Partly cloudy", 322 3 => "Overcast", 323 45 => "Foggy", 324 48 => "Rime fog", 325 51 => "Light drizzle", 326 53 => "Drizzle", 327 55 => "Heavy drizzle", 328 56 => "Light freezing drizzle", 329 57 => "Freezing drizzle", 330 61 => "Light rain", 331 63 => "Rain", 332 65 => "Heavy rain", 333 66 => "Light freezing rain", 334 67 => "Freezing rain", 335 71 => "Light snow", 336 73 => "Snow", 337 75 => "Heavy snow", 338 77 => "Snow grains", 339 80 => "Light showers", 340 81 => "Showers", 341 82 => "Heavy showers", 342 85 => "Light snow showers", 343 86 => "Snow showers", 344 95 => "Thunderstorm", 345 96 => "Light thunderstorm with hail", 346 99 => "Thunderstorm with hail", 347 _ => "Unknown", 348 }; 349 350 let icon_condition = match weather_code { 351 0 => 0, 352 1 => 1, 353 2 => 2, 354 3 => 3, 355 45 | 48 => 45, 356 51 | 53 | 55 | 56 | 57 => 51, 357 61 | 63 | 65 | 66 | 67 => 61, 358 71 | 73 | 75 => 71, 359 77 => 77, 360 80 | 81 | 82 | 85 | 86 => 80, 361 95 => 95, 362 96 | 99 => 96, 363 _ => 100, 364 }; 365 366 Some(( 367 format!("{temperature}{temperature_units} {condition_text}"), 368 format!("icons/weather/{icon_condition}-{is_day}.svg"), 369 )) 370 }; 371 372 if let Some((forecast_text, forecast_icon)) = forecast() { 373 self.forecast_text = forecast_text; 374 self.forecast_icon = forecast_icon; 375 } 376 377 Task::none() 378 } 379 }, 380 } 381 } 382 383 #[cfg(not(target_arch = "wasm32"))] 384 fn view(&self, window_id: window::Id) -> Element<Message> { 385 if self.main_window == window_id { 386 self.view_main() 387 } else { 388 self.settings_window 389 .as_ref() 390 .expect("settings window") 391 .view() 392 .map(Message::Settings) 393 } 394 } 395 396 #[cfg(target_arch = "wasm32")] 397 fn view(&self) -> Element<Message> { 398 if let Some(settings) = &self.settings_window { 399 settings.view().map(Message::Settings) 400 } else { 401 self.view_main() 402 } 403 } 404 405 fn view_main(&self) -> Element<Message> { 406 #[cfg_attr(target_arch = "wasm32", allow(unused_mut))] 407 let mut bold = Font::DEFAULT; 408 #[cfg(not(target_arch = "wasm32"))] 409 { 410 bold.weight = Weight::Bold; 411 } 412 413 let time_text = self.time.format_with_items(self.format_parsed.iter()); 414 let time_widget = text(time_text.to_string()) 415 .size(200) 416 .font(bold) 417 .color(Color::WHITE) 418 .width(Length::Fill) 419 .center(); 420 421 let weather_widget = container(row![ 422 icon(&self.forecast_icon) 423 .height(Length::Fixed(32.0)) 424 .width(Length::Fixed(32.0)), 425 horizontal_space().width(Length::Fixed(7.25)), 426 text(&self.forecast_text).color(Color::WHITE).size(25) 427 ]) 428 .center_x(Length::Fill); 429 430 let settings = icon_button("icons/settings.svg", Message::OpenSettings); 431 432 let mut main_column = column![settings, center(column![time_widget, weather_widget])]; 433 434 if self.background.mode == BackgroundMode::Unsplash { 435 main_column = main_column.push( 436 container( 437 row![ 438 icon_button("icons/previous.svg", Message::Media(MediaControl::Previous)), 439 icon_button("icons/pause.svg", Message::Media(MediaControl::Pause)), 440 icon_button("icons/next.svg", Message::Media(MediaControl::Next)), 441 ] 442 .spacing(5), 443 ) 444 .center_x(Length::Fill), 445 ) 446 } 447 448 stack![ 449 self.background.view().map(Message::Background), 450 container(main_column).padding(15) 451 ] 452 .height(Length::Fill) 453 .width(Length::Fill) 454 .into() 455 } 456 457 fn subscription(&self) -> Subscription<Message> { 458 Subscription::batch([ 459 time::every(time::Duration::from_secs(1)).map(|_| Message::Tick(Local::now())), 460 time::every(time::Duration::from_secs(60 * 15)).map(|_| Message::RequestForecastUpdate), 461 time::every(time::Duration::from_secs(60 * 15)) 462 .map(|_| Message::Background(background::Message::RequestUnsplash(1))), 463 window::close_events().map(Message::WindowClosed), 464 window::resize_events().map(Message::WindowResized), 465 ]) 466 } 467 } 468 469 fn main() -> iced::Result { 470 #[cfg(not(target_arch = "wasm32"))] 471 { 472 env_logger::init(); 473 474 iced::daemon(Fjordgard::title, Fjordgard::update, Fjordgard::view) 475 .subscription(Fjordgard::subscription) 476 .run_with(Fjordgard::new) 477 } 478 479 #[cfg(target_arch = "wasm32")] 480 { 481 std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 482 console_log::init_with_level(log::Level::Info).unwrap(); 483 484 iced::application(Fjordgard::title, Fjordgard::update, Fjordgard::view) 485 .subscription(Fjordgard::subscription) 486 .run_with(Fjordgard::new) 487 } 488 }