feat: add jello-types crate and update dependencies with backtrace support

This commit is contained in:
uttarayan21
2025-12-09 23:28:51 +05:30
parent 05ae9ff570
commit 73fcf9bad1
15 changed files with 636 additions and 507 deletions

78
Cargo.lock generated
View File

@@ -37,6 +37,15 @@ dependencies = [
"nom 7.1.3",
]
[[package]]
name = "addr2line"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.1"
@@ -222,7 +231,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a"
dependencies = [
"object",
"object 0.32.2",
]
[[package]]
@@ -649,6 +658,21 @@ dependencies = [
"arrayvec",
]
[[package]]
name = "backtrace"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object 0.37.3",
"rustc-demangle",
"windows-link 0.2.1",
]
[[package]]
name = "base64"
version = "0.22.1"
@@ -1167,6 +1191,17 @@ dependencies = [
"clap_derive",
]
[[package]]
name = "clap-verbosity-flag"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d92b1fab272fe943881b77cc6e920d6543e5b1bfadbd5ed81c7c5a755742394"
dependencies = [
"clap",
"log",
"tracing-core",
]
[[package]]
name = "clap_builder"
version = "4.5.53"
@@ -1316,6 +1351,16 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "color-backtrace"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308329d5d62e877ba02943db3a8e8c052de9fde7ab48283395ba0e6494efbabd"
dependencies = [
"backtrace",
"termcolor",
]
[[package]]
name = "color_quant"
version = "1.1.0"
@@ -2504,6 +2549,12 @@ dependencies = [
"weezl",
]
[[package]]
name = "gimli"
version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]]
name = "gio-sys"
version = "0.21.2"
@@ -3859,7 +3910,9 @@ version = "0.1.0"
dependencies = [
"api",
"clap",
"clap-verbosity-flag",
"clap_complete",
"color-backtrace",
"dotenvy",
"error-stack",
"thiserror 2.0.17",
@@ -3871,6 +3924,14 @@ dependencies = [
"ui-iced",
]
[[package]]
name = "jello-types"
version = "0.1.0"
dependencies = [
"serde",
"uuid",
]
[[package]]
name = "jiff"
version = "0.2.16"
@@ -5129,6 +5190,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "object"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -6147,6 +6217,12 @@ dependencies = [
"walkdir",
]
[[package]]
name = "rustc-demangle"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustc-hash"
version = "1.1.0"

View File

@@ -5,7 +5,7 @@ members = [
"typegen",
"ui-gpui",
"ui-iced",
"crates/iced_video_player", "store",
"crates/iced_video_player", "store", "jello-types",
]
[workspace.dependencies]
iced = { git = "https://github.com/iced-rs/iced", features = [
@@ -27,7 +27,9 @@ license = "MIT"
[dependencies]
api = { version = "0.1.0", path = "api" }
clap = { version = "4.5", features = ["derive"] }
clap-verbosity-flag = { version = "3.0.4", features = ["tracing"] }
clap_complete = "4.5"
color-backtrace = "0.7.2"
dotenvy = "0.15.7"
error-stack = "0.6"
thiserror = "2.0"

View File

@@ -590,7 +590,10 @@ pub struct BaseItemDto {
/// Gets or sets the trickplay manifest.
#[serde(rename = "Trickplay")]
pub trickplay: Option<
std::collections::HashMap<String, Option<std::collections::HashMap<String, TrickplayInfo>>>,
std::collections::HashMap<
String,
Option<std::collections::HashMap<String, TrickplayInfo>>,
>,
>,
/// Gets or sets the type of the location.
#[serde(rename = "LocationType")]
@@ -1522,7 +1525,9 @@ pub struct EncodingOptions {
pub hardware_decoding_codecs: Option<Vec<String>>,
/// Gets or sets the file extensions on-demand metadata based keyframe extraction is enabled for.
#[serde(rename = "AllowOnDemandMetadataBasedKeyframeExtractionForExtensions")]
pub allow_on_demand_metadata_based_keyframe_extraction_for_extensions: Option<Vec<String>>,
pub allow_on_demand_metadata_based_keyframe_extraction_for_extensions: Option<
Vec<String>,
>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct EndPointInfo {

View File

@@ -55,7 +55,7 @@ impl JellyfinClient {
pub fn pre_authenticated(token: impl AsRef<str>, config: JellyfinConfig) -> Result<Self> {
let auth_header = core::iter::once((
reqwest::header::HeaderName::from_static("X-Emby-Authorization"),
reqwest::header::HeaderName::from_static("x-emby-authorization"),
reqwest::header::HeaderValue::from_str(&format!(
"MediaBrowser Client=\"{}\", Device=\"{}\", DeviceId=\"{}\", Version=\"{}\"",
config.client_name, config.device_name, config.device_id, config.version

8
jello-types/Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[package]
name = "jello-types"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { version = "1.0.228", features = ["derive"] }
uuid = { version = "1.18.1", features = ["serde"] }

6
jello-types/src/lib.rs Normal file
View File

@@ -0,0 +1,6 @@
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct User {
id: uuid::Uuid,
name: Option<String>,
primary_image_tag: Option<String>,
}

View File

@@ -1,36 +1,38 @@
#[derive(Debug, clap::Parser)]
pub struct Cli {
#[clap(subcommand)]
pub cmd: SubCommand,
// #[clap(subcommand)]
// pub cmd: SubCommand,
#[command(flatten)]
pub verbosity: clap_verbosity_flag::Verbosity,
}
#[derive(Debug, clap::Subcommand)]
pub enum SubCommand {
#[clap(name = "add")]
Add(Add),
#[clap(name = "list")]
List(List),
#[clap(name = "completions")]
Completions { shell: clap_complete::Shell },
}
#[derive(Debug, clap::Args)]
pub struct Add {
#[clap(short, long)]
pub name: String,
}
#[derive(Debug, clap::Args)]
pub struct List {}
impl Cli {
pub fn completions(shell: clap_complete::Shell) {
let mut command = <Cli as clap::CommandFactory>::command();
clap_complete::generate(
shell,
&mut command,
env!("CARGO_BIN_NAME"),
&mut std::io::stdout(),
);
}
}
// #[derive(Debug, clap::Subcommand)]
// pub enum SubCommand {
// #[clap(name = "add")]
// Add(Add),
// #[clap(name = "list")]
// List(List),
// #[clap(name = "completions")]
// Completions { shell: clap_complete::Shell },
// }
//
// #[derive(Debug, clap::Args)]
// pub struct Add {
// #[clap(short, long)]
// pub name: String,
// }
//
// #[derive(Debug, clap::Args)]
// pub struct List {}
//
// impl Cli {
// pub fn completions(shell: clap_complete::Shell) {
// let mut command = <Cli as clap::CommandFactory>::command();
// clap_complete::generate(
// shell,
// &mut command,
// env!("CARGO_BIN_NAME"),
// &mut std::io::stdout(),
// );
// }
// }

View File

@@ -1,9 +1,14 @@
mod cli;
mod errors;
use api::JellyfinConfig;
use errors::*;
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
color_backtrace::install();
let args = <cli::Cli as clap::Parser>::parse();
tracing_subscriber::fmt()
.with_max_level(args.verbosity)
.init();
ui_iced::ui().change_context(Error)?;
Ok(())
}

View File

@@ -1,217 +1,10 @@
use std::{
borrow::Borrow,
collections::VecDeque,
marker::PhantomData,
path::Path,
sync::{Arc, RwLock, atomic::AtomicBool},
};
pub mod redb;
pub mod sqlite;
pub mod toml;
use futures::task::AtomicWaker;
use redb::{Error, Key, ReadableDatabase, TableDefinition, Value};
use serde::{Serialize, de::DeserializeOwned};
const USERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("users");
const SERVERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("servers");
const SETTINGS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("settings");
#[derive(Debug)]
pub struct TableInner<T> {
db: Arc<T>,
pub trait Store {
fn image(&self, id: &str) -> Option<Vec<u8>>;
fn save_image(&mut self, id: &str, data: &[u8]);
}
impl<T> Clone for TableInner<T> {
fn clone(&self) -> Self {
Self {
db: Arc::clone(&self.db),
}
}
}
impl<T> TableInner<T> {
fn new(db: Arc<T>) -> Self {
Self { db }
}
}
impl TableInner<DatabaseHandle> {
async fn get<'a, K: Key, V: Serialize + DeserializeOwned>(
&self,
table: TableDefinition<'static, K, Vec<u8>>,
key: impl Borrow<K::SelfType<'a>>,
) -> Result<Option<V>> {
let db: &redb::Database = &self.db.as_ref().database;
let db_reader = db.begin_read()?;
let table = db_reader.open_table(table)?;
table
.get(key)?
.map(|value| bson::deserialize_from_slice(&value.value()))
.transpose()
.map_err(|e| redb::Error::Io(std::io::Error::other(e)))
}
async fn insert<'a, 'b, K: Key + Send, V: Serialize + DeserializeOwned + Send + 'a>(
&'b self,
table: TableDefinition<'static, K, Vec<u8>>,
key: impl Borrow<K::SelfType<'a>> + Send + 'b,
value: V,
) -> Result<Option<V>> {
let db: &redb::Database = &self.db.as_ref().database;
// self.db
// .writing
// .store(true, std::sync::atomic::Ordering::SeqCst);
let out = tokio::task::spawn_blocking(move || -> Result<Option<V>> {
let db_writer = db.begin_write()?;
let out = {
let mut table = db_writer.open_table(table)?;
let serialized_value = bson::serialize_to_vec(&value)
.map_err(|e| redb::Error::Io(std::io::Error::other(e)))?;
let previous = table.insert(key, &serialized_value)?;
let out = previous
.map(|value| bson::deserialize_from_slice(&value.value()))
.transpose()
.map_err(|e| redb::Error::Io(std::io::Error::other(e)));
out
};
db_writer.commit()?;
out
})
.await
.expect("Failed to run blocking task")?;
Ok(out)
}
}
// impl<K: Key, V: Serialize + DeserializeOwned> Table<K, V> for TableInner {
// async fn get(&self, key: K) -> Result<Option<Value>> {}
// async fn insert(&self, key: K, value: V) -> Result<Option<Value>> {}
// async fn modify(&self, key: K, v: FnOnce(V) -> V) -> Result<bool> {}
// async fn remove(&self, key: K) -> Result<Option<Value>> {}
// }
#[derive(Debug)]
pub struct Users<T>(TableInner<T>);
impl<T> Clone for Users<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Users<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = USERS;
}
#[derive(Debug)]
pub struct Servers<T>(TableInner<T>);
impl<T> Clone for Servers<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Servers<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SERVERS;
}
#[derive(Debug)]
pub struct Settings<T>(TableInner<T>);
impl<T> Clone for Settings<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Settings<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SETTINGS;
}
#[derive(Debug, Clone)]
pub struct Database {
users: Users<DatabaseHandle>,
servers: Servers<DatabaseHandle>,
settings: Settings<DatabaseHandle>,
handle: Arc<DatabaseHandle>,
}
#[derive(Debug)]
pub struct DatabaseHandle {
database: redb::Database,
writing: AtomicBool,
wakers: RwLock<VecDeque<AtomicWaker>>,
}
#[derive(Debug)]
pub struct DatabaseWriterGuard<'a> {
handle: &'a DatabaseHandle,
dropper: Arc<AtomicBool>,
}
// impl Drop for DatabaseWriterGuard<'_> {
// fn drop(&mut self) {
// self.handle
// .writing
// .store(false, std::sync::atomic::Ordering::SeqCst);
// let is_panicking = std::thread::panicking();
// let Ok(writer) = self.handle.wakers.write() else {
// if is_panicking {
// return;
// } else {
// panic!("Wakers lock poisoned");
// }
// }
// if let Some(waker) = (self.handle.wakers.write()).pop() {
// waker.wake();
// };
// // let mut wakers = self.handle.wakers.write().expect();
// // if let Some(waker) = self.handle.wakers.write().expect("Wakers lock poisoned").pop_front() {
// // waker.wake();
// // }
// // while let Some(waker) = wakers.pop_front() {
// // waker.wake();
// // }
// }
// }
type Result<O, E = redb::Error> = core::result::Result<O, E>;
pub trait Table<K: Key> {
fn insert<V: Serialize + DeserializeOwned>(
&self,
key: K,
value: V,
) -> impl Future<Output = Result<Option<V>>> + Send;
fn modify<V: Serialize + DeserializeOwned, O: Serialize + DeserializeOwned>(
&self,
key: K,
v: impl FnOnce(V) -> O,
) -> impl Future<Output = Result<bool>> + Send;
fn remove<V: Serialize + DeserializeOwned>(
&self,
key: K,
) -> impl Future<Output = Result<Option<V>>> + Send;
fn get<V: Serialize + DeserializeOwned>(
&self,
key: K,
) -> impl Future<Output = Result<Option<V>>> + Send;
}
impl Database {
pub fn create(path: impl AsRef<Path>) -> Result<Self, Error> {
let writing = AtomicBool::new(false);
let wakers = RwLock::new(VecDeque::new());
let db = redb::Database::create(path)?;
let db = Arc::new(DatabaseHandle {
database: db,
writing,
wakers,
});
let table_inner = TableInner::new(Arc::clone(&db));
let users = Users(table_inner.clone());
let servers = Servers(table_inner.clone());
let settings = Settings(table_inner.clone());
Ok(Self {
servers,
users,
settings,
handle: db,
})
}
}
pub struct Settings {}

226
store/src/redb.rs Normal file
View File

@@ -0,0 +1,226 @@
use std::{
borrow::Borrow,
collections::VecDeque,
marker::PhantomData,
path::Path,
sync::{Arc, RwLock, atomic::AtomicBool},
};
use futures::task::AtomicWaker;
use redb::{Error, Key, ReadableDatabase, TableDefinition, Value};
use serde::{Serialize, de::DeserializeOwned};
const USERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("users");
const SERVERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("servers");
const SETTINGS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("settings");
#[derive(Debug)]
pub struct TableInner<T> {
db: Arc<T>,
}
impl<T> Clone for TableInner<T> {
fn clone(&self) -> Self {
Self {
db: Arc::clone(&self.db),
}
}
}
impl<T> TableInner<T> {
fn new(db: Arc<T>) -> Self {
Self { db }
}
}
impl TableInner<DatabaseHandle> {
async fn get<'a, K: Key, V: Serialize + DeserializeOwned>(
&self,
table: TableDefinition<'static, K, Vec<u8>>,
key: impl Borrow<K::SelfType<'a>>,
) -> Result<Option<V>> {
let db: &redb::Database = &self.db.as_ref().database;
let db_reader = db.begin_read()?;
let table = db_reader.open_table(table)?;
table
.get(key)?
.map(|value| bson::deserialize_from_slice(&value.value()))
.transpose()
.map_err(|e| redb::Error::Io(std::io::Error::other(e)))
}
async fn insert<
'a,
'b,
K: Key + Send + Sync,
V: Serialize + DeserializeOwned + Send + Sync + 'a,
>(
&'b self,
table: TableDefinition<'static, K, Vec<u8>>,
key: impl Borrow<K::SelfType<'a>> + Send + 'b,
value: V,
) -> Result<Option<V>> {
let db: &redb::Database = &self.db.as_ref().database;
// self.db
// .writing
// .store(true, std::sync::atomic::Ordering::SeqCst);
// let out = tokio::task::spawn_blocking(move || -> Result<Option<V>>
let out = tokio::task::spawn_blocking(|| -> Result<Option<V>> {
let db_writer = db.begin_write()?;
let out = {
let mut table = db_writer.open_table(table)?;
let serialized_value = bson::serialize_to_vec(&value)
.map_err(|e| redb::Error::Io(std::io::Error::other(e)))?;
let previous = table.insert(key, &serialized_value)?;
let out = previous
.map(|value| bson::deserialize_from_slice(&value.value()))
.transpose()
.map_err(|e| redb::Error::Io(std::io::Error::other(e)));
out
};
db_writer.commit()?;
out
})
.await
.expect("Task panicked");
out
}
}
// impl<K: Key, V: Serialize + DeserializeOwned> Table<K, V> for TableInner {
// async fn get(&self, key: K) -> Result<Option<Value>> {}
// async fn insert(&self, key: K, value: V) -> Result<Option<Value>> {}
// async fn modify(&self, key: K, v: FnOnce(V) -> V) -> Result<bool> {}
// async fn remove(&self, key: K) -> Result<Option<Value>> {}
// }
#[derive(Debug)]
pub struct Users<T>(TableInner<T>);
impl<T> Clone for Users<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Users<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = USERS;
}
#[derive(Debug)]
pub struct Servers<T>(TableInner<T>);
impl<T> Clone for Servers<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Servers<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SERVERS;
}
#[derive(Debug)]
pub struct Settings<T>(TableInner<T>);
impl<T> Clone for Settings<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Settings<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SETTINGS;
}
#[derive(Debug, Clone)]
pub struct Database {
users: Users<DatabaseHandle>,
servers: Servers<DatabaseHandle>,
settings: Settings<DatabaseHandle>,
handle: Arc<DatabaseHandle>,
}
#[derive(Debug)]
pub struct DatabaseHandle {
database: redb::Database,
writing: AtomicBool,
wakers: RwLock<VecDeque<AtomicWaker>>,
}
#[derive(Debug)]
pub struct DatabaseWriterGuard<'a> {
handle: &'a DatabaseHandle,
dropper: Arc<AtomicBool>,
}
// impl Drop for DatabaseWriterGuard<'_> {
// fn drop(&mut self) {
// self.handle
// .writing
// .store(false, std::sync::atomic::Ordering::SeqCst);
// let is_panicking = std::thread::panicking();
// let Ok(writer) = self.handle.wakers.write() else {
// if is_panicking {
// return;
// } else {
// panic!("Wakers lock poisoned");
// }
// }
// if let Some(waker) = (self.handle.wakers.write()).pop() {
// waker.wake();
// };
// // let mut wakers = self.handle.wakers.write().expect();
// // if let Some(waker) = self.handle.wakers.write().expect("Wakers lock poisoned").pop_front() {
// // waker.wake();
// // }
// // while let Some(waker) = wakers.pop_front() {
// // waker.wake();
// // }
// }
// }
type Result<O, E = redb::Error> = core::result::Result<O, E>;
pub trait Table<K: Key> {
fn insert<V: Serialize + DeserializeOwned>(
&self,
key: K,
value: V,
) -> impl Future<Output = Result<Option<V>>> + Send;
fn modify<V: Serialize + DeserializeOwned, O: Serialize + DeserializeOwned>(
&self,
key: K,
v: impl FnOnce(V) -> O,
) -> impl Future<Output = Result<bool>> + Send;
fn remove<V: Serialize + DeserializeOwned>(
&self,
key: K,
) -> impl Future<Output = Result<Option<V>>> + Send;
fn get<V: Serialize + DeserializeOwned>(
&self,
key: K,
) -> impl Future<Output = Result<Option<V>>> + Send;
}
impl Database {
pub fn create(path: impl AsRef<Path>) -> Result<Self, Error> {
let writing = AtomicBool::new(false);
let wakers = RwLock::new(VecDeque::new());
let db = redb::Database::create(path)?;
let db = Arc::new(DatabaseHandle {
database: db,
writing,
wakers,
});
let table_inner = TableInner::new(Arc::clone(&db));
let users = Users(table_inner.clone());
let servers = Servers(table_inner.clone());
let settings = Settings(table_inner.clone());
Ok(Self {
servers,
users,
settings,
handle: db,
})
}
}

0
store/src/sqlite.rs Normal file
View File

0
store/src/toml.rs Normal file
View File

View File

@@ -1,4 +1,6 @@
// mod settings;
mod settings;
mod video;
mod shared_string;
use iced_video_player::{Video, VideoPlayer};
use shared_string::SharedString;
@@ -9,9 +11,9 @@ mod blur_hash;
use blur_hash::BlurHash;
mod preview;
use preview::Preview;
// use preview::Preview;
use iced::{Alignment, Element, Length, Shadow, Task, widget::*};
use iced::{Alignment, Element, Length, Task, widget::*};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone)]
@@ -104,37 +106,57 @@ pub enum Screen {
User,
Video,
}
#[derive(Debug, Clone)]
pub struct Config {
pub server_url: Option<String>,
pub device_id: Option<String>,
pub device_name: Option<String>,
pub client_name: Option<String>,
pub version: Option<String>,
}
impl Default for Config {
fn default() -> Self {
Config {
server_url: Some("http://localhost:8096".to_string()),
device_id: Some("jello-iced".to_string()),
device_name: Some("Jello Iced".to_string()),
client_name: Some("Jello".to_string()),
version: Some("0.1.0".to_string()),
}
}
}
#[derive(Debug, Clone)]
struct State {
loading: Option<Loading>,
current: Option<uuid::Uuid>,
cache: ItemCache,
jellyfin_client: api::JellyfinClient,
jellyfin_client: Option<api::JellyfinClient>,
messages: Vec<String>,
history: Vec<Option<uuid::Uuid>>,
query: Option<String>,
screen: Screen,
// Login form state
username_input: String,
password_input: String,
settings: settings::SettingsState,
is_authenticated: bool,
// Video
video: Option<Arc<Video>>,
}
impl State {
pub fn new(jellyfin_client: api::JellyfinClient) -> Self {
pub fn new() -> Self {
State {
loading: None,
current: None,
cache: ItemCache::default(),
jellyfin_client,
jellyfin_client: None,
messages: Vec::new(),
history: Vec::new(),
query: None,
screen: Screen::Home,
username_input: String::new(),
password_input: String::new(),
settings: settings::SettingsState::default(),
// username_input: String::new(),
// password_input: String::new(),
is_authenticated: false,
video: None,
}
@@ -143,8 +165,7 @@ impl State {
#[derive(Debug, Clone)]
pub enum Message {
OpenSettings,
CloseSettings,
Settings(settings::SettingsMessage),
Refresh,
Search,
SearchQueryChanged(String),
@@ -154,95 +175,22 @@ pub enum Message {
SetToken(String),
Back,
Home,
// Login-related messages
UsernameChanged(String),
PasswordChanged(String),
Login,
LoginSuccess(String),
LoadedClient(api::JellyfinClient, bool),
Logout,
Video(VideoMessage),
}
#[derive(Debug, Clone)]
pub enum VideoMessage {
EndOfStream,
Open(url::Url),
Pause,
Play,
Seek(f64),
Stop,
Test,
// Login {
// username: String,
// password: String,
// config: api::JellyfinConfig,
// },
// LoginSuccess(String),
// LoadedClient(api::JellyfinClient, bool),
// Logout,
Video(video::VideoMessage),
}
fn update(state: &mut State, message: Message) -> Task<Message> {
// if let Some(client) = state.jellyfin_client.clone() {
match message {
Message::OpenSettings => {
state.screen = Screen::Settings;
Task::none()
}
Message::CloseSettings => {
state.screen = Screen::Home;
Task::none()
}
Message::UsernameChanged(username) => {
state.username_input = username;
Task::none()
}
Message::PasswordChanged(password) => {
state.password_input = password;
Task::none()
}
Message::Login => {
let username = state.username_input.clone();
let password = state.password_input.clone();
let config = (*state.jellyfin_client.config).clone();
Task::perform(
async move { api::JellyfinClient::authenticate(username, password, config).await },
|result| match result {
Ok(client) => Message::LoadedClient(client, true),
Err(e) => Message::Error(format!("Login failed: {}", e)),
},
)
}
Message::LoginSuccess(token) => {
state.jellyfin_client.set_token(token.clone());
state.is_authenticated = true;
state.password_input.clear();
state.messages.push("Login successful!".to_string());
state.screen = Screen::Home;
// Save token and refresh items
let client = state.jellyfin_client.clone();
Task::perform(
async move {
let _ = client.save_token(".session").await;
},
|_| Message::Refresh,
)
}
Message::LoadedClient(client, is_authenticated) => {
state.jellyfin_client = client;
state.is_authenticated = is_authenticated;
if is_authenticated {
Task::done(Message::Refresh)
} else {
Task::none()
}
}
Message::Logout => {
state.is_authenticated = false;
state.jellyfin_client.set_token("");
state.cache = ItemCache::default();
state.current = None;
state.username_input.clear();
state.password_input.clear();
state.messages.push("Logged out successfully".to_string());
Task::none()
}
Message::OpenItem(id) => {
let client = state.jellyfin_client.clone();
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)
@@ -250,7 +198,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
let url = client
.stream_url(id.expect("ID exists"))
.expect("Failed to get stream URL");
Task::done(Message::Video(VideoMessage::Open(url)))
Task::done(Message::Video(video::VideoMessage::Open(url)))
} else {
Task::perform(
async move {
@@ -273,9 +221,9 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
state.current = id;
Task::none()
}
Message::Refresh => {
Message::Refresh if let Some(client) = state.jellyfin_client.clone() => {
// Handle refresh logic
let client = state.jellyfin_client.clone();
// let client = state.jellyfin_client.clone();
let current = state.current;
Task::perform(
async move {
@@ -298,7 +246,10 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
}
Message::SetToken(token) => {
tracing::info!("Authenticated with token: {}", token);
state.jellyfin_client.set_token(token);
state
.jellyfin_client
.as_mut()
.map(|mut client| client.set_token(token));
state.is_authenticated = true;
Task::none()
}
@@ -315,9 +266,9 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
// Handle search query change
Task::none()
}
Message::Search => {
Message::Search if let Some(client) = state.jellyfin_client.clone() => {
// Handle search action
let client = state.jellyfin_client.clone();
// 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)),
@@ -327,63 +278,14 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
}
})
}
Message::Video(msg) => match msg {
VideoMessage::EndOfStream => {
state.video = None;
Task::none()
}
VideoMessage::Open(url) => {
state.video = Video::new(&url)
.inspect_err(|err| {
tracing::error!("Failed to play video at {}: {:?}", url, err);
})
.ok()
.map(Arc::new);
Task::none()
}
VideoMessage::Pause => {
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
video.set_paused(true);
}
Task::none()
}
VideoMessage::Play => {
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
video.set_paused(false);
}
Task::none()
}
VideoMessage::Seek(position) => {
// if let Some(ref video) = state.video {
// // video.seek(position, true);
// }
Task::none()
}
VideoMessage::Stop => {
state.video = None;
Task::none()
}
VideoMessage::Test => {
let url = url::Url::parse(
// "file:///home/servius/Projects/jello/crates/iced_video_player/.media/test.mp4",
"https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm",
)
.unwrap();
state.video = Video::new(&url)
.inspect_err(|err| {
tracing::error!("{err:?}");
})
.ok()
.map(Arc::new);
Task::none()
}
},
Message::Video(msg) => video::update(state, msg),
_ => todo!(),
}
}
fn view(state: &State) -> Element<'_, Message> {
match state.screen {
// Screen::Settings => settings::settings(state),
Screen::Settings => settings::settings(state),
Screen::Home | _ => home(state),
}
}
@@ -395,25 +297,9 @@ fn home(state: &State) -> Element<'_, Message> {
.into()
}
fn player(video: &Video) -> Element<'_, Message> {
container(
VideoPlayer::new(video)
.width(Length::Fill)
.height(Length::Fill)
.content_fit(iced::ContentFit::Contain)
.on_end_of_stream(Message::Video(VideoMessage::EndOfStream)),
)
.style(|_| container::background(iced::Color::BLACK))
.width(Length::Fill)
.height(Length::Fill)
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.into()
}
fn body(state: &State) -> Element<'_, Message> {
if let Some(ref video) = state.video {
player(video)
video::player(video)
} else {
scrollable(
container(
@@ -436,7 +322,13 @@ fn header(state: &State) -> Element<'_, Message> {
row([
container(
Button::new(
Text::new(state.jellyfin_client.config.server_url.as_str())
Text::new(
state
.jellyfin_client
.as_ref()
.map(|c| c.config.server_url.as_str())
.unwrap_or("No Server"),
)
.align_x(Alignment::Start),
)
.on_press(Message::Home),
@@ -452,9 +344,11 @@ fn header(state: &State) -> Element<'_, Message> {
container(
row([
button("Refresh").on_press(Message::Refresh).into(),
button("Settings").on_press(Message::OpenSettings).into(),
button("Settings")
.on_press(Message::Settings(settings::SettingsMessage::Open))
.into(),
button("TestVideo")
.on_press(Message::Video(VideoMessage::Test))
.on_press(Message::Video(video::VideoMessage::Test))
.into(),
])
.spacing(10),
@@ -544,20 +438,20 @@ fn card(item: &Item) -> Element<'_, Message> {
fn init() -> (State, Task<Message>) {
// Create a default config for initial state
let default_config = api::JellyfinConfig {
server_url: "http://localhost:8096".parse().expect("Valid URL"),
device_id: "jello-iced".to_string(),
device_name: "Jello Iced".to_string(),
client_name: "Jello".to_string(),
version: "0.1.0".to_string(),
};
let default_client = api::JellyfinClient::new_with_config(default_config);
// let default_config = api::JellyfinConfig {
// server_url: "http://localhost:8096".parse().expect("Valid URL"),
// device_id: "jello-iced".to_string(),
// device_name: "Jello Iced".to_string(),
// client_name: "Jello".to_string(),
// version: "0.1.0".to_string(),
// };
// let default_client = api::JellyfinClient::new_with_config(default_config);
(
State::new(default_client),
State::new(),
Task::perform(
async move {
// Load config from file
let config_str = std::fs::read_to_string("config.toml")
.map_err(|e| api::JellyfinApiError::IoError(e))?;
let config: api::JellyfinConfig = toml::from_str(&config_str).map_err(|e| {
@@ -581,8 +475,9 @@ fn init() -> (State, Task<Message>) {
}
},
|result: Result<_, api::JellyfinApiError>| match result {
Ok((client, is_authenticated)) => Message::LoadedClient(client, is_authenticated),
// Ok((client, is_authenticated)) => Message::LoadedClient(client, is_authenticated),
Err(e) => Message::Error(format!("Initialization failed: {}", e)),
_ => Message::Error("Login Unimplemented".to_string()),
},
)
.chain(Task::done(Message::Refresh)),

View File

@@ -1,8 +1,26 @@
use crate::*;
use iced::Element;
pub fn settings(state: &State) -> Element<'_, Message> {}
pub fn settings(state: &State) -> Element<'_, Message> {
empty()
}
#[derive(Debug, Clone)]
pub fn update(_state: &mut SettingsState, message: SettingsMessage) -> Task<Message> {
match message {
SettingsMessage::Open => {}
SettingsMessage::Close => {}
SettingsMessage::Select(screen) => {
tracing::trace!("Switching settings screen to {:?}", screen);
}
}
Task::none()
}
pub fn empty() -> Element<'static, Message> {
column([]).into()
}
#[derive(Debug, Clone, Default)]
pub struct SettingsState {
login_form: LoginForm,
server_form: ServerForm,
@@ -16,8 +34,9 @@ pub enum SettingsMessage {
Select(SettingsScreen),
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub enum SettingsScreen {
#[default]
Main,
Users,
Servers,
@@ -37,20 +56,27 @@ pub struct UserItem {
pub name: SharedString,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct LoginForm {
username: String,
password: String,
username: Option<String>,
password: Option<String>,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct ServerForm {
name: String,
url: String,
name: Option<String>,
url: Option<String>,
}
mod screens {
pub fn main(state: &State) -> Element<'_, Message> {}
pub fn server(state: &State) -> Element<'_, Message> {}
pub fn user(state: &State) -> Element<'_, Message> {}
use super::*;
pub fn main(state: &State) -> Element<'_, Message> {
empty()
}
pub fn server(state: &State) -> Element<'_, Message> {
empty()
}
pub fn user(state: &State) -> Element<'_, Message> {
empty()
}
}

85
ui-iced/src/video.rs Normal file
View File

@@ -0,0 +1,85 @@
use super::*;
#[derive(Debug, Clone)]
pub enum VideoMessage {
EndOfStream,
Open(url::Url),
Pause,
Play,
Seek(f64),
Stop,
Test,
}
pub fn update(state: &mut State, message: VideoMessage) -> Task<Message> {
match message {
VideoMessage::EndOfStream => {
state.video = None;
Task::none()
}
VideoMessage::Open(url) => {
state.video = Video::new(&url)
.inspect_err(|err| {
tracing::error!("Failed to play video at {}: {:?}", url, err);
})
.inspect(|video| {
tracing::info!("Framerate {}", video.framerate());
})
.ok()
.map(Arc::new);
Task::none()
}
VideoMessage::Pause => {
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
video.set_paused(true);
}
Task::none()
}
VideoMessage::Play => {
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
video.set_paused(false);
}
Task::none()
}
VideoMessage::Seek(position) => {
// if let Some(ref video) = state.video {
// // video.seek(position, true);
// }
Task::none()
}
VideoMessage::Stop => {
state.video = None;
Task::none()
}
VideoMessage::Test => {
let url = url::Url::parse(
// "file:///home/servius/Projects/jello/crates/iced_video_player/.media/test.mp4",
"https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm",
// "https://www.youtube.com/watch?v=QbUUaXGA3C4",
)
.unwrap();
state.video = Video::new(&url)
.inspect_err(|err| {
tracing::error!("{err:?}");
})
.ok()
.map(Arc::new);
Task::none()
}
}
}
pub fn player(video: &Video) -> Element<'_, Message> {
container(
VideoPlayer::new(video)
.width(Length::Fill)
.height(Length::Fill)
.content_fit(iced::ContentFit::Contain)
.on_end_of_stream(Message::Video(VideoMessage::EndOfStream)),
)
.style(|_| container::background(iced::Color::BLACK))
.width(Length::Fill)
.height(Length::Fill)
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.into()
}