fjordgard

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

background.rs (13490B)


      1 use fjordgard_unsplash::{
      2     UnsplashClient,
      3     model::{Collection, CollectionPhotos, CollectionPhotosOptions, Format, PhotoFetchOptions},
      4 };
      5 use iced::{
      6     Color, ContentFit, Element, Length, Size, Task,
      7     widget::{button, container, image, row, stack, text},
      8 };
      9 use log::{debug, error};
     10 
     11 use crate::config::{BackgroundMode, Config};
     12 
     13 pub struct UnsplashState {
     14     collection: String,
     15     current: usize,
     16     total: usize,
     17     paused: bool,
     18 
     19     current_page_photos: Option<CollectionPhotos>,
     20     current_page: usize,
     21 }
     22 
     23 pub struct BackgroundHandle {
     24     pub mode: BackgroundMode,
     25     background: String,
     26     size: Size,
     27 
     28     image_handle: Option<image::Handle>,
     29 
     30     unsplash_key: Option<String>,
     31     unsplash_client: Option<UnsplashClient>,
     32     unsplash_state: Option<UnsplashState>,
     33 }
     34 
     35 #[derive(Debug, Clone)]
     36 pub enum Message {
     37     BackgroundRead(Result<Vec<u8>, String>),
     38     UnsplashCollection(Box<Result<Collection, String>>),
     39     UnsplashCollectionPhotos(Result<CollectionPhotos, String>),
     40     RequestUnsplash(isize),
     41     PauseUnsplash,
     42     OpenUrl(String),
     43 }
     44 
     45 impl BackgroundHandle {
     46     pub fn new(config: &Config, size: Size) -> (Self, Task<Message>) {
     47         let mut handle = Self {
     48             mode: config.background_mode,
     49             background: config.background.clone(),
     50             size,
     51 
     52             image_handle: None,
     53 
     54             unsplash_key: config.unsplash_key.clone(),
     55             unsplash_client: None,
     56             unsplash_state: None,
     57         };
     58 
     59         let task = handle.refresh(true);
     60 
     61         (handle, task)
     62     }
     63 
     64     pub fn load_config(&mut self, config: &Config, size: Size) -> Task<Message> {
     65         self.mode = config.background_mode;
     66         self.background = config.background.clone();
     67         self.size = size;
     68 
     69         if self.unsplash_key != config.unsplash_key {
     70             self.unsplash_key = config.unsplash_key.clone();
     71             self.unsplash_state = None;
     72             self.refresh(true)
     73         } else {
     74             self.refresh(false)
     75         }
     76     }
     77 
     78     fn refresh(&mut self, refresh_unsplash: bool) -> Task<Message> {
     79         debug!(
     80             "refreshing background (mode={}, background={})",
     81             self.mode, &self.background
     82         );
     83 
     84         match self.mode {
     85             #[cfg(not(target_arch = "wasm32"))]
     86             BackgroundMode::Local => {
     87                 let path = self.background.clone();
     88 
     89                 Task::future(async move { tokio::fs::read(&path).await })
     90                     .map(|r| Message::BackgroundRead(r.map_err(|e| e.to_string())))
     91             }
     92             BackgroundMode::Unsplash => {
     93                 if !refresh_unsplash {
     94                     return Task::none();
     95                 }
     96 
     97                 if let Some(key) = &self.unsplash_key {
     98                     self.unsplash_client = match UnsplashClient::new(key) {
     99                         Ok(c) => Some(c),
    100                         Err(e) => {
    101                             error!("failed to create Unsplash client: {e}");
    102 
    103                             return Task::none();
    104                         }
    105                     };
    106 
    107                     let collection = self.background.clone();
    108                     let client = self.unsplash_client.clone().unwrap();
    109 
    110                     Task::future(async move { client.collection(&collection).await }).map(|r| {
    111                         Message::UnsplashCollection(Box::new(r.map_err(|e| e.to_string())))
    112                     })
    113                 } else {
    114                     Task::none()
    115                 }
    116             }
    117             _ => Task::none(),
    118         }
    119     }
    120 
    121     pub fn update(&mut self, msg: Message) -> Task<Message> {
    122         match msg {
    123             Message::BackgroundRead(res) => match res {
    124                 Err(e) => {
    125                     error!("failed to load image: {e}");
    126                     Task::none()
    127                 }
    128                 Ok(bytes) => {
    129                     self.image_handle = Some(image::Handle::from_bytes(bytes));
    130                     Task::none()
    131                 }
    132             },
    133             Message::UnsplashCollection(res) => match *res {
    134                 Err(e) => {
    135                     error!("failed to fetch collection: {e}");
    136                     Task::none()
    137                 }
    138                 Ok(collection) => {
    139                     self.unsplash_state = Some(UnsplashState {
    140                         collection: collection.id,
    141                         current: 0,
    142                         total: collection.total_photos,
    143                         paused: false,
    144 
    145                         current_page: 0,
    146                         current_page_photos: None,
    147                     });
    148 
    149                     Task::done(Message::RequestUnsplash(0))
    150                 }
    151             },
    152             Message::RequestUnsplash(direction) => {
    153                 match (&self.unsplash_client, &mut self.unsplash_state) {
    154                     (Some(client), Some(state)) => {
    155                         if state.paused {
    156                             return Task::none();
    157                         }
    158 
    159                         let mut new = state.current as isize + direction;
    160 
    161                         if new < 0 {
    162                             new = state.total as isize;
    163                         } else if new > state.total as isize {
    164                             new = 0;
    165                         }
    166 
    167                         state.current = new as usize;
    168 
    169                         let page = (state.current / 10) + 1;
    170 
    171                         if page == state.current_page && state.current_page_photos.is_some() {
    172                             return Task::done(Message::UnsplashCollectionPhotos(Ok(state
    173                                 .current_page_photos
    174                                 .as_ref()
    175                                 .unwrap()
    176                                 .clone())));
    177                         }
    178 
    179                         let collection = state.collection.clone();
    180                         let client = client.clone();
    181 
    182                         Task::future(async move {
    183                             client
    184                                 .collection_photos(
    185                                     &collection,
    186                                     Some(CollectionPhotosOptions {
    187                                         page: Some(page),
    188                                         per_page: Some(10),
    189                                         ..Default::default()
    190                                     }),
    191                                 )
    192                                 .await
    193                         })
    194                         .map(|r| Message::UnsplashCollectionPhotos(r.map_err(|e| e.to_string())))
    195                     }
    196                     _ => Task::none(),
    197                 }
    198             }
    199             Message::UnsplashCollectionPhotos(res) => match res {
    200                 Err(e) => {
    201                     error!("failed to fetch collection photos: {e}");
    202                     Task::none()
    203                 }
    204                 Ok(photos) => match (&self.unsplash_client, &mut self.unsplash_state) {
    205                     (Some(client), Some(state)) => {
    206                         state.current_page_photos = Some(photos.clone());
    207                         state.current_page = (state.current / 10) + 1;
    208 
    209                         let idx = state.current % 10;
    210                         let photo = match photos.photos.get(idx) {
    211                             Some(photo) => photo,
    212                             None => {
    213                                 error!("photo not found, current={}", state.current);
    214                                 return Task::none();
    215                             }
    216                         };
    217 
    218                         let client = client.clone();
    219                         let photo = photo.clone();
    220                         let size = self.size;
    221 
    222                         Task::future(async move {
    223                             client
    224                                 .download_photo(
    225                                     &photo,
    226                                     Some(PhotoFetchOptions {
    227                                         fm: Some(Format::Png),
    228                                         w: Some(size.width.round().into()),
    229                                         h: Some(size.height.round().into()),
    230                                         ..Default::default()
    231                                     }),
    232                                 )
    233                                 .await
    234                                 .map(|b| b.to_vec())
    235                         })
    236                         .map(|r| Message::BackgroundRead(r.map_err(|e| e.to_string())))
    237                     }
    238                     _ => Task::none(),
    239                 },
    240             },
    241             Message::PauseUnsplash => {
    242                 if let Some(state) = &mut self.unsplash_state {
    243                     state.paused = !state.paused;
    244                     Task::none()
    245                 } else {
    246                     Task::none()
    247                 }
    248             }
    249             #[cfg(not(target_arch = "wasm32"))]
    250             Message::OpenUrl(url) => {
    251                 if let Err(e) = open::that_detached(url) {
    252                     error!("failed to open link: {e}")
    253                 }
    254 
    255                 Task::none()
    256             }
    257             #[cfg(target_arch = "wasm32")]
    258             Message::OpenUrl(url) => {
    259                 if let Some(window) = web_sys::window() {
    260                     if window.open_with_url(&url).is_err() {
    261                         error!("failed to open link")
    262                     }
    263                 }
    264 
    265                 Task::none()
    266             }
    267         }
    268     }
    269 
    270     fn solid<'a>(color: Color) -> Element<'a, Message> {
    271         container("")
    272             .width(Length::Fill)
    273             .height(Length::Fill)
    274             .style(move |_| container::background(color))
    275             .into()
    276     }
    277 
    278     pub fn view(&self) -> Element<Message> {
    279         match self.mode {
    280             BackgroundMode::Solid => {
    281                 Self::solid(Color::parse(&self.background).unwrap_or(Color::BLACK))
    282             }
    283             _ => {
    284                 if let Some(handle) = &self.image_handle {
    285                     let img = image(handle)
    286                         .content_fit(ContentFit::Cover)
    287                         .width(Length::Fill)
    288                         .height(Length::Fill);
    289 
    290                     #[cfg(not(target_arch = "wasm32"))]
    291                     if self.mode == BackgroundMode::Local {
    292                         return img.into();
    293                     }
    294 
    295                     if let Some(state) = &self.unsplash_state {
    296                         let idx = state.current % 10;
    297                         if let Some(photo) = state
    298                             .current_page_photos
    299                             .as_ref()
    300                             .and_then(|c| c.photos.get(idx))
    301                         {
    302                             let suffix = "?utm_source=fjordgard&utm_medium=referral";
    303 
    304                             let photo_url = format!("{}{suffix}", photo.links.html);
    305 
    306                             let user = &photo.user;
    307 
    308                             let author = format!(
    309                                 "{}{}",
    310                                 user.first_name,
    311                                 user.last_name
    312                                     .as_ref()
    313                                     .map(|l| format!(" {l}"))
    314                                     .unwrap_or_default()
    315                             );
    316                             let author_url = format!("{}{suffix}", user.links.html);
    317 
    318                             stack![
    319                                 img,
    320                                 container(
    321                                     row![
    322                                         button(text("Photo").color(Color::WHITE))
    323                                             .style(button::text)
    324                                             .on_press_with(move || Message::OpenUrl(
    325                                                 photo_url.clone()
    326                                             )),
    327                                         text(".").color(Color::WHITE),
    328                                         button(text(author).color(Color::WHITE))
    329                                             .style(button::text)
    330                                             .on_press_with(move || Message::OpenUrl(
    331                                                 author_url.clone()
    332                                             )),
    333                                         text(".").color(Color::WHITE),
    334                                         button(text("Unsplash").color(Color::WHITE))
    335                                             .style(button::text)
    336                                             .on_press_with(move || Message::OpenUrl(format!(
    337                                                 "https://unsplash.com/{suffix}"
    338                                             ))),
    339                                     ]
    340                                     .spacing(0)
    341                                 )
    342                                 .align_left(Length::Fill)
    343                                 .align_bottom(Length::Fill)
    344                                 .padding(15)
    345                             ]
    346                             .into()
    347                         } else {
    348                             img.into()
    349                         }
    350                     } else {
    351                         img.into()
    352                     }
    353                 } else {
    354                     Self::solid(Color::BLACK)
    355                 }
    356             }
    357         }
    358     }
    359 }