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) + } +}