diff --git a/api/src/lib.rs b/api/src/lib.rs index 17928cb..ed52a16 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -34,10 +34,10 @@ impl JellyfinClient { username: impl AsRef, password: impl AsRef, config: JellyfinConfig, - ) -> Result<(Self, jellyfin::AuthenticationResult)> { + ) -> Result { let url = format!("{}/Users/AuthenticateByName", config.server_url); let client = reqwest::Client::new(); - let auth_result = client + let token = client .post(url) .json(&jellyfin::AuthenticateUserByName { username: Some(username.as_ref().to_string()), @@ -47,14 +47,10 @@ impl JellyfinClient { .await? .error_for_status()? .json::() - .await?; - - let token = auth_result + .await? .access_token - .as_ref() .ok_or_else(|| std::io::Error::other("No field access_token in auth response"))?; - - Ok((Self::pre_authenticated(token, config)?, auth_result)) + Self::pre_authenticated(token, config) } pub fn pre_authenticated(token: impl AsRef, config: JellyfinConfig) -> Result { @@ -84,10 +80,6 @@ impl JellyfinClient { } } - pub fn access_token(&self) -> Option<&str> { - self.access_token.as_deref().map(|s| &*s) - } - pub async fn save_token(&self, path: impl AsRef) -> std::io::Result<()> { if let Some(token) = &self.access_token { tokio::fs::write(path, &**token).await @@ -112,19 +104,6 @@ impl JellyfinClient { self.access_token = Some(token.as_ref().into()); } - pub async fn me(&self) -> Result { - let uri = "Users/Me"; - let text = self - .request_builder(reqwest::Method::GET, uri) - .send() - .await? - .error_for_status()? - .text() - .await?; - let out: jellyfin::UserDto = serde_json::from_str(&text)?; - Ok(out) - } - pub fn request_builder( &self, method: reqwest::Method, diff --git a/ui-iced/src/lib.rs b/ui-iced/src/lib.rs index 647b41e..5ed259d 100644 --- a/ui-iced/src/lib.rs +++ b/ui-iced/src/lib.rs @@ -141,7 +141,6 @@ struct State { settings: settings::SettingsState, is_authenticated: bool, video: Option>>, - user: Option, } impl State { @@ -160,14 +159,12 @@ impl State { // password_input: String::new(), is_authenticated: false, video: None, - user: None, } } } -#[derive(Clone, Debug)] +#[derive(Debug, Clone)] pub enum Message { - Noop, Settings(settings::SettingsMessage), Refresh, Search, @@ -178,30 +175,17 @@ pub enum Message { SetToken(String), Back, Home, + // Login { + // username: String, + // password: String, + // config: api::JellyfinConfig, + // }, + // LoginSuccess(String), + // LoadedClient(api::JellyfinClient, bool), + // Logout, Video(video::VideoMessage), } -// impl std::fmt::Debug for Message { -// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -// match self { -// Message::Settings(msg) => f.debug_tuple("Settings").field(msg).finish(), -// Message::Refresh => f.write_str("Refresh"), -// Message::Search => f.write_str("Search"), -// Message::SearchQueryChanged(q) => f.debug_tuple("SearchQueryChanged").field(q).finish(), -// Message::OpenItem(id) => f.debug_tuple("OpenItem").field(id).finish(), -// Message::LoadedItem(id, items) => { -// f.debug_tuple("LoadedItem").field(id).field(items).finish() -// } -// Message::Error(e) => f.debug_tuple("Error").field(e).finish(), -// Message::SetToken(_t) => f.debug_tuple("SetToken").field(&"...").finish(), // Mask token -// Message::Back => f.write_str("Back"), -// Message::Home => f.write_str("Home"), -// Message::Video(msg) => f.debug_tuple("Video").field(msg).finish(), -// Message::Noop => f.write_str("Noop"), -// } -// } -// } - fn update(state: &mut State, message: Message) -> Task { match message { Message::Settings(msg) => settings::update(state, msg), @@ -304,40 +288,14 @@ fn update(state: &mut State, message: Message) -> Task { } } Message::Video(msg) => video::update(state, msg), - Message::Noop => Task::none(), + _ => todo!(), } } fn view(state: &State) -> Element<'_, Message> { - let content = home(state); - - if matches!(state.screen, Screen::Settings) { - stack![ - content, - mouse_area( - container(mouse_area(settings::settings(state)).on_press(Message::Refresh)) - .width(Length::Fill) - .height(Length::Fill) - .align_x(Alignment::Center) - .align_y(Alignment::Center) - .style(|_theme| { - container::Style { - background: Some( - iced::Color { - a: 0.3, - ..iced::Color::BLACK - } - .into(), - ), - ..container::Style::default() - } - }) - ) - .on_press(Message::Settings(settings::SettingsMessage::Close)), - ] - .into() - } else { - content + match state.screen { + Screen::Settings => settings::settings(state), + Screen::Home | _ => home(state), } } @@ -370,31 +328,27 @@ fn body(state: &State) -> Element<'_, Message> { } fn header(state: &State) -> Element<'_, Message> { - let mut left_content = row![ - text( - state - .jellyfin_client - .as_ref() - .map(|c| c.config.server_url.as_str()) - .unwrap_or("No Server"), - ) - .align_x(Alignment::Start), - ]; - - if let Some(user) = &state.user { - left_content = - left_content.push(text(format!(" | {}", user.name.as_deref().unwrap_or("?")))); - } - row([ - container(Button::new(left_content).on_press(Message::Home)) - .padding(10) - .width(Length::Fill) - .height(Length::Fill) - .align_x(Alignment::Start) - .align_y(Alignment::Center) - .style(container::rounded_box) - .into(), + container( + Button::new( + 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), + ) + .padding(10) + .width(Length::Fill) + .height(Length::Fill) + .align_x(Alignment::Start) + .align_y(Alignment::Center) + .style(container::rounded_box) + .into(), search(state), container( row([ @@ -520,14 +474,8 @@ fn init() -> (State, Task) { match std::fs::read_to_string(".session") { Ok(token) => { let client = api::JellyfinClient::pre_authenticated(token.trim(), config)?; - - // We need to fetch the current user to fully restore state - // let _user_id = client.user_id.unwrap_or_default(); // user_id field doesn't exist on client - - // We will fetch the user info in the chain if we are authenticated Ok((client, true)) } - Err(_) => { // No cached token, create unauthenticated client let client = api::JellyfinClient::new_with_config(config); @@ -536,10 +484,12 @@ fn init() -> (State, Task) { } }, |result: Result<_, api::JellyfinApiError>| match result { - Ok((client, is_auth)) => Message::LoadedClient(client, is_auth), + // 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)), + ) + .chain(Task::done(Message::Refresh)), ) } diff --git a/ui-iced/src/settings.rs b/ui-iced/src/settings.rs index 0bad62e..c7b9b9f 100644 --- a/ui-iced/src/settings.rs +++ b/ui-iced/src/settings.rs @@ -10,61 +10,20 @@ pub fn update(state: &mut State, message: SettingsMessage) -> Task { SettingsMessage::Open => { tracing::trace!("Opening settings"); state.screen = Screen::Settings; - Task::none() } SettingsMessage::Close => { tracing::trace!("Closing settings"); state.screen = Screen::Home; - Task::none() } SettingsMessage::Select(screen) => { tracing::trace!("Switching settings screen to {:?}", screen); state.settings.screen = screen; - Task::none() - } - // LoginResult(Result<(api::JellyfinClient, api::jellyfin::AuthenticationResult), String>), - // LoadedClient(api::JellyfinClient, bool), - // UserLoaded(Result), - // ConfigSaved(Result), - // Logout, - SettingsMessage::User(user) => { - if let UserMessage::Add = user { - // Handle adding user / login - let username = state.settings.login_form.username.clone(); - let password = state.settings.login_form.password.clone(); - - let mut config = api::JellyfinConfig { - server_url: "http://localhost:8096".parse().unwrap(), // Default fallback - device_id: "jello-iced".to_string(), - device_name: "Jello Iced".to_string(), - client_name: "Jello".to_string(), - version: "0.1.0".to_string(), - }; - - // Try to use existing config if possible - if let Some(client) = &state.jellyfin_client { - config = client.config.as_ref().clone(); - } else if let Ok(config_str) = std::fs::read_to_string("config.toml") { - if let Ok(loaded_config) = toml::from_str(&config_str) { - config = loaded_config; - } - } - - return Task::perform( - async move { - api::JellyfinClient::authenticate(username, password, config) - .await - .map_err(|e| e.to_string()) - }, - Message::LoginResult, - ); - } - state.settings.login_form.update(user); - Task::none() } + SettingsMessage::User(user) => state.settings.login_form.update(user), SettingsMessage::Server(server) => state.settings.server_form.update(server), } + Task::none() } pub fn empty() -> Element<'static, Message> { @@ -73,9 +32,9 @@ pub fn empty() -> Element<'static, Message> { #[derive(Debug, Clone, Default)] pub struct SettingsState { - pub login_form: LoginForm, - pub server_form: ServerForm, - pub screen: SettingsScreen, + login_form: LoginForm, + server_form: ServerForm, + screen: SettingsScreen, } #[derive(Debug, Clone)] @@ -95,7 +54,6 @@ pub enum UserMessage { // Edit(uuid::Uuid), // Delete(uuid::Uuid), Clear, - Error(String), } #[derive(Debug, Clone)] @@ -132,9 +90,8 @@ pub struct UserItem { #[derive(Debug, Clone, Default)] pub struct LoginForm { - pub username: String, - pub password: String, - pub error: Option, + username: String, + password: String, } impl LoginForm { @@ -142,11 +99,9 @@ impl LoginForm { match message { UserMessage::UsernameChanged(data) => { self.username = data; - self.error = None; // Clear error on input } UserMessage::PasswordChanged(data) => { self.password = data; - self.error = None; // Clear error on input } UserMessage::Add => { // Handle adding user @@ -154,48 +109,30 @@ impl LoginForm { UserMessage::Clear => { self.username.clear(); self.password.clear(); - self.error = None; - } - UserMessage::Error(msg) => { - self.error = Some(msg); } } } - pub fn view(&self, is_authenticated: bool) -> Element<'_, Message> { - let mut col = iced::widget::column![text("Login Form"),]; - - if !is_authenticated { - let mut inputs = iced::widget::column![ - text_input("Enter Username", &self.username).on_input(|data| { - Message::Settings(SettingsMessage::User(UserMessage::UsernameChanged(data))) + pub fn view(&self) -> Element<'_, Message> { + iced::widget::column![ + text("Login Form"), + text_input("Enter Username", &self.username).on_input(|data| { + Message::Settings(SettingsMessage::User(UserMessage::UsernameChanged(data))) + }), + text_input("Enter Password", &self.password) + .secure(true) + .on_input(|data| { + Message::Settings(SettingsMessage::User(UserMessage::PasswordChanged(data))) }), - text_input("Enter Password", &self.password) - .secure(true) - .on_input(|data| { - Message::Settings(SettingsMessage::User(UserMessage::PasswordChanged(data))) - }), + row![ + button(text("Add User")).on_press_maybe(self.validate()), + button(text("Cancel")) + .on_press(Message::Settings(SettingsMessage::User(UserMessage::Clear))), ] - .spacing(10); - - if let Some(err) = &self.error { - inputs = inputs.push(text(err).style(|_| text::Style { - color: Some(iced::Color::from_rgb(0.8, 0.0, 0.0)), - })); - } - - col = col.push(inputs).push( - row![ - button(text("Login")).on_press_maybe(self.validate()), - button(text("Cancel")) - .on_press(Message::Settings(SettingsMessage::User(UserMessage::Clear))), - ] - .spacing(10), - ); - } else { - col = col.push(row![button(text("Logout")).on_press(Message::Logout)].spacing(10)); - } - - col.spacing(10).padding([10, 0]).into() + .spacing(10), + ] + .spacing(10) + .padding([10, 0]) + .into() } pub fn validate(&self) -> Option { @@ -211,7 +148,7 @@ pub struct ServerForm { } impl ServerForm { - pub fn update(&mut self, message: ServerMessage) -> Task { + pub fn update(&mut self, message: ServerMessage) { match message { ServerMessage::NameChanged(data) => { self.name = data; @@ -220,36 +157,7 @@ impl ServerForm { self.url = data; } ServerMessage::Add => { - // Handle adding server (saving config) - let name = self.name.clone(); - let url_str = self.url.clone(); - - return Task::perform( - async move { - // Try to parse the URL - let url_parsed = url::Url::parse(&url_str) - .or_else(|_| url::Url::parse(&format!("http://{}", url_str))) - .map_err(|e| format!("Invalid URL: {}", e))?; - - // Create new config - let config = api::JellyfinConfig { - server_url: url_parsed.to_string().parse().unwrap(), - device_id: "jello-iced".to_string(), - device_name: name, - client_name: "Jello".to_string(), - version: "0.1.0".to_string(), - }; - - // Save to config.toml - let toml_str = toml::to_string(&config) - .map_err(|e| format!("Failed to serialize config: {}", e))?; - std::fs::write("config.toml", toml_str) - .map_err(|e| format!("Failed to write config file: {}", e))?; - - Ok(config) - }, - Message::ConfigSaved, - ); + // Handle adding server } ServerMessage::Clear => { self.name.clear(); @@ -257,7 +165,6 @@ impl ServerForm { } _ => {} } - Task::none() } pub fn view(&self) -> Element<'_, Message> { iced::widget::column![ @@ -290,15 +197,7 @@ impl ServerForm { mod screens { use super::*; pub fn settings(state: &State) -> Element<'_, Message> { - container( - row([settings_list(state), settings_screen(state)]) - .spacing(20) - .width(Length::Fixed(800.0)) - .height(Length::Fixed(600.0)), - ) - .padding(20) - .style(container::rounded_box) - .into() + row([settings_list(state), settings_screen(state)]).into() } pub fn settings_screen(state: &State) -> Element<'_, Message> { @@ -357,24 +256,10 @@ mod screens { .into() } pub fn user(state: &State) -> Element<'_, Message> { - let user_display = if let Some(user) = &state.user { - iced::widget::column![ - text(format!( - "Logged in as: {}", - user.name.as_deref().unwrap_or("Unknown") - )), - // We could add an avatar here if we had image loading for it - ] - .spacing(10) - } else { - iced::widget::column![].into() - }; - container( Column::new() .push(text("User Settings")) - .push(user_display) - .push(state.settings.login_form.view(state.is_authenticated)) + .push(state.settings.login_form.view()) // .push(userlist(&state)) .spacing(20) .padding(20), @@ -389,100 +274,3 @@ pub fn center_text(content: &str) -> Element<'_, Message> { .width(Length::Fill) .into() } - -// Message::ConfigSaved(result) => { -// match result { -// Ok(config) => { -// tracing::info!("Configuration saved successfully."); -// state.messages.push("Configuration saved.".to_string()); -// -// // Re-initialize client with new config -// // This invalidates the current session as the server might have changed -// state.jellyfin_client = Some(api::JellyfinClient::new_with_config(config)); -// state.is_authenticated = false; -// state.user = None; -// -// // Clear session file as it likely belongs to the old server -// let _ = std::fs::remove_file(".session"); -// -// // Reset cache -// state.cache = ItemCache::default(); -// state.current = None; -// state.history.clear(); -// -// Task::none() -// } -// Err(e) => { -// tracing::error!("Failed to save configuration: {}", e); -// state.messages.push(format!("Failed to save config: {}", e)); -// Task::none() -// } -// } -// } -// Message::LoadedClient(client, is_auth) => { -// state.jellyfin_client = Some(client.clone()); -// state.is_authenticated = is_auth; -// if is_auth { -// // Fetch user if authenticated -// Task::perform( -// async move { client.me().await.map_err(|e| e.to_string()) }, -// Message::UserLoaded, -// ) -// } else { -// Task::done(Message::Refresh) -// } -// } -// Message::UserLoaded(result) => { -// match result { -// Ok(user) => { -// state.user = Some(user); -// } -// Err(e) => { -// tracing::warn!("Failed to load user profile: {}", e); -// } -// } -// Task::done(Message::Refresh) -// } -// Message::LoginResult(result) => { -// match result { -// Ok((client, auth)) => { -// if let Some(token) = client.access_token() { -// if let Err(e) = std::fs::write(".session", token) { -// tracing::error!("Failed to save session token: {}", e); -// } -// } -// // Fetch user here too since authentication just succeeded -// state.jellyfin_client = Some(client.clone()); -// state.is_authenticated = true; -// state.settings.login_form = settings::LoginForm::default(); -// -// // We can use the auth.user if present, or fetch it. -// if let Some(user_dto) = auth.user { -// state.user = Some(user_dto); -// Task::none() -// } else { -// Task::perform( -// async move { client.me().await.map_err(|e| e.to_string()) }, -// Message::UserLoaded, -// ) -// } -// } -// Err(e) => { -// tracing::error!("Login failed: {}", e); -// state.messages.push(format!("Login failed: {}", e)); -// // Pass the error to the settings/login form so it can be displayed -// state -// .settings -// .login_form -// .update(settings::UserMessage::Error(e)); -// Task::none() -// } -// } -// } -// Message::Logout => { -// state.jellyfin_client = None; -// state.is_authenticated = false; -// state.user = None; -// let _ = std::fs::remove_file(".session"); -// Task::none() -// }