fjordgard

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

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 }