From 05ae9ff57011a6dc8bf9e41990a12cf3e4574246 Mon Sep 17 00:00:00 2001 From: uttarayan21 Date: Wed, 26 Nov 2025 16:15:41 +0530 Subject: [PATCH] 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 --- Cargo.lock | 147 ++++++++++++++++++++++++ Cargo.toml | 3 +- api/src/lib.rs | 1 - flake.nix | 24 ++-- store/Cargo.toml | 13 +++ store/src/lib.rs | 217 +++++++++++++++++++++++++++++++++++ ui-iced/Cargo.toml | 1 + ui-iced/src/lib.rs | 198 +++++++++----------------------- ui-iced/src/settings.rs | 56 +++++++++ ui-iced/src/shared_string.rs | 21 ++++ 10 files changed, 527 insertions(+), 154 deletions(-) create mode 100644 store/Cargo.toml create mode 100644 store/src/lib.rs create mode 100644 ui-iced/src/settings.rs diff --git a/Cargo.lock b/Cargo.lock index c9ce27b..45b09f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -726,6 +726,18 @@ dependencies = [ "core2", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blade-graphics" version = "0.7.0" @@ -845,6 +857,28 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e79769241dcd44edf79a732545e8b5cec84c247ac060f5252cd51885d093a8fc" +[[package]] +name = "bson" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3f109694c4f45353972af96bf97d8a057f82e2d6e496457f4d135b9867a518c" +dependencies = [ + "ahash", + "base64", + "bitvec", + "getrandom 0.3.4", + "hex", + "indexmap", + "js-sys", + "rand 0.9.2", + "serde", + "serde_bytes", + "simdutf8", + "thiserror 2.0.17", + "time", + "uuid", +] + [[package]] name = "bstr" version = "1.12.1" @@ -1646,6 +1680,15 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -2270,6 +2313,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -4596,6 +4645,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.4.2" @@ -5462,6 +5517,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -5683,6 +5744,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -5859,6 +5926,16 @@ dependencies = [ "font-types", ] +[[package]] +name = "redb" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae323eb086579a3769daa2c753bb96deb95993c534711e0dbe881b5192906a06" +dependencies = [ + "libc", + "uuid", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -6357,6 +6434,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -6521,6 +6608,12 @@ dependencies = [ "quote", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simplecss" version = "0.2.2" @@ -6792,6 +6885,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "store" +version = "0.1.0" +dependencies = [ + "bson", + "futures", + "parking_lot", + "redb", + "serde", + "tokio", + "uuid", +] + [[package]] name = "strict-num" version = "0.1.1" @@ -7172,6 +7278,37 @@ dependencies = [ "zune-jpeg 0.4.21", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -7615,6 +7752,7 @@ dependencies = [ "iced_video_player", "reqwest", "tap", + "toml 0.9.8", "tracing", "url", "uuid", @@ -9089,6 +9227,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11-dl" version = "2.21.0" diff --git a/Cargo.toml b/Cargo.toml index a1f4020..48d8366 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = [ "typegen", "ui-gpui", "ui-iced", - "crates/iced_video_player", + "crates/iced_video_player", "store", ] [workspace.dependencies] iced = { git = "https://github.com/iced-rs/iced", features = [ @@ -16,7 +16,6 @@ iced = { git = "https://github.com/iced-rs/iced", features = [ "tokio", "debug", ] } -iced_wgpu = { git = "https://github.com/iced-rs/iced" } iced_video_player = { path = "crates/iced_video_player" } [package] diff --git a/api/src/lib.rs b/api/src/lib.rs index c11cd5c..c57fe7a 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -255,7 +255,6 @@ impl JellyfinClient { "{}/Videos/{}/stream?static=true", self.config.server_url.as_str(), item, - // item, ); Ok(url::Url::parse(&stream_url).expect("Failed to parse stream URL")) } diff --git a/flake.nix b/flake.nix index 32557d1..bb630a1 100644 --- a/flake.nix +++ b/flake.nix @@ -70,7 +70,10 @@ nativeBuildInputs = with pkgs; [ pkg-config ]; + # LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [pkgs.wayland]; LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs; + # SYSTEM_DEPS_LINK = "static"; + # PKG_CONFIG_ALL_STATIC = "1"; buildInputs = with pkgs; [ @@ -84,11 +87,12 @@ glib glib-networking - libsysprof-capture - pcre2 - libunwind - elfutils - zstd + # bzip2_1_1 + # libsysprof-capture + # pcre2 + # libunwind + # elfutils + # zstd openssl vulkan-loader @@ -97,14 +101,18 @@ gst_all_1.gstreamermm gst_all_1.gst-vaapi + # util-linux + # libselinux + # libsepol + alsa-lib-with-plugins libxkbcommon udev wayland wayland-protocols - xorg.libX11 - xorg.libXi - xorg.libXrandr + # xorg.libX11 + # xorg.libXi + # xorg.libXrandr ]) ++ (lib.optionals pkgs.stdenv.isDarwin [ libiconv diff --git a/store/Cargo.toml b/store/Cargo.toml new file mode 100644 index 0000000..33577e0 --- /dev/null +++ b/store/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "store" +version = "0.1.0" +edition = "2024" + +[dependencies] +bson = { version = "3.1.0", features = ["serde"] } +futures = "0.3.31" +parking_lot = "0.12.5" +redb = { version = "3.1.0", features = ["uuid"] } +serde = "1.0.228" +tokio = { version = "1.48.0", features = ["rt"] } +uuid = "1.18.1" diff --git a/store/src/lib.rs b/store/src/lib.rs new file mode 100644 index 0000000..996afa8 --- /dev/null +++ b/store/src/lib.rs @@ -0,0 +1,217 @@ +use std::{ + borrow::Borrow, + collections::VecDeque, + marker::PhantomData, + path::Path, + sync::{Arc, RwLock, atomic::AtomicBool}, +}; + +use futures::task::AtomicWaker; +use redb::{Error, Key, ReadableDatabase, TableDefinition, Value}; +use serde::{Serialize, de::DeserializeOwned}; + +const USERS: TableDefinition> = TableDefinition::new("users"); +const SERVERS: TableDefinition> = TableDefinition::new("servers"); +const SETTINGS: TableDefinition> = TableDefinition::new("settings"); + +#[derive(Debug)] +pub struct TableInner { + db: Arc, +} + +impl Clone for TableInner { + fn clone(&self) -> Self { + Self { + db: Arc::clone(&self.db), + } + } +} + +impl TableInner { + fn new(db: Arc) -> Self { + Self { db } + } +} + +impl TableInner { + async fn get<'a, K: Key, V: Serialize + DeserializeOwned>( + &self, + table: TableDefinition<'static, K, Vec>, + key: impl Borrow>, + ) -> Result> { + let db: &redb::Database = &self.db.as_ref().database; + let db_reader = db.begin_read()?; + let table = db_reader.open_table(table)?; + table + .get(key)? + .map(|value| bson::deserialize_from_slice(&value.value())) + .transpose() + .map_err(|e| redb::Error::Io(std::io::Error::other(e))) + } + + async fn insert<'a, 'b, K: Key + Send, V: Serialize + DeserializeOwned + Send + 'a>( + &'b self, + table: TableDefinition<'static, K, Vec>, + key: impl Borrow> + Send + 'b, + value: V, + ) -> Result> { + let db: &redb::Database = &self.db.as_ref().database; + // self.db + // .writing + // .store(true, std::sync::atomic::Ordering::SeqCst); + + let out = tokio::task::spawn_blocking(move || -> Result> { + let db_writer = db.begin_write()?; + let out = { + let mut table = db_writer.open_table(table)?; + let serialized_value = bson::serialize_to_vec(&value) + .map_err(|e| redb::Error::Io(std::io::Error::other(e)))?; + let previous = table.insert(key, &serialized_value)?; + let out = previous + .map(|value| bson::deserialize_from_slice(&value.value())) + .transpose() + .map_err(|e| redb::Error::Io(std::io::Error::other(e))); + out + }; + db_writer.commit()?; + out + }) + .await + .expect("Failed to run blocking task")?; + Ok(out) + } +} + +// impl Table for TableInner { +// async fn get(&self, key: K) -> Result> {} +// async fn insert(&self, key: K, value: V) -> Result> {} +// async fn modify(&self, key: K, v: FnOnce(V) -> V) -> Result {} +// async fn remove(&self, key: K) -> Result> {} +// } + +#[derive(Debug)] +pub struct Users(TableInner); + +impl Clone for Users { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} +impl Users { + const TABLE: TableDefinition<'static, uuid::Uuid, Vec> = USERS; +} + +#[derive(Debug)] +pub struct Servers(TableInner); +impl Clone for Servers { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} +impl Servers { + const TABLE: TableDefinition<'static, uuid::Uuid, Vec> = SERVERS; +} + +#[derive(Debug)] +pub struct Settings(TableInner); +impl Clone for Settings { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} +impl Settings { + const TABLE: TableDefinition<'static, uuid::Uuid, Vec> = SETTINGS; +} + +#[derive(Debug, Clone)] +pub struct Database { + users: Users, + servers: Servers, + settings: Settings, + handle: Arc, +} + +#[derive(Debug)] +pub struct DatabaseHandle { + database: redb::Database, + writing: AtomicBool, + wakers: RwLock>, +} + +#[derive(Debug)] +pub struct DatabaseWriterGuard<'a> { + handle: &'a DatabaseHandle, + dropper: Arc, +} + +// impl Drop for DatabaseWriterGuard<'_> { +// fn drop(&mut self) { +// self.handle +// .writing +// .store(false, std::sync::atomic::Ordering::SeqCst); +// let is_panicking = std::thread::panicking(); +// let Ok(writer) = self.handle.wakers.write() else { +// if is_panicking { +// return; +// } else { +// panic!("Wakers lock poisoned"); +// } +// } +// if let Some(waker) = (self.handle.wakers.write()).pop() { +// waker.wake(); +// }; +// // let mut wakers = self.handle.wakers.write().expect(); +// // if let Some(waker) = self.handle.wakers.write().expect("Wakers lock poisoned").pop_front() { +// // waker.wake(); +// // } +// // while let Some(waker) = wakers.pop_front() { +// // waker.wake(); +// // } +// } +// } + +type Result = core::result::Result; + +pub trait Table { + fn insert( + &self, + key: K, + value: V, + ) -> impl Future>> + Send; + fn modify( + &self, + key: K, + v: impl FnOnce(V) -> O, + ) -> impl Future> + Send; + fn remove( + &self, + key: K, + ) -> impl Future>> + Send; + fn get( + &self, + key: K, + ) -> impl Future>> + Send; +} + +impl Database { + pub fn create(path: impl AsRef) -> Result { + let writing = AtomicBool::new(false); + let wakers = RwLock::new(VecDeque::new()); + let db = redb::Database::create(path)?; + let db = Arc::new(DatabaseHandle { + database: db, + writing, + wakers, + }); + let table_inner = TableInner::new(Arc::clone(&db)); + let users = Users(table_inner.clone()); + let servers = Servers(table_inner.clone()); + let settings = Settings(table_inner.clone()); + Ok(Self { + servers, + users, + settings, + handle: db, + }) + } +} diff --git a/ui-iced/Cargo.toml b/ui-iced/Cargo.toml index 1e618c1..e8bbb19 100644 --- a/ui-iced/Cargo.toml +++ b/ui-iced/Cargo.toml @@ -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" diff --git a/ui-iced/src/lib.rs b/ui-iced/src/lib.rs index 8cd6de5..50b7507 100644 --- a/ui-iced/src/lib.rs +++ b/ui-iced/src/lib.rs @@ -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::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::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 { .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 { 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) { - 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)), diff --git a/ui-iced/src/settings.rs b/ui-iced/src/settings.rs new file mode 100644 index 0000000..4a4de63 --- /dev/null +++ b/ui-iced/src/settings.rs @@ -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, +} + +#[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> {} +} diff --git a/ui-iced/src/shared_string.rs b/ui-iced/src/shared_string.rs index 90dd052..f72b09d 100644 --- a/ui-iced/src/shared_string.rs +++ b/ui-iced/src/shared_string.rs @@ -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 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) + } +}