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 }