feat: add jello-types crate and update dependencies with backtrace support
This commit is contained in:
@@ -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)),
|
||||
|
||||
@@ -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
85
ui-iced/src/video.rs
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user