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

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