diff --git a/crates/components/src/network_image.rs b/crates/components/src/network_image.rs index 54d60b776..426e613bf 100644 --- a/crates/components/src/network_image.rs +++ b/crates/components/src/network_image.rs @@ -45,7 +45,7 @@ pub enum ImageState { Errored, /// Image has been fetched. - Loaded(Signal), + Loaded(Bytes), } /// Image component that automatically fetches and caches remote (HTTP) images. @@ -74,7 +74,7 @@ pub fn NetworkImage(props: NetworkImageProps) -> Element { let NetworkImageTheme { width, height } = use_applied_theme!(&props.theme, network_image); let alt = props.alt.as_deref(); - use_memo(move || { + use_effect(move || { let url = props.url.read().clone(); // Cancel previous asset fetching requests for asset_task in assets_tasks.write().drain(..) { @@ -101,10 +101,13 @@ pub fn NetworkImage(props: NetworkImageProps) -> Element { let asset_task = spawn(async move { let asset = fetch_image(url).await; if let Ok(asset_bytes) = asset { - let asset_signal = - asset_cacher.cache(asset_configuration.clone(), asset_bytes, true); + asset_cacher.cache_asset( + asset_configuration.clone(), + asset_bytes.clone(), + true, + ); // Image loaded - status.set(ImageState::Loaded(asset_signal)); + status.set(ImageState::Loaded(asset_bytes)); cached_assets.write().push(asset_configuration); } else if let Err(_err) = asset { // Image errored @@ -116,45 +119,51 @@ pub fn NetworkImage(props: NetworkImageProps) -> Element { } }); - if let ImageState::Loaded(bytes) = &*status.read_unchecked() { - let image_data = dynamic_bytes(bytes.read().clone()); - rsx!(image { - height: "{height}", - width: "{width}", - a11y_id, - image_data, - a11y_role: "image", - a11y_name: alt - }) - } else if *status.read() == ImageState::Loading { - if let Some(loading_element) = &props.loading { - rsx!({ loading_element }) - } else { - rsx!( - rect { - height: "{height}", - width: "{width}", - main_align: "center", - cross_align: "center", - Loader {} - } - ) - } - } else if let Some(fallback_element) = &props.fallback { - rsx!({ fallback_element }) - } else { - rsx!( - rect { + match &*status.read_unchecked() { + ImageState::Loaded(bytes) => { + let image_data = dynamic_bytes(bytes.clone()); + rsx!(image { height: "{height}", width: "{width}", - main_align: "center", - cross_align: "center", - label { - text_align: "center", - "Error" - } + a11y_id, + image_data, + a11y_role: "image", + a11y_name: alt + }) + } + ImageState::Loading => { + if let Some(loading_element) = props.loading { + rsx!({ loading_element }) + } else { + rsx!( + rect { + height: "{height}", + width: "{width}", + main_align: "center", + cross_align: "center", + Loader {} + } + ) + } + } + _ => { + if let Some(fallback_element) = props.fallback { + rsx!({ fallback_element }) + } else { + rsx!( + rect { + height: "{height}", + width: "{width}", + main_align: "center", + cross_align: "center", + label { + text_align: "center", + "Error" + } + } + ) } - ) + } } } diff --git a/crates/hooks/Cargo.toml b/crates/hooks/Cargo.toml index f9347f6c6..c9e2db128 100644 --- a/crates/hooks/Cargo.toml +++ b/crates/hooks/Cargo.toml @@ -41,6 +41,7 @@ nokhwa = { version = "0.10.7", features = ["input-native"], optional = true } paste = "1.0.14" bitflags = "2.4.1" bytes = "1.5.0" +tracing.workspace = true [dev-dependencies] dioxus = { workspace = true } diff --git a/crates/hooks/src/use_asset_cacher.rs b/crates/hooks/src/use_asset_cacher.rs index 73f27de85..dc2e0db36 100644 --- a/crates/hooks/src/use_asset_cacher.rs +++ b/crates/hooks/src/use_asset_cacher.rs @@ -7,11 +7,14 @@ use std::{ }; use bytes::Bytes; -use dioxus_core::prelude::{ - current_scope_id, - spawn_forever, - ScopeId, - Task, +use dioxus_core::{ + prelude::{ + current_scope_id, + spawn_forever, + ScopeId, + Task, + }, + schedule_update_any, }; use dioxus_hooks::{ use_context, @@ -23,6 +26,7 @@ use dioxus_signals::{ Writable, }; use tokio::time::sleep; +use tracing::info; /// Defines the duration for which an Asset will remain cached after it's user has stopped using it. /// The default is 1h (3600s). @@ -36,7 +40,7 @@ pub enum AssetAge { impl Default for AssetAge { fn default() -> Self { - Self::Duration(Duration::from_secs(3600)) // 1h + Self::Duration(Duration::from_secs(10)) // 1h } } @@ -62,7 +66,7 @@ enum AssetUsers { struct AssetState { users: AssetUsers, - asset_bytes: Signal, + asset_bytes: Bytes, } #[derive(Clone, Copy, Default)] @@ -71,38 +75,37 @@ pub struct AssetCacher { } impl AssetCacher { - /// Cache the given [`AssetConfiguration`] - pub fn cache( + /// Cache the given [`AssetConfiguration`]. If it already exists and has a pending clear-task, it will get cancelled. + pub fn cache_asset( &mut self, asset_config: AssetConfiguration, asset_bytes: Bytes, subscribe: bool, - ) -> Signal { - // Cancel previous caches + ) { + // Invalidate previous caches if let Some(asset_state) = self.registry.write().remove(&asset_config) { if let AssetUsers::ClearTask(task) = asset_state.users { task.cancel(); - asset_state.asset_bytes.manually_drop(); + info!("Clear task of asset with ID '{}' has been cancelled as the asset has been revalidated", asset_config.id); } } // Insert the asset into the cache - let value = ScopeId::ROOT.in_runtime(|| asset_bytes); - let asset_bytes = Signal::new_in_scope(value, ScopeId::ROOT); + let current_scope_id = current_scope_id().unwrap(); self.registry.write().insert( asset_config.clone(), AssetState { asset_bytes, users: AssetUsers::Scopes(if subscribe { - HashSet::from([current_scope_id().unwrap()]) + HashSet::from([current_scope_id]) } else { HashSet::default() }), }, ); - asset_bytes + schedule_update_any()(current_scope_id); } /// Stop using an asset. It will get removed after the specified duration if it's not used until then. @@ -136,15 +139,13 @@ impl AssetCacher { if spawn_clear_task { // Only clear the asset if a duration was specified if let AssetAge::Duration(duration) = asset_config.age { - // Why not use `spawn_forever`? Reason: https://github.com/DioxusLabs/dioxus/issues/2215 let clear_task = spawn_forever({ let asset_config = asset_config.clone(); async move { + info!("Waiting asset with ID '{}' to be cleared", asset_config.id); sleep(duration).await; - if let Some(asset_state) = registry.write().remove(&asset_config) { - // Clear the asset - asset_state.asset_bytes.manually_drop(); - } + registry.write().remove(&asset_config); + info!("Cleared asset with ID '{}'", asset_config.id); } }) .unwrap(); @@ -158,14 +159,17 @@ impl AssetCacher { } /// Start using an Asset. Your scope will get subscribed, to stop using an asset use [`Self::unuse_asset`] - pub fn use_asset(&mut self, config: &AssetConfiguration) -> Option> { + pub fn use_asset(&mut self, asset_config: &AssetConfiguration) -> Option { let mut registry = self.registry.write(); - if let Some(asset_state) = registry.get_mut(config) { + if let Some(asset_state) = registry.get_mut(asset_config) { match &mut asset_state.users { AssetUsers::ClearTask(task) => { - // Cancel clear-tasks + // Cancel clear-task task.cancel(); - asset_state.asset_bytes.manually_drop(); + info!( + "Clear task of asset with ID '{}' has been cancelled", + asset_config.id + ); // Start using this asset asset_state.users = @@ -176,32 +180,38 @@ impl AssetCacher { scopes.insert(current_scope_id().unwrap()); } } + + // Reruns those subscribed components + if let AssetUsers::Scopes(scopes) = &asset_state.users { + let schedule = schedule_update_any(); + for scope in scopes { + schedule(*scope); + } + info!( + "Reran {} scopes subscribed to asset with id '{}'", + scopes.len(), + asset_config.id + ); + } } - registry.get(config).map(|s| s.asset_bytes) + registry.get(asset_config).map(|s| s.asset_bytes.clone()) } - /// Get the size of the cache registry. + /// Read the size of the cache registry. pub fn size(&self) -> usize { self.registry.read().len() } - - /// Clear all the assets from the cache registry. - pub fn clear(&mut self) { - self.registry.try_write().unwrap().clear(); - } } -/// Global caching system for assets. -/// -/// This is a "low level" hook, so you probably won't need it. +/// Get access to the global cache of assets. pub fn use_asset_cacher() -> AssetCacher { use_context() } -/// Initialize the global caching system for assets. +/// Initialize the global cache of assets. /// -/// This is a "low level" hook, so you probably won't need it. +/// This is a **low level** hook that **runs by default** in all Freya apps, you don't need it. pub fn use_init_asset_cacher() { use_context_provider(AssetCacher::default); } diff --git a/crates/hooks/tests/use_asset_cacher.rs b/crates/hooks/tests/use_asset_cacher.rs index 3b0060add..d4ae1a8f1 100644 --- a/crates/hooks/tests/use_asset_cacher.rs +++ b/crates/hooks/tests/use_asset_cacher.rs @@ -26,7 +26,7 @@ async fn asset_cacher() { cacher.unuse_asset(asset_config.clone()); }); - rsx!(label { "{asset.read()[2]}" }) + rsx!(label { "{asset[2]}" }) } fn asset_cacher_app() -> Element { @@ -39,7 +39,7 @@ async fn asset_cacher() { id: "test-asset".to_string(), }; - cacher.cache(asset_config.clone(), vec![9, 8, 7, 6].into(), false); + cacher.cache_asset(asset_config.clone(), vec![9, 8, 7, 6].into(), false); }); rsx!( diff --git a/examples/app_dog.rs b/examples/app_dog.rs index 1f28c24ac..3cee1aec3 100644 --- a/examples/app_dog.rs +++ b/examples/app_dog.rs @@ -4,6 +4,7 @@ )] use freya::prelude::*; +use rand::seq::SliceRandom; use reqwest::Url; use serde::Deserialize; @@ -17,11 +18,21 @@ struct DogApiResponse { } async fn fetch_random_dog() -> Option { - let res = reqwest::get("https://dog.ceo/api/breeds/image/random") - .await - .ok()?; - let data = res.json::().await.ok()?; - data.message.parse().ok() + // let res = reqwest::get("https://dog.ceo/api/breeds/image/random") + // .await + // .ok()?; + // let data = res.json::().await.ok()?; + // data.message.parse().ok() + vec![ + "https://images.dog.ceo/breeds/terrier-norwich/n02094258_2617.jpg" + .parse::() + .unwrap(), + "https://images.dog.ceo/breeds/weimaraner/n02092339_2157.jpg" + .parse::() + .unwrap(), + ] + .choose(&mut rand::thread_rng()) + .map(|e| e.clone()) } fn app() -> Element {