feat: Initial working prototype
Some checks failed
build / checks-matrix (push) Has been cancelled
build / checks-build (push) Has been cancelled
build / codecov (push) Has been cancelled
docs / docs (push) Has been cancelled

This commit is contained in:
uttarayan21
2025-11-19 01:39:20 +05:30
parent 3222c26bb6
commit a1c36e4fb2
10 changed files with 269 additions and 76 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@
.session .session
api/config.toml api/config.toml
api/items.json api/items.json
config.toml

2
Cargo.lock generated
View File

@@ -3582,6 +3582,7 @@ dependencies = [
"error-stack", "error-stack",
"thiserror 2.0.17", "thiserror 2.0.17",
"tokio", "tokio",
"toml 0.9.8",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"ui-gpui", "ui-gpui",
@@ -7317,6 +7318,7 @@ name = "ui-iced"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"api", "api",
"blurhash",
"gpui_util", "gpui_util",
"iced", "iced",
"tracing", "tracing",

View File

@@ -15,6 +15,7 @@ dotenvy = "0.15.7"
error-stack = "0.6" error-stack = "0.6"
thiserror = "2.0" thiserror = "2.0"
tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"] }
toml = "0.9.8"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View File

@@ -138,7 +138,7 @@ pub struct AlbumInfo {
pub album_artists: Vec<String>, pub album_artists: Vec<String>,
/// Gets or sets the artist provider ids. /// Gets or sets the artist provider ids.
#[serde(rename = "ArtistProviderIds")] #[serde(rename = "ArtistProviderIds")]
pub artist_provider_ids: std::collections::HashMap<String, Option<String>>, pub artist_provider_ids: Option<std::collections::HashMap<String, Option<String>>>,
#[serde(rename = "SongInfos")] #[serde(rename = "SongInfos")]
pub song_infos: Vec<SongInfo>, pub song_infos: Vec<SongInfo>,
} }
@@ -592,7 +592,7 @@ Maps image type to dictionary mapping image tag to blurhash value.*/
pub trickplay: Option< pub trickplay: Option<
std::collections::HashMap< std::collections::HashMap<
String, String,
std::collections::HashMap<String, TrickplayInfo>, Option<std::collections::HashMap<String, TrickplayInfo>>,
>, >,
>, >,
/// Gets or sets the type of the location. /// Gets or sets the type of the location.
@@ -729,31 +729,31 @@ Maps image type to dictionary mapping image tag to blurhash value.*/
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BaseItemDtoImageBlurHashes { pub struct BaseItemDtoImageBlurHashes {
#[serde(rename = "Primary")] #[serde(rename = "Primary")]
pub primary: std::collections::HashMap<String, String>, pub primary: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Art")] #[serde(rename = "Art")]
pub art: std::collections::HashMap<String, String>, pub art: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Backdrop")] #[serde(rename = "Backdrop")]
pub backdrop: std::collections::HashMap<String, String>, pub backdrop: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Banner")] #[serde(rename = "Banner")]
pub banner: std::collections::HashMap<String, String>, pub banner: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Logo")] #[serde(rename = "Logo")]
pub logo: std::collections::HashMap<String, String>, pub logo: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Thumb")] #[serde(rename = "Thumb")]
pub thumb: std::collections::HashMap<String, String>, pub thumb: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Disc")] #[serde(rename = "Disc")]
pub disc: std::collections::HashMap<String, String>, pub disc: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Box")] #[serde(rename = "Box")]
pub _box: std::collections::HashMap<String, String>, pub _box: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Screenshot")] #[serde(rename = "Screenshot")]
pub screenshot: std::collections::HashMap<String, String>, pub screenshot: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Menu")] #[serde(rename = "Menu")]
pub menu: std::collections::HashMap<String, String>, pub menu: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Chapter")] #[serde(rename = "Chapter")]
pub chapter: std::collections::HashMap<String, String>, pub chapter: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "BoxRear")] #[serde(rename = "BoxRear")]
pub box_rear: std::collections::HashMap<String, String>, pub box_rear: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Profile")] #[serde(rename = "Profile")]
pub profile: std::collections::HashMap<String, String>, pub profile: Option<std::collections::HashMap<String, String>>,
} }
/// Query result container. /// Query result container.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -794,31 +794,31 @@ pub struct BaseItemPerson {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BaseItemPersonImageBlurHashes { pub struct BaseItemPersonImageBlurHashes {
#[serde(rename = "Primary")] #[serde(rename = "Primary")]
pub primary: std::collections::HashMap<String, String>, pub primary: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Art")] #[serde(rename = "Art")]
pub art: std::collections::HashMap<String, String>, pub art: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Backdrop")] #[serde(rename = "Backdrop")]
pub backdrop: std::collections::HashMap<String, String>, pub backdrop: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Banner")] #[serde(rename = "Banner")]
pub banner: std::collections::HashMap<String, String>, pub banner: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Logo")] #[serde(rename = "Logo")]
pub logo: std::collections::HashMap<String, String>, pub logo: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Thumb")] #[serde(rename = "Thumb")]
pub thumb: std::collections::HashMap<String, String>, pub thumb: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Disc")] #[serde(rename = "Disc")]
pub disc: std::collections::HashMap<String, String>, pub disc: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Box")] #[serde(rename = "Box")]
pub _box: std::collections::HashMap<String, String>, pub _box: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Screenshot")] #[serde(rename = "Screenshot")]
pub screenshot: std::collections::HashMap<String, String>, pub screenshot: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Menu")] #[serde(rename = "Menu")]
pub menu: std::collections::HashMap<String, String>, pub menu: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Chapter")] #[serde(rename = "Chapter")]
pub chapter: std::collections::HashMap<String, String>, pub chapter: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "BoxRear")] #[serde(rename = "BoxRear")]
pub box_rear: std::collections::HashMap<String, String>, pub box_rear: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "Profile")] #[serde(rename = "Profile")]
pub profile: std::collections::HashMap<String, String>, pub profile: Option<std::collections::HashMap<String, String>>,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BookInfo { pub struct BookInfo {
@@ -1362,7 +1362,7 @@ pub struct DisplayPreferencesDto {
pub primary_image_width: i32, pub primary_image_width: i32,
/// Gets or sets the custom prefs. /// Gets or sets the custom prefs.
#[serde(rename = "CustomPrefs")] #[serde(rename = "CustomPrefs")]
pub custom_prefs: std::collections::HashMap<String, Option<String>>, pub custom_prefs: Option<std::collections::HashMap<String, Option<String>>>,
/// Gets or sets the scroll direction. /// Gets or sets the scroll direction.
#[serde(rename = "ScrollDirection")] #[serde(rename = "ScrollDirection")]
pub scroll_direction: ScrollDirection, pub scroll_direction: ScrollDirection,
@@ -1640,7 +1640,7 @@ pub struct GeneralCommand {
#[serde(rename = "ControllingUserId")] #[serde(rename = "ControllingUserId")]
pub controlling_user_id: uuid::Uuid, pub controlling_user_id: uuid::Uuid,
#[serde(rename = "Arguments")] #[serde(rename = "Arguments")]
pub arguments: std::collections::HashMap<String, Option<String>>, pub arguments: Option<std::collections::HashMap<String, Option<String>>>,
} }
/// General command websocket message. /// General command websocket message.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]

View File

@@ -24,7 +24,7 @@ type Result<T, E = JellyfinApiError> = std::result::Result<T, E>;
pub struct JellyfinClient { pub struct JellyfinClient {
client: reqwest::Client, client: reqwest::Client,
access_token: Option<Arc<str>>, access_token: Option<Arc<str>>,
config: Arc<JellyfinConfig>, pub config: Arc<JellyfinConfig>,
} }
impl JellyfinClient { impl JellyfinClient {
@@ -56,7 +56,7 @@ impl JellyfinClient {
Ok(token) Ok(token)
} }
pub async fn set_token(&mut self, token: impl AsRef<str>) { pub fn set_token(&mut self, token: impl AsRef<str>) {
self.access_token = Some(token.as_ref().into()); self.access_token = Some(token.as_ref().into());
} }
@@ -143,10 +143,11 @@ impl JellyfinClient {
.await .await
.inspect_err(|err| tracing::warn!("Failed to load cached token: {}", err)) .inspect_err(|err| tracing::warn!("Failed to load cached token: {}", err))
{ {
self.authenticate().await?; tracing::info!("Authenticating with cached token from {:?}", path);
self.save_token(path).await?; self.access_token = Some(token.clone().into());
Ok(token) Ok(token)
} else { } else {
tracing::info!("No cached token found at {:?}, authenticating...", path);
let token = self let token = self
.authenticate() .authenticate()
.await? .await?

View File

@@ -3,29 +3,22 @@ use api::{JellyfinClient, JellyfinConfig};
use errors::*; use errors::*;
fn jellyfin_config_try() -> Result<JellyfinConfig> { fn jellyfin_config_try() -> Result<JellyfinConfig> {
dotenvy::dotenv() let file = std::fs::read("config.toml").change_context(Error)?;
let config: JellyfinConfig = toml::from_slice(&file)
.change_context(Error) .change_context(Error)
.inspect_err(|err| { .attach("Failed to parse Jellyfin Config")?;
eprintln!("Failed to load .env file: {}", err);
})
.ok();
let config = JellyfinConfig::new(
std::env::var("JELLYFIN_USERNAME").change_context(Error)?,
std::env::var("JELLYFIN_PASSWORD").change_context(Error)?,
std::env::var("JELLYFIN_SERVER_URL").change_context(Error)?,
"jello".to_string(),
);
Ok(config) Ok(config)
} }
fn jellyfin_config() -> JellyfinConfig { fn jellyfin_config() -> JellyfinConfig {
jellyfin_config_try().unwrap_or_else(|err| { jellyfin_config_try().unwrap_or_else(|err| {
eprintln!("Error loading Jellyfin configuration: {}", err); eprintln!("Error loading Jellyfin configuration: {:?}", err);
std::process::exit(1); std::process::exit(1);
}) })
} }
fn main() -> Result<()> { fn main() -> Result<()> {
tracing_subscriber::fmt::init();
ui_iced::ui(jellyfin_config).change_context(Error)?; ui_iced::ui(jellyfin_config).change_context(Error)?;
Ok(()) Ok(())
} }

View File

@@ -112,6 +112,8 @@ impl Property {
}; };
if let Some(true) = self.nullable { if let Some(true) = self.nullable {
format!("Option<{}>", out) format!("Option<{}>", out)
} else if self.nullable.is_none() && self._type == Some(Types::Object) {
format!("Option<{}>", out)
} else { } else {
out out
} }

View File

@@ -5,6 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
api = { version = "0.1.0", path = "../api" } api = { version = "0.1.0", path = "../api" }
blurhash = "0.2.3"
gpui_util = "0.2.2" gpui_util = "0.2.2"
iced = { git = "https://github.com/iced-rs/iced", features = [ iced = { git = "https://github.com/iced-rs/iced", features = [
"advanced", "advanced",

119
ui-iced/src/blur_hash.rs Normal file
View File

@@ -0,0 +1,119 @@
use std::sync::{Arc, LazyLock, atomic::AtomicBool};
use iced::{Element, advanced::Widget, widget::Image};
use crate::shared_string::SharedString;
#[derive(Clone)]
pub struct BlurHash {
hash: SharedString,
handle: Arc<iced::advanced::image::Handle>,
width: u32,
height: u32,
punch: f32,
}
impl BlurHash {
pub fn recompute(&mut self) {
let pixels = blurhash::decode(&self.hash, self.width, self.height, self.punch)
.unwrap_or_else(|_| vec![0; (self.width * self.height * 4) as usize]);
let handle = iced::advanced::image::Handle::from_rgba(self.width, self.height, pixels);
self.handle = Arc::new(handle);
}
pub fn new(hash: impl AsRef<str>) -> Self {
let hash = SharedString::from(hash.as_ref().to_string());
let pixels = blurhash::decode(&hash, 32, 32, 1.0).unwrap_or_else(|_| vec![0; 32 * 32 * 4]);
let handle = iced::advanced::image::Handle::from_rgba(32, 32, pixels);
let handle = Arc::new(handle);
BlurHash {
hash,
handle,
width: 32,
height: 32,
punch: 1.0,
}
}
pub fn width(mut self, height: u32) -> Self {
self.width = height;
self.recompute();
self
}
pub fn height(mut self, height: u32) -> Self {
self.height = height;
self.recompute();
self
}
pub fn punch(mut self, punch: f32) -> Self {
self.punch = punch;
self.recompute();
self
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for BlurHash
where
Renderer: iced::advanced::image::Renderer<Handle = iced::advanced::image::Handle>,
{
fn size(&self) -> iced::Size<iced::Length> {
iced::Size {
width: iced::Length::Fixed(self.width as f32),
height: iced::Length::Fixed(self.height as f32),
}
}
fn layout(
&mut self,
_tree: &mut iced::advanced::widget::Tree,
renderer: &Renderer,
limits: &iced::advanced::layout::Limits,
) -> iced::advanced::layout::Node {
iced::widget::image::layout(
renderer,
limits,
&self.handle,
self.width.into(),
self.height.into(),
None,
iced::ContentFit::default(),
iced::Rotation::default(),
false,
)
}
fn draw(
&self,
_state: &iced::advanced::widget::Tree,
renderer: &mut Renderer,
_theme: &Theme,
_style: &iced::advanced::renderer::Style,
layout: iced::advanced::Layout<'_>,
_cursor: iced::advanced::mouse::Cursor,
_viewport: &iced::Rectangle,
) {
iced::widget::image::draw(
renderer,
layout,
&self.handle,
None,
iced::border::Radius::default(),
iced::ContentFit::default(),
iced::widget::image::FilterMethod::default(),
iced::Rotation::default(),
1.0,
1.0,
);
}
}
impl<'a, Message, Theme, Renderer> From<BlurHash> for iced::Element<'a, Message, Theme, Renderer>
where
Renderer: iced::advanced::image::Renderer<Handle = iced::advanced::image::Handle>,
{
fn from(blur_hash: BlurHash) -> Element<'a, Message, Theme, Renderer> {
iced::Element::new(blur_hash)
}
}

View File

@@ -1,8 +1,11 @@
mod shared_string; mod shared_string;
use shared_string::SharedString; use shared_string::SharedString;
mod blur_hash;
use blur_hash::BlurHash;
use iced::{Alignment, Element, Length, Task, widget::*}; use iced::{Alignment, Element, Length, Task, widget::*};
use std::collections::BTreeMap; use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Loading { pub struct Loading {
@@ -13,20 +16,41 @@ pub struct Loading {
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
pub struct ItemCache { pub struct ItemCache {
pub items: BTreeMap<uuid::Uuid, Item>, pub items: BTreeMap<uuid::Uuid, Item>,
pub tree: BTreeMap<Option<uuid::Uuid>, BTreeSet<uuid::Uuid>>,
} }
impl ItemCache { impl ItemCache {
pub fn insert(&mut self, item: Item) { pub fn insert(&mut self, parent: impl Into<Option<uuid::Uuid>>, item: Item) {
let parent = parent.into();
self.tree.entry(parent).or_default().insert(item.id);
self.items.insert(item.id, item); self.items.insert(item.id, item);
} }
pub fn extend<I: IntoIterator<Item = Item>>(&mut self, items: I) { pub fn extend<I: IntoIterator<Item = Item>>(
self.items &mut self,
.extend(items.into_iter().map(|item| (item.id, item))); parent: impl Into<Option<uuid::Uuid>>,
items: I,
) {
let parent = parent.into();
items.into_iter().for_each(|item| {
self.insert(parent, item);
});
}
pub fn items_of(&self, parent: impl Into<Option<uuid::Uuid>>) -> Vec<&Item> {
let parent = parent.into();
self.tree.get(&None);
self.tree
.get(&parent)
.map(|ids| {
ids.iter()
.filter_map(|id| self.items.get(id))
.collect::<Vec<&Item>>()
})
.unwrap_or_default()
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Image { pub struct Thumbnail {
pub id: SharedString, pub id: SharedString,
pub blur_hash: Option<SharedString>, pub blur_hash: Option<SharedString>,
} }
@@ -36,15 +60,16 @@ impl From<api::jellyfin::BaseItemDto> for Item {
Item { Item {
id: dto.id, id: dto.id,
name: dto.name.map(Into::into), name: dto.name.map(Into::into),
picture: dto parent_id: dto.parent_id,
thumbnail: dto
.image_tags .image_tags
.and_then(|tags| tags.get("Primary").cloned()) .and_then(|tags| tags.get("Primary").cloned())
.map(|tag| Image { .map(|tag| Thumbnail {
id: tag.clone().into(), id: tag.clone().into(),
blur_hash: dto blur_hash: dto
.image_blur_hashes .image_blur_hashes
.primary .primary
.get(&tag) .and_then(|hashes| hashes.get(&tag).cloned())
.map(|s| s.clone().into()), .map(|s| s.clone().into()),
}), }),
} }
@@ -54,8 +79,9 @@ impl From<api::jellyfin::BaseItemDto> for Item {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Item { pub struct Item {
pub id: uuid::Uuid, pub id: uuid::Uuid,
pub parent_id: Option<uuid::Uuid>,
pub name: Option<SharedString>, pub name: Option<SharedString>,
pub picture: Option<Image>, pub thumbnail: Option<Thumbnail>,
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@@ -71,6 +97,7 @@ struct State {
current: Option<uuid::Uuid>, current: Option<uuid::Uuid>,
cache: ItemCache, cache: ItemCache,
jellyfin_client: api::JellyfinClient, jellyfin_client: api::JellyfinClient,
messages: Vec<String>,
} }
impl State { impl State {
@@ -80,6 +107,7 @@ impl State {
current: None, current: None,
cache: ItemCache::default(), cache: ItemCache::default(),
jellyfin_client, jellyfin_client,
messages: Vec::new(),
} }
} }
} }
@@ -88,8 +116,8 @@ impl State {
pub enum Message { pub enum Message {
OpenSettings, OpenSettings,
Refresh, Refresh,
OpenItem(uuid::Uuid), OpenItem(Option<uuid::Uuid>),
LoadedItem(uuid::Uuid, Vec<Item>), LoadedItem(Option<uuid::Uuid>, Vec<Item>),
Error(String), Error(String),
SetToken(String), SetToken(String),
} }
@@ -117,19 +145,35 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
) )
} }
Message::LoadedItem(id, items) => { Message::LoadedItem(id, items) => {
state.cache.extend(items); state.cache.extend(id, items);
state.current = Some(id); state.current = id;
Task::none() Task::none()
} }
Message::Refresh => { Message::Refresh => {
// Handle refresh logic // Handle refresh logic
Task::none() let client = state.jellyfin_client.clone();
let current = state.current;
Task::perform(
async move {
let items: Result<Vec<Item>, api::JellyfinApiError> = client
.items(current)
.await
.map(|items| items.into_iter().map(Item::from).collect());
(current, items)
},
|(msg, items)| match items {
Err(e) => Message::Error(format!("Failed to refresh items: {}", e)),
Ok(items) => Message::LoadedItem(msg, items),
},
)
} }
Message::Error(err) => { Message::Error(err) => {
tracing::error!("Error: {}", err); tracing::error!("Error: {}", err);
state.messages.push(err);
Task::none() Task::none()
} }
Message::SetToken(token) => { Message::SetToken(token) => {
tracing::info!("Authenticated with token: {}", token);
state.jellyfin_client.set_token(token); state.jellyfin_client.set_token(token);
Task::none() Task::none()
} }
@@ -137,22 +181,25 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
} }
fn view(state: &State) -> Element<'_, Message> { fn view(state: &State) -> Element<'_, Message> {
column([header(), body(state)]).into() column([header(state), body(state), footer(state)]).into()
} }
fn body(state: &State) -> Element<'_, Message> { fn body(state: &State) -> Element<'_, Message> {
container(Text::new("Home Screen")) container(
Grid::with_children(state.cache.items_of(state.current).into_iter().map(card)).spacing(70),
)
.padding(70)
.align_x(Alignment::Center) .align_x(Alignment::Center)
.align_y(Alignment::Center) // .align_y(Alignment::Center)
.height(Length::Fill) .height(Length::Fill)
.width(Length::Fill) .width(Length::Fill)
.into() .into()
} }
fn header() -> Element<'static, Message> { fn header(state: &State) -> Element<'_, Message> {
row([ row([
container( container(
Text::new("Jello") Text::new(state.jellyfin_client.config.server_url.as_str())
.width(Length::Fill) .width(Length::Fill)
.align_x(Alignment::Start), .align_x(Alignment::Start),
) )
@@ -184,6 +231,24 @@ fn header() -> Element<'static, Message> {
.into() .into()
} }
fn footer(state: &State) -> Element<'_, Message> {
container(
column(
state
.messages
.iter()
.map(|msg| Text::new(msg).size(12).into())
.collect::<Vec<Element<'_, Message>>>(),
)
.spacing(5),
)
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(container::rounded_box)
.into()
}
fn card(item: &Item) -> Element<'_, Message> { fn card(item: &Item) -> Element<'_, Message> {
let name = item let name = item
.name .name
@@ -192,15 +257,23 @@ fn card(item: &Item) -> Element<'_, Message> {
.unwrap_or("Unnamed Item"); .unwrap_or("Unnamed Item");
container( container(
column([ column([
Text::new(name).size(16).into(), BlurHash::new(
iced::widget::Image::new("placeholder.png") item.thumbnail
.width(Length::Fill) .as_ref()
.height(150) .and_then(|t| t.blur_hash.as_ref())
.into(), .map(|s| s.as_ref())
]) .unwrap_or(""),
.spacing(10),
) )
.padding(10) .width(200)
.height(400)
.into(),
Text::new(name).size(16).into(),
])
.align_x(Alignment::Center)
.width(Length::Fill)
.height(Length::Fill),
)
.padding(70)
.width(Length::FillPortion(5)) .width(Length::FillPortion(5))
.height(Length::FillPortion(5)) .height(Length::FillPortion(5))
.style(container::rounded_box) .style(container::rounded_box)