From 73fcf9bad17ae328cdd1c3c261a00ba9df5185ee Mon Sep 17 00:00:00 2001 From: uttarayan21 Date: Tue, 9 Dec 2025 23:28:51 +0530 Subject: [PATCH] feat: add jello-types crate and update dependencies with backtrace support --- Cargo.lock | 78 +++++++++++- Cargo.toml | 4 +- api/src/jellyfin.rs | 117 ++++++++--------- api/src/lib.rs | 2 +- jello-types/Cargo.toml | 8 ++ jello-types/src/lib.rs | 6 + src/cli.rs | 66 +++++----- src/main.rs | 7 +- store/src/lib.rs | 221 ++------------------------------ store/src/redb.rs | 226 +++++++++++++++++++++++++++++++++ store/src/sqlite.rs | 0 store/src/toml.rs | 0 ui-iced/src/lib.rs | 273 +++++++++++++--------------------------- ui-iced/src/settings.rs | 50 ++++++-- ui-iced/src/video.rs | 85 +++++++++++++ 15 files changed, 636 insertions(+), 507 deletions(-) create mode 100644 jello-types/Cargo.toml create mode 100644 jello-types/src/lib.rs create mode 100644 store/src/redb.rs create mode 100644 store/src/sqlite.rs create mode 100644 store/src/toml.rs create mode 100644 ui-iced/src/video.rs diff --git a/Cargo.lock b/Cargo.lock index 45b09f7..6b1197e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 48d8366..6a27e46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/api/src/jellyfin.rs b/api/src/jellyfin.rs index 05607b7..c105f3b 100644 --- a/api/src/jellyfin.rs +++ b/api/src/jellyfin.rs @@ -77,7 +77,7 @@ pub struct ActivityLogEntryQueryResult { #[serde(rename = "StartIndex")] pub start_index: i32, } -/** Activity log entry start message. +/** Activity log entry start message. Data is the timing data encoded as "$initialDelay,$interval" in ms.*/ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ActivityLogEntryStartMessage { @@ -290,7 +290,7 @@ pub struct AuthenticationResult { #[serde(rename = "ServerId")] pub server_id: Option, } -/** This is strictly used as a data transfer object from the api layer. +/** This is strictly used as a data transfer object from the api layer. This holds information about a BaseItem in a format that is convenient for the client.*/ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct BaseItemDto { @@ -565,8 +565,8 @@ pub struct BaseItemDto { /// Gets or sets the series thumb image tag. #[serde(rename = "SeriesThumbImageTag")] pub series_thumb_image_tag: Option, - /** Gets or sets the blurhashes for the image tags. - Maps image type to dictionary mapping image tag to blurhash value.*/ + /** Gets or sets the blurhashes for the image tags. +Maps image type to dictionary mapping image tag to blurhash value.*/ #[serde(rename = "ImageBlurHashes")] pub image_blur_hashes: BaseItemDtoImageBlurHashes, /// Gets or sets the series studio. @@ -590,7 +590,10 @@ pub struct BaseItemDto { /// Gets or sets the trickplay manifest. #[serde(rename = "Trickplay")] pub trickplay: Option< - std::collections::HashMap>>, + std::collections::HashMap< + String, + Option>, + >, >, /// Gets or sets the type of the location. #[serde(rename = "LocationType")] @@ -721,7 +724,7 @@ pub struct BaseItemDto { #[serde(rename = "CurrentProgram")] pub current_program: Option>, } -/** Gets or sets the blurhashes for the image tags. +/** Gets or sets the blurhashes for the image tags. Maps image type to dictionary mapping image tag to blurhash value.*/ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct BaseItemDtoImageBlurHashes { @@ -1275,11 +1278,11 @@ pub struct DeviceOptionsDto { #[serde(rename = "CustomName")] pub custom_name: Option, } -/** A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play. -
-Specifically, it defines the supported containers and -codecs (video and/or audio, including codec profiles and levels) -the device is able to direct play (without transcoding or remuxing), +/** A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play. +
+Specifically, it defines the supported containers and +codecs (video and/or audio, including codec profiles and levels) +the device is able to direct play (without transcoding or remuxing), as well as which containers/codecs to transcode to in case it isn't.*/ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct DeviceProfile { @@ -1522,7 +1525,9 @@ pub struct EncodingOptions { pub hardware_decoding_codecs: Option>, /// 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>, + pub allow_on_demand_metadata_based_keyframe_extraction_for_extensions: Option< + Vec, + >, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct EndPointInfo { @@ -1540,10 +1545,10 @@ pub struct ExternalIdInfo { /// Gets or sets the unique key for this id. This key should be unique across all providers. #[serde(rename = "Key")] pub key: String, - /** Gets or sets the specific media type for this id. This is used to distinguish between the different - external id types for providers with multiple ids. - A null value indicates there is no specific media type associated with the external id, or this is the - default id for the external provider so there is no need to specify a type.*/ + /** Gets or sets the specific media type for this id. This is used to distinguish between the different +external id types for providers with multiple ids. +A null value indicates there is no specific media type associated with the external id, or this is the +default id for the external provider so there is no need to specify a type.*/ #[serde(rename = "Type")] pub _type: Option, /// Gets or sets the URL format string. @@ -2438,8 +2443,8 @@ pub struct MediaSourceInfo { pub size: Option, #[serde(rename = "Name")] pub name: Option, - /** Gets or sets a value indicating whether the media is remote. - Differentiate internet url vs local network.*/ + /** Gets or sets a value indicating whether the media is remote. +Differentiate internet url vs local network.*/ #[serde(rename = "IsRemote")] pub is_remote: bool, #[serde(rename = "ETag")] @@ -2500,8 +2505,8 @@ pub struct MediaSourceInfo { pub required_http_headers: Option>>, #[serde(rename = "TranscodingUrl")] pub transcoding_url: Option, - /** Media streaming protocol. - Lowercase for backwards compatibility.*/ + /** Media streaming protocol. +Lowercase for backwards compatibility.*/ #[serde(rename = "TranscodingSubProtocol")] pub transcoding_sub_protocol: MediaStreamProtocol, #[serde(rename = "TranscodingContainer")] @@ -2651,9 +2656,9 @@ pub struct MediaStream { /// Gets or sets the real frame rate. #[serde(rename = "RealFrameRate")] pub real_frame_rate: Option, - /** Gets the framerate used as reference. - Prefer AverageFrameRate, if that is null or an unrealistic value - then fallback to RealFrameRate.*/ + /** Gets the framerate used as reference. +Prefer AverageFrameRate, if that is null or an unrealistic value +then fallback to RealFrameRate.*/ #[serde(rename = "ReferenceFrameRate")] pub reference_frame_rate: Option, /// Gets or sets the profile. @@ -2714,8 +2719,8 @@ pub struct MediaUpdateInfoPathDto { /// Gets or sets media path. #[serde(rename = "Path")] pub path: Option, - /** Gets or sets media update type. - Created, Modified, Deleted.*/ + /** Gets or sets media update type. +Created, Modified, Deleted.*/ #[serde(rename = "UpdateType")] pub update_type: Option, } @@ -2963,8 +2968,8 @@ pub struct NetworkConfiguration { /// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests. #[serde(rename = "EnablePublishedServerUriByRequest")] pub enable_published_server_uri_by_request: bool, - /** Gets or sets the PublishedServerUriBySubnet - Gets or sets PublishedServerUri to advertise for specific subnets.*/ + /** Gets or sets the PublishedServerUriBySubnet +Gets or sets PublishedServerUri to advertise for specific subnets.*/ #[serde(rename = "PublishedServerUriBySubnet")] pub published_server_uri_by_subnet: Vec, /// Gets or sets the filter for remote IP connectivity. Used in conjunction with . @@ -3027,12 +3032,12 @@ pub struct OpenLiveStreamDto { /// Gets or sets a value indicating whether always burn in subtitles when transcoding. #[serde(rename = "AlwaysBurnInSubtitleWhenTranscoding")] pub always_burn_in_subtitle_when_transcoding: Option, - /** A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play. -
- Specifically, it defines the supported containers and - codecs (video and/or audio, including codec profiles and levels) - the device is able to direct play (without transcoding or remuxing), - as well as which containers/codecs to transcode to in case it isn't.*/ + /** A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play. +
+Specifically, it defines the supported containers and +codecs (video and/or audio, including codec profiles and levels) +the device is able to direct play (without transcoding or remuxing), +as well as which containers/codecs to transcode to in case it isn't.*/ #[serde(rename = "DeviceProfile")] pub device_profile: Option, /// Gets or sets the device play protocols. @@ -3067,8 +3072,8 @@ pub struct PackageInfo { /// Gets or sets the category. #[serde(rename = "category")] pub category: String, - /** Gets or sets the guid of the assembly associated with this plugin. - This is used to identify the proper item for automatic updates.*/ + /** Gets or sets the guid of the assembly associated with this plugin. +This is used to identify the proper item for automatic updates.*/ #[serde(rename = "guid")] pub guid: uuid::Uuid, /// Gets or sets the versions. @@ -3186,12 +3191,12 @@ pub struct PlaybackInfoDto { /// Gets or sets the live stream id. #[serde(rename = "LiveStreamId")] pub live_stream_id: Option, - /** A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play. -
- Specifically, it defines the supported containers and - codecs (video and/or audio, including codec profiles and levels) - the device is able to direct play (without transcoding or remuxing), - as well as which containers/codecs to transcode to in case it isn't.*/ + /** A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play. +
+Specifically, it defines the supported containers and +codecs (video and/or audio, including codec profiles and levels) +the device is able to direct play (without transcoding or remuxing), +as well as which containers/codecs to transcode to in case it isn't.*/ #[serde(rename = "DeviceProfile")] pub device_profile: Option, /// Gets or sets a value indicating whether to enable direct play. @@ -4036,7 +4041,7 @@ pub struct ScheduledTasksInfoMessage { #[serde(rename = "MessageType")] pub message_type: SessionMessageType, } -/** Scheduled tasks info start message. +/** Scheduled tasks info start message. Data is the timing data encoded as "$initialDelay,$interval" in ms.*/ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ScheduledTasksInfoStartMessage { @@ -4387,8 +4392,8 @@ pub struct ServerConfiguration { /// Gets or sets the last known version that was ran using the configuration. #[serde(rename = "PreviousVersion")] pub previous_version: Option, - /** Gets or sets the stringified PreviousVersion to be stored/loaded, - because System.Version itself isn't xml-serializable.*/ + /** Gets or sets the stringified PreviousVersion to be stored/loaded, +because System.Version itself isn't xml-serializable.*/ #[serde(rename = "PreviousVersionStr")] pub previous_version_str: Option, /// Gets or sets a value indicating whether to enable prometheus metrics exporting. @@ -4440,13 +4445,13 @@ pub struct ServerConfiguration { /// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. #[serde(rename = "MaxAudiobookResume")] pub max_audiobook_resume: i32, - /** Gets or sets the threshold in minutes after a inactive session gets closed automatically. - If set to 0 the check for inactive sessions gets disabled.*/ + /** Gets or sets the threshold in minutes after a inactive session gets closed automatically. +If set to 0 the check for inactive sessions gets disabled.*/ #[serde(rename = "InactiveSessionThreshold")] pub inactive_session_threshold: i32, - /** Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed - Some delay is necessary with some items because their creation is not atomic. It involves the creation of several - different directories and files.*/ + /** Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed +Some delay is necessary with some items because their creation is not atomic. It involves the creation of several +different directories and files.*/ #[serde(rename = "LibraryMonitorDelay")] pub library_monitor_delay: i32, /// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification. @@ -4665,7 +4670,7 @@ pub struct SessionsMessage { #[serde(rename = "MessageType")] pub message_type: SessionMessageType, } -/** Sessions start message. +/** Sessions start message. Data is the timing data encoded as "$initialDelay,$interval" in ms.*/ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SessionsStartMessage { @@ -5324,7 +5329,7 @@ pub struct TranscodingInfo { #[serde(rename = "TranscodeReasons")] pub transcode_reasons: Vec, } -/** A class for transcoding profile information. +/** A class for transcoding profile information. Note for client developers: Conditions defined in MediaBrowser.Model.Dlna.CodecProfile has higher priority and can override values defined here.*/ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TranscodingProfile { @@ -5414,8 +5419,8 @@ pub struct TrickplayOptions { /// Gets or sets a value indicating whether or not to use HW accelerated MJPEG encoding. #[serde(rename = "EnableHwEncoding")] pub enable_hw_encoding: bool, - /** Gets or sets a value indicating whether to only extract key frames. - Significantly faster, but is not compatible with all decoders and/or video files.*/ + /** Gets or sets a value indicating whether to only extract key frames. +Significantly faster, but is not compatible with all decoders and/or video files.*/ #[serde(rename = "EnableKeyFrameOnlyExtraction")] pub enable_key_frame_only_extraction: bool, /// Gets or sets the behavior used by trickplay provider on library scan/update. @@ -5706,8 +5711,8 @@ pub struct UserDto { /// Gets or sets the server identifier. #[serde(rename = "ServerId")] pub server_id: Option, - /** Gets or sets the name of the server. - This is not used by the server and is for client-side usage only.*/ + /** Gets or sets the name of the server. +This is not used by the server and is for client-side usage only.*/ #[serde(rename = "ServerName")] pub server_name: Option, /// Gets or sets the id. @@ -7009,7 +7014,7 @@ pub enum MediaSourceType { #[serde(rename = "Placeholder")] Placeholder, } -/** Media streaming protocol. +/** Media streaming protocol. Lowercase for backwards compatibility.*/ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum MediaStreamProtocol { diff --git a/api/src/lib.rs b/api/src/lib.rs index c57fe7a..ed52a16 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -55,7 +55,7 @@ impl JellyfinClient { pub fn pre_authenticated(token: impl AsRef, config: JellyfinConfig) -> Result { 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 diff --git a/jello-types/Cargo.toml b/jello-types/Cargo.toml new file mode 100644 index 0000000..7c31270 --- /dev/null +++ b/jello-types/Cargo.toml @@ -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"] } diff --git a/jello-types/src/lib.rs b/jello-types/src/lib.rs new file mode 100644 index 0000000..e6dcb32 --- /dev/null +++ b/jello-types/src/lib.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct User { + id: uuid::Uuid, + name: Option, + primary_image_tag: Option, +} diff --git a/src/cli.rs b/src/cli.rs index eaa3f04..0fe68ff 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -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 = ::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 = ::command(); +// clap_complete::generate( +// shell, +// &mut command, +// env!("CARGO_BIN_NAME"), +// &mut std::io::stdout(), +// ); +// } +// } diff --git a/src/main.rs b/src/main.rs index b171020..7dd1291 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 = ::parse(); + tracing_subscriber::fmt() + .with_max_level(args.verbosity) + .init(); ui_iced::ui().change_context(Error)?; Ok(()) } diff --git a/store/src/lib.rs b/store/src/lib.rs index 996afa8..8fe7884 100644 --- a/store/src/lib.rs +++ b/store/src/lib.rs @@ -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> = TableDefinition::new("users"); -const SERVERS: TableDefinition> = TableDefinition::new("servers"); -const SETTINGS: TableDefinition> = TableDefinition::new("settings"); - -#[derive(Debug)] -pub struct TableInner { - db: Arc, +pub trait Store { + fn image(&self, id: &str) -> Option>; + fn save_image(&mut self, id: &str, data: &[u8]); } -impl Clone for TableInner { - fn clone(&self) -> Self { - Self { - db: Arc::clone(&self.db), - } - } -} - -impl TableInner { - fn new(db: Arc) -> Self { - Self { db } - } -} - -impl TableInner { - async fn get<'a, K: Key, V: Serialize + DeserializeOwned>( - &self, - table: TableDefinition<'static, K, Vec>, - key: impl Borrow>, - ) -> Result> { - 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>, - key: impl Borrow> + Send + 'b, - value: V, - ) -> Result> { - 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> { - 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 Table for TableInner { -// async fn get(&self, key: K) -> Result> {} -// async fn insert(&self, key: K, value: V) -> Result> {} -// async fn modify(&self, key: K, v: FnOnce(V) -> V) -> Result {} -// async fn remove(&self, key: K) -> Result> {} -// } - -#[derive(Debug)] -pub struct Users(TableInner); - -impl Clone for Users { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} -impl Users { - const TABLE: TableDefinition<'static, uuid::Uuid, Vec> = USERS; -} - -#[derive(Debug)] -pub struct Servers(TableInner); -impl Clone for Servers { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} -impl Servers { - const TABLE: TableDefinition<'static, uuid::Uuid, Vec> = SERVERS; -} - -#[derive(Debug)] -pub struct Settings(TableInner); -impl Clone for Settings { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} -impl Settings { - const TABLE: TableDefinition<'static, uuid::Uuid, Vec> = SETTINGS; -} - -#[derive(Debug, Clone)] -pub struct Database { - users: Users, - servers: Servers, - settings: Settings, - handle: Arc, -} - -#[derive(Debug)] -pub struct DatabaseHandle { - database: redb::Database, - writing: AtomicBool, - wakers: RwLock>, -} - -#[derive(Debug)] -pub struct DatabaseWriterGuard<'a> { - handle: &'a DatabaseHandle, - dropper: Arc, -} - -// 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 = core::result::Result; - -pub trait Table { - fn insert( - &self, - key: K, - value: V, - ) -> impl Future>> + Send; - fn modify( - &self, - key: K, - v: impl FnOnce(V) -> O, - ) -> impl Future> + Send; - fn remove( - &self, - key: K, - ) -> impl Future>> + Send; - fn get( - &self, - key: K, - ) -> impl Future>> + Send; -} - -impl Database { - pub fn create(path: impl AsRef) -> Result { - 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 {} diff --git a/store/src/redb.rs b/store/src/redb.rs new file mode 100644 index 0000000..121d297 --- /dev/null +++ b/store/src/redb.rs @@ -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> = TableDefinition::new("users"); +const SERVERS: TableDefinition> = TableDefinition::new("servers"); +const SETTINGS: TableDefinition> = TableDefinition::new("settings"); + +#[derive(Debug)] +pub struct TableInner { + db: Arc, +} + +impl Clone for TableInner { + fn clone(&self) -> Self { + Self { + db: Arc::clone(&self.db), + } + } +} + +impl TableInner { + fn new(db: Arc) -> Self { + Self { db } + } +} + +impl TableInner { + async fn get<'a, K: Key, V: Serialize + DeserializeOwned>( + &self, + table: TableDefinition<'static, K, Vec>, + key: impl Borrow>, + ) -> Result> { + 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>, + key: impl Borrow> + Send + 'b, + value: V, + ) -> Result> { + 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> + + let out = tokio::task::spawn_blocking(|| -> Result> { + 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 Table for TableInner { +// async fn get(&self, key: K) -> Result> {} +// async fn insert(&self, key: K, value: V) -> Result> {} +// async fn modify(&self, key: K, v: FnOnce(V) -> V) -> Result {} +// async fn remove(&self, key: K) -> Result> {} +// } + +#[derive(Debug)] +pub struct Users(TableInner); + +impl Clone for Users { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} +impl Users { + const TABLE: TableDefinition<'static, uuid::Uuid, Vec> = USERS; +} + +#[derive(Debug)] +pub struct Servers(TableInner); +impl Clone for Servers { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} +impl Servers { + const TABLE: TableDefinition<'static, uuid::Uuid, Vec> = SERVERS; +} + +#[derive(Debug)] +pub struct Settings(TableInner); +impl Clone for Settings { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} +impl Settings { + const TABLE: TableDefinition<'static, uuid::Uuid, Vec> = SETTINGS; +} + +#[derive(Debug, Clone)] +pub struct Database { + users: Users, + servers: Servers, + settings: Settings, + handle: Arc, +} + +#[derive(Debug)] +pub struct DatabaseHandle { + database: redb::Database, + writing: AtomicBool, + wakers: RwLock>, +} + +#[derive(Debug)] +pub struct DatabaseWriterGuard<'a> { + handle: &'a DatabaseHandle, + dropper: Arc, +} + +// 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 = core::result::Result; + +pub trait Table { + fn insert( + &self, + key: K, + value: V, + ) -> impl Future>> + Send; + fn modify( + &self, + key: K, + v: impl FnOnce(V) -> O, + ) -> impl Future> + Send; + fn remove( + &self, + key: K, + ) -> impl Future>> + Send; + fn get( + &self, + key: K, + ) -> impl Future>> + Send; +} + +impl Database { + pub fn create(path: impl AsRef) -> Result { + 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, + }) + } +} diff --git a/store/src/sqlite.rs b/store/src/sqlite.rs new file mode 100644 index 0000000..e69de29 diff --git a/store/src/toml.rs b/store/src/toml.rs new file mode 100644 index 0000000..e69de29 diff --git a/ui-iced/src/lib.rs b/ui-iced/src/lib.rs index 50b7507..a376384 100644 --- a/ui-iced/src/lib.rs +++ b/ui-iced/src/lib.rs @@ -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, + pub device_id: Option, + pub device_name: Option, + pub client_name: Option, + pub version: Option, +} + +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, current: Option, cache: ItemCache, - jellyfin_client: api::JellyfinClient, + jellyfin_client: Option, messages: Vec, history: Vec>, query: Option, screen: Screen, - // Login form state - username_input: String, - password_input: String, + settings: settings::SettingsState, is_authenticated: bool, - // Video video: Option>, } 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 { + // 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 { 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 { 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::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 { // 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::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,8 +322,14 @@ fn header(state: &State) -> Element<'_, Message> { row([ container( Button::new( - Text::new(state.jellyfin_client.config.server_url.as_str()) - .align_x(Alignment::Start), + 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) { // 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) { } }, |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)), diff --git a/ui-iced/src/settings.rs b/ui-iced/src/settings.rs index 4a4de63..db73d92 100644 --- a/ui-iced/src/settings.rs +++ b/ui-iced/src/settings.rs @@ -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 { + 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, + password: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct ServerForm { - name: String, - url: String, + name: Option, + url: Option, } 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() + } } diff --git a/ui-iced/src/video.rs b/ui-iced/src/video.rs new file mode 100644 index 0000000..7407e45 --- /dev/null +++ b/ui-iced/src/video.rs @@ -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 { + 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() +}