freya_components/
network_image.rs1use 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#[derive(Props, Clone, PartialEq)]
17pub struct NetworkImageProps {
18 #[props(default = "auto".into())]
20 pub width: String,
21 #[props(default = "auto".into())]
23 pub height: String,
24 pub min_width: Option<String>,
26 pub min_height: Option<String>,
28 pub url: ReadOnlySignal<Url>,
30 pub fallback: Option<Element>,
32 pub loading: Option<Element>,
34 pub alt: Option<String>,
36 pub aspect_ratio: Option<String>,
38 pub cover: Option<String>,
40 pub sampling: Option<String>,
42}
43
44#[doc(hidden)]
46#[derive(PartialEq)]
47pub enum ImageState {
48 Loading,
50
51 Errored,
53
54 Loaded(Bytes),
56}
57
58#[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 for asset_task in assets_tasks.write().drain(..) {
121 asset_task.cancel();
122 }
123
124 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 status.set(ImageState::Loading);
136 if let Some(asset) = asset_cacher.use_asset(&asset_configuration) {
137 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 status.set(ImageState::Loaded(asset_bytes));
151 cached_assets.write().push(asset_configuration);
152 } else if let Err(_err) = asset {
153 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}