diff --git a/ui-gpui/src/lib.rs b/ui-gpui/src/lib.rs index 777c6cd..f4778ab 100644 --- a/ui-gpui/src/lib.rs +++ b/ui-gpui/src/lib.rs @@ -1,262 +1,262 @@ -use ::tap::*; - -use std::{collections::BTreeMap, sync::Arc}; - -use gpui::{ - App, Application, Bounds, ClickEvent, Context, ImageId, ImageSource, RenderImage, Resource, - SharedString, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size, -}; - -#[derive(Clone, Debug)] -pub struct AppState { - pub title: SharedString, - pub items: BTreeMap, - pub item_ids: BTreeMap, - pub current_item: Option, - pub errors: Vec, - pub jellyfin_client: api::JellyfinClient, -} - -#[derive(Clone, Debug)] -pub struct Item { - pub id: SharedString, - pub name: SharedString, - pub item_type: SharedString, - pub media_type: SharedString, -} - -impl Render for AppState { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .flex() - .flex_col() - .size_full() - .justify_center() - .text_color(rgb(0xffffff)) - .child(Self::header()) - .child(Self::body(self, window, cx)) - .child(Self::footer()) - } -} - -actions!(jello_actions, [OpenItem, OnLoadItem, MouseDownEvent]); - -impl AppState { - fn new(title: impl AsRef, jellyfin_client: api::JellyfinClient) -> Self { - AppState { - title: SharedString::new(title.as_ref()), - items: BTreeMap::new(), - item_ids: BTreeMap::new(), - current_item: None, - errors: Vec::new(), - jellyfin_client, - } - } - - // fn on_mouse_down( - // &mut self, - // event: &MouseDownEvent, - // window: &mut Window, - // cx: &mut Context, - // ) { - // // Handle mouse down event - // } - - fn load_item(id: usize) -> impl Fn(&mut Self, &ClickEvent, &mut Window, &mut Context) { - move |state: &mut Self, event: &ClickEvent, window: &mut Window, cx: &mut Context| { - let item_id = id; - cx.spawn(async move |entity, app| { - tracing::info!("Loading item with id: {}", item_id); - }); - } - } - - fn hover_item(id: usize) -> impl Fn(&mut Self, &bool, &mut Window, &mut Context) { - move |state: &mut Self, item: &bool, window: &mut Window, cx: &mut Context| { - dbg!("Hovering over item: {:?}", id); - } - } - - fn header() -> impl IntoElement { - div() - .flex() - .flex_row() - .w_full() - .justify_end() - .h_20() - .border_10() - .bg(rgb(0x333333)) - .child(Self::button("Refresh")) - } - - fn footer() -> impl IntoElement { - div().flex().flex_row().w_full().h_20().bg(rgb(0x333333)) - } - - fn body(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .flex() - .flex_row() - .size_full() - .child(Self::content(self, window, cx)) - .child(Self::sidebar(self, window, cx)) - } - - fn button(label: &str) -> impl IntoElement { - div() - .flex() - .justify_center() - .items_center() - .bg(rgb(0xff00ff)) - .text_color(rgb(0xffffff)) - .border_5() - .rounded_lg() - .child(label.to_string()) - } - - fn content(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .debug_below() - .w_3_4() - // .flex() - // .flex_wrap() - .bg(rgb(0x111111)) - .justify_start() - .items_start() - .overflow_hidden() - .child( - div() - .size_full() - .flex() - .flex_wrap() - .justify_start() - .items_start() - .content_start() - .gap_y_10() - .gap_x_10() - .border_t_10() - .p_5() - .child(Self::card(cx, 1)) - .child(Self::card(cx, 2)) - .child(Self::card(cx, 3)) - .child(Self::card(cx, 4)) - .child(Self::card(cx, 5)) - .child(Self::card(cx, 6)) - .child(Self::card(cx, 7)) - .child(Self::card(cx, 8)) - .child(Self::card(cx, 9)), - ) - } - - fn sidebar(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .flex() - .flex_col() - .w_1_4() - .min_w_1_6() - .bg(rgb(0x222222)) - .child(div().size_full().bg(gpui::yellow())) - } - - fn card(cx: &mut Context, number: usize) -> impl IntoElement { - div() - .id(number) - .on_click(cx.listener(Self::load_item(number))) - .on_hover(cx.listener(Self::hover_item(number))) - .flex() - .flex_col() - .w_48() - .h_64() - .p_10() - .bg(rgb(0xff00ff)) - .rounded_lg() - } -} - -pub fn ui(jellyfin_client: api::JellyfinClient) { - Application::new().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| AppState::new("Jello Media Browser", jellyfin_client)), - ) - .expect("Failed to open window"); - }) -} - -#[derive(Clone, Debug)] -pub struct Card { - pub id: usize, - pub title: SharedString, - pub description: SharedString, - pub image: SharedString, - pub image_blurhash: BlurHash, - pub media_type: SharedString, - pub loading: bool, -} - -impl Render for Card { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .id(self.id) - .flex() - .flex_col() - .w_48() - .h_64() - .p_10() - .bg(rgb(0xff00ff)) - .rounded_lg() - .pipe(|card| { - if self.loading { - card.child(self.image_blurhash.clone()) - } else { - card.child(gpui::img(self.image.clone())) - } - }) - } -} - -#[derive(Clone, Debug)] -pub struct BlurHash { - pub id: ImageId, - pub data: Arc, -} - -impl BlurHash { - pub fn new( - data: impl AsRef, - width: u32, - height: u32, - punch: f32, - ) -> Result> { - use error_stack::ResultExt; - let decoded = - blurhash::decode(data.as_ref(), width, height, punch).change_context(crate::Error)?; - let buffer = image::RgbaImage::from_raw(width, height, decoded) - .ok_or(crate::Error) - .attach("Failed to convert")?; - let frame = image::Frame::new(buffer); - let render_image = RenderImage::new([frame]); - Ok(Self { - id: render_image.id, - data: Arc::from(render_image), - }) - } -} - -impl Render for BlurHash { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - gpui::img(ImageSource::Render(self.data.clone())) - } -} - -impl IntoElement for BlurHash { - type Element = gpui::Img; - - fn into_element(self) -> Self::Element { - gpui::img(ImageSource::Render(self.data.clone())) - } -} +// use ::tap::*; +// +// use std::{collections::BTreeMap, sync::Arc}; +// +// use gpui::{ +// App, Application, Bounds, ClickEvent, Context, ImageId, ImageSource, RenderImage, Resource, +// SharedString, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size, +// }; +// +// #[derive(Clone, Debug)] +// pub struct AppState { +// pub title: SharedString, +// pub items: BTreeMap, +// pub item_ids: BTreeMap, +// pub current_item: Option, +// pub errors: Vec, +// pub jellyfin_client: api::JellyfinClient, +// } +// +// #[derive(Clone, Debug)] +// pub struct Item { +// pub id: SharedString, +// pub name: SharedString, +// pub item_type: SharedString, +// pub media_type: SharedString, +// } +// +// impl Render for AppState { +// fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { +// div() +// .flex() +// .flex_col() +// .size_full() +// .justify_center() +// .text_color(rgb(0xffffff)) +// .child(Self::header()) +// .child(Self::body(self, window, cx)) +// .child(Self::footer()) +// } +// } +// +// actions!(jello_actions, [OpenItem, OnLoadItem, MouseDownEvent]); +// +// impl AppState { +// fn new(title: impl AsRef, jellyfin_client: api::JellyfinClient) -> Self { +// AppState { +// title: SharedString::new(title.as_ref()), +// items: BTreeMap::new(), +// item_ids: BTreeMap::new(), +// current_item: None, +// errors: Vec::new(), +// jellyfin_client, +// } +// } +// +// // fn on_mouse_down( +// // &mut self, +// // event: &MouseDownEvent, +// // window: &mut Window, +// // cx: &mut Context, +// // ) { +// // // Handle mouse down event +// // } +// +// fn load_item(id: usize) -> impl Fn(&mut Self, &ClickEvent, &mut Window, &mut Context) { +// move |state: &mut Self, event: &ClickEvent, window: &mut Window, cx: &mut Context| { +// let item_id = id; +// cx.spawn(async move |entity, app| { +// tracing::info!("Loading item with id: {}", item_id); +// }); +// } +// } +// +// fn hover_item(id: usize) -> impl Fn(&mut Self, &bool, &mut Window, &mut Context) { +// move |state: &mut Self, item: &bool, window: &mut Window, cx: &mut Context| { +// dbg!("Hovering over item: {:?}", id); +// } +// } +// +// fn header() -> impl IntoElement { +// div() +// .flex() +// .flex_row() +// .w_full() +// .justify_end() +// .h_20() +// .border_10() +// .bg(rgb(0x333333)) +// .child(Self::button("Refresh")) +// } +// +// fn footer() -> impl IntoElement { +// div().flex().flex_row().w_full().h_20().bg(rgb(0x333333)) +// } +// +// fn body(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { +// div() +// .flex() +// .flex_row() +// .size_full() +// .child(Self::content(self, window, cx)) +// .child(Self::sidebar(self, window, cx)) +// } +// +// fn button(label: &str) -> impl IntoElement { +// div() +// .flex() +// .justify_center() +// .items_center() +// .bg(rgb(0xff00ff)) +// .text_color(rgb(0xffffff)) +// .border_5() +// .rounded_lg() +// .child(label.to_string()) +// } +// +// fn content(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { +// div() +// .debug_below() +// .w_3_4() +// // .flex() +// // .flex_wrap() +// .bg(rgb(0x111111)) +// .justify_start() +// .items_start() +// .overflow_hidden() +// .child( +// div() +// .size_full() +// .flex() +// .flex_wrap() +// .justify_start() +// .items_start() +// .content_start() +// .gap_y_10() +// .gap_x_10() +// .border_t_10() +// .p_5() +// .child(Self::card(cx, 1)) +// .child(Self::card(cx, 2)) +// .child(Self::card(cx, 3)) +// .child(Self::card(cx, 4)) +// .child(Self::card(cx, 5)) +// .child(Self::card(cx, 6)) +// .child(Self::card(cx, 7)) +// .child(Self::card(cx, 8)) +// .child(Self::card(cx, 9)), +// ) +// } +// +// fn sidebar(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { +// div() +// .flex() +// .flex_col() +// .w_1_4() +// .min_w_1_6() +// .bg(rgb(0x222222)) +// .child(div().size_full().bg(gpui::yellow())) +// } +// +// fn card(cx: &mut Context, number: usize) -> impl IntoElement { +// div() +// .id(number) +// .on_click(cx.listener(Self::load_item(number))) +// .on_hover(cx.listener(Self::hover_item(number))) +// .flex() +// .flex_col() +// .w_48() +// .h_64() +// .p_10() +// .bg(rgb(0xff00ff)) +// .rounded_lg() +// } +// } +// +// pub fn ui(jellyfin_client: api::JellyfinClient) { +// Application::new().run(|cx: &mut App| { +// let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx); +// cx.open_window( +// WindowOptions { +// window_bounds: Some(WindowBounds::Windowed(bounds)), +// ..Default::default() +// }, +// |_, cx| cx.new(|_| AppState::new("Jello Media Browser", jellyfin_client)), +// ) +// .expect("Failed to open window"); +// }) +// } +// +// #[derive(Clone, Debug)] +// pub struct Card { +// pub id: usize, +// pub title: SharedString, +// pub description: SharedString, +// pub image: SharedString, +// pub image_blurhash: BlurHash, +// pub media_type: SharedString, +// pub loading: bool, +// } +// +// impl Render for Card { +// fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { +// div() +// .id(self.id) +// .flex() +// .flex_col() +// .w_48() +// .h_64() +// .p_10() +// .bg(rgb(0xff00ff)) +// .rounded_lg() +// .pipe(|card| { +// if self.loading { +// card.child(self.image_blurhash.clone()) +// } else { +// card.child(gpui::img(self.image.clone())) +// } +// }) +// } +// } +// +// #[derive(Clone, Debug)] +// pub struct BlurHash { +// pub id: ImageId, +// pub data: Arc, +// } +// +// impl BlurHash { +// pub fn new( +// data: impl AsRef, +// width: u32, +// height: u32, +// punch: f32, +// ) -> Result> { +// use error_stack::ResultExt; +// let decoded = +// blurhash::decode(data.as_ref(), width, height, punch).change_context(crate::Error)?; +// let buffer = image::RgbaImage::from_raw(width, height, decoded) +// .ok_or(crate::Error) +// .attach("Failed to convert")?; +// let frame = image::Frame::new(buffer); +// let render_image = RenderImage::new([frame]); +// Ok(Self { +// id: render_image.id, +// data: Arc::from(render_image), +// }) +// } +// } +// +// impl Render for BlurHash { +// fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { +// gpui::img(ImageSource::Render(self.data.clone())) +// } +// } +// +// impl IntoElement for BlurHash { +// type Element = gpui::Img; +// +// fn into_element(self) -> Self::Element { +// gpui::img(ImageSource::Render(self.data.clone())) +// } +// } diff --git a/ui-iced/src/lib.rs b/ui-iced/src/lib.rs index a376384..ebb8a44 100644 --- a/ui-iced/src/lib.rs +++ b/ui-iced/src/lib.rs @@ -190,29 +190,33 @@ fn update(state: &mut State, message: Message) -> Task { // if let Some(client) = state.jellyfin_client.clone() { match message { Message::Settings(msg) => settings::update(&mut state.settings, msg), - Message::OpenItem(id) if let Some(client) = state.jellyfin_client.clone() => { - use api::jellyfin::BaseItemKind::*; - if let Some(cached) = id.as_ref().and_then(|id| state.cache.get(id)) - && matches!(cached._type, Video | Movie | Episode) - { - let url = client - .stream_url(id.expect("ID exists")) - .expect("Failed to get stream URL"); - Task::done(Message::Video(video::VideoMessage::Open(url))) + Message::OpenItem(id) => { + if let Some(client) = state.jellyfin_client.clone() { + use api::jellyfin::BaseItemKind::*; + if let Some(cached) = id.as_ref().and_then(|id| state.cache.get(id)) + && matches!(cached._type, Video | Movie | Episode) + { + let url = client + .stream_url(id.expect("ID exists")) + .expect("Failed to get stream URL"); + Task::done(Message::Video(video::VideoMessage::Open(url))) + } else { + Task::perform( + async move { + let items: Result, api::JellyfinApiError> = client + .items(id) + .await + .map(|items| items.into_iter().map(Item::from).collect()); + (id, items) + }, + |(msg, items)| match items { + Err(e) => Message::Error(format!("Failed to load item: {}", e)), + Ok(items) => Message::LoadedItem(msg, items), + }, + ) + } } else { - Task::perform( - async move { - let items: Result, api::JellyfinApiError> = client - .items(id) - .await - .map(|items| items.into_iter().map(Item::from).collect()); - (id, items) - }, - |(msg, items)| match items { - Err(e) => Message::Error(format!("Failed to load item: {}", e)), - Ok(items) => Message::LoadedItem(msg, items), - }, - ) + Task::none() } } Message::LoadedItem(id, items) => { @@ -221,23 +225,25 @@ fn update(state: &mut State, message: Message) -> Task { state.current = id; Task::none() } - Message::Refresh if let Some(client) = state.jellyfin_client.clone() => { - // Handle refresh logic - // let client = state.jellyfin_client.clone(); - let current = state.current; - Task::perform( - async move { - let items: Result, 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::Refresh => { + if let Some(client) = state.jellyfin_client.clone() { + let current = state.current; + Task::perform( + async move { + let items: Result, 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), + }, + ) + } else { + Task::none() + } } Message::Error(err) => { tracing::error!("Error: {}", err); @@ -266,17 +272,21 @@ fn update(state: &mut State, message: Message) -> Task { // Handle search query change Task::none() } - Message::Search if let Some(client) = state.jellyfin_client.clone() => { + Message::Search => { // Handle search action // let client = state.jellyfin_client.clone(); - let query = state.query.clone().unwrap_or_default(); - Task::perform(async move { client.search(query).await }, |r| match r { - Err(e) => Message::Error(format!("Search failed: {}", e)), - Ok(items) => { - let items = items.into_iter().map(Item::from).collect(); - Message::LoadedItem(None, items) - } - }) + if let Some(client) = state.jellyfin_client.clone() { + let query = state.query.clone().unwrap_or_default(); + Task::perform(async move { client.search(query).await }, |r| match r { + Err(e) => Message::Error(format!("Search failed: {}", e)), + Ok(items) => { + let items = items.into_iter().map(Item::from).collect(); + Message::LoadedItem(None, items) + } + }) + } else { + Task::none() + } } Message::Video(msg) => video::update(state, msg), _ => todo!(),