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

View File

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