feat(store): add database storage with redb and bson support

This commit introduces a new `store` crate that provides database functionality using redb for storage and bson for serialization. It includes tables for users, servers, and settings, along with async operations for getting, inserting, modifying, and removing data. The store supports UUID keys and integrates with the existing Jellyfin client authentication flow.

The changes also include:
- Adding new dependencies to Cargo.lock for bitvec, bson, deranged, funty, num-conv, powerfmt, radium, serde_bytes, simdutf8, time, and wyz
- Updating Cargo.toml to include the new store crate in workspace members
- Modifying ui-iced to use the new database initialization flow with config loading from TOML
- Adding a settings module to ui-iced with UI components for managing server and user configuration
- Implementing secret string handling for sensitive data like passwords
- Updating API client to support pre-authenticated clients with cached tokens
This commit is contained in:
uttarayan21
2025-11-26 16:15:41 +05:30
parent ca1fd2e977
commit 05ae9ff570
10 changed files with 527 additions and 154 deletions

View File

@@ -12,6 +12,7 @@ iced = { workspace = true }
iced_video_player = { workspace = true }
reqwest = "0.12.24"
tap = "1.0.1"
toml = "0.9.8"
tracing = "0.1.41"
url = "2.5.7"
uuid = "1.18.1"

View File

@@ -1,6 +1,8 @@
// mod settings;
mod shared_string;
use iced_video_player::{Video, VideoPlayer};
use shared_string::SharedString;
use std::sync::Arc;
mod blur_hash;
@@ -157,6 +159,7 @@ pub enum Message {
PasswordChanged(String),
Login,
LoginSuccess(String),
LoadedClient(api::JellyfinClient, bool),
Logout,
Video(VideoMessage),
}
@@ -193,25 +196,12 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
Message::Login => {
let username = state.username_input.clone();
let password = state.password_input.clone();
// Update the client config with the new credentials
let mut config = (*state.jellyfin_client.config).clone();
config.username = username;
config.password = password;
let config = (*state.jellyfin_client.config).clone();
Task::perform(
async move {
let mut client = api::JellyfinClient::new_with_config(config);
client.authenticate().await
},
async move { api::JellyfinClient::authenticate(username, password, config).await },
|result| match result {
Ok(auth_result) => {
if let Some(token) = auth_result.access_token {
Message::LoginSuccess(token)
} else {
Message::Error("Authentication failed: No token received".to_string())
}
}
Ok(client) => Message::LoadedClient(client, true),
Err(e) => Message::Error(format!("Login failed: {}", e)),
},
)
@@ -232,6 +222,15 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
|_| 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("");
@@ -372,7 +371,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
.unwrap();
state.video = Video::new(&url)
.inspect_err(|err| {
dbg!(err);
tracing::error!("{err:?}");
})
.ok()
.map(Arc::new);
@@ -384,7 +383,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
fn view(state: &State) -> Element<'_, Message> {
match state.screen {
Screen::Settings => settings(state),
// Screen::Settings => settings::settings(state),
Screen::Home | _ => home(state),
}
}
@@ -508,123 +507,6 @@ fn footer(state: &State) -> Element<'_, Message> {
.into()
}
fn settings(state: &State) -> Element<'_, Message> {
let content = if state.is_authenticated {
// Authenticated view - show user info and logout
column([
Text::new("Settings").size(32).into(),
container(
column([
Text::new("Account").size(24).into(),
Text::new("Server URL").size(14).into(),
Text::new(state.jellyfin_client.config.server_url.as_str())
.size(12)
.into(),
container(Text::new("Status: Logged In").size(14))
.padding(10)
.width(Length::Fill)
.into(),
container(
row([
Button::new(Text::new("Logout"))
.padding(10)
.on_press(Message::Logout)
.into(),
Button::new(Text::new("Close"))
.padding(10)
.on_press(Message::CloseSettings)
.into(),
])
.spacing(10),
)
.padding(10)
.width(Length::Fill)
.into(),
])
.spacing(10)
.max_width(400)
.align_x(Alignment::Center),
)
.padding(20)
.width(Length::Fill)
.align_x(Alignment::Center)
.style(container::rounded_box)
.into(),
])
.spacing(20)
.padding(50)
.align_x(Alignment::Center)
} else {
// Not authenticated view - show login form
column([
Text::new("Settings").size(32).into(),
container(
column([
Text::new("Login to Jellyfin").size(24).into(),
Text::new("Server URL").size(14).into(),
Text::new(state.jellyfin_client.config.server_url.as_str())
.size(12)
.into(),
container(
TextInput::new("Username", &state.username_input)
.padding(10)
.size(16)
.on_input(Message::UsernameChanged),
)
.padding(10)
.width(Length::Fill)
.into(),
container(
TextInput::new("Password", &state.password_input)
.padding(10)
.size(16)
.secure(true)
.on_input(Message::PasswordChanged)
.on_submit(Message::Login),
)
.padding(10)
.width(Length::Fill)
.into(),
container(
row([
Button::new(Text::new("Login"))
.padding(10)
.on_press(Message::Login)
.into(),
Button::new(Text::new("Cancel"))
.padding(10)
.on_press(Message::CloseSettings)
.into(),
])
.spacing(10),
)
.padding(10)
.width(Length::Fill)
.into(),
])
.spacing(10)
.max_width(400)
.align_x(Alignment::Center),
)
.padding(20)
.width(Length::Fill)
.align_x(Alignment::Center)
.style(container::rounded_box)
.into(),
])
.spacing(20)
.padding(50)
.align_x(Alignment::Center)
};
container(content)
.width(Length::Fill)
.height(Length::Fill)
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.into()
}
fn card(item: &Item) -> Element<'_, Message> {
let name = item
.name
@@ -660,17 +542,47 @@ fn card(item: &Item) -> Element<'_, Message> {
.into()
}
// fn video(url: &str
fn init() -> (State, Task<Message>) {
let mut jellyfin = api::JellyfinClient::new_with_config();
// 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);
(
State::new(jellyfin.clone()),
State::new(default_client),
Task::perform(
async move { jellyfin.authenticate_with_cached_token(".session").await },
|token| match token {
Ok(token) => Message::SetToken(token),
Err(e) => Message::Error(format!("Authentication failed: {}", e)),
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| {
api::JellyfinApiError::IoError(std::io::Error::new(
std::io::ErrorKind::InvalidData,
e,
))
})?;
// Try to load cached token and authenticate
match std::fs::read_to_string(".session") {
Ok(token) => {
let client = api::JellyfinClient::pre_authenticated(token.trim(), config)?;
Ok((client, true))
}
Err(_) => {
// No cached token, create unauthenticated client
let client = api::JellyfinClient::new_with_config(config);
Ok((client, false))
}
}
},
|result: Result<_, api::JellyfinApiError>| match result {
Ok((client, is_authenticated)) => Message::LoadedClient(client, is_authenticated),
Err(e) => Message::Error(format!("Initialization failed: {}", e)),
},
)
.chain(Task::done(Message::Refresh)),

56
ui-iced/src/settings.rs Normal file
View File

@@ -0,0 +1,56 @@
use crate::*;
pub fn settings(state: &State) -> Element<'_, Message> {}
#[derive(Debug, Clone)]
pub struct SettingsState {
login_form: LoginForm,
server_form: ServerForm,
screen: SettingsScreen,
}
#[derive(Debug, Clone)]
pub enum SettingsMessage {
Open,
Close,
Select(SettingsScreen),
}
#[derive(Debug, Clone)]
pub enum SettingsScreen {
Main,
Users,
Servers,
}
#[derive(Debug, Clone)]
pub struct ServerItem {
pub id: uuid::Uuid,
pub name: SharedString,
pub url: SharedString,
pub users: Vec<uuid::Uuid>,
}
#[derive(Debug, Clone)]
pub struct UserItem {
pub id: uuid::Uuid,
pub name: SharedString,
}
#[derive(Debug, Clone)]
pub struct LoginForm {
username: String,
password: String,
}
#[derive(Debug, Clone)]
pub struct ServerForm {
name: String,
url: String,
}
mod screens {
pub fn main(state: &State) -> Element<'_, Message> {}
pub fn server(state: &State) -> Element<'_, Message> {}
pub fn user(state: &State) -> Element<'_, Message> {}
}

View File

@@ -49,6 +49,21 @@ impl std::ops::Deref for SharedString {
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct SecretSharedString(ArcCow<'static, str>);
impl core::fmt::Debug for SecretSharedString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("(..secret..)")
}
}
impl From<String> for SecretSharedString {
fn from(s: String) -> Self {
Self(ArcCow::Owned(Arc::from(s)))
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ArcCow<'a, T: ?Sized> {
Borrowed(&'a T),
@@ -66,3 +81,9 @@ where
}
}
}
impl<'a, T> From<&'a T> for ArcCow<'a, T> {
fn from(value: &'a T) -> Self {
ArcCow::Borrowed(value)
}
}