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())) } }