freya_components/
network_image.rs

1use bytes::Bytes;
2use dioxus::prelude::*;
3use freya_core::custom_attributes::dynamic_bytes;
4use freya_elements as dioxus_elements;
5use freya_hooks::{
6    use_asset_cacher,
7    use_focus,
8    AssetAge,
9    AssetConfiguration,
10};
11use reqwest::Url;
12
13use crate::Loader;
14
15/// Properties for the [`NetworkImage`] component.
16#[derive(Props, Clone, PartialEq)]
17pub struct NetworkImageProps {
18    /// Width of the image container. Default to `auto`.
19    #[props(default = "auto".into())]
20    pub width: String,
21    /// Height of the image container. Default to `auto`.
22    #[props(default = "auto".into())]
23    pub height: String,
24    /// Min width of the image container.
25    pub min_width: Option<String>,
26    /// Min height of the image container.
27    pub min_height: Option<String>,
28    /// URL of the image.
29    pub url: ReadOnlySignal<Url>,
30    /// Fallback element.
31    pub fallback: Option<Element>,
32    /// Loading element.
33    pub loading: Option<Element>,
34    /// Information about the image.
35    pub alt: Option<String>,
36    /// Aspect ratio of the image.
37    pub aspect_ratio: Option<String>,
38    /// Cover of the image.
39    pub cover: Option<String>,
40    /// Image sampling algorithm.
41    pub sampling: Option<String>,
42}
43
44/// Image status.
45#[doc(hidden)]
46#[derive(PartialEq)]
47pub enum ImageState {
48    /// Image is being fetched.
49    Loading,
50
51    /// Image fetching threw an error.
52    Errored,
53
54    /// Image has been fetched.
55    Loaded(Bytes),
56}
57
58/// Image component that automatically fetches and caches remote (HTTP) images.
59///
60/// # Example
61///
62/// ```rust
63/// # use reqwest::Url;
64/// # use freya::prelude::*;
65/// fn app() -> Element {
66///     rsx!(
67///         NetworkImage {
68///             width: "100%",
69///             height: "100%",
70///             url: "https://raw.githubusercontent.com/marc2332/freya/refs/heads/main/examples/rust_logo.png".parse::<Url>().unwrap()
71///         }
72///     )
73/// }
74/// # use freya_testing::prelude::*;
75/// # import_image!(Rust, "../../../examples/rust_logo.png", {
76/// #   width: "100%",
77/// #   height: "100%"
78/// # });
79/// # launch_doc(|| {
80/// #   rsx!(
81/// #       Preview {
82/// #           Rust { }
83/// #       }
84/// #   )
85/// # }, (250., 250.).into(), "./images/gallery_network_image.png");
86/// ```
87///
88/// # Preview
89/// ![NetworkImage Preview][network_image]
90#[cfg_attr(feature = "docs",
91    doc = embed_doc_image::embed_image!("network_image", "images/gallery_network_image.png")
92)]
93#[allow(non_snake_case)]
94pub fn NetworkImage(
95    NetworkImageProps {
96        width,
97        height,
98        min_width,
99        min_height,
100        url,
101        fallback,
102        loading,
103        alt,
104        aspect_ratio,
105        cover,
106        sampling,
107    }: NetworkImageProps,
108) -> Element {
109    let mut asset_cacher = use_asset_cacher();
110    let focus = use_focus();
111    let mut status = use_signal(|| ImageState::Loading);
112    let mut cached_assets = use_signal::<Vec<AssetConfiguration>>(Vec::new);
113    let mut assets_tasks = use_signal::<Vec<Task>>(Vec::new);
114
115    let a11y_id = focus.attribute();
116
117    use_effect(move || {
118        let url = url.read().clone();
119        // Cancel previous asset fetching requests
120        for asset_task in assets_tasks.write().drain(..) {
121            asset_task.cancel();
122        }
123
124        // Stop using previous assets
125        for cached_asset in cached_assets.write().drain(..) {
126            asset_cacher.unuse_asset(cached_asset);
127        }
128
129        let asset_configuration = AssetConfiguration {
130            age: AssetAge::default(),
131            id: url.to_string(),
132        };
133
134        // Loading image
135        status.set(ImageState::Loading);
136        if let Some(asset) = asset_cacher.use_asset(&asset_configuration) {
137            // Image loaded from cache
138            status.set(ImageState::Loaded(asset));
139            cached_assets.write().push(asset_configuration);
140        } else {
141            let asset_task = spawn(async move {
142                let asset = fetch_image(url).await;
143                if let Ok(asset_bytes) = asset {
144                    asset_cacher.cache_asset(
145                        asset_configuration.clone(),
146                        asset_bytes.clone(),
147                        true,
148                    );
149                    // Image loaded
150                    status.set(ImageState::Loaded(asset_bytes));
151                    cached_assets.write().push(asset_configuration);
152                } else if let Err(_err) = asset {
153                    // Image errored
154                    status.set(ImageState::Errored);
155                }
156            });
157
158            assets_tasks.write().push(asset_task);
159        }
160    });
161
162    match &*status.read_unchecked() {
163        ImageState::Loaded(bytes) => {
164            let image_data = dynamic_bytes(bytes.clone());
165            rsx!(image {
166                height,
167                width,
168                min_width,
169                min_height,
170                a11y_id,
171                image_data,
172                a11y_role: "image",
173                a11y_name: alt,
174                aspect_ratio,
175                cover,
176                cache_key: "{url}",
177                sampling,
178            })
179        }
180        ImageState::Loading => {
181            if let Some(loading_element) = loading {
182                rsx!({ loading_element })
183            } else {
184                rsx!(
185                    rect {
186                        height,
187                        width,
188                        min_width,
189                        min_height,
190                        main_align: "center",
191                        cross_align: "center",
192                        Loader {}
193                    }
194                )
195            }
196        }
197        _ => {
198            if let Some(fallback_element) = fallback {
199                rsx!({ fallback_element })
200            } else {
201                rsx!(
202                    rect {
203                        height,
204                        width,
205                        min_width,
206                        min_height,
207                        main_align: "center",
208                        cross_align: "center",
209                        label {
210                            text_align: "center",
211                            "Error"
212                        }
213                    }
214                )
215            }
216        }
217    }
218}
219
220async fn fetch_image(url: Url) -> reqwest::Result<Bytes> {
221    let res = reqwest::get(url).await?;
222    res.bytes().await
223}