feat: Initial working prototype
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@
|
|||||||
.session
|
.session
|
||||||
api/config.toml
|
api/config.toml
|
||||||
api/items.json
|
api/items.json
|
||||||
|
config.toml
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
17
src/main.rs
17
src/main.rs
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
119
ui-iced/src/blur_hash.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
.align_x(Alignment::Center)
|
Grid::with_children(state.cache.items_of(state.current).into_iter().map(card)).spacing(70),
|
||||||
.align_y(Alignment::Center)
|
)
|
||||||
.height(Length::Fill)
|
.padding(70)
|
||||||
.width(Length::Fill)
|
.align_x(Alignment::Center)
|
||||||
.into()
|
// .align_y(Alignment::Center)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.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([
|
||||||
|
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(),
|
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))
|
.width(Length::FillPortion(5))
|
||||||
.height(Length::FillPortion(5))
|
.height(Length::FillPortion(5))
|
||||||
.style(container::rounded_box)
|
.style(container::rounded_box)
|
||||||
|
|||||||
Reference in New Issue
Block a user