Compare commits

..

3 Commits

Author SHA1 Message Date
8aa698e65e chore: Update cargo.lock
Some checks failed
build / checks-matrix (push) Has been cancelled
build / codecov (push) Has been cancelled
docs / docs (push) Has been cancelled
build / checks-build (push) Has been cancelled
2026-01-20 21:06:57 +05:30
542679e386 chore: Update flake.lock 2026-01-20 21:02:14 +05:30
599d91700e Revert "feat(api): return (Self, AuthenticationResult) from authenticate"
This reverts commit c8c371230f.
2026-01-16 21:50:06 +05:30
5 changed files with 406 additions and 695 deletions

652
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,10 +34,10 @@ impl JellyfinClient {
username: impl AsRef<str>, username: impl AsRef<str>,
password: impl AsRef<str>, password: impl AsRef<str>,
config: JellyfinConfig, config: JellyfinConfig,
) -> Result<(Self, jellyfin::AuthenticationResult)> { ) -> Result<Self> {
let url = format!("{}/Users/AuthenticateByName", config.server_url); let url = format!("{}/Users/AuthenticateByName", config.server_url);
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let auth_result = client let token = client
.post(url) .post(url)
.json(&jellyfin::AuthenticateUserByName { .json(&jellyfin::AuthenticateUserByName {
username: Some(username.as_ref().to_string()), username: Some(username.as_ref().to_string()),
@@ -47,14 +47,10 @@ impl JellyfinClient {
.await? .await?
.error_for_status()? .error_for_status()?
.json::<jellyfin::AuthenticationResult>() .json::<jellyfin::AuthenticationResult>()
.await?; .await?
let token = auth_result
.access_token .access_token
.as_ref()
.ok_or_else(|| std::io::Error::other("No field access_token in auth response"))?; .ok_or_else(|| std::io::Error::other("No field access_token in auth response"))?;
Self::pre_authenticated(token, config)
Ok((Self::pre_authenticated(token, config)?, auth_result))
} }
pub fn pre_authenticated(token: impl AsRef<str>, config: JellyfinConfig) -> Result<Self> { pub fn pre_authenticated(token: impl AsRef<str>, config: JellyfinConfig) -> Result<Self> {
@@ -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::path::Path>) -> std::io::Result<()> { pub async fn save_token(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
if let Some(token) = &self.access_token { if let Some(token) = &self.access_token {
tokio::fs::write(path, &**token).await tokio::fs::write(path, &**token).await
@@ -112,19 +104,6 @@ impl JellyfinClient {
self.access_token = Some(token.as_ref().into()); self.access_token = Some(token.as_ref().into());
} }
pub async fn me(&self) -> Result<jellyfin::UserDto> {
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( pub fn request_builder(
&self, &self,
method: reqwest::Method, method: reqwest::Method,

24
flake.lock generated
View File

@@ -3,11 +3,11 @@
"advisory-db": { "advisory-db": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1766435619, "lastModified": 1768679419,
"narHash": "sha256-3A5Z5K28YB45REOHMWtyQ24cEUXW76MOtbT6abPrARE=", "narHash": "sha256-l9rM4lXBeS2mIAJsJjVfl0UABx3S3zg5tul7bv+bn50=",
"owner": "rustsec", "owner": "rustsec",
"repo": "advisory-db", "repo": "advisory-db",
"rev": "a98dbc80b16730a64c612c6ab5d5fecb4ebb79ba", "rev": "c700e1cd023ca87343cbd9217d50d47023e9adc7",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -18,11 +18,11 @@
}, },
"crane": { "crane": {
"locked": { "locked": {
"lastModified": 1766194365, "lastModified": 1768873933,
"narHash": "sha256-4AFsUZ0kl6MXSm4BaQgItD0VGlEKR3iq7gIaL7TjBvc=", "narHash": "sha256-CfyzdaeLNGkyAHp3kT5vjvXhA1pVVK7nyDziYxCPsNk=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "7d8ec2c71771937ab99790b45e6d9b93d15d9379", "rev": "0bda7e7d005ccb5522a76d11ccfbf562b71953ca",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -106,11 +106,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1766309749, "lastModified": 1768564909,
"narHash": "sha256-3xY8CZ4rSnQ0NqGhMKAy5vgC+2IVK0NoVEzDoOh4DA4=", "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "a6531044f6d0bef691ea18d4d4ce44d0daa6e816", "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -138,11 +138,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1766371695, "lastModified": 1768877311,
"narHash": "sha256-W7CX9vy7H2Jj3E8NI4djHyF8iHSxKpb2c/7uNQ/vGFU=", "narHash": "sha256-abSDl0cNr0B+YCsIDpO1SjXD9JMxE4s8EFnhLEFVovI=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "d81285ba8199b00dc31847258cae3c655b605e8c", "rev": "59e4ab96304585fde3890025fd59bd2717985cc1",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -141,7 +141,6 @@ struct State {
settings: settings::SettingsState, settings: settings::SettingsState,
is_authenticated: bool, is_authenticated: bool,
video: Option<Arc<VideoHandle<Message, Ready>>>, video: Option<Arc<VideoHandle<Message, Ready>>>,
user: Option<api::jellyfin::UserDto>,
} }
impl State { impl State {
@@ -160,14 +159,12 @@ impl State {
// password_input: String::new(), // password_input: String::new(),
is_authenticated: false, is_authenticated: false,
video: None, video: None,
user: None,
} }
} }
} }
#[derive(Clone, Debug)] #[derive(Debug, Clone)]
pub enum Message { pub enum Message {
Noop,
Settings(settings::SettingsMessage), Settings(settings::SettingsMessage),
Refresh, Refresh,
Search, Search,
@@ -178,30 +175,17 @@ pub enum Message {
SetToken(String), SetToken(String),
Back, Back,
Home, Home,
// Login {
// username: String,
// password: String,
// config: api::JellyfinConfig,
// },
// LoginSuccess(String),
// LoadedClient(api::JellyfinClient, bool),
// Logout,
Video(video::VideoMessage), 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<Message> { fn update(state: &mut State, message: Message) -> Task<Message> {
match message { match message {
Message::Settings(msg) => settings::update(state, msg), Message::Settings(msg) => settings::update(state, msg),
@@ -304,40 +288,14 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
} }
} }
Message::Video(msg) => video::update(state, msg), Message::Video(msg) => video::update(state, msg),
Message::Noop => Task::none(), _ => todo!(),
} }
} }
fn view(state: &State) -> Element<'_, Message> { fn view(state: &State) -> Element<'_, Message> {
let content = home(state); match state.screen {
Screen::Settings => settings::settings(state),
if matches!(state.screen, Screen::Settings) { Screen::Home | _ => home(state),
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
} }
} }
@@ -370,31 +328,27 @@ fn body(state: &State) -> Element<'_, Message> {
} }
fn header(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([ row([
container(Button::new(left_content).on_press(Message::Home)) container(
.padding(10) Button::new(
.width(Length::Fill) Text::new(
.height(Length::Fill) state
.align_x(Alignment::Start) .jellyfin_client
.align_y(Alignment::Center) .as_ref()
.style(container::rounded_box) .map(|c| c.config.server_url.as_str())
.into(), .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), search(state),
container( container(
row([ row([
@@ -520,14 +474,8 @@ fn init() -> (State, Task<Message>) {
match std::fs::read_to_string(".session") { match std::fs::read_to_string(".session") {
Ok(token) => { Ok(token) => {
let client = api::JellyfinClient::pre_authenticated(token.trim(), config)?; 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)) Ok((client, true))
} }
Err(_) => { Err(_) => {
// No cached token, create unauthenticated client // No cached token, create unauthenticated client
let client = api::JellyfinClient::new_with_config(config); let client = api::JellyfinClient::new_with_config(config);
@@ -536,10 +484,12 @@ fn init() -> (State, Task<Message>) {
} }
}, },
|result: Result<_, api::JellyfinApiError>| match result { |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)), Err(e) => Message::Error(format!("Initialization failed: {}", e)),
_ => Message::Error("Login Unimplemented".to_string()),
}, },
), // .chain(Task::done(Message::Refresh)), )
.chain(Task::done(Message::Refresh)),
) )
} }

View File

@@ -10,61 +10,20 @@ pub fn update(state: &mut State, message: SettingsMessage) -> Task<Message> {
SettingsMessage::Open => { SettingsMessage::Open => {
tracing::trace!("Opening settings"); tracing::trace!("Opening settings");
state.screen = Screen::Settings; state.screen = Screen::Settings;
Task::none()
} }
SettingsMessage::Close => { SettingsMessage::Close => {
tracing::trace!("Closing settings"); tracing::trace!("Closing settings");
state.screen = Screen::Home; state.screen = Screen::Home;
Task::none()
} }
SettingsMessage::Select(screen) => { SettingsMessage::Select(screen) => {
tracing::trace!("Switching settings screen to {:?}", screen); tracing::trace!("Switching settings screen to {:?}", screen);
state.settings.screen = screen; state.settings.screen = screen;
Task::none()
}
// LoginResult(Result<(api::JellyfinClient, api::jellyfin::AuthenticationResult), String>),
// LoadedClient(api::JellyfinClient, bool),
// UserLoaded(Result<api::jellyfin::UserDto, String>),
// ConfigSaved(Result<api::JellyfinConfig, String>),
// 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), SettingsMessage::Server(server) => state.settings.server_form.update(server),
} }
Task::none()
} }
pub fn empty() -> Element<'static, Message> { pub fn empty() -> Element<'static, Message> {
@@ -73,9 +32,9 @@ pub fn empty() -> Element<'static, Message> {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct SettingsState { pub struct SettingsState {
pub login_form: LoginForm, login_form: LoginForm,
pub server_form: ServerForm, server_form: ServerForm,
pub screen: SettingsScreen, screen: SettingsScreen,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -95,7 +54,6 @@ pub enum UserMessage {
// Edit(uuid::Uuid), // Edit(uuid::Uuid),
// Delete(uuid::Uuid), // Delete(uuid::Uuid),
Clear, Clear,
Error(String),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -132,9 +90,8 @@ pub struct UserItem {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct LoginForm { pub struct LoginForm {
pub username: String, username: String,
pub password: String, password: String,
pub error: Option<String>,
} }
impl LoginForm { impl LoginForm {
@@ -142,11 +99,9 @@ impl LoginForm {
match message { match message {
UserMessage::UsernameChanged(data) => { UserMessage::UsernameChanged(data) => {
self.username = data; self.username = data;
self.error = None; // Clear error on input
} }
UserMessage::PasswordChanged(data) => { UserMessage::PasswordChanged(data) => {
self.password = data; self.password = data;
self.error = None; // Clear error on input
} }
UserMessage::Add => { UserMessage::Add => {
// Handle adding user // Handle adding user
@@ -154,48 +109,30 @@ impl LoginForm {
UserMessage::Clear => { UserMessage::Clear => {
self.username.clear(); self.username.clear();
self.password.clear(); self.password.clear();
self.error = None;
}
UserMessage::Error(msg) => {
self.error = Some(msg);
} }
} }
} }
pub fn view(&self, is_authenticated: bool) -> Element<'_, Message> { pub fn view(&self) -> Element<'_, Message> {
let mut col = iced::widget::column![text("Login Form"),]; iced::widget::column![
text("Login Form"),
if !is_authenticated { text_input("Enter Username", &self.username).on_input(|data| {
let mut inputs = iced::widget::column![ Message::Settings(SettingsMessage::User(UserMessage::UsernameChanged(data)))
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) row![
.secure(true) button(text("Add User")).on_press_maybe(self.validate()),
.on_input(|data| { button(text("Cancel"))
Message::Settings(SettingsMessage::User(UserMessage::PasswordChanged(data))) .on_press(Message::Settings(SettingsMessage::User(UserMessage::Clear))),
}),
] ]
.spacing(10); .spacing(10),
]
if let Some(err) = &self.error { .spacing(10)
inputs = inputs.push(text(err).style(|_| text::Style { .padding([10, 0])
color: Some(iced::Color::from_rgb(0.8, 0.0, 0.0)), .into()
}));
}
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()
} }
pub fn validate(&self) -> Option<Message> { pub fn validate(&self) -> Option<Message> {
@@ -211,7 +148,7 @@ pub struct ServerForm {
} }
impl ServerForm { impl ServerForm {
pub fn update(&mut self, message: ServerMessage) -> Task<Message> { pub fn update(&mut self, message: ServerMessage) {
match message { match message {
ServerMessage::NameChanged(data) => { ServerMessage::NameChanged(data) => {
self.name = data; self.name = data;
@@ -220,36 +157,7 @@ impl ServerForm {
self.url = data; self.url = data;
} }
ServerMessage::Add => { ServerMessage::Add => {
// Handle adding server (saving config) // Handle adding server
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,
);
} }
ServerMessage::Clear => { ServerMessage::Clear => {
self.name.clear(); self.name.clear();
@@ -257,7 +165,6 @@ impl ServerForm {
} }
_ => {} _ => {}
} }
Task::none()
} }
pub fn view(&self) -> Element<'_, Message> { pub fn view(&self) -> Element<'_, Message> {
iced::widget::column![ iced::widget::column![
@@ -290,15 +197,7 @@ impl ServerForm {
mod screens { mod screens {
use super::*; use super::*;
pub fn settings(state: &State) -> Element<'_, Message> { pub fn settings(state: &State) -> Element<'_, Message> {
container( row([settings_list(state), settings_screen(state)]).into()
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()
} }
pub fn settings_screen(state: &State) -> Element<'_, Message> { pub fn settings_screen(state: &State) -> Element<'_, Message> {
@@ -357,24 +256,10 @@ mod screens {
.into() .into()
} }
pub fn user(state: &State) -> Element<'_, Message> { 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( container(
Column::new() Column::new()
.push(text("User Settings")) .push(text("User Settings"))
.push(user_display) .push(state.settings.login_form.view())
.push(state.settings.login_form.view(state.is_authenticated))
// .push(userlist(&state)) // .push(userlist(&state))
.spacing(20) .spacing(20)
.padding(20), .padding(20),
@@ -389,100 +274,3 @@ pub fn center_text(content: &str) -> Element<'_, Message> {
.width(Length::Fill) .width(Length::Fill)
.into() .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()
// }